// // 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)moveToTempTrashFile:(NSString *)filePath pithosContainer:(ASIPithosContainer *)pithosContainer; - (void)emptyTempTrash; - (BOOL)findLocalCopyForObjectWithHash:(NSString *)hash forFile:(NSString *)filePath; - (void)updateLocalStateWithObject:(ASIPithosObject *)object localFilePath:(NSString *)filePath pithosContainer:(ASIPithosContainer *)pithosContainer; - (void)updateServerStateWithCurrentState:(PithosLocalObjectState *)currentState object:(ASIPithosObject *)object localFilePath:(NSString *)filePath pithosContainer:(ASIPithosContainer *)pithosContainer; - (void)listRequestFailed:(ASIPithosContainerRequest *)containerRequest; - (void)requestFailed:(ASIPithosRequest *)request; - (void)syncOperationStarted; - (void)syncOperationFinishedWithSuccess:(BOOL)operationSuccessfull; @end @implementation PithosSyncDaemon @synthesize directoryPath, containersDictionary, pithos; @synthesize pithosContainers; @synthesize lastCompletedSync, remoteObjects, previousRemoteObjects, storedLocalObjectStates, currentLocalObjectStates; @synthesize pithosStateFilePath, tempDownloadsDirPath, tempTrashDirPath; #pragma mark - #pragma Object Lifecycle - (id)initWithDirectoryPath:(NSString *)aDirectoryPath pithosAccount:(PithosAccount *)aPithosAccount containersDictionary:(NSDictionary *)aContainersDictionary resetLocalState:(BOOL)resetLocalState { if ((self = [super init])) { directoryPath = [aDirectoryPath copy]; pithosAccount = [aPithosAccount retain]; containersDictionary = [aContainersDictionary copy]; self.pithos = pithosAccount.pithos; containersCount = [containersDictionary count]; self.pithosContainers = [NSMutableArray arrayWithCapacity:containersCount]; for (NSString *containerName in containersDictionary) { ASIPithosContainer *pithosContainer = [ASIPithosContainer container]; pithosContainer.name = containerName; [pithosContainers addObject:pithosContainer]; } 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 { NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; if ([[NSFileManager defaultManager] fileExistsAtPath:self.pithosStateFilePath]) self.storedLocalObjectStates = [NSKeyedUnarchiver unarchiveObjectWithFile:self.pithosStateFilePath]; else self.storedLocalObjectStates = [NSMutableDictionary dictionary]; if (!storedLocalObjectStates) self.storedLocalObjectStates = [NSMutableDictionary dictionary]; for (ASIPithosContainer *pithosContainer in pithosContainers) { if (![storedLocalObjectStates objectForKey:pithosContainer.name]) { [storedLocalObjectStates setObject:[NSMutableDictionary dictionary] forKey:pithosContainer.name]; } } [pool drain]; } - (void)resetLocalStateWithAll:(BOOL)all { NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; self.lastCompletedSync = nil; NSFileManager *fileManager = [NSFileManager defaultManager]; NSError *error; 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) { error = nil; for (NSString *subPath in [fileManager contentsOfDirectoryAtPath:self.tempDownloadsDirPath error:&error]) { if (error) { [PithosUtilities fileActionFailedAlertWithTitle:@"Directory Contents Error" message:[NSString stringWithFormat:@"Cannot get contents of directory at '%@'", self.tempDownloadsDirPath] error:error]; break; } NSString *subFilePath = [self.tempDownloadsDirPath stringByAppendingPathComponent:subPath]; if (![fileManager removeItemAtPath:subFilePath error:&error] || error) { [PithosUtilities fileActionFailedAlertWithTitle:@"Remove File Error" message:[NSString stringWithFormat:@"Cannot remove file at '%@'", subFilePath] error:error]; } error = nil; } } } else { // Remove containers that don't interest us anymore and save if (!storedLocalObjectStates) [self loadLocalState]; for (NSString *containerName in storedLocalObjectStates) { if (![containersDictionary objectForKey:containerName]) { [storedLocalObjectStates removeObjectForKey:containerName]; if (self.tempDownloadsDirPath) { NSString *containerTempDownloadsDirPath = [self.tempDownloadsDirPath stringByAppendingPathComponent:containerName]; BOOL isDirectory; BOOL fileExists = [fileManager fileExistsAtPath:containerTempDownloadsDirPath isDirectory:&isDirectory]; if (fileExists && isDirectory) { error = nil; for (NSString *subPath in [fileManager contentsOfDirectoryAtPath:containerTempDownloadsDirPath error:&error]) { if (error) { [PithosUtilities fileActionFailedAlertWithTitle:@"Directory Contents Error" message:[NSString stringWithFormat:@"Cannot get contents of directory at '%@'", containerTempDownloadsDirPath] error:error]; break; } NSString *subFilePath = [containerTempDownloadsDirPath stringByAppendingPathComponent:subPath]; if (![fileManager removeItemAtPath:subFilePath error:&error] || error) { [PithosUtilities fileActionFailedAlertWithTitle:@"Remove File Error" message:[NSString stringWithFormat:@"Cannot remove file at '%@'", subFilePath] error:error]; } error = nil; } } else if (fileExists && !isDirectory) { error = nil; if (![fileManager removeItemAtPath:containerTempDownloadsDirPath error:&error] || error) { [PithosUtilities fileActionFailedAlertWithTitle:@"Remove File Error" message:[NSString stringWithFormat:@"Cannot remove file at '%@'", containerTempDownloadsDirPath] error:error]; } } } } } [self saveLocalState]; } [pool drain]; } - (void)saveLocalState { NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; [NSKeyedArchiver archiveRootObject:storedLocalObjectStates toFile:self.pithosStateFilePath]; [pool drain]; } - (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]; [callbackQueue release]; [networkQueue release]; [tempTrashDirPath release]; [tempDownloadsDirPath release]; [pithosStateFilePath release]; [currentLocalObjectStates release]; [storedLocalObjectStates release]; [previousRemoteObjects release]; [remoteObjects release]; [objects release]; [lastCompletedSync release]; [pithosContainers release]; [pithos release]; [containersDictionary release]; [pithosAccount release]; [directoryPath release]; [super dealloc]; } #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]] retain]; 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] autorelease]; } - (NSString *)tempDownloadsDirPath { if (!tempDownloadsDirPath) { tempDownloadsDirPath = [[[[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) objectAtIndex:0] stringByAppendingPathComponent:[[NSBundle mainBundle] bundleIdentifier]] stringByAppendingPathComponent:[NSString stringWithFormat:@"%@-TempDownloads", pithosAccount.uniqueName]] retain]; 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]] retain]; 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 release]; directoryPath = [aDirectoryPath copy]; } } - (void)setContainersDictionary:(NSDictionary *)aContainersDictionary { if (aContainersDictionary && ![aContainersDictionary isEqualToDictionary:containersDictionary]) { [self resetDaemon]; [containersDictionary release]; containersDictionary = [aContainersDictionary copy]; containersCount = [containersDictionary count]; self.pithosContainers = [NSMutableArray arrayWithCapacity:containersCount]; for (NSString *containerName in aContainersDictionary) { ASIPithosContainer *pithosContainer = [ASIPithosContainer container]; pithosContainer.name = containerName; [pithosContainers addObject:pithosContainer]; } [self resetLocalStateWithAll:NO]; } } - (void)setPithos:(ASIPithos *)aPithos { if (!pithos) { pithos = [[ASIPithos pithos] retain]; pithos.authUser = [[aPithos.authUser copy] autorelease]; pithos.authToken = [[aPithos.authToken copy] autorelease]; pithos.storageURLPrefix = [[aPithos.storageURLPrefix copy] autorelease]; pithos.authURL = [[aPithos.authURL copy] autorelease]; pithos.publicURLPrefix = [[aPithos.publicURLPrefix copy] autorelease]; } 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] autorelease]; pithos.authToken = [[aPithos.authToken copy] autorelease]; pithos.storageURLPrefix = [[aPithos.storageURLPrefix copy] autorelease]; pithos.authURL = [[aPithos.authURL copy] autorelease]; pithos.publicURLPrefix = [[aPithos.publicURLPrefix copy] autorelease]; } } #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 NSLog(@"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 && containersCount) { // The first operation is the server listing [self syncOperationStarted]; newSyncRequested = NO; syncIncomplete = NO; syncLate = NO; } else { return; } } NSFileManager *fileManager = [NSFileManager defaultManager]; BOOL isDirectory; NSError *error = nil; if (![fileManager fileExistsAtPath:directoryPath isDirectory:&isDirectory]) { if (![fileManager createDirectoryAtPath:directoryPath withIntermediateDirectories:YES attributes:nil error:&error] || error) { [PithosUtilities fileActionFailedAlertWithTitle:@"Local Sync Directory Error" message:[NSString stringWithFormat:@"Cannot create local sync directory at '%@'", directoryPath] error:error]; [self syncOperationFinishedWithSuccess:NO]; return; } } else if (!isDirectory) { [PithosUtilities fileActionFailedAlertWithTitle:@"Local Sync Directory Error" message:[NSString stringWithFormat:@"File already exists at the local sync directory path at '%@'", directoryPath] error:nil]; [self syncOperationFinishedWithSuccess:NO]; return; } for (ASIPithosContainer *pithosContainer in pithosContainers) { NSString *containerDirectoryPath = [directoryPath stringByAppendingPathComponent:pithosContainer.name]; error = nil; if (![fileManager fileExistsAtPath:containerDirectoryPath isDirectory:&isDirectory]) { if (![fileManager createDirectoryAtPath:containerDirectoryPath withIntermediateDirectories:YES attributes:nil error:&error] || error) { [PithosUtilities fileActionFailedAlertWithTitle:@"Local Sync Directory Error" message:[NSString stringWithFormat:@"Cannot create local sync directory at '%@'", containerDirectoryPath] error:error]; [self syncOperationFinishedWithSuccess:NO]; return; } } else if (!isDirectory) { [PithosUtilities fileActionFailedAlertWithTitle:@"Local Sync Directory Error" message:[NSString stringWithFormat:@"File already exists at the local sync directory path at '%@'", containerDirectoryPath] error:nil]; [self syncOperationFinishedWithSuccess:NO]; return; } } containersIndex = 0; self.remoteObjects = [NSMutableDictionary dictionaryWithCapacity:containersCount]; ASIPithosContainerRequest *containerRequest = [ASIPithosContainerRequest listObjectsRequestWithPithos:pithos containerName:[[pithosContainers objectAtIndex:containersIndex] name] limit:0 marker:nil prefix:nil delimiter:nil path:nil meta:nil shared:NO until:nil ifModifiedSince:[[pithosContainers objectAtIndex:containersIndex] lastModified]]; 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 { NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; 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) { dispatch_async(dispatch_get_main_queue(), ^{ [PithosUtilities fileActionFailedAlertWithTitle:@"Directory Contents Error" message:[NSString stringWithFormat:@"Cannot get contents of directory at '%@'", trashDirPath] error:error]; }); [pool drain]; 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) { // dispatch_async(dispatch_get_main_queue(), ^{ // [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) dispatch_async(dispatch_get_main_queue(), ^{ [PithosUtilities fileActionFailedAlertWithTitle:@"Remove File Error" message:[NSString stringWithFormat:@"Cannot remove file at '%@'", subFilePath] error:error]; }); } } } [pool drain]; } - (BOOL)moveToTempTrashFile:(NSString *)filePath pithosContainer:(ASIPithosContainer *)pithosContainer { NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; if (!self.tempTrashDirPath) { [pool drain]; return NO; } NSFileManager *fileManager = [NSFileManager defaultManager]; BOOL isDirectory; BOOL fileExists = [fileManager fileExistsAtPath:filePath isDirectory:&isDirectory]; NSError *error = nil; NSString *containerDirectoryPath = [directoryPath stringByAppendingPathComponent: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) { dispatch_async(dispatch_get_main_queue(), ^{ [PithosUtilities fileActionFailedAlertWithTitle:@"Directory Contents Error" message:[NSString stringWithFormat:@"Cannot get contents of directory at '%@'", filePath] error:error]; }); [pool drain]; return NO; } if (![fileManager createDirectoryAtPath:newDirPath withIntermediateDirectories:YES attributes:nil error:&error] || error) { dispatch_async(dispatch_get_main_queue(), ^{ [PithosUtilities fileActionFailedAlertWithTitle:@"Create Directory Error" message:[NSString stringWithFormat:@"Cannot create directory at '%@'", newDirPath] error:error]; }); [pool drain]; return NO; } if (![fileManager moveItemAtPath:filePath toPath:newFilePath error:&error] || error) { dispatch_async(dispatch_get_main_queue(), ^{ [PithosUtilities fileActionFailedAlertWithTitle:@"Move File Error" message:[NSString stringWithFormat:@"Cannot move file at '%@' to '%@'", filePath, newFilePath] error:error]; }); [pool drain]; 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) { dispatch_async(dispatch_get_main_queue(), ^{ [PithosUtilities fileActionFailedAlertWithTitle:@"Create Directory Error" message:[NSString stringWithFormat:@"Cannot create directory at '%@'", newDirPath] error:error]; }); [pool drain]; return NO; } if (![fileManager moveItemAtPath:filePath toPath:newFilePath error:&error] || error) { dispatch_async(dispatch_get_main_queue(), ^{ [PithosUtilities fileActionFailedAlertWithTitle:@"Move File Error" message:[NSString stringWithFormat:@"Cannot move file at '%@' to '%@'", filePath, newFilePath] error:error]; }); [pool drain]; 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]; } } [pool drain]; return YES; } - (BOOL)findLocalCopyForObjectWithHash:(NSString *)hash forFile:(NSString *)filePath { if ([hash length] != 64) return NO; NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; 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) { dispatch_async(dispatch_get_main_queue(), ^{ [PithosUtilities fileActionFailedAlertWithTitle:@"Copy File Error" message:[NSString stringWithFormat:@"Cannot copy file at '%@' to '%@'", localFilePath, filePath] error:error]; }); } else { [pool drain]; return YES; } } else if (self.tempTrashDirPath && [localFilePath hasPrefix:self.tempTrashDirPath]) { if (![fileManager moveItemAtPath:localFilePath toPath:filePath error:&error] || error) { dispatch_async(dispatch_get_main_queue(), ^{ [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]; [pool drain]; return YES; } } } } [pool drain]; return NO; } - (void)updateLocalStateWithObject:(ASIPithosObject *)object localFilePath:(NSString *)filePath pithosContainer:(ASIPithosContainer *)pithosContainer { NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; NSFileManager *fileManager = [NSFileManager defaultManager]; NSError *error; BOOL isDirectory; BOOL fileExists = [fileManager fileExistsAtPath:filePath isDirectory:&isDirectory]; NSString *fileDirectoryPath = [filePath stringByDeletingLastPathComponent]; NSMutableDictionary *containerStoredLocalObjectStates = [storedLocalObjectStates 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:nil]; if (!object || !object.objectHash) { // Delete local object if (remoteObjectExists) { // Remote object created in the meantime, just mark the sync cycle as incomplete, but do delete the local object syncIncomplete = YES; } NSLog(@"Sync::delete local object: %@", filePath); if (!fileExists || [self moveToTempTrashFile:filePath 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; [pool drain]; return; } NSLog(@"Sync::create local directory object: %@", filePath); BOOL directoryCreated = NO; if (!fileExists || (!isDirectory && [self moveToTempTrashFile:filePath pithosContainer:pithosContainer])) { NSLog(@"Sync::local directory object doesn't exist: %@", filePath); error = nil; if (![fileManager createDirectoryAtPath:filePath withIntermediateDirectories:YES attributes:nil error:&error] || error) { dispatch_async(dispatch_get_main_queue(), ^{ [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 { NSLog(@"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)", 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; [pool drain]; return; } NSLog(@"Sync::create local zero length object: %@", filePath); BOOL fileCreated = NO; if (!fileExists || ((isDirectory || [PithosUtilities bytesOfFile:filePath]) && [self moveToTempTrashFile:filePath pithosContainer:pithosContainer])) { NSLog(@"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]) { dispatch_async(dispatch_get_main_queue(), ^{ [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 { NSLog(@"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%%)", 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; [pool drain]; return; } // 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]; }); } // 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%%)", 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]; objectRequest.delegate = self; objectRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:); objectRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:); PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityDownload message:[NSString stringWithFormat:@"Sync: Downloading '%@/%@' (0%%)", pithosContainer.name, object.name] totalBytes:object.bytes currentBytes:0 pithosAccount:pithosAccount]; dispatch_async(dispatch_get_main_queue(), ^{ [activityFacility updateActivity:activity withMessage:activity.message]; }); objectRequest.userInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys: pithosContainer, @"pithosContainer", object, @"pithosObject", [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, (NSUInteger)ceil((object.bytes +0.0)/(pithosContainer.blockSize + 0.0)))], @"missingBlocks", [NSNumber numberWithUnsignedInteger:0], @"missingBlockIndex", filePath, @"filePath", activity, @"activity", [NSString stringWithFormat:@"Sync: Downloading '%@/%@' (stopped)", pithosContainer.name, object.name], @"stoppedActivityMessage", [NSString stringWithFormat:@"Sync: Downloading '%@/%@' (failed)", pithosContainer.name, object.name], @"failedActivityMessage", [NSString stringWithFormat:@"Sync: Downloading '%@/%@' (100%%)", pithosContainer.name, object.name], @"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:@"Sync: Downloading '%@/%@' (%.0f%%)", objectRequest.containerName, [[objectRequest.userInfo objectForKey:@"pithosObject"] name], (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; [pool drain]; return; } // 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]; }); } // 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%%)", pithosContainer.name, object.name] pithosAccount:pithosAccount]; }); } else { [self syncOperationStarted]; ASIPithosObjectRequest *objectRequest = [ASIPithosObjectRequest objectHashmapRequestWithPithos:pithos containerName:pithosContainer.name objectName:object.name]; objectRequest.delegate = self; objectRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:); objectRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:); PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityDownload message:[NSString stringWithFormat:@"Sync: Downloading '%@/%@' (0%%)", pithosContainer.name, object.name] totalBytes:object.bytes currentBytes:0 pithosAccount:pithosAccount]; dispatch_async(dispatch_get_main_queue(), ^{ [activityFacility updateActivity:activity withMessage:activity.message]; }); objectRequest.userInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys: pithosContainer, @"pithosContainer", object, @"pithosObject", filePath, @"filePath", activity, @"activity", [NSString stringWithFormat:@"Sync: Downloading '%@/%@' (stopped)", pithosContainer.name, object.name], @"stoppedActivityMessage", [NSString stringWithFormat:@"Sync: Downloading '%@/%@' (failed)", pithosContainer.name, object.name], @"failedActivityMessage", [NSString stringWithFormat:@"Sync: Downloading '%@/%@' (100%%)", pithosContainer.name, object.name], @"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]]; } } [pool drain]; } -(void)updateServerStateWithCurrentState:(PithosLocalObjectState *)currentState object:(ASIPithosObject *)object localFilePath:(NSString *)filePath pithosContainer:(ASIPithosContainer *)pithosContainer { NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; [self syncOperationStarted]; NSFileManager *fileManager = [NSFileManager defaultManager]; BOOL isDirectory; BOOL fileExists = [fileManager fileExistsAtPath:filePath isDirectory:&isDirectory]; if (currentState.isDirectory) { // Create remote directory object 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]; [pool drain]; 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]]; objectRequest.delegate = self; objectRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:); objectRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:); PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityCreateDirectory message:[NSString stringWithFormat:@"Sync: Creating directory '%@/%@'", pithosContainer.name, object.name] pithosAccount:pithosAccount]; dispatch_async(dispatch_get_main_queue(), ^{ [activityFacility updateActivity:activity withMessage:activity.message]; }); objectRequest.userInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys: pithosContainer, @"pithosContainer", object, @"pithosObject", activity, @"activity", [NSString stringWithFormat:@"Sync: Creating directory '%@/%@' (stopped)", pithosContainer.name, object.name], @"stoppedActivityMessage", [NSString stringWithFormat:@"Sync: Creating directory '%@/%@' (failed)", pithosContainer.name, object.name], @"failedActivityMessage", [NSString stringWithFormat:@"Sync: Creating directory '%@/%@' (finished)", pithosContainer.name, object.name], @"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 (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:); PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityDelete message:[NSString stringWithFormat:@"Sync: Deleting '%@/%@'", pithosContainer.name, object.name] pithosAccount:pithosAccount]; dispatch_async(dispatch_get_main_queue(), ^{ [activityFacility updateActivity:activity withMessage:activity.message]; }); objectRequest.userInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys: pithosContainer, @"pithosContainer", object, @"pithosObject", activity, @"activity", [NSString stringWithFormat:@"Sync: Deleting '%@/%@' (stopped)", pithosContainer.name, object.name], @"stoppedActivityMessage", [NSString stringWithFormat:@"Sync: Deleting '%@/%@' (failed)", pithosContainer.name, object.name], @"failedActivityMessage", [NSString stringWithFormat:@"Sync: Deleting '%@/%@' (finished)", pithosContainer.name, object.name], @"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:); PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityDelete message:[NSString stringWithFormat:@"Sync: Moving to trash '%@/%@'", pithosContainer.name, object.name] pithosAccount:pithosAccount]; dispatch_async(dispatch_get_main_queue(), ^{ [activityFacility updateActivity:activity withMessage:activity.message]; }); objectRequest.userInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys: pithosContainer, @"pithosContainer", object, @"pithosObject", activity, @"activity", [NSString stringWithFormat:@"Sync: Moving to trash '%@/%@' (stopped)", pithosContainer.name, object.name], @"stoppedActivityMessage", [NSString stringWithFormat:@"Sync: Moving to trash '%@/%@' (failed)", pithosContainer.name, object.name], @"failedActivityMessage", [NSString stringWithFormat:@"Sync: Moving to trash '%@/%@' (finished)", pithosContainer.name, object.name], @"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 (!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]; [pool drain]; return; } NSError *error = nil; object.contentType = [PithosUtilities contentTypeOfFile:filePath error:&error]; if (object.contentType == nil) object.contentType = @"application/octet-stream"; if (error) NSLog(@"contentType detection error: %@", error); 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:nil]; if (objectRequest) { objectRequest.delegate = self; objectRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:); objectRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:); PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityUpload message:[NSString stringWithFormat:@"Sync: Uploading '%@/%@' (0%%)", pithosContainer.name, object.name] 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: pithosContainer, @"pithosContainer", object, @"pithosObject", filePath, @"filePath", hashes, @"hashes", [NSNumber numberWithUnsignedInteger:10], @"iteration", activity, @"activity", [NSString stringWithFormat:@"Sync: Uploading '%@' (stopped)", object.name], @"stoppedActivityMessage", [NSString stringWithFormat:@"Sync: Uploading '%@' (failed)", object.name], @"failedActivityMessage", [NSString stringWithFormat:@"Sync: Uploading '%@' (100%%)", object.name], @"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]; } } [pool drain]; } #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] autorelease]; operation.completionBlock = ^{ NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; 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]; } [pool drain]; }; [(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] autorelease]; operation.completionBlock = ^{ NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; 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]; } [pool drain]; }; [(NSMutableDictionary *)request.userInfo setObject:operation forKey:@"operation"]; [callbackQueue addOperation:operation]; } } - (void)listRequestFinished:(ASIPithosContainerRequest *)containerRequest { NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; NSOperation *operation = [containerRequest.userInfo objectForKey:@"operation"]; NSLog(@"Sync::list request finished: %@", containerRequest.url); if (operation.isCancelled) { [self listRequestFailed:containerRequest]; } else if ((containerRequest.responseStatusCode == 200) || (containerRequest.responseStatusCode == 304)) { if (containerRequest.responseStatusCode == 200) { NSArray *someObjects = [containerRequest objects]; if (objects == nil) { objects = [[NSMutableArray alloc] initWithArray:someObjects]; } else { [objects addObjectsFromArray:someObjects]; } if ([someObjects count] < 10000) { ASIPithosContainer *pithosContainer = [pithosContainers objectAtIndex:containersIndex]; 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 setObject:containerRemoteObjects forKey:pithosContainer.name]; [objects release]; objects = nil; } else { // Do an additional request to fetch more objects ASIPithosContainerRequest *newContainerRequest = [ASIPithosContainerRequest listObjectsRequestWithPithos:pithos containerName:[[pithosContainers objectAtIndex:containersIndex] name] limit:0 marker:[[someObjects lastObject] name] prefix:nil delimiter:nil path:nil meta:nil shared:NO until:nil ifModifiedSince:[[pithosContainers objectAtIndex:containersIndex] lastModified]]; 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]]]; [pool drain]; return; } } else { ASIPithosContainer *pithosContainer = [pithosContainers objectAtIndex:containersIndex]; NSMutableDictionary *containerRemoteObjects = [previousRemoteObjects objectForKey:pithosContainer.name]; if (containerRemoteObjects) [remoteObjects setObject:containerRemoteObjects forKey:pithosContainer.name]; } containersIndex++; if (containersIndex < containersCount) { // Do a request for the next container ASIPithosContainerRequest *newContainerRequest = [ASIPithosContainerRequest listObjectsRequestWithPithos:pithos containerName:[[pithosContainers objectAtIndex:containersIndex] name] limit:0 marker:nil prefix:nil delimiter:nil path:nil meta:nil shared:NO until:nil ifModifiedSince:[[pithosContainers objectAtIndex:containersIndex] lastModified]]; 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]]]; [pool drain]; return; } self.previousRemoteObjects = remoteObjects; // remoteObjects contains all remote objects for the legal containers, without enforcing directory exclusions if (operation.isCancelled) { [self listRequestFailed:containerRequest]; [pool drain]; 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 (ASIPithosContainer *pithosContainer in pithosContainers) { NSString *containerDirectoryPath = [directoryPath stringByAppendingPathComponent:pithosContainer.name]; NSArray *containerExcludedDirectories = [containersDictionary objectForKey:pithosContainer.name]; BOOL containerExludeRootFiles = [containerExcludedDirectories containsObject:@""]; NSMutableDictionary *containerStoredLocalObjectStates = [storedLocalObjectStates objectForKey:pithosContainer.name]; NSDirectoryEnumerator *dirEnumerator = [fileManager enumeratorAtPath:containerDirectoryPath]; for (NSString *objectName in dirEnumerator) { if (operation.isCancelled) { operation.completionBlock = nil; [self saveLocalState]; [self syncOperationFinishedWithSuccess:NO]; [pool drain]; 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]) { dispatch_async(dispatch_get_main_queue(), ^{ [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) { NSArray *pathComponents = [objectName pathComponents]; if ([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 && containerExludeRootFiles) { // 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]; [pool drain]; return; } // Add an empty stored state for legal new remote objects since last sync for (ASIPithosContainer *pithosContainer in pithosContainers) { NSArray *containerExcludedDirectories = [containersDictionary objectForKey:pithosContainer.name]; BOOL containerExludeRootFiles = [containerExcludedDirectories containsObject:@""]; NSMutableDictionary *containerStoredLocalObjectStates = [storedLocalObjectStates objectForKey:pithosContainer.name]; NSMutableDictionary *containerRemoteObjects = [remoteObjects objectForKey:pithosContainer.name]; for (NSString *objectName in containerRemoteObjects) { if (operation.isCancelled) { operation.completionBlock = nil; [self saveLocalState]; [self syncOperationFinishedWithSuccess:NO]; [pool drain]; 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 ([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 (containerExludeRootFiles && ([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]; [pool drain]; 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 (ASIPithosContainer *pithosContainer in pithosContainers) { NSString *containerDirectoryPath = [directoryPath stringByAppendingPathComponent:pithosContainer.name]; NSArray *containerExcludedDirectories = [containersDictionary objectForKey:pithosContainer.name]; BOOL containerExludeRootFiles = [containerExcludedDirectories containsObject:@""]; NSMutableDictionary *containerStoredLocalObjectStates = [storedLocalObjectStates objectForKey:pithosContainer.name]; NSMutableDictionary *containerRemoteObjects = [remoteObjects objectForKey:pithosContainer.name]; for (NSString *objectName in [[containerStoredLocalObjectStates allKeys] sortedArrayUsingSelector:@selector(compare:)]) { if (operation.isCancelled) { operation.completionBlock = nil; [self syncOperationFinishedWithSuccess:NO]; [pool drain]; return; } NSString *filePath = [containerDirectoryPath stringByAppendingPathComponent:objectName]; if ([objectName hasSuffix:@"/"]) filePath = [filePath stringByAppendingString:@":"]; ASIPithosObject *object = [ASIPithosObject object]; object.name = objectName; NSLog(@"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 ([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 (containerExludeRootFiles && ([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]; NSLog(@"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 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 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.", 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.", 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.", pithosContainer.name, object.name]; firstButtonText = @"Keep server version"; secondButtonText = @"Keep local version"; } NSAlert *alert = [[[NSAlert alloc] init] autorelease]; [alert setMessageText:@"Conflict"]; [alert setInformativeText:informativeText]; [alert addButtonWithTitle:firstButtonText]; [alert addButtonWithTitle:secondButtonText]; [alert addButtonWithTitle:@"Do nothing"]; NSInteger 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 pithosContainer:pithosContainer]; } if (choice == NSAlertSecondButtonReturn) { if (currentLocalObjectState.isDirectory) object.contentType = @"application/directory"; else object.objectHash = currentLocalObjectState.hash; [self updateServerStateWithCurrentState:currentLocalObjectState object:object localFilePath:filePath pithosContainer:pithosContainer]; } } } } } } [self syncOperationFinishedWithSuccess:YES]; } else { [self listRequestFailed:containerRequest]; } [pool drain]; } - (void)listRequestFailed:(ASIPithosContainerRequest *)containerRequest { NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; NSOperation *operation = [containerRequest.userInfo objectForKey:@"operation"]; if (operation.isCancelled) { [objects release]; objects = nil; [pool drain]; return; } if (containerRequest.isCancelled) { dispatch_async(dispatch_get_main_queue(), ^{ [activityFacility endActivity:[containerRequest.userInfo objectForKey:@"activity"] withMessage:[containerRequest.userInfo objectForKey:@"stoppedActivityMessage"]]; }); [objects release]; objects = nil; [self syncOperationFinishedWithSuccess:NO]; [pool drain]; 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] autorelease]; [(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 release]; objects = nil; // Since the server listing failed in all retries, the operation finished and the sync cycle is completeted unsuccesfully [self syncOperationFinishedWithSuccess:NO]; } [pool drain]; } - (void)downloadObjectBlockFinished:(ASIPithosObjectRequest *)objectRequest { NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; NSOperation *operation = [objectRequest.userInfo objectForKey:@"operation"]; NSLog(@"Sync::download object block finished: %@", objectRequest.url); if (operation.isCancelled) { [self requestFailed:objectRequest]; } else if (objectRequest.responseStatusCode == 206) { 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]; [pool drain]; return; } PithosLocalObjectState *storedState = [[storedLocalObjectStates 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) { dispatch_async(dispatch_get_main_queue(), ^{ [PithosUtilities fileActionFailedAlertWithTitle:@"Create Temporary File Error" message:[NSString stringWithFormat:@"Cannot create temporary file at '%@'", tempFilePath] error:nil]; [activityFacility endActivity:activity withMessage:[objectRequest.userInfo objectForKey:@"failedActivityMessage"]]; }); [self syncOperationFinishedWithSuccess:NO]; [pool drain]; 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 pithosContainer:pithosContainer]) { dispatch_async(dispatch_get_main_queue(), ^{ [activityFacility endActivity:activity withMessage:[objectRequest.userInfo objectForKey:@"failedActivityMessage"]]; }); [self syncOperationFinishedWithSuccess:NO]; [pool drain]; 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) { dispatch_async(dispatch_get_main_queue(), ^{ [PithosUtilities fileActionFailedAlertWithTitle:@"Create Directory Error" message:[NSString stringWithFormat:@"Cannot create directory at '%@'", dirPath] error:error]; [activityFacility endActivity:[objectRequest.userInfo objectForKey:@"activity"] withMessage:[objectRequest.userInfo objectForKey:@"failedActivityMessage"]]; }); [self syncOperationFinishedWithSuccess:NO]; [pool drain]; return; } } // Move file from tmp download error = nil; [fileManager moveItemAtPath:storedState.tmpFilePath toPath:filePath error:&error]; if (error != nil) { dispatch_async(dispatch_get_main_queue(), ^{ [PithosUtilities fileActionFailedAlertWithTitle:@"Move File Error" message:[NSString stringWithFormat:@"Cannot move file at '%@' to '%@'", storedState.tmpFilePath, filePath] error:error]; [activityFacility endActivity:activity withMessage:[objectRequest.userInfo objectForKey:@"failedActivityMessage"]]; }); [self syncOperationFinishedWithSuccess:NO]; [pool drain]; 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]; [pool drain]; 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]; 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:@"Sync: Downloading '%@/%@' (%.0f%%)", newObjectRequest.containerName, object.name, (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]; } [pool drain]; } - (void)downloadObjectHashMapFinished:(ASIPithosObjectRequest *)objectRequest { NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; NSOperation *operation = [objectRequest.userInfo objectForKey:@"operation"]; NSLog(@"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 { ASIPithosContainer *pithosContainer = [objectRequest.userInfo objectForKey:@"pithosContainer"]; ASIPithosObject *object = [objectRequest.userInfo objectForKey:@"pithosObject"]; PithosLocalObjectState *storedState = [[storedLocalObjectStates 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:@"Sync: Downloading '%@/%@' (%.0f%%)", pithosContainer.name, object.name, (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]; 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:@"Sync: Downloading '%@/%@' (%.0f%%)", newObjectRequest.containerName, object.name, (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]; } [pool drain]; } - (void)uploadDirectoryObjectFinished:(ASIPithosObjectRequest *)objectRequest { NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; NSOperation *operation = [objectRequest.userInfo objectForKey:@"operation"]; NSLog(@"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:@"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]; } [pool drain]; } - (void)moveObjectToTrashFinished:(ASIPithosObjectRequest *)objectRequest { NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; NSOperation *operation = [objectRequest.userInfo objectForKey:@"operation"]; NSLog(@"Sync::move object to trash finished: %@", objectRequest.url); if (operation.isCancelled) { [self requestFailed:objectRequest]; } else if (objectRequest.responseStatusCode == 201) { [[storedLocalObjectStates 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]; } [pool drain]; } - (void)deleteObjectFinished:(ASIPithosObjectRequest *)objectRequest { NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; NSOperation *operation = [objectRequest.userInfo objectForKey:@"operation"]; NSLog(@"Sync::delete object finished: %@", objectRequest.url); if (operation.isCancelled) { [self requestFailed:objectRequest]; } else if (objectRequest.responseStatusCode == 204) { [[storedLocalObjectStates 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]; } [pool drain]; } - (void)uploadObjectUsingHashMapFinished:(ASIPithosObjectRequest *)objectRequest { NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; NSOperation *operation = [objectRequest.userInfo objectForKey:@"operation"]; NSLog(@"Sync::upload using hashmap finished: %@", objectRequest.url); ASIPithosContainer *pithosContainer = [objectRequest.userInfo objectForKey:@"pithosContainer"]; ASIPithosObject *object = [objectRequest.userInfo objectForKey:@"pithosObject"]; PithosLocalObjectState *storedState = [[storedLocalObjectStates 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) { NSLog(@"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) { NSLog(@"Sync::upload iteration limit reached: %@", objectRequest.url); dispatch_async(dispatch_get_main_queue(), ^{ [activityFacility endActivity:activity withMessage:[objectRequest.userInfo objectForKey:@"stoppedActivityMessage"]]; }); [self syncOperationFinishedWithSuccess:NO]; [pool drain]; return; } NSLog(@"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:@"Sync: Uploading '%@/%@' (%.0f%%)", pithosContainer.name, object.name, (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:nil]; 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:@"Sync: Uploading '%@/%@' (%.0f%%)", newContainerRequest.containerName, object.name, (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]; } [pool drain]; } - (void)uploadMissingBlockFinished:(ASIPithosContainerRequest *)containerRequest { NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; NSOperation *operation = [containerRequest.userInfo objectForKey:@"operation"]; NSLog(@"Sync::upload of missing block finished: %@", containerRequest.url); if (operation.isCancelled) { [self requestFailed:containerRequest]; } else if (containerRequest.responseStatusCode == 202) { 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:nil]; 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:nil]; 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:@"Sync: Uploading '%@/%@' (%.0f%%)", newContainerRequest.containerName, object.name, (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]; } [pool drain]; } - (void)requestFailed:(ASIPithosRequest *)request { NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; NSOperation *operation = [request.userInfo objectForKey:@"operation"]; NSLog(@"Sync::request failed: %@", request.url); if (operation.isCancelled) { [pool drain]; 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]; [pool drain]; return; } NSUInteger retries = [[request.userInfo objectForKey:@"retries"] unsignedIntegerValue]; if (retries > 0) { ASIPithosRequest *newRequest = (ASIPithosRequest *)[[PithosUtilities copyRequest:request] autorelease]; [(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]; } [pool drain]; } @end