// // PithosSyncDaemon.m // pithos-macos // // Copyright 2011-2012 GRNET S.A. All rights reserved. // // Redistribution and use in source and binary forms, with or // without modification, are permitted provided that the following // conditions are met: // // 1. Redistributions of source code must retain the above // copyright notice, this list of conditions and the following // disclaimer. // // 2. Redistributions in binary form must reproduce the above // copyright notice, this list of conditions and the following // disclaimer in the documentation and/or other materials // provided with the distribution. // // THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS // OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED // WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR // PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR // CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF // USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED // AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT // LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN // ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE // POSSIBILITY OF SUCH DAMAGE. // // The views and conclusions contained in the software and // documentation are those of the authors and should not be // interpreted as representing official policies, either expressed // or implied, of GRNET S.A. #import "PithosSyncDaemon.h" #import "PithosAccount.h" #import "PithosLocalObjectState.h" #import "PithosActivityFacility.h" #import "PithosUtilities.h" #import "ASINetworkQueue.h" #import "ASIPithosRequest.h" #import "ASIPithos.h" #import "ASIPithosContainer.h" #import "ASIPithosContainerRequest.h" #import "ASIPithosObjectRequest.h" #import "ASIPithosObject.h" @interface PithosSyncDaemon (Private) - (void)loadLocalState; - (void)resetLocalStateWithAll:(BOOL)all; - (void)saveLocalState; - (BOOL)createSyncDirectory:(NSString *)dirPath; - (NSString *)dirPathForAccount:(NSString *)accountName container:(NSString *)containerName; - (NSString *)relativeDirPathForAccount:(NSString *)accountName container:(NSString *)containerName; - (BOOL)moveToTempTrashFile:(NSString *)filePath accountName:(NSString *)accountName pithosContainer:(ASIPithosContainer *)pithosContainer; - (void)emptyTempTrash; - (BOOL)findLocalCopyForObjectWithHash:(NSString *)hash forFile:(NSString *)filePath; - (void)updateLocalStateWithObject:(ASIPithosObject *)object localFilePath:(NSString *)filePath accountName:(NSString *)accountName pithosContainer:(ASIPithosContainer *)pithosContainer; - (void)updateServerStateWithCurrentState:(PithosLocalObjectState *)currentState object:(ASIPithosObject *)object localFilePath:(NSString *)filePath accountName:(NSString *)accountName pithosContainer:(ASIPithosContainer *)pithosContainer; - (void)listRequestFailed:(ASIPithosContainerRequest *)containerRequest; - (void)requestFailed:(ASIPithosRequest *)request; - (void)syncOperationStarted; - (void)syncOperationFinishedWithSuccess:(BOOL)operationSuccessfull; @end @implementation PithosSyncDaemon @synthesize directoryPath, accountsDictionary, skipHidden, pithos; @synthesize accountsNames, accountsPithosContainers; @synthesize lastCompletedSync, remoteObjects, previousRemoteObjects, storedLocalObjectStates, currentLocalObjectStates; @synthesize pithosStateFilePath, tempDownloadsDirPath, tempTrashDirPath; #pragma mark - #pragma Object Lifecycle - (id)initWithDirectoryPath:(NSString *)aDirectoryPath pithosAccount:(PithosAccount *)aPithosAccount accountsDictionary:(NSDictionary *)anAccountsDictionary skipHidden:(BOOL)aSkipHidden resetLocalState:(BOOL)resetLocalState { if ((self = [super init])) { directoryPath = [aDirectoryPath copy]; pithosAccount = aPithosAccount; self.accountsDictionary = anAccountsDictionary; skipHidden = aSkipHidden; self.pithos = pithosAccount.pithos; activityFacility = [PithosActivityFacility defaultPithosActivityFacility]; if (resetLocalState) [self resetLocalStateWithAll:YES]; else [self resetLocalStateWithAll:NO]; networkQueue = [[ASINetworkQueue alloc] init]; networkQueue.showAccurateProgress = YES; networkQueue.shouldCancelAllRequestsOnFailure = NO; // networkQueue.maxConcurrentOperationCount = 1; callbackQueue = [[NSOperationQueue alloc] init]; [callbackQueue setSuspended:YES]; callbackQueue.name = [NSString stringWithFormat:@"gr.grnet.pithos.SyncCallbackQueue.%@", pithosAccount.uniqueName]; // callbackQueue.maxConcurrentOperationCount = 1; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationWillTerminate:) name:NSApplicationWillTerminateNotification object:[NSApplication sharedApplication]]; } return self; } - (void)loadLocalState { @autoreleasepool { if ([[NSFileManager defaultManager] fileExistsAtPath:self.pithosStateFilePath]) self.storedLocalObjectStates = [NSKeyedUnarchiver unarchiveObjectWithFile:self.pithosStateFilePath]; else self.storedLocalObjectStates = [NSMutableDictionary dictionary]; if (!storedLocalObjectStates) self.storedLocalObjectStates = [NSMutableDictionary dictionary]; for (NSString *accountName in accountsNames) { NSMutableDictionary *accountStoredLocalObjectStates = [storedLocalObjectStates objectForKey:accountName]; if (!accountStoredLocalObjectStates) { accountStoredLocalObjectStates = [NSMutableDictionary dictionary]; [storedLocalObjectStates setObject:accountStoredLocalObjectStates forKey:accountName]; } for (ASIPithosContainer *pithosContainer in [accountsPithosContainers objectForKey:accountName]) { if (![accountStoredLocalObjectStates objectForKey:pithosContainer.name]) [accountStoredLocalObjectStates setObject:[NSMutableDictionary dictionary] forKey:pithosContainer.name]; } } } } - (void)resetLocalStateWithAll:(BOOL)all { @autoreleasepool { self.lastCompletedSync = nil; if (all) { self.storedLocalObjectStates = [NSMutableDictionary dictionary]; [self saveLocalState]; // Save an empty dictionary [self loadLocalState]; // Load to populate with containers [self saveLocalState]; // Save again if (self.tempDownloadsDirPath) [PithosUtilities removeContentsAtPath:self.tempDownloadsDirPath]; } else { // Remove containers that don't interest us anymore and save if (!storedLocalObjectStates) [self loadLocalState]; for (NSString *accountName in storedLocalObjectStates) { NSMutableDictionary *accountStoredLocalObjectStates = [storedLocalObjectStates objectForKey:accountName]; NSDictionary *containersDictionary = [accountsDictionary objectForKey:accountName]; if (!containersDictionary) { if (self.tempDownloadsDirPath) { if ([accountName isEqualToString:@""]) { for (NSString *containerName in accountStoredLocalObjectStates) { [PithosUtilities removeContentsAtPath:[self.tempDownloadsDirPath stringByAppendingPathComponent:containerName]]; } } else { [PithosUtilities removeContentsAtPath:[[self.tempDownloadsDirPath stringByAppendingPathComponent:@"shared with me"] stringByAppendingPathComponent:accountName]]; } } [storedLocalObjectStates removeObjectForKey:accountName]; } else { // Check the account's containers for (NSString *containerName in accountStoredLocalObjectStates) { if (![containersDictionary objectForKey:containerName]) { if ([accountName isEqualToString:@""]) [PithosUtilities removeContentsAtPath:[self.tempDownloadsDirPath stringByAppendingPathComponent:containerName]]; else [PithosUtilities removeContentsAtPath:[[[self.tempDownloadsDirPath stringByAppendingPathComponent:@"shared with me"] stringByAppendingPathComponent:accountName] stringByAppendingPathComponent:containerName]]; [accountStoredLocalObjectStates removeObjectForKey:containerName]; } } } } [self saveLocalState]; } } } - (void)saveLocalState { @autoreleasepool { [NSKeyedArchiver archiveRootObject:storedLocalObjectStates toFile:self.pithosStateFilePath]; } } - (void)resetDaemon { @synchronized(self) { if (!daemonActive) return; } [networkQueue reset]; [callbackQueue cancelAllOperations]; [callbackQueue setSuspended:YES]; [self emptyTempTrash]; syncOperationCount = 0; @synchronized(self) { daemonActive = NO; } } - (void)startDaemon { @synchronized(self) { if (daemonActive) return; } // In the improbable case of leftover operations [networkQueue reset]; [callbackQueue cancelAllOperations]; syncOperationCount = 0; newSyncRequested = NO; syncIncomplete = NO; syncLate = NO; [self loadLocalState]; [networkQueue go]; [callbackQueue setSuspended:NO]; @synchronized(self) { daemonActive = YES; } } - (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; [self resetDaemon]; } #pragma mark - #pragma mark Observers - (void)applicationWillTerminate:(NSNotification *)notification { [self saveLocalState]; } #pragma mark - #pragma mark Properties - (NSString *)pithosStateFilePath { if (!pithosStateFilePath) { pithosStateFilePath = [[[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) objectAtIndex:0] stringByAppendingPathComponent:[[NSBundle mainBundle] bundleIdentifier]] stringByAppendingPathComponent:[NSString stringWithFormat:@"%@-PithosLocalObjectStates.archive", pithosAccount.uniqueName]]; NSString *pithosStateFileDirPath = [pithosStateFilePath stringByDeletingLastPathComponent]; NSFileManager *fileManager = [NSFileManager defaultManager]; BOOL isDirectory; BOOL fileExists = [fileManager fileExistsAtPath:pithosStateFileDirPath isDirectory:&isDirectory]; NSError *error = nil; if (fileExists && !isDirectory) [fileManager removeItemAtPath:pithosStateFileDirPath error:&error]; if (!error && !fileExists) [fileManager createDirectoryAtPath:pithosStateFileDirPath withIntermediateDirectories:YES attributes:nil error:&error]; //if (error) // pithosStateFilePath = nil; // XXX create a dir using mktmps? } return [pithosStateFilePath copy]; } - (NSString *)tempDownloadsDirPath { if (!tempDownloadsDirPath) { tempDownloadsDirPath = [[[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) objectAtIndex:0] stringByAppendingPathComponent:[[NSBundle mainBundle] bundleIdentifier]] stringByAppendingPathComponent:[NSString stringWithFormat:@"%@-TempDownloads", pithosAccount.uniqueName]]; NSFileManager *fileManager = [NSFileManager defaultManager]; BOOL isDirectory; BOOL fileExists = [fileManager fileExistsAtPath:tempDownloadsDirPath isDirectory:&isDirectory]; NSError *error = nil; if (fileExists && !isDirectory) [fileManager removeItemAtPath:tempDownloadsDirPath error:&error]; if (!error && !fileExists) [fileManager createDirectoryAtPath:tempDownloadsDirPath withIntermediateDirectories:YES attributes:nil error:&error]; //if (error) // tempDownloadsDirPath = nil; // XXX create a dir using mktmps? } return tempDownloadsDirPath; } - (NSString *)tempTrashDirPath { if (!tempTrashDirPath) { tempTrashDirPath = [[[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) objectAtIndex:0] stringByAppendingPathComponent:[[NSBundle mainBundle] bundleIdentifier]] stringByAppendingPathComponent:[NSString stringWithFormat:@"%@-TempTrash", pithosAccount.uniqueName]]; NSFileManager *fileManager = [NSFileManager defaultManager]; BOOL isDirectory; BOOL fileExists = [fileManager fileExistsAtPath:tempTrashDirPath isDirectory:&isDirectory]; NSError *error = nil; if (fileExists && !isDirectory) [fileManager removeItemAtPath:tempTrashDirPath error:&error]; if (!error && !fileExists) [fileManager createDirectoryAtPath:tempTrashDirPath withIntermediateDirectories:YES attributes:nil error:&error]; //if (error) // tempTrashDirPath = nil; // XXX create a dir using mktmps? } return tempTrashDirPath; } - (void)setDirectoryPath:(NSString *)aDirectoryPath { if (aDirectoryPath && ![aDirectoryPath isEqualToString:directoryPath]) { [self resetDaemon]; [self resetLocalStateWithAll:YES]; directoryPath = [aDirectoryPath copy]; } } - (void)setAccountsDictionary:(NSDictionary *)anAccountsDictionary { if (anAccountsDictionary && ![anAccountsDictionary isEqualToDictionary:accountsDictionary]) { BOOL reset = (accountsDictionary != nil); if (reset) [self resetDaemon]; accountsDictionary = [anAccountsDictionary copy]; accountsCount = [accountsDictionary count]; self.accountsNames = [NSMutableArray arrayWithCapacity:accountsCount]; self.accountsPithosContainers = [NSMutableDictionary dictionaryWithCapacity:accountsCount]; NSDictionary *containersDictionary = [accountsDictionary objectForKey:@""]; if (containersDictionary && [containersDictionary count]) { NSMutableArray *pithosContainers = [NSMutableArray arrayWithCapacity:[containersDictionary count]]; for (NSString *containerName in containersDictionary) { ASIPithosContainer *pithosContainer = [ASIPithosContainer container]; pithosContainer.name = containerName; [pithosContainers addObject:pithosContainer]; } [accountsNames addObject:@""]; [accountsPithosContainers setObject:pithosContainers forKey:@""]; } for (NSString *accountName in accountsDictionary) { NSDictionary *containersDictionary = [accountsDictionary objectForKey:accountName]; if (![accountsNames containsObject:accountName] && [containersDictionary count]) { NSMutableArray *pithosContainers = [NSMutableArray arrayWithCapacity:[containersDictionary count]]; for (NSString *containerName in containersDictionary) { ASIPithosContainer *pithosContainer = [ASIPithosContainer container]; pithosContainer.name = containerName; [pithosContainers addObject:pithosContainer]; } [accountsNames addObject:accountName]; [accountsPithosContainers setObject:pithosContainers forKey:accountName]; } } if (reset) [self resetLocalStateWithAll:NO]; } } - (void)setPithos:(ASIPithos *)aPithos { if (!pithos) { pithos = [ASIPithos pithos]; pithos.authUser = [aPithos.authUser copy]; pithos.authToken = [aPithos.authToken copy]; pithos.storageURLPrefix = [aPithos.storageURLPrefix copy]; pithos.authURL = [aPithos.authURL copy]; pithos.publicURLPrefix = [aPithos.publicURLPrefix copy]; pithos.userCatalogURL = [aPithos.userCatalogURL copy]; } if (aPithos && (![aPithos.authUser isEqualToString:pithos.authUser] || ![aPithos.authToken isEqualToString:pithos.authToken] || ![aPithos.storageURLPrefix isEqualToString:pithos.storageURLPrefix])) { [self resetDaemon]; if (![aPithos.authUser isEqualToString:pithos.authUser] || ![aPithos.storageURLPrefix isEqualToString:pithos.storageURLPrefix]) [self resetLocalStateWithAll:YES]; pithos.authUser = [aPithos.authUser copy]; pithos.authToken = [aPithos.authToken copy]; pithos.storageURLPrefix = [aPithos.storageURLPrefix copy]; pithos.authURL = [aPithos.authURL copy]; pithos.publicURLPrefix = [aPithos.publicURLPrefix copy]; pithos.userCatalogURL = [aPithos.userCatalogURL copy]; } } #pragma mark - #pragma mark Helper Methods - (BOOL)createSyncDirectory:(NSString *)dirPath { NSFileManager *fileManager = [NSFileManager defaultManager]; BOOL isDirectory; NSError *error = nil; if (![fileManager fileExistsAtPath:dirPath isDirectory:&isDirectory]) { if (![fileManager createDirectoryAtPath:dirPath withIntermediateDirectories:YES attributes:nil error:&error] || error) { [PithosUtilities fileActionFailedAlertWithTitle:@"Local Sync Directory Error" message:[NSString stringWithFormat:@"Cannot create local sync directory at '%@'", dirPath] error:error]; return NO; } } else if (!isDirectory) { [PithosUtilities fileActionFailedAlertWithTitle:@"Local Sync Directory Error" message:[NSString stringWithFormat:@"File already exists at the local sync directory path at '%@'", dirPath] error:nil]; return NO; } return YES; } - (NSString *)dirPathForAccount:(NSString *)accountName container:(NSString *)containerName { if ([accountName isEqualToString:@""]) return [directoryPath stringByAppendingPathComponent:containerName]; else return [[[directoryPath stringByAppendingPathComponent:@"shared with me"] stringByAppendingPathComponent:accountName] stringByAppendingPathComponent:containerName]; } - (NSString *)relativeDirPathForAccount:(NSString *)accountName container:(NSString *)containerName { if ([accountName isEqualToString:@""]) return containerName; else return [[@"shared with me" stringByAppendingPathComponent:accountName] stringByAppendingPathComponent:containerName]; } #pragma mark - #pragma mark Sync - (void)syncOperationStarted { @synchronized(self) { syncOperationCount++; } } - (void)syncOperationFinishedWithSuccess:(BOOL)operationSuccessfull { @synchronized(self) { if (!operationSuccessfull) syncIncomplete = YES; if (syncOperationCount == 0) { // XXX This shouldn't happen, maybe change operationCount to a BOOL as operationsPending in browser DLog(@"Sync::WARNING: tried to decrease syncOperationCount when 0"); return; } syncOperationCount--; if (syncOperationCount == 0) { if (!syncIncomplete) { self.lastCompletedSync = [NSDate date]; dispatch_async(dispatch_get_main_queue(), ^{ [activityFacility startAndEndActivityWithType:PithosActivityOther message:[NSString stringWithFormat:@"Sync: Completed %@", lastCompletedSync] pithosAccount:pithosAccount]; }); } [self emptyTempTrash]; if (newSyncRequested && daemonActive) [self sync]; } } } - (BOOL)isSyncing { @synchronized(self) { return ((syncOperationCount > 0) && daemonActive); } } - (void)syncLate { @synchronized(self) { if ([self isSyncing]) syncLate = YES; } } - (void)sync { @synchronized(self) { if ([self isSyncing]) { // If at least one operation is running return newSyncRequested = YES; return; } else if (daemonActive && accountsCount) { // The first operation is the server listing [self syncOperationStarted]; newSyncRequested = NO; syncIncomplete = NO; syncLate = NO; } else { return; } } if (![self createSyncDirectory:directoryPath]) { [self syncOperationFinishedWithSuccess:NO]; return; } for (NSString *accountName in accountsNames) { for (ASIPithosContainer *pithosContainer in [accountsPithosContainers objectForKey:accountName]) { if (![self createSyncDirectory:[self dirPathForAccount:accountName container:pithosContainer.name]]) { [self syncOperationFinishedWithSuccess:NO]; return; } } } self.remoteObjects = [NSMutableDictionary dictionaryWithCapacity:accountsCount]; for (NSString *accountName in accountsNames) { [remoteObjects setObject:[NSMutableDictionary dictionary] forKey:accountName]; } accountsIndex = 0; containersIndex = 0; NSString *accountName = [accountsNames objectAtIndex:accountsIndex]; ASIPithosContainer *pithosContainer = [[accountsPithosContainers objectForKey:accountName] objectAtIndex:containersIndex]; ASIPithosContainerRequest *containerRequest = [ASIPithosContainerRequest listObjectsRequestWithPithos:pithos containerName:pithosContainer.name limit:0 marker:nil prefix:nil delimiter:nil path:nil meta:nil shared:NO until:nil ifModifiedSince:pithosContainer.lastModified]; if (![accountName isEqualToString:@""]) [containerRequest setRequestUserFromDefaultTo:accountName withPithos:pithos]; containerRequest.delegate = self; containerRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:); containerRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:); PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityOther message:@"Sync: Getting server listing" pithosAccount:pithosAccount]; containerRequest.userInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys: activity, @"activity", @"Sync: Getting server listing (stopped)", @"stoppedActivityMessage", @"Sync: Getting server listing (failed)", @"failedActivityMessage", @"Sync: Getting server listing (finished)", @"finishedActivityMessage", [NSNumber numberWithInteger:NSOperationQueuePriorityHigh], @"priority", [NSNumber numberWithUnsignedInteger:10], @"retries", NSStringFromSelector(@selector(listRequestFinished:)), @"didFinishSelector", NSStringFromSelector(@selector(listRequestFailed:)), @"didFailSelector", nil]; [networkQueue addOperation:[PithosUtilities prepareRequest:containerRequest priority:NSOperationQueuePriorityHigh]]; } - (void)emptyTempTrash { @autoreleasepool { NSString *trashDirPath = self.tempTrashDirPath; if (trashDirPath) { NSFileManager *fileManager = [NSFileManager defaultManager]; NSError *error = nil; // NSArray *subPaths = [[NSFileManager defaultManager] subpathsOfDirectoryAtPath:trashDirPath error:&error]; NSArray *subPaths = [fileManager contentsOfDirectoryAtPath:trashDirPath error:&error]; if (error) { [PithosUtilities fileActionFailedAlertWithTitle:@"Directory Contents Error" message:[NSString stringWithFormat:@"Cannot get contents of directory at '%@'", trashDirPath] error:error]; return; } if ([subPaths count]) { // NSMutableArray *subURLs = [NSMutableArray arrayWithCapacity:[subPaths count]]; // for (NSString *subPath in subPaths) { // [subURLs addObject:[NSURL fileURLWithPath:[trashDirPath stringByAppendingPathComponent:subPath]]]; // } // [self syncOperationStarted]; // [[NSWorkspace sharedWorkspace] recycleURLs:subURLs completionHandler:^(NSDictionary *newURLs, NSError *error) { // if (error) { // [PithosUtilities fileActionFailedAlertWithTitle:@"Move to Trash Error" // message:@"Cannot move files to Trash" // error:error]; // } // [self syncOperationFinishedWithSuccess:YES]; // }]; for (NSString *subPath in subPaths) { NSString *subFilePath = [trashDirPath stringByAppendingPathComponent:subPath]; error = nil; if (![fileManager removeItemAtPath:subFilePath error:&error] || error) [PithosUtilities fileActionFailedAlertWithTitle:@"Remove File Error" message:[NSString stringWithFormat:@"Cannot remove file at '%@'", subFilePath] error:error]; } } } } } - (BOOL)moveToTempTrashFile:(NSString *)filePath accountName:(NSString *)accountName pithosContainer:(ASIPithosContainer *)pithosContainer { @autoreleasepool { if (!self.tempTrashDirPath) return NO; NSFileManager *fileManager = [NSFileManager defaultManager]; BOOL isDirectory; BOOL fileExists = [fileManager fileExistsAtPath:filePath isDirectory:&isDirectory]; NSError *error = nil; NSString *containerDirectoryPath = [self dirPathForAccount:accountName container:pithosContainer.name]; NSString *newFilePath = [filePath stringByReplacingOccurrencesOfString:containerDirectoryPath withString:self.tempTrashDirPath]; NSString *newDirPath = [newFilePath stringByDeletingLastPathComponent]; if (fileExists && isDirectory) { NSArray *subPaths = [fileManager subpathsOfDirectoryAtPath:filePath error:&error]; if (error) { [PithosUtilities fileActionFailedAlertWithTitle:@"Directory Contents Error" message:[NSString stringWithFormat:@"Cannot get contents of directory at '%@'", filePath] error:error]; return NO; } if (![fileManager createDirectoryAtPath:newDirPath withIntermediateDirectories:YES attributes:nil error:&error] || error) { [PithosUtilities fileActionFailedAlertWithTitle:@"Create Directory Error" message:[NSString stringWithFormat:@"Cannot create directory at '%@'", newDirPath] error:error]; return NO; } if (![fileManager moveItemAtPath:filePath toPath:newFilePath error:&error] || error) { [PithosUtilities fileActionFailedAlertWithTitle:@"Move File Error" message:[NSString stringWithFormat:@"Cannot move file at '%@' to '%@'", filePath, newFilePath] error:error]; return NO; } PithosLocalObjectState *currentState = [currentLocalObjectStates objectForKey:filePath]; if (currentState) { currentState.filePath = newFilePath; [currentLocalObjectStates setObject:currentState forKey:newFilePath]; [currentLocalObjectStates removeObjectForKey:filePath]; } else { [currentLocalObjectStates setObject:[PithosLocalObjectState localObjectStateWithFile:newFilePath blockHash:pithosContainer.blockHash blockSize:pithosContainer.blockSize] forKey:newFilePath]; } for (NSString *subPath in subPaths) { NSString *subFilePath = [filePath stringByAppendingPathComponent:subPath]; NSString *newSubFilePath = [subFilePath stringByReplacingOccurrencesOfString:containerDirectoryPath withString:self.tempTrashDirPath]; currentState = [currentLocalObjectStates objectForKey:subFilePath]; if (currentState) { currentState.filePath = newSubFilePath; [currentLocalObjectStates setObject:currentState forKey:newSubFilePath]; [currentLocalObjectStates removeObjectForKey:subFilePath]; } else { [currentLocalObjectStates setObject:[PithosLocalObjectState localObjectStateWithFile:newSubFilePath blockHash:pithosContainer.blockHash blockSize:pithosContainer.blockSize] forKey:newSubFilePath]; } } } else if (fileExists && !isDirectory) { if (![fileManager createDirectoryAtPath:newDirPath withIntermediateDirectories:YES attributes:nil error:&error] || error) { [PithosUtilities fileActionFailedAlertWithTitle:@"Create Directory Error" message:[NSString stringWithFormat:@"Cannot create directory at '%@'", newDirPath] error:error]; return NO; } if (![fileManager moveItemAtPath:filePath toPath:newFilePath error:&error] || error) { [PithosUtilities fileActionFailedAlertWithTitle:@"Move File Error" message:[NSString stringWithFormat:@"Cannot move file at '%@' to '%@'", filePath, newFilePath] error:error]; return NO; } PithosLocalObjectState *currentState = [currentLocalObjectStates objectForKey:filePath]; if (currentState) { currentState.filePath = newFilePath; [currentLocalObjectStates setObject:currentState forKey:newFilePath]; [currentLocalObjectStates removeObjectForKey:filePath]; } else { [currentLocalObjectStates setObject:[PithosLocalObjectState localObjectStateWithFile:newFilePath blockHash:pithosContainer.blockHash blockSize:pithosContainer.blockSize] forKey:newFilePath]; } } } return YES; } - (BOOL)findLocalCopyForObjectWithHash:(NSString *)hash forFile:(NSString *)filePath { if ([hash length] != 64) return NO; @autoreleasepool { PithosLocalObjectState *localState; NSFileManager *fileManager = [NSFileManager defaultManager]; BOOL isDirectory; NSError *error = nil; for (NSString *localFilePath in currentLocalObjectStates) { localState = [currentLocalObjectStates objectForKey:localFilePath]; if (!localState.isDirectory && [hash isEqualToString:localState.hash] && [fileManager fileExistsAtPath:localFilePath isDirectory:&isDirectory] && !isDirectory) { if ([localFilePath hasPrefix:directoryPath]) { if (![fileManager copyItemAtPath:localFilePath toPath:filePath error:&error] || error) { [PithosUtilities fileActionFailedAlertWithTitle:@"Copy File Error" message:[NSString stringWithFormat:@"Cannot copy file at '%@' to '%@'", localFilePath, filePath] error:error]; } else { return YES; } } else if (self.tempTrashDirPath && [localFilePath hasPrefix:self.tempTrashDirPath]) { if (![fileManager moveItemAtPath:localFilePath toPath:filePath error:&error] || error) { [PithosUtilities fileActionFailedAlertWithTitle:@"Move File Error" message:[NSString stringWithFormat:@"Cannot move file at '%@' to '%@'", localFilePath, filePath] error:error]; } else { localState.filePath = filePath; [currentLocalObjectStates setObject:localState forKey:filePath]; [currentLocalObjectStates removeObjectForKey:localFilePath]; return YES; } } } } } return NO; } - (void)updateLocalStateWithObject:(ASIPithosObject *)object localFilePath:(NSString *)filePath accountName:(NSString *)accountName pithosContainer:(ASIPithosContainer *)pithosContainer { @autoreleasepool { NSFileManager *fileManager = [NSFileManager defaultManager]; NSError *error; BOOL isDirectory; BOOL fileExists = [fileManager fileExistsAtPath:filePath isDirectory:&isDirectory]; NSString *fileDirectoryPath = [filePath stringByDeletingLastPathComponent]; NSMutableDictionary *containerStoredLocalObjectStates = [[storedLocalObjectStates objectForKey:accountName] objectForKey:pithosContainer.name]; PithosLocalObjectState *storedState = [containerStoredLocalObjectStates objectForKey:object.name]; // Remote updated info NSError *remoteError; BOOL remoteIsDirectory; BOOL remoteObjectExists = [PithosUtilities objectExistsAtPithos:pithos containerName:pithosContainer.name objectName:object.name error:&remoteError isDirectory:&remoteIsDirectory sharingAccount:([accountName isEqualToString:@""] ? nil : accountName)]; if (!object || !object.objectHash) { // Delete local object if (![accountName isEqualToString:@""]) // If "shared with me" skip return; if (remoteObjectExists) { // Remote object created in the meantime, just mark the sync cycle as incomplete, but do delete the local object syncIncomplete = YES; } DLog(@"Sync::delete local object: %@", filePath); if (!fileExists || [self moveToTempTrashFile:filePath accountName:accountName pithosContainer:pithosContainer]) { dispatch_async(dispatch_get_main_queue(), ^{ [activityFacility startAndEndActivityWithType:PithosActivityOther message:[NSString stringWithFormat:@"Sync: Deleting '%@/%@' locally (finished)", pithosContainer.name, object.name] pithosAccount:pithosAccount]; }); [containerStoredLocalObjectStates removeObjectForKey:object.name]; [self saveLocalState]; } } else if ([PithosUtilities isContentTypeDirectory:object.contentType]) { // Create local directory object if (!remoteObjectExists || !remoteIsDirectory) { // Remote directory object deleted or changed to a file object in the meantime, mark the sync cycle as incomplete and skip syncIncomplete = YES; return; } DLog(@"Sync::create local directory object: %@", filePath); BOOL directoryCreated = NO; if (!fileExists || (!isDirectory && [self moveToTempTrashFile:filePath accountName:accountName pithosContainer:pithosContainer])) { DLog(@"Sync::local directory object doesn't exist: %@", filePath); error = nil; if (![fileManager createDirectoryAtPath:filePath withIntermediateDirectories:YES attributes:nil error:&error] || error) { [PithosUtilities fileActionFailedAlertWithTitle:@"Create Directory Error" message:[NSString stringWithFormat:@"Cannot create directory at '%@'", filePath] error:error]; } else { directoryCreated = YES; storedState.filePath = filePath; storedState.isDirectory = YES; [self saveLocalState]; } } else { DLog(@"Sync::local directory object exists: %@", filePath); directoryCreated = YES; } if (directoryCreated) dispatch_async(dispatch_get_main_queue(), ^{ [activityFacility startAndEndActivityWithType:PithosActivityOther message:[NSString stringWithFormat:@"Sync: Creating directory '%@/%@' locally (finished)", [self relativeDirPathForAccount:accountName container:pithosContainer.name], object.name] pithosAccount:pithosAccount]; }); } else if (object.bytes == 0) { // Create local object with zero length if (!remoteObjectExists || remoteIsDirectory) { // Remote file object deleted or changed to a directory object in the meantime, mark the sync cycle as incomplete and skip syncIncomplete = YES; return; } DLog(@"Sync::create local zero length object: %@", filePath); BOOL fileCreated = NO; if (!fileExists || ((isDirectory || [PithosUtilities bytesOfFile:filePath]) && [self moveToTempTrashFile:filePath accountName:accountName pithosContainer:pithosContainer])) { DLog(@"Sync::local zero length object doesn't exist: %@", filePath); // Create directory of the file, if it doesn't exist error = nil; if (![PithosUtilities safeCreateDirectory:fileDirectoryPath error:&error]) { dispatch_async(dispatch_get_main_queue(), ^{ [PithosUtilities fileActionFailedAlertWithTitle:@"Create Directory Error" message:[NSString stringWithFormat:@"Cannot create directory at '%@'", fileDirectoryPath] error:error]; }); } error = nil; if (![fileManager createFileAtPath:filePath contents:nil attributes:nil]) { [PithosUtilities fileActionFailedAlertWithTitle:@"Create File Error" message:[NSString stringWithFormat:@"Cannot create file at '%@'", filePath] error:error]; } else { fileCreated = YES; storedState.filePath = filePath; storedState.hash = object.objectHash; storedState.tmpFilePath = nil; [self saveLocalState]; } } else { DLog(@"Sync::local zero length object exists: %@", filePath); fileCreated = YES; } if (fileCreated) dispatch_async(dispatch_get_main_queue(), ^{ [activityFacility startAndEndActivityWithType:PithosActivityOther message:[NSString stringWithFormat:@"Sync: Downloading '%@/%@' (100%%)", [self relativeDirPathForAccount:accountName container:pithosContainer.name], object.name] pithosAccount:pithosAccount]; }); } else if (storedState.tmpFilePath == nil) { // Create new local object if (!remoteObjectExists || remoteIsDirectory) { // Remote file object deleted or changed to a directory object in the meantime, mark the sync cycle as incomplete and skip syncIncomplete = YES; return; } // Create directory of the file, if it doesn't exist error = nil; if (![PithosUtilities safeCreateDirectory:fileDirectoryPath error:&error]) { [PithosUtilities fileActionFailedAlertWithTitle:@"Create Directory Error" message:[NSString stringWithFormat:@"Cannot create directory at '%@'", fileDirectoryPath] error:error]; } // Check first if a local copy exists if ([self findLocalCopyForObjectWithHash:object.objectHash forFile:filePath]) { storedState.filePath = filePath; storedState.hash = object.objectHash; [self saveLocalState]; dispatch_async(dispatch_get_main_queue(), ^{ [activityFacility startAndEndActivityWithType:PithosActivityOther message:[NSString stringWithFormat:@"Sync: Downloading '%@/%@' (100%%)", [self relativeDirPathForAccount:accountName container:pithosContainer.name], object.name] pithosAccount:pithosAccount]; }); } else { [self syncOperationStarted]; __block ASIPithosObjectRequest *objectRequest = [PithosUtilities objectBlockDataRequestWithPithos:pithos containerName:pithosContainer.name object:object blockIndex:0 blockSize:pithosContainer.blockSize]; if (![accountName isEqualToString:@""]) [objectRequest setRequestUserFromDefaultTo:accountName withPithos:pithos]; objectRequest.delegate = self; objectRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:); objectRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:); NSString *messagePrefix = [NSString stringWithFormat:@"Sync: Downloading '%@/%@'", [self relativeDirPathForAccount:accountName container:pithosContainer.name], object.name]; PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityDownload message:[messagePrefix stringByAppendingString:@" (0%%)"] totalBytes:object.bytes currentBytes:0 pithosAccount:pithosAccount]; dispatch_async(dispatch_get_main_queue(), ^{ [activityFacility updateActivity:activity withMessage:activity.message]; }); objectRequest.userInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys: accountName, @"accountName", pithosContainer, @"pithosContainer", object, @"pithosObject", messagePrefix, @"messagePrefix", [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, (NSUInteger)ceil((object.bytes +0.0)/(pithosContainer.blockSize + 0.0)))], @"missingBlocks", [NSNumber numberWithUnsignedInteger:0], @"missingBlockIndex", filePath, @"filePath", activity, @"activity", [messagePrefix stringByAppendingString:@" (stopped)"], @"stoppedActivityMessage", [messagePrefix stringByAppendingString:@" (failed)"], @"failedActivityMessage", [messagePrefix stringByAppendingString:@" (100%%)"], @"finishedActivityMessage", [NSNumber numberWithInteger:NSOperationQueuePriorityNormal], @"priority", [NSNumber numberWithUnsignedInteger:10], @"retries", NSStringFromSelector(@selector(downloadObjectBlockFinished:)), @"didFinishSelector", NSStringFromSelector(@selector(requestFailed:)), @"didFailSelector", nil]; [objectRequest setBytesReceivedBlock:^(unsigned long long size, unsigned long long total){ [activityFacility updateActivity:activity withMessage:[NSString stringWithFormat:@"%@ (%.0f%%)", messagePrefix, (100*(activity.currentBytes + size + 0.0)/(activity.totalBytes + 0.0))] totalBytes:activity.totalBytes currentBytes:(activity.currentBytes + size)]; }]; [networkQueue addOperation:[PithosUtilities prepareRequest:objectRequest priority:NSOperationQueuePriorityNormal]]; } } else { // Resume local object download if (!remoteObjectExists || remoteIsDirectory) { // Remote file object deleted or changed to a directory object in the meantime, mark the sync cycle as incomplete and skip syncIncomplete = YES; return; } // Create directory of the file, if it doesn't exist error = nil; if (![PithosUtilities safeCreateDirectory:fileDirectoryPath error:&error]) { [PithosUtilities fileActionFailedAlertWithTitle:@"Create Directory Error" message:[NSString stringWithFormat:@"Cannot create directory at '%@'", fileDirectoryPath] error:error]; } // Check first if a local copy exists if ([self findLocalCopyForObjectWithHash:object.objectHash forFile:filePath]) { storedState.filePath = filePath; storedState.hash = object.objectHash; // Delete incomplete temp download storedState.tmpFilePath = nil; [self saveLocalState]; dispatch_async(dispatch_get_main_queue(), ^{ [activityFacility startAndEndActivityWithType:PithosActivityOther message:[NSString stringWithFormat:@"Sync: Downloading '%@/%@' (100%%)", [self relativeDirPathForAccount:accountName container:pithosContainer.name], object.name] pithosAccount:pithosAccount]; }); } else { [self syncOperationStarted]; ASIPithosObjectRequest *objectRequest = [ASIPithosObjectRequest objectHashmapRequestWithPithos:pithos containerName:pithosContainer.name objectName:object.name]; if (![accountName isEqualToString:@""]) [objectRequest setRequestUserFromDefaultTo:accountName withPithos:pithos]; objectRequest.delegate = self; objectRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:); objectRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:); NSString *messagePrefix = [NSString stringWithFormat:@"Sync: Downloading '%@/%@'", [self relativeDirPathForAccount:accountName container:pithosContainer.name], object.name]; PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityDownload message:[messagePrefix stringByAppendingString:@" (0%%)"] totalBytes:object.bytes currentBytes:0 pithosAccount:pithosAccount]; dispatch_async(dispatch_get_main_queue(), ^{ [activityFacility updateActivity:activity withMessage:activity.message]; }); objectRequest.userInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys: accountName, @"accountName", pithosContainer, @"pithosContainer", object, @"pithosObject", messagePrefix, @"messagePrefix", filePath, @"filePath", activity, @"activity", [messagePrefix stringByAppendingString:@" (stopped)"], @"stoppedActivityMessage", [messagePrefix stringByAppendingString:@" (failed)"], @"failedActivityMessage", [messagePrefix stringByAppendingString:@" (100%%)"], @"finishedActivityMessage", [NSNumber numberWithInteger:NSOperationQueuePriorityNormal], @"priority", [NSNumber numberWithUnsignedInteger:10], @"retries", NSStringFromSelector(@selector(downloadObjectHashMapFinished:)), @"didFinishSelector", NSStringFromSelector(@selector(requestFailed:)), @"didFailSelector", nil]; [networkQueue addOperation:[PithosUtilities prepareRequest:objectRequest priority:NSOperationQueuePriorityNormal]]; } } } } -(void)updateServerStateWithCurrentState:(PithosLocalObjectState *)currentState object:(ASIPithosObject *)object localFilePath:(NSString *)filePath accountName:(NSString *)accountName pithosContainer:(ASIPithosContainer *)pithosContainer { @autoreleasepool { [self syncOperationStarted]; NSFileManager *fileManager = [NSFileManager defaultManager]; BOOL isDirectory; BOOL fileExists = [fileManager fileExistsAtPath:filePath isDirectory:&isDirectory]; if (currentState.isDirectory) { // Create remote directory object if (![accountName isEqualToString:@""]) { if (!object.allowedTo) { NSMutableDictionary *containerRemoteObjects = [[remoteObjects objectForKey:accountName] objectForKey:pithosContainer.name]; NSString *objectAncestorName = [object.name stringByDeletingLastPathComponent]; while ([objectAncestorName length] && !object.allowedTo) { object.allowedTo = [[containerRemoteObjects objectForKey:objectAncestorName] allowedTo]; objectAncestorName = [objectAncestorName stringByDeletingLastPathComponent]; } } if (![object.allowedTo isEqualToString:@"write"]) { // If read-only "shared with me" skip [self syncOperationFinishedWithSuccess:YES]; return; } } if (!fileExists || !isDirectory) { // Local directory object deleted or changed to a file in the meantime, mark the sync cycle as incomplete and skip [self syncOperationFinishedWithSuccess:NO]; return; } ASIPithosObjectRequest *objectRequest = [ASIPithosObjectRequest writeObjectDataRequestWithPithos:pithos containerName:pithosContainer.name objectName:object.name eTag:nil contentType:@"application/directory" contentEncoding:nil contentDisposition:nil manifest:nil sharing:nil isPublic:ASIPithosObjectRequestPublicIgnore metadata:nil data:[NSData data]]; if (![accountName isEqualToString:@""]) [objectRequest setRequestUserFromDefaultTo:accountName withPithos:pithos]; objectRequest.delegate = self; objectRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:); objectRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:); NSString *messagePrefix = [NSString stringWithFormat:@"Sync: Creating directory '%@/%@'", [self relativeDirPathForAccount:accountName container:pithosContainer.name], object.name]; PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityCreateDirectory message:messagePrefix pithosAccount:pithosAccount]; dispatch_async(dispatch_get_main_queue(), ^{ [activityFacility updateActivity:activity withMessage:activity.message]; }); objectRequest.userInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys: accountName, @"accountName", pithosContainer, @"pithosContainer", object, @"pithosObject", messagePrefix, @"messagePrefix", activity, @"activity", [messagePrefix stringByAppendingString:@" (stopped)"], @"stoppedActivityMessage", [messagePrefix stringByAppendingString:@" (failed)"], @"failedActivityMessage", [messagePrefix stringByAppendingString:@" (finished)"], @"finishedActivityMessage", [NSNumber numberWithInteger:NSOperationQueuePriorityHigh], @"priority", [NSNumber numberWithUnsignedInteger:10], @"retries", NSStringFromSelector(@selector(uploadDirectoryObjectFinished:)), @"didFinishSelector", NSStringFromSelector(@selector(requestFailed:)), @"didFailSelector", nil]; [networkQueue addOperation:[PithosUtilities prepareRequest:objectRequest priority:NSOperationQueuePriorityHigh]]; } else if (![currentState exists]) { // Delete remote object if (![accountName isEqualToString:@""]) { // If "shared with me" skip [self syncOperationFinishedWithSuccess:YES]; return; } if (fileExists) { // Local object created in the meantime, just mark the sync cycle as incomplete, but do delete the server object syncIncomplete = YES; } if ([pithosContainer.name isEqualToString:@"trash"]) { // Delete ASIPithosObjectRequest *objectRequest = [ASIPithosObjectRequest deleteObjectRequestWithPithos:pithos containerName:pithosContainer.name objectName:object.name]; objectRequest.delegate = self; objectRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:); objectRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:); NSString *messagePrefix = [NSString stringWithFormat:@"Sync: Deleting '%@/%@'", [self relativeDirPathForAccount:accountName container:pithosContainer.name], object.name]; PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityDelete message:messagePrefix pithosAccount:pithosAccount]; dispatch_async(dispatch_get_main_queue(), ^{ [activityFacility updateActivity:activity withMessage:activity.message]; }); objectRequest.userInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys: accountName, @"accountName", pithosContainer, @"pithosContainer", object, @"pithosObject", messagePrefix, @"messagePrefix", activity, @"activity", [messagePrefix stringByAppendingString:@" (stopped)"], @"stoppedActivityMessage", [messagePrefix stringByAppendingString:@" (failed)"], @"failedActivityMessage", [messagePrefix stringByAppendingString:@" (finished)"], @"finishedActivityMessage", [NSNumber numberWithInteger:NSOperationQueuePriorityHigh], @"priority", [NSNumber numberWithUnsignedInteger:10], @"retries", NSStringFromSelector(@selector(deleteObjectFinished:)), @"didFinishSelector", NSStringFromSelector(@selector(requestFailed:)), @"didFailSelector", nil]; [networkQueue addOperation:[PithosUtilities prepareRequest:objectRequest priority:NSOperationQueuePriorityHigh]]; } else { // Move to container trash NSString *safeName; if ([PithosUtilities isContentTypeDirectory:object.contentType]) safeName = [PithosUtilities safeSubdirNameForPithos:pithos containerName:@"trash" subdirName:object.name]; else safeName = [PithosUtilities safeObjectNameForPithos:pithos containerName:@"trash" objectName:object.name]; if (safeName) { ASIPithosObjectRequest *objectRequest = [PithosUtilities moveObjectRequestWithPithos:pithos containerName:pithosContainer.name objectName:object.name destinationContainerName:@"trash" destinationObjectName:safeName checkIfExists:NO]; if (objectRequest) { objectRequest.delegate = self; objectRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:); objectRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:); NSString *messagePrefix = [NSString stringWithFormat:@"Sync: Moving to trash '%@/%@'", [self relativeDirPathForAccount:accountName container:pithosContainer.name], object.name]; PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityDelete message:messagePrefix pithosAccount:pithosAccount]; dispatch_async(dispatch_get_main_queue(), ^{ [activityFacility updateActivity:activity withMessage:activity.message]; }); objectRequest.userInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys: accountName, @"accountName", pithosContainer, @"pithosContainer", object, @"pithosObject", messagePrefix, @"messagePrefix", activity, @"activity", [messagePrefix stringByAppendingString:@" (stopped)"], @"stoppedActivityMessage", [messagePrefix stringByAppendingString:@" (failed)"], @"failedActivityMessage", [messagePrefix stringByAppendingString:@" (finished)"], @"finishedActivityMessage", [NSNumber numberWithInteger:NSOperationQueuePriorityHigh], @"priority", [NSNumber numberWithUnsignedInteger:10], @"retries", NSStringFromSelector(@selector(moveObjectToTrashFinished:)), @"didFinishSelector", NSStringFromSelector(@selector(requestFailed:)), @"didFailSelector", nil]; [networkQueue addOperation:[PithosUtilities prepareRequest:objectRequest priority:NSOperationQueuePriorityHigh]]; } else { [self syncOperationFinishedWithSuccess:NO]; } } else { [self syncOperationFinishedWithSuccess:NO]; } } } else { // Upload file to remote object if (![accountName isEqualToString:@""]) { if (!object.allowedTo) { NSMutableDictionary *containerRemoteObjects = [[remoteObjects objectForKey:accountName] objectForKey:pithosContainer.name]; NSString *objectAncestorName = [object.name stringByDeletingLastPathComponent]; while ([objectAncestorName length] && !object.allowedTo) { object.allowedTo = [[containerRemoteObjects objectForKey:objectAncestorName] allowedTo]; objectAncestorName = [objectAncestorName stringByDeletingLastPathComponent]; } } if (![object.allowedTo isEqualToString:@"write"]) { // If read-only "shared with me" skip [self syncOperationFinishedWithSuccess:YES]; return; } } if (!fileExists || isDirectory) { // Local file object deleted or changed to a directory in the meantime, mark the sync cycle as incomplete and skip [self syncOperationFinishedWithSuccess:NO]; return; } NSError *error = nil; object.contentType = [PithosUtilities contentTypeOfFile:filePath error:&error]; if (object.contentType == nil) object.contentType = @"application/octet-stream"; #if DEBUG_PITHOS if (error) DLog(@"contentType detection error: %@", error); #endif NSArray *hashes = nil; ASIPithosObjectRequest *objectRequest = [PithosUtilities writeObjectDataRequestWithPithos:pithos containerName:pithosContainer.name objectName:object.name contentType:object.contentType blockSize:pithosContainer.blockSize blockHash:pithosContainer.blockHash forFile:filePath checkIfExists:NO hashes:&hashes sharingAccount:([accountName isEqualToString:@""] ? nil : accountName)]; if (objectRequest) { objectRequest.delegate = self; objectRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:); objectRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:); NSString *messagePrefix = [NSString stringWithFormat:@"Sync: Uploading '%@/%@'", [self relativeDirPathForAccount:accountName container:pithosContainer.name], object.name]; PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityUpload message:[messagePrefix stringByAppendingString:@" (0%%)"] totalBytes:[[objectRequest.userInfo objectForKey:@"bytes"] unsignedIntegerValue] currentBytes:0 pithosAccount:pithosAccount]; dispatch_async(dispatch_get_main_queue(), ^{ [activityFacility updateActivity:activity withMessage:activity.message]; }); [(NSMutableDictionary *)objectRequest.userInfo addEntriesFromDictionary: [NSDictionary dictionaryWithObjectsAndKeys: accountName, @"accountName", pithosContainer, @"pithosContainer", object, @"pithosObject", messagePrefix, @"messagePrefix", filePath, @"filePath", hashes, @"hashes", [NSNumber numberWithUnsignedInteger:10], @"iteration", activity, @"activity", [messagePrefix stringByAppendingString:@" (stopped)"], @"stoppedActivityMessage", [messagePrefix stringByAppendingString:@" (failed)"], @"failedActivityMessage", [messagePrefix stringByAppendingString:@" (100%%)"], @"finishedActivityMessage", [NSNumber numberWithInteger:NSOperationQueuePriorityNormal], @"priority", [NSNumber numberWithUnsignedInteger:10], @"retries", NSStringFromSelector(@selector(uploadObjectUsingHashMapFinished:)), @"didFinishSelector", NSStringFromSelector(@selector(requestFailed:)), @"didFailSelector", nil]]; [networkQueue addOperation:[PithosUtilities prepareRequest:objectRequest priority:NSOperationQueuePriorityNormal]]; } else { [self syncOperationFinishedWithSuccess:NO]; } } } } #pragma mark - #pragma mark ASIHTTPRequestDelegate - (void)performRequestFinishedDelegateInBackground:(ASIPithosRequest *)request { // Add an operation to the callbackQueue with a completionBlock for the case of cancellation NSInvocationOperation *operation = [[NSInvocationOperation alloc] initWithTarget:self selector:NSSelectorFromString([request.userInfo objectForKey:@"didFinishSelector"]) object:request]; operation.completionBlock = ^{ @autoreleasepool { if ([[request.userInfo objectForKey:@"operation"] isCancelled]) { dispatch_async(dispatch_get_main_queue(), ^{ [activityFacility endActivity:[request.userInfo objectForKey:@"activity"] withMessage:[request.userInfo objectForKey:@"stoppedActivityMessage"]]; }); [self syncOperationFinishedWithSuccess:NO]; } } }; [(NSMutableDictionary *)request.userInfo setObject:operation forKey:@"operation"]; [callbackQueue addOperation:operation]; } - (void)performRequestFailedDelegateInBackground:(ASIPithosRequest *)request { if (request.isCancelled) { // Request has been cancelled // The callbackQueue might be suspended so we call directly the callback method, since it does minimal work anyway [self performSelector:NSSelectorFromString([request.userInfo objectForKey:@"didFailSelector"]) withObject:request]; } else { // Add an operation to the callbackQueue with a completionBlock for the case of cancellation NSInvocationOperation *operation = [[NSInvocationOperation alloc] initWithTarget:self selector:NSSelectorFromString([request.userInfo objectForKey:@"didFailSelector"]) object:request]; operation.completionBlock = ^{ @autoreleasepool { if ([[request.userInfo objectForKey:@"operation"] isCancelled]) { dispatch_async(dispatch_get_main_queue(), ^{ [activityFacility endActivity:[request.userInfo objectForKey:@"activity"] withMessage:[request.userInfo objectForKey:@"stoppedActivityMessage"]]; }); [self syncOperationFinishedWithSuccess:NO]; } } }; [(NSMutableDictionary *)request.userInfo setObject:operation forKey:@"operation"]; [callbackQueue addOperation:operation]; } } - (void)listRequestFinished:(ASIPithosContainerRequest *)containerRequest { @autoreleasepool { NSOperation *operation = [containerRequest.userInfo objectForKey:@"operation"]; DLog(@"Sync::list request finished: %@", containerRequest.url); if (operation.isCancelled) { [self listRequestFailed:containerRequest]; } else if ((containerRequest.responseStatusCode == 200) || (containerRequest.responseStatusCode == 304) || (containerRequest.responseStatusCode == 403)) { NSString *accountName = [accountsNames objectAtIndex:accountsIndex]; ASIPithosContainer *pithosContainer = [[accountsPithosContainers objectForKey:accountName] objectAtIndex:containersIndex]; if (containerRequest.responseStatusCode == 200) { NSArray *someObjects = [containerRequest objects]; if (objects == nil) { objects = [[NSMutableArray alloc] initWithArray:someObjects]; } else { [objects addObjectsFromArray:someObjects]; } if ([someObjects count] < 10000) { pithosContainer.blockHash = [containerRequest blockHash]; pithosContainer.blockSize = [containerRequest blockSize]; pithosContainer.lastModified = [containerRequest lastModified]; NSMutableDictionary *containerRemoteObjects = [NSMutableDictionary dictionaryWithCapacity:[objects count]]; for (ASIPithosObject *object in objects) { [containerRemoteObjects setObject:object forKey:object.name]; } [[remoteObjects objectForKey:accountName] setObject:containerRemoteObjects forKey:pithosContainer.name]; objects = nil; } else { // Do an additional request to fetch more objects ASIPithosContainerRequest *newContainerRequest = [ASIPithosContainerRequest listObjectsRequestWithPithos:pithos containerName:pithosContainer.name limit:0 marker:[[someObjects lastObject] name] prefix:nil delimiter:nil path:nil meta:nil shared:NO until:nil ifModifiedSince:pithosContainer.lastModified]; if (![accountName isEqualToString:@""]) [newContainerRequest setRequestUserFromDefaultTo:accountName withPithos:pithos]; newContainerRequest.delegate = self; newContainerRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:); newContainerRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:); newContainerRequest.userInfo = containerRequest.userInfo; [(NSMutableDictionary *)newContainerRequest.userInfo setObject:[NSNumber numberWithUnsignedInteger:10] forKey:@"retries"]; [networkQueue addOperation:[PithosUtilities prepareRequest:newContainerRequest priority:[[newContainerRequest.userInfo objectForKey:@"priority"] integerValue]]]; return; } } else if (containerRequest.responseStatusCode == 304) { NSMutableDictionary *containerRemoteObjects = [[previousRemoteObjects objectForKey:accountName] objectForKey:pithosContainer.name]; if (containerRemoteObjects) [[remoteObjects objectForKey:accountName] setObject:containerRemoteObjects forKey:pithosContainer.name]; } else if (containerRequest.responseStatusCode == 403) { [[remoteObjects objectForKey:accountName] setObject:[NSMutableDictionary dictionary] forKey:pithosContainer.name]; } containersIndex++; if (containersIndex == [[accountsPithosContainers objectForKey:accountName] count]) { accountsIndex++; containersIndex = 0; } if (accountsIndex < accountsCount) { accountName = [accountsNames objectAtIndex:accountsIndex]; pithosContainer = [[accountsPithosContainers objectForKey:accountName] objectAtIndex:containersIndex]; // Do a request for the next container ASIPithosContainerRequest *newContainerRequest = [ASIPithosContainerRequest listObjectsRequestWithPithos:pithos containerName:pithosContainer.name limit:0 marker:nil prefix:nil delimiter:nil path:nil meta:nil shared:NO until:nil ifModifiedSince:pithosContainer.lastModified]; if (![accountName isEqualToString:@""]) [newContainerRequest setRequestUserFromDefaultTo:accountName withPithos:pithos]; newContainerRequest.delegate = self; newContainerRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:); newContainerRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:); newContainerRequest.userInfo = containerRequest.userInfo; [(NSMutableDictionary *)newContainerRequest.userInfo setObject:[NSNumber numberWithUnsignedInteger:10] forKey:@"retries"]; [networkQueue addOperation:[PithosUtilities prepareRequest:newContainerRequest priority:[[newContainerRequest.userInfo objectForKey:@"priority"] integerValue]]]; return; } self.previousRemoteObjects = remoteObjects; // remoteObjects contains all remote objects for the legal containers, without enforcing directory exclusions if (operation.isCancelled) { [self listRequestFailed:containerRequest]; return; } dispatch_async(dispatch_get_main_queue(), ^{ [activityFacility endActivity:[containerRequest.userInfo objectForKey:@"activity"] withMessage:[containerRequest.userInfo objectForKey:@"finishedActivityMessage"]]; }); NSFileManager *fileManager = [NSFileManager defaultManager]; // Compute current state of legal existing local objects // and add an empty stored state for legal new local objects since last sync self.currentLocalObjectStates = [NSMutableDictionary dictionary]; for (NSString *accountName in accountsNames) { for (ASIPithosContainer *pithosContainer in [accountsPithosContainers objectForKey:accountName]) { NSString *containerDirectoryPath = [self dirPathForAccount:accountName container:pithosContainer.name]; NSSet *containerExcludedDirectories = [[accountsDictionary objectForKey:accountName] objectForKey:pithosContainer.name]; BOOL containerExcludeRootFiles = [containerExcludedDirectories containsObject:@""]; NSMutableDictionary *containerStoredLocalObjectStates = [[storedLocalObjectStates objectForKey:accountName] objectForKey:pithosContainer.name]; NSDirectoryEnumerator *dirEnumerator = [fileManager enumeratorAtPath:containerDirectoryPath]; for (__strong NSString *objectName in dirEnumerator) { objectName = [objectName precomposedStringWithCanonicalMapping]; if (operation.isCancelled) { operation.completionBlock = nil; [self saveLocalState]; [self syncOperationFinishedWithSuccess:NO]; return; } NSString *filePath = [containerDirectoryPath stringByAppendingPathComponent:objectName]; NSDictionary *attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:nil]; BOOL isDirectory; BOOL fileExists = [fileManager fileExistsAtPath:filePath isDirectory:&isDirectory]; if ([[attributes fileType] isEqualToString:NSFileTypeSymbolicLink]) { [PithosUtilities fileActionFailedAlertWithTitle:@"Sync Error" message:[NSString stringWithFormat:@"Sync directory at '%@' contains symbolic links. Remove them and re-activate sync for account '%@'.", containerDirectoryPath, pithosAccount.name] error:nil]; pithosAccount.syncActive = NO; return; } else if (fileExists) { if (skipHidden && [[objectName lastPathComponent] hasPrefix:@"."]) { // Skip hidden directory and its descendants, or hidden file if (isDirectory) [dirEnumerator skipDescendants]; // Remove stored state if any [containerStoredLocalObjectStates removeObjectForKey:objectName]; continue; } else if ([[objectName pathComponents] count] == 1) { if ([containerExcludedDirectories containsObject:[objectName lowercaseString]]) { // Skip excluded directory and its descendants, or root file with same name if (isDirectory) [dirEnumerator skipDescendants]; // Remove stored state if any [containerStoredLocalObjectStates removeObjectForKey:objectName]; continue; } else if (!isDirectory && containerExcludeRootFiles) { // Skip excluded root file // Remove stored state if any [containerStoredLocalObjectStates removeObjectForKey:objectName]; continue; } } // Include local object PithosLocalObjectState *storedLocalObjectState = [containerStoredLocalObjectStates objectForKey:objectName]; if (!storedLocalObjectState || [storedLocalObjectState isModified]) { // New or modified existing local object, compute current state if (!storedLocalObjectState) // For new local object, also create empty stored state [containerStoredLocalObjectStates setObject:[PithosLocalObjectState localObjectState] forKey:objectName]; [currentLocalObjectStates setObject:[PithosLocalObjectState localObjectStateWithFile:filePath blockHash:pithosContainer.blockHash blockSize:pithosContainer.blockSize] forKey:filePath]; } else { // Local object hasn't changed, set stored state also to current [currentLocalObjectStates setObject:storedLocalObjectState forKey:filePath]; } } } [self saveLocalState]; } } if (operation.isCancelled) { operation.completionBlock = nil; [self syncOperationFinishedWithSuccess:NO]; return; } // Add an empty stored state for legal new remote objects since last sync for (NSString *accountName in accountsNames) { for (ASIPithosContainer *pithosContainer in [accountsPithosContainers objectForKey:accountName]) { NSSet *containerExcludedDirectories = [[accountsDictionary objectForKey:accountName] objectForKey:pithosContainer.name]; BOOL containerExcludeRootFiles = [containerExcludedDirectories containsObject:@""]; NSMutableDictionary *containerStoredLocalObjectStates = [[storedLocalObjectStates objectForKey:accountName] objectForKey:pithosContainer.name]; NSMutableDictionary *containerRemoteObjects = [[remoteObjects objectForKey:accountName] objectForKey:pithosContainer.name]; for (NSString *objectName in containerRemoteObjects) { if (operation.isCancelled) { operation.completionBlock = nil; [self saveLocalState]; [self syncOperationFinishedWithSuccess:NO]; return; } ASIPithosObject *object = [containerRemoteObjects objectForKey:objectName]; NSString *localObjectName; if ([object.name hasSuffix:@"/"]) localObjectName = [NSString stringWithFormat:@"%@:", [object.name substringToIndex:([object.name length] -1)]]; else localObjectName = [NSString stringWithString:object.name]; NSArray *pathComponents = [localObjectName pathComponents]; if (skipHidden) { BOOL skipObject = NO; for (NSString *pathComponent in pathComponents) { if ([pathComponent hasPrefix:@"."]) { // Skip hidden directory and its descendants, or hidden file // Remove stored state if any [containerStoredLocalObjectStates removeObjectForKey:objectName]; skipObject = YES; break; } } if (skipObject) continue; } if ([containerExcludedDirectories containsObject:[[pathComponents objectAtIndex:0] lowercaseString]]) { // Skip excluded directory object and its descendants, or root file object with same name // Remove stored state if any [containerStoredLocalObjectStates removeObjectForKey:object.name]; continue; } else if (containerExcludeRootFiles && ([pathComponents count] == 1) && ![PithosUtilities isContentTypeDirectory:object.contentType]) { // Skip root file object // Remove stored state if any [containerStoredLocalObjectStates removeObjectForKey:object.name]; continue; } if (![containerStoredLocalObjectStates objectForKey:object.name]) [containerStoredLocalObjectStates setObject:[PithosLocalObjectState localObjectState] forKey:object.name]; } [self saveLocalState]; } } if (operation.isCancelled) { operation.completionBlock = nil; [self syncOperationFinishedWithSuccess:NO]; return; } // For each stored state compare with current and remote state // Stored states of local objects that have been deleted, // haven't been checked for legality (only existing local remote objects) // These should be identified and skipped for (NSString *accountName in accountsNames) { for (ASIPithosContainer *pithosContainer in [accountsPithosContainers objectForKey:accountName]) { NSString *containerDirectoryPath = [self dirPathForAccount:accountName container:pithosContainer.name]; NSSet *containerExcludedDirectories = [[accountsDictionary objectForKey:accountName] objectForKey:pithosContainer.name]; BOOL containerExcludeRootFiles = [containerExcludedDirectories containsObject:@""]; NSMutableDictionary *containerStoredLocalObjectStates = [[storedLocalObjectStates objectForKey:accountName] objectForKey:pithosContainer.name]; NSMutableDictionary *containerRemoteObjects = [[remoteObjects objectForKey:accountName] objectForKey:pithosContainer.name]; for (NSString *objectName in [[containerStoredLocalObjectStates allKeys] sortedArrayUsingSelector:@selector(compare:)]) { if (operation.isCancelled) { operation.completionBlock = nil; [self syncOperationFinishedWithSuccess:NO]; return; } NSString *filePath = [containerDirectoryPath stringByAppendingPathComponent:objectName]; if ([objectName hasSuffix:@"/"]) filePath = [filePath stringByAppendingString:@":"]; ASIPithosObject *object = [ASIPithosObject object]; object.name = objectName; DLog(@"Sync::object name: %@", object.name); PithosLocalObjectState *storedLocalObjectState = [containerStoredLocalObjectStates objectForKey:object.name]; PithosLocalObjectState *currentLocalObjectState = [currentLocalObjectStates objectForKey:filePath]; if (!currentLocalObjectState) { // The stored state corresponds to a remote or deleted local object, that's why there is no current state // In the latter case it must be checked for legality, which can be determined by its stored state // If it existed locally, but was deleted, state.exists is true, // else if the stored state is an empty state that was created due to the server object, state.exists is false if (storedLocalObjectState.exists) { NSString *localObjectName; if ([object.name hasSuffix:@"/"]) localObjectName = [NSString stringWithFormat:@"%@:", [object.name substringToIndex:([object.name length] -1)]]; else localObjectName = [NSString stringWithString:object.name]; NSArray *pathComponents = [localObjectName pathComponents]; if (skipHidden) { BOOL skipObject = NO; for (NSString *pathComponent in pathComponents) { if ([pathComponent hasPrefix:@"."]) { // Skip hidden directory and its descendants, or hidden file // Remove stored state if any [containerStoredLocalObjectStates removeObjectForKey:objectName]; [self saveLocalState]; skipObject = YES; break; } } if (skipObject) continue; } if ([containerExcludedDirectories containsObject:[[pathComponents objectAtIndex:0] lowercaseString]]) { // Skip excluded directory object and its descendants, or root file object with same name // Remove stored state [containerStoredLocalObjectStates removeObjectForKey:object.name]; [self saveLocalState]; continue; } else if (containerExcludeRootFiles && ([pathComponents count] == 1) && !storedLocalObjectState.isDirectory) { // Skip root file object // Remove stored state [containerStoredLocalObjectStates removeObjectForKey:object.name]; [self saveLocalState]; continue; } } // There is also the off case that a local object has been created in the meantime // This call works in any case, existent or non-existent local object currentLocalObjectState = [PithosLocalObjectState localObjectStateWithFile:filePath blockHash:pithosContainer.blockHash blockSize:pithosContainer.blockSize]; } PithosLocalObjectState *remoteObjectState = [PithosLocalObjectState localObjectState]; ASIPithosObject *remoteObject = [containerRemoteObjects objectForKey:object.name]; if (remoteObject) { if ([PithosUtilities isContentTypeDirectory:remoteObject.contentType]) { remoteObjectState.isDirectory = YES; } else { remoteObjectState.hash = remoteObject.objectHash; } } BOOL localStateHasChanged = ![currentLocalObjectState isEqualToState:storedLocalObjectState]; BOOL serverStateHasChanged = ![remoteObjectState isEqualToState:storedLocalObjectState]; DLog(@"Sync::localStateHasChanged: %d, serverStateHasChanged: %d", localStateHasChanged, serverStateHasChanged); if (!localStateHasChanged) { // Local state hasn't changed if (serverStateHasChanged) { // Server state has changed // Update local state to match that of the server object.bytes = remoteObject.bytes; object.version = remoteObject.version; object.contentType = remoteObject.contentType; object.objectHash = remoteObject.objectHash; [self updateLocalStateWithObject:object localFilePath:filePath accountName:accountName pithosContainer:pithosContainer]; } else if (!remoteObject && ![currentLocalObjectState exists]) { // Server state hasn't changed // If the object doesn't exist neither in the server or locally, it should be removed from the stored local objects [containerStoredLocalObjectStates removeObjectForKey:objectName]; [self saveLocalState]; } } else { // Local state has changed if (!serverStateHasChanged) { // Server state hasn't changed if (currentLocalObjectState.isDirectory) object.contentType = @"application/directory"; else object.objectHash = currentLocalObjectState.hash; [self updateServerStateWithCurrentState:currentLocalObjectState object:object localFilePath:filePath accountName:accountName pithosContainer:pithosContainer]; } else { // Server state has also changed if (remoteObjectState.isDirectory && currentLocalObjectState.isDirectory) { // Both did the same change (directory) storedLocalObjectState.filePath = filePath; storedLocalObjectState.isDirectory = YES; [self saveLocalState]; } else if ([remoteObjectState isEqualToState:currentLocalObjectState]) { // Both did the same change (object edit or delete) if (![remoteObjectState exists]) { [containerStoredLocalObjectStates removeObjectForKey:object.name]; } else { storedLocalObjectState.filePath = filePath; storedLocalObjectState.hash = remoteObjectState.hash; } [self saveLocalState]; } else { // Conflict, we ask the user which change to keep NSString *informativeText; NSString *firstButtonText; NSString *secondButtonText; if (![remoteObjectState exists]) { // Remote object has been deleted informativeText = [NSString stringWithFormat:@"'%@/%@' has been modified locally, while it has been deleted from server.", [self relativeDirPathForAccount:accountName container:pithosContainer.name], object.name ]; firstButtonText = @"Delete local file"; secondButtonText = @"Upload file to server"; } else if (![currentLocalObjectState exists]) { informativeText = [NSString stringWithFormat:@"'%@/%@' has been modified on the server, while it has been deleted locally.", [self relativeDirPathForAccount:accountName container:pithosContainer.name], object.name]; firstButtonText = @"Download file from server"; secondButtonText = @"Delete file on server"; } else { informativeText = [NSString stringWithFormat:@"'%@/%@' has been modified both locally and on the server.", [self relativeDirPathForAccount:accountName container:pithosContainer.name], object.name]; firstButtonText = @"Keep server version"; secondButtonText = @"Keep local version"; } __block NSInteger choice; dispatch_sync(dispatch_get_main_queue(), ^{ NSAlert *alert = [[NSAlert alloc] init]; [alert setMessageText:@"Conflict"]; [alert setInformativeText:informativeText]; [alert addButtonWithTitle:firstButtonText]; [alert addButtonWithTitle:secondButtonText]; [alert addButtonWithTitle:@"Do nothing"]; choice = [alert runModal]; }); if (choice == NSAlertFirstButtonReturn) { object.bytes = remoteObject.bytes; object.version = remoteObject.version; object.contentType = remoteObject.contentType; object.objectHash = remoteObject.objectHash; [self updateLocalStateWithObject:object localFilePath:filePath accountName:accountName pithosContainer:pithosContainer]; } if (choice == NSAlertSecondButtonReturn) { if (currentLocalObjectState.isDirectory) object.contentType = @"application/directory"; else object.objectHash = currentLocalObjectState.hash; [self updateServerStateWithCurrentState:currentLocalObjectState object:object localFilePath:filePath accountName:accountName pithosContainer:pithosContainer]; } } } } } } } [self syncOperationFinishedWithSuccess:YES]; } else { [self listRequestFailed:containerRequest]; } } } - (void)listRequestFailed:(ASIPithosContainerRequest *)containerRequest { @autoreleasepool { NSOperation *operation = [containerRequest.userInfo objectForKey:@"operation"]; if (operation.isCancelled) { objects = nil; return; } if (containerRequest.isCancelled) { dispatch_async(dispatch_get_main_queue(), ^{ [activityFacility endActivity:[containerRequest.userInfo objectForKey:@"activity"] withMessage:[containerRequest.userInfo objectForKey:@"stoppedActivityMessage"]]; }); objects = nil; [self syncOperationFinishedWithSuccess:NO]; return; } // If the server listing fails, the sync should start over, so just retrying is enough NSUInteger retries = [[containerRequest.userInfo objectForKey:@"retries"] unsignedIntegerValue]; if (retries > 0) { ASIPithosContainerRequest *newContainerRequest = (ASIPithosContainerRequest *)[PithosUtilities copyRequest:containerRequest]; [(NSMutableDictionary *)(newContainerRequest.userInfo)setObject:[NSNumber numberWithUnsignedInteger:(--retries)] forKey:@"retries"]; [networkQueue addOperation:[PithosUtilities prepareRequest:newContainerRequest priority:[[newContainerRequest.userInfo objectForKey:@"priority"] integerValue]]]; } else { dispatch_async(dispatch_get_main_queue(), ^{ [activityFacility endActivity:[containerRequest.userInfo objectForKey:@"activity"] withMessage:[containerRequest.userInfo objectForKey:@"failedActivityMessage"]]; }); objects = nil; // Since the server listing failed in all retries, the operation finished and the sync cycle is completeted unsuccesfully [self syncOperationFinishedWithSuccess:NO]; } } } - (void)downloadObjectBlockFinished:(ASIPithosObjectRequest *)objectRequest { @autoreleasepool { NSOperation *operation = [objectRequest.userInfo objectForKey:@"operation"]; DLog(@"Sync::download object block finished: %@", objectRequest.url); if (operation.isCancelled) { [self requestFailed:objectRequest]; } else if (objectRequest.responseStatusCode == 206) { NSString *accountName = [objectRequest.userInfo objectForKey:@"accountName"]; ASIPithosContainer *pithosContainer = [objectRequest.userInfo objectForKey:@"pithosContainer"]; ASIPithosObject *object = [objectRequest.userInfo objectForKey:@"pithosObject"]; NSFileManager *fileManager = [NSFileManager defaultManager]; NSError *error; PithosActivity *activity = [objectRequest.userInfo objectForKey:@"activity"]; NSString *downloadsDirPath = self.tempDownloadsDirPath; if (!downloadsDirPath) { dispatch_async(dispatch_get_main_queue(), ^{ [activityFacility endActivity:activity withMessage:[objectRequest.userInfo objectForKey:@"failedActivityMessage"]]; }); [self syncOperationFinishedWithSuccess:NO]; return; } PithosLocalObjectState *storedState = [[[storedLocalObjectStates objectForKey:accountName] objectForKey:pithosContainer.name] objectForKey:object.name]; if ((storedState.tmpFilePath == nil) || ![fileManager fileExistsAtPath:storedState.tmpFilePath]) { NSString *tempFileTemplate = [downloadsDirPath stringByAppendingPathComponent:@"download.XXXXXX"]; const char *tempFileTemplateCString = [tempFileTemplate fileSystemRepresentation]; char *tempFileNameCString = (char *)malloc(strlen(tempFileTemplateCString) + 1); strcpy(tempFileNameCString, tempFileTemplateCString); int fileDescriptor = mkstemp(tempFileNameCString); NSString *tempFilePath = [fileManager stringWithFileSystemRepresentation:tempFileNameCString length:strlen(tempFileNameCString)]; free(tempFileNameCString); if (fileDescriptor == -1) { [PithosUtilities fileActionFailedAlertWithTitle:@"Create Temporary File Error" message:[NSString stringWithFormat:@"Cannot create temporary file at '%@'", tempFilePath] error:nil]; dispatch_async(dispatch_get_main_queue(), ^{ [activityFacility endActivity:activity withMessage:[objectRequest.userInfo objectForKey:@"failedActivityMessage"]]; }); [self syncOperationFinishedWithSuccess:NO]; return; } close(fileDescriptor); storedState.tmpFilePath = tempFilePath; [self saveLocalState]; } NSUInteger missingBlockIndex = [[objectRequest.userInfo objectForKey:@"missingBlockIndex"] unsignedIntegerValue]; NSFileHandle *tempFileHandle = [NSFileHandle fileHandleForWritingAtPath:storedState.tmpFilePath]; [tempFileHandle seekToFileOffset:missingBlockIndex*pithosContainer.blockSize]; [tempFileHandle writeData:[objectRequest responseData]]; [tempFileHandle closeFile]; NSIndexSet *missingBlocks = [objectRequest.userInfo objectForKey:@"missingBlocks"]; missingBlockIndex = [missingBlocks indexGreaterThanIndex:missingBlockIndex]; if (missingBlockIndex == NSNotFound) { NSString *filePath = [objectRequest.userInfo objectForKey:@"filePath"]; NSString *dirPath = [filePath stringByDeletingLastPathComponent]; if ([fileManager fileExistsAtPath:filePath] && ![self moveToTempTrashFile:filePath accountName:accountName pithosContainer:pithosContainer]) { dispatch_async(dispatch_get_main_queue(), ^{ [activityFacility endActivity:activity withMessage:[objectRequest.userInfo objectForKey:@"failedActivityMessage"]]; }); [self syncOperationFinishedWithSuccess:NO]; return; } else if (![fileManager fileExistsAtPath:dirPath]) { // File doesn't exist but also the containing directory doesn't exist // In most cases this should have been resolved as an update of the corresponding local object, // but it never hurts to check error = nil; [fileManager createDirectoryAtPath:dirPath withIntermediateDirectories:YES attributes:nil error:nil]; if (error != nil) { [PithosUtilities fileActionFailedAlertWithTitle:@"Create Directory Error" message:[NSString stringWithFormat:@"Cannot create directory at '%@'", dirPath] error:error]; dispatch_async(dispatch_get_main_queue(), ^{ [activityFacility endActivity:[objectRequest.userInfo objectForKey:@"activity"] withMessage:[objectRequest.userInfo objectForKey:@"failedActivityMessage"]]; }); [self syncOperationFinishedWithSuccess:NO]; return; } } // Move file from tmp download error = nil; [fileManager moveItemAtPath:storedState.tmpFilePath toPath:filePath error:&error]; if (error != nil) { [PithosUtilities fileActionFailedAlertWithTitle:@"Move File Error" message:[NSString stringWithFormat:@"Cannot move file at '%@' to '%@'", storedState.tmpFilePath, filePath] error:error]; dispatch_async(dispatch_get_main_queue(), ^{ [activityFacility endActivity:activity withMessage:[objectRequest.userInfo objectForKey:@"failedActivityMessage"]]; }); [self syncOperationFinishedWithSuccess:NO]; return; } dispatch_async(dispatch_get_main_queue(), ^{ [activityFacility endActivity:activity withMessage:[objectRequest.userInfo objectForKey:@"finishedActivityMessage"] totalBytes:activity.totalBytes currentBytes:activity.totalBytes]; }); storedState.filePath = filePath; storedState.hash = object.objectHash; storedState.tmpFilePath = nil; [self saveLocalState]; [self syncOperationFinishedWithSuccess:YES]; return; } else { if (newSyncRequested || syncLate || operation.isCancelled) { [self requestFailed:objectRequest]; } else { __block ASIPithosObjectRequest *newObjectRequest = [PithosUtilities objectBlockDataRequestWithPithos:pithos containerName:pithosContainer.name object:object blockIndex:missingBlockIndex blockSize:pithosContainer.blockSize]; if (![accountName isEqualToString:@""]) [newObjectRequest setRequestUserFromDefaultTo:accountName withPithos:pithos]; newObjectRequest.delegate = self; newObjectRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:); newObjectRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:); newObjectRequest.userInfo = objectRequest.userInfo; [(NSMutableDictionary *)(newObjectRequest.userInfo) setObject:[NSNumber numberWithUnsignedInteger:missingBlockIndex] forKey:@"missingBlockIndex"]; [(NSMutableDictionary *)(newObjectRequest.userInfo) setObject:[NSNumber numberWithUnsignedInteger:10] forKey:@"retries"]; [newObjectRequest setBytesReceivedBlock:^(unsigned long long size, unsigned long long total){ [activityFacility updateActivity:activity withMessage:[NSString stringWithFormat:@"%@ (%.0f%%)", [newObjectRequest.userInfo objectForKey:@"messagePrefix"], (100*(activity.currentBytes + size + 0.0)/(activity.totalBytes + 0.0))] totalBytes:activity.totalBytes currentBytes:(activity.currentBytes + size)]; }]; [networkQueue addOperation:[PithosUtilities prepareRequest:newObjectRequest priority:[[newObjectRequest.userInfo objectForKey:@"priority"] integerValue]]]; } } } else if (objectRequest.responseStatusCode == 412) { // The object has changed on the server dispatch_async(dispatch_get_main_queue(), ^{ [activityFacility endActivity:[objectRequest.userInfo objectForKey:@"activity"] withMessage:[objectRequest.userInfo objectForKey:@"stoppedActivityMessage"]]; }); [self syncOperationFinishedWithSuccess:NO]; } else { [self requestFailed:objectRequest]; } } } - (void)downloadObjectHashMapFinished:(ASIPithosObjectRequest *)objectRequest { @autoreleasepool { NSOperation *operation = [objectRequest.userInfo objectForKey:@"operation"]; DLog(@"Sync::download object hashmap finished: %@", objectRequest.url); if (operation.isCancelled) { [self requestFailed:objectRequest]; } else if (objectRequest.responseStatusCode == 200) { if (newSyncRequested || syncLate || operation.isCancelled) { [self requestFailed:objectRequest]; } else { NSString *accountName = [objectRequest.userInfo objectForKey:@"accountName"]; ASIPithosContainer *pithosContainer = [objectRequest.userInfo objectForKey:@"pithosContainer"]; ASIPithosObject *object = [objectRequest.userInfo objectForKey:@"pithosObject"]; PithosLocalObjectState *storedState = [[[storedLocalObjectStates objectForKey:accountName] objectForKey:pithosContainer.name] objectForKey:object.name]; if ([PithosUtilities bytesOfFile:storedState.tmpFilePath] > object.bytes) [[NSFileHandle fileHandleForWritingAtPath:storedState.tmpFilePath] truncateFileAtOffset:object.bytes]; PithosActivity *activity = [objectRequest.userInfo objectForKey:@"activity"]; NSIndexSet *missingBlocks = [PithosUtilities missingBlocksForFile:storedState.tmpFilePath blockSize:pithosContainer.blockSize blockHash:pithosContainer.blockHash withHashes:[objectRequest hashes]]; NSUInteger missingBlockIndex = [missingBlocks firstIndex]; dispatch_async(dispatch_get_main_queue(), ^{ [activityFacility updateActivity:activity withMessage:[NSString stringWithFormat:@"%@ (%.0f%%)", [objectRequest.userInfo objectForKey:@"messagePrefix"], (100*(activity.totalBytes - [missingBlocks count]*pithosContainer.blockSize + 0.0)/(activity.totalBytes + 0.0))] totalBytes:activity.totalBytes currentBytes:(activity.totalBytes - [missingBlocks count]*pithosContainer.blockSize)]; }); __block ASIPithosObjectRequest *newObjectRequest = [PithosUtilities objectBlockDataRequestWithPithos:pithos containerName:pithosContainer.name object:object blockIndex:missingBlockIndex blockSize:pithosContainer.blockSize]; if (![accountName isEqualToString:@""]) [newObjectRequest setRequestUserFromDefaultTo:accountName withPithos:pithos]; newObjectRequest.delegate = self; newObjectRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:); newObjectRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:); newObjectRequest.userInfo = objectRequest.userInfo; [(NSMutableDictionary *)(newObjectRequest.userInfo) setObject:missingBlocks forKey:@"missingBlocks"]; [(NSMutableDictionary *)(newObjectRequest.userInfo) setObject:[NSNumber numberWithUnsignedInteger:missingBlockIndex] forKey:@"missingBlockIndex"]; [(NSMutableDictionary *)(newObjectRequest.userInfo) setObject:[NSNumber numberWithUnsignedInteger:10] forKey:@"retries"]; [(NSMutableDictionary *)(newObjectRequest.userInfo) setObject:NSStringFromSelector(@selector(downloadObjectBlockFinished:)) forKey:@"didFinishSelector"]; [newObjectRequest setBytesReceivedBlock:^(unsigned long long size, unsigned long long total){ [activityFacility updateActivity:activity withMessage:[NSString stringWithFormat:@"%@ (%.0f%%)", [newObjectRequest.userInfo objectForKey:@"messagePrefix"], (100*(activity.currentBytes + size + 0.0)/(activity.totalBytes + 0.0))] totalBytes:activity.totalBytes currentBytes:(activity.currentBytes + size)]; }]; [networkQueue addOperation:[PithosUtilities prepareRequest:newObjectRequest priority:[[newObjectRequest.userInfo objectForKey:@"priority"] integerValue]]]; } } else { [self requestFailed:objectRequest]; } } } - (void)uploadDirectoryObjectFinished:(ASIPithosObjectRequest *)objectRequest { @autoreleasepool { NSOperation *operation = [objectRequest.userInfo objectForKey:@"operation"]; DLog(@"Sync::upload directory object finished: %@", objectRequest.url); if (operation.isCancelled) { [self requestFailed:objectRequest]; } else if (objectRequest.responseStatusCode == 201) { PithosLocalObjectState *storedState = [[[storedLocalObjectStates objectForKey:[objectRequest.userInfo objectForKey:@"accountName"]] objectForKey:[[objectRequest.userInfo objectForKey:@"pithosContainer"] name]] objectForKey:[[objectRequest.userInfo objectForKey:@"pithosObject"] name]]; storedState.isDirectory = YES; [self saveLocalState]; dispatch_async(dispatch_get_main_queue(), ^{ [activityFacility endActivity:[objectRequest.userInfo objectForKey:@"activity"] withMessage:[objectRequest.userInfo objectForKey:@"finishedActivityMessage"]]; }); [self syncOperationFinishedWithSuccess:YES]; } else { [self requestFailed:objectRequest]; } } } - (void)moveObjectToTrashFinished:(ASIPithosObjectRequest *)objectRequest { @autoreleasepool { NSOperation *operation = [objectRequest.userInfo objectForKey:@"operation"]; DLog(@"Sync::move object to trash finished: %@", objectRequest.url); if (operation.isCancelled) { [self requestFailed:objectRequest]; } else if (objectRequest.responseStatusCode == 201) { [[[storedLocalObjectStates objectForKey:[objectRequest.userInfo objectForKey:@"accountName"]] objectForKey:[[objectRequest.userInfo objectForKey:@"pithosContainer"] name]] removeObjectForKey:[[objectRequest.userInfo objectForKey:@"pithosObject"] name]]; [self saveLocalState]; dispatch_async(dispatch_get_main_queue(), ^{ [activityFacility endActivity:[objectRequest.userInfo objectForKey:@"activity"] withMessage:[objectRequest.userInfo objectForKey:@"finishedActivityMessage"]]; }); [self syncOperationFinishedWithSuccess:YES]; } else { [self requestFailed:objectRequest]; } } } - (void)deleteObjectFinished:(ASIPithosObjectRequest *)objectRequest { @autoreleasepool { NSOperation *operation = [objectRequest.userInfo objectForKey:@"operation"]; DLog(@"Sync::delete object finished: %@", objectRequest.url); if (operation.isCancelled) { [self requestFailed:objectRequest]; } else if (objectRequest.responseStatusCode == 204) { [[[storedLocalObjectStates objectForKey:[objectRequest.userInfo objectForKey:@"accountName"]] objectForKey:[[objectRequest.userInfo objectForKey:@"pithosContainer"] name]] removeObjectForKey:[[objectRequest.userInfo objectForKey:@"pithosObject"] name]]; [self saveLocalState]; dispatch_async(dispatch_get_main_queue(), ^{ [activityFacility endActivity:[objectRequest.userInfo objectForKey:@"activity"] withMessage:[objectRequest.userInfo objectForKey:@"finishedActivityMessage"]]; }); [self syncOperationFinishedWithSuccess:YES]; } else { [self requestFailed:objectRequest]; } } } - (void)uploadObjectUsingHashMapFinished:(ASIPithosObjectRequest *)objectRequest { @autoreleasepool { NSOperation *operation = [objectRequest.userInfo objectForKey:@"operation"]; DLog(@"Sync::upload using hashmap finished: %@", objectRequest.url); NSString *accountName = [objectRequest.userInfo objectForKey:@"accountName"]; ASIPithosContainer *pithosContainer = [objectRequest.userInfo objectForKey:@"pithosContainer"]; ASIPithosObject *object = [objectRequest.userInfo objectForKey:@"pithosObject"]; PithosLocalObjectState *storedState = [[[storedLocalObjectStates objectForKey:accountName] objectForKey:pithosContainer.name] objectForKey:object.name]; PithosActivity *activity = [objectRequest.userInfo objectForKey:@"activity"]; NSUInteger totalBytes = activity.totalBytes; NSUInteger currentBytes = activity.currentBytes; if (operation.isCancelled) { [self requestFailed:objectRequest]; } else if (objectRequest.responseStatusCode == 201) { DLog(@"Sync::object created: %@", objectRequest.url); storedState.filePath = [objectRequest.userInfo objectForKey:@"filePath"]; storedState.hash = object.objectHash; [self saveLocalState]; dispatch_async(dispatch_get_main_queue(), ^{ [activityFacility endActivity:activity withMessage:[objectRequest.userInfo objectForKey:@"finishedActivityMessage"] totalBytes:totalBytes currentBytes:totalBytes]; }); [self syncOperationFinishedWithSuccess:YES]; } else if (objectRequest.responseStatusCode == 409) { if (newSyncRequested || syncLate || operation.isCancelled) { [self requestFailed:objectRequest]; } else { NSUInteger iteration = [[objectRequest.userInfo objectForKey:@"iteration"] unsignedIntegerValue]; if (iteration == 0) { DLog(@"Sync::upload iteration limit reached: %@", objectRequest.url); dispatch_async(dispatch_get_main_queue(), ^{ [activityFacility endActivity:activity withMessage:[objectRequest.userInfo objectForKey:@"stoppedActivityMessage"]]; }); [self syncOperationFinishedWithSuccess:NO]; return; } DLog(@"Sync::object is missing hashes: %@", objectRequest.url); NSIndexSet *missingBlocks = [PithosUtilities missingBlocksForHashes:[objectRequest.userInfo objectForKey:@"hashes"] withMissingHashes:[objectRequest hashes]]; if (totalBytes >= [missingBlocks count]*pithosContainer.blockSize) currentBytes = totalBytes - [missingBlocks count]*pithosContainer.blockSize; dispatch_async(dispatch_get_main_queue(), ^{ [activityFacility updateActivity:activity withMessage:[NSString stringWithFormat:@"%@ (%.0f%%)", [objectRequest.userInfo objectForKey:@"messagePrefix"], (100*(currentBytes + 0.0)/(totalBytes + 0.0))] totalBytes:totalBytes currentBytes:currentBytes]; }); NSUInteger missingBlockIndex = [missingBlocks firstIndex]; __block ASIPithosContainerRequest *newContainerRequest = [PithosUtilities updateContainerDataRequestWithPithos:pithos containerName:pithosContainer.name blockSize:pithosContainer.blockSize forFile:[objectRequest.userInfo objectForKey:@"filePath"] missingBlockIndex:missingBlockIndex sharingAccount:([accountName isEqualToString:@""] ? nil : accountName)]; newContainerRequest.delegate = self; newContainerRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:); newContainerRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:); newContainerRequest.userInfo = objectRequest.userInfo; [(NSMutableDictionary *)(newContainerRequest.userInfo) setObject:[NSNumber numberWithUnsignedInteger:(--iteration)] forKey:@"iteration"]; [(NSMutableDictionary *)(newContainerRequest.userInfo) setObject:[NSNumber numberWithUnsignedInteger:10] forKey:@"retries"]; [(NSMutableDictionary *)(newContainerRequest.userInfo) setObject:missingBlocks forKey:@"missingBlocks"]; [(NSMutableDictionary *)(newContainerRequest.userInfo) setObject:[NSNumber numberWithUnsignedInteger:missingBlockIndex] forKey:@"missingBlockIndex"]; [(NSMutableDictionary *)(newContainerRequest.userInfo) setObject:NSStringFromSelector(@selector(uploadMissingBlockFinished:)) forKey:@"didFinishSelector"]; [newContainerRequest setBytesSentBlock:^(unsigned long long size, unsigned long long total){ [activityFacility updateActivity:activity withMessage:[NSString stringWithFormat:@"%@ (%.0f%%)", [newContainerRequest.userInfo objectForKey:@"messagePrefix"], (100*(activity.currentBytes + size + 0.0)/(activity.totalBytes + 0.0))] totalBytes:activity.totalBytes currentBytes:(activity.currentBytes + size)]; }]; [networkQueue addOperation:[PithosUtilities prepareRequest:newContainerRequest priority:[[newContainerRequest.userInfo objectForKey:@"priority"] integerValue]]]; } } else { [self requestFailed:objectRequest]; } } } - (void)uploadMissingBlockFinished:(ASIPithosContainerRequest *)containerRequest { @autoreleasepool { NSOperation *operation = [containerRequest.userInfo objectForKey:@"operation"]; DLog(@"Sync::upload of missing block finished: %@", containerRequest.url); if (operation.isCancelled) { [self requestFailed:containerRequest]; } else if (containerRequest.responseStatusCode == 202) { NSString *accountName = [containerRequest.userInfo objectForKey:@"accountName"]; ASIPithosContainer *pithosContainer = [containerRequest.userInfo objectForKey:@"pithosContainer"]; ASIPithosObject *object = [containerRequest.userInfo objectForKey:@"pithosObject"]; PithosActivity *activity = [containerRequest.userInfo objectForKey:@"activity"]; NSIndexSet *missingBlocks = [containerRequest.userInfo objectForKey:@"missingBlocks"]; NSUInteger missingBlockIndex = [[containerRequest.userInfo objectForKey:@"missingBlockIndex"] unsignedIntegerValue]; missingBlockIndex = [missingBlocks indexGreaterThanIndex:missingBlockIndex]; if (operation.isCancelled) { [self requestFailed:containerRequest]; } else if (missingBlockIndex == NSNotFound) { NSArray *hashes = [containerRequest.userInfo objectForKey:@"hashes"]; ASIPithosObjectRequest *newObjectRequest = [PithosUtilities writeObjectDataRequestWithPithos:pithos containerName:pithosContainer.name objectName:object.name contentType:object.contentType blockSize:pithosContainer.blockSize blockHash:pithosContainer.blockHash forFile:[containerRequest.userInfo objectForKey:@"filePath"] checkIfExists:NO hashes:&hashes sharingAccount:([accountName isEqualToString:@""] ? nil : accountName)]; newObjectRequest.delegate = self; newObjectRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:); newObjectRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:); newObjectRequest.userInfo = containerRequest.userInfo; [(NSMutableDictionary *)(newObjectRequest.userInfo) setObject:[NSNumber numberWithUnsignedInteger:10] forKey:@"retries"]; [(NSMutableDictionary *)(newObjectRequest.userInfo) removeObjectForKey:@"missingBlocks"]; [(NSMutableDictionary *)(newObjectRequest.userInfo) removeObjectForKey:@"missingBlockIndex"]; [(NSMutableDictionary *)(newObjectRequest.userInfo) setObject:NSStringFromSelector(@selector(uploadObjectUsingHashMapFinished:)) forKey:@"didFinishSelector"]; [networkQueue addOperation:[PithosUtilities prepareRequest:newObjectRequest priority:[[newObjectRequest.userInfo objectForKey:@"priority"] integerValue]]]; } else { if (newSyncRequested || syncLate || operation.isCancelled) { [self requestFailed:containerRequest]; } else { __block ASIPithosContainerRequest *newContainerRequest = [PithosUtilities updateContainerDataRequestWithPithos:pithos containerName:pithosContainer.name blockSize:pithosContainer.blockSize forFile:[containerRequest.userInfo objectForKey:@"filePath"] missingBlockIndex:missingBlockIndex sharingAccount:([accountName isEqualToString:@""] ? nil : accountName)]; newContainerRequest.delegate = self; newContainerRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:); newContainerRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:); newContainerRequest.userInfo = containerRequest.userInfo; [(NSMutableDictionary *)(newContainerRequest.userInfo) setObject:[NSNumber numberWithUnsignedInteger:10] forKey:@"retries"]; [(NSMutableDictionary *)(newContainerRequest.userInfo) setObject:[NSNumber numberWithUnsignedInteger:missingBlockIndex] forKey:@"missingBlockIndex"]; [newContainerRequest setBytesSentBlock:^(unsigned long long size, unsigned long long total){ [activityFacility updateActivity:activity withMessage:[NSString stringWithFormat:@"%@ (%.0f%%)", [newContainerRequest.userInfo objectForKey:@"messagePrefix"], (100*(activity.currentBytes + size + 0.0)/(activity.totalBytes + 0.0))] totalBytes:activity.totalBytes currentBytes:(activity.currentBytes + size)]; }]; [networkQueue addOperation:[PithosUtilities prepareRequest:newContainerRequest priority:[[newContainerRequest.userInfo objectForKey:@"priority"] integerValue]]]; } } } else { [self requestFailed:containerRequest]; } } } - (void)requestFailed:(ASIPithosRequest *)request { @autoreleasepool { NSOperation *operation = [request.userInfo objectForKey:@"operation"]; DLog(@"Sync::request failed: %@", request.url); if (operation.isCancelled) return; if (request.isCancelled || newSyncRequested || syncLate) { dispatch_async(dispatch_get_main_queue(), ^{ [activityFacility endActivity:[request.userInfo objectForKey:@"activity"] withMessage:[request.userInfo objectForKey:@"stoppedActivityMessage"]]; }); [self syncOperationFinishedWithSuccess:NO]; return; } NSUInteger retries = [[request.userInfo objectForKey:@"retries"] unsignedIntegerValue]; if (retries > 0) { ASIPithosRequest *newRequest = (ASIPithosRequest *)[PithosUtilities copyRequest:request]; [(NSMutableDictionary *)(newRequest.userInfo)setObject:[NSNumber numberWithUnsignedInteger:(--retries)] forKey:@"retries"]; [networkQueue addOperation:[PithosUtilities prepareRequest:newRequest priority:[[newRequest.userInfo objectForKey:@"priority"] integerValue]]]; } else { dispatch_async(dispatch_get_main_queue(), ^{ [activityFacility endActivity:[request.userInfo objectForKey:@"activity"] withMessage:[request.userInfo objectForKey:@"failedActivityMessage"]]; }); [self syncOperationFinishedWithSuccess:NO]; } } } @end