X-Git-Url: https://code.grnet.gr/git/pithos-macos/blobdiff_plain/d7a69f390a871496ab56d87bf7edd722b4e37bb7..ca913781230d971ba51fc78ece6215fb1911315c:/pithos-macos/PithosSyncDaemon.m diff --git a/pithos-macos/PithosSyncDaemon.m b/pithos-macos/PithosSyncDaemon.m index 3db6144..5cf5606 100644 --- a/pithos-macos/PithosSyncDaemon.m +++ b/pithos-macos/PithosSyncDaemon.m @@ -2,7 +2,7 @@ // PithosSyncDaemon.m // pithos-macos // -// Copyright 2011 GRNET S.A. All rights reserved. +// 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 @@ -36,249 +36,257 @@ // 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) -- (NSString *)pithosStateFilePath; +- (void)loadLocalState; +- (void)resetLocalStateWithAll:(BOOL)all; - (void)saveLocalState; -- (BOOL)moveToTempTrashFile:(NSString *)filePath; +- (BOOL)moveToTempTrashFile:(NSString *)filePath pithosContainer:(ASIPithosContainer *)pithosContainer; - (void)emptyTempTrash; - (BOOL)findLocalCopyForObjectWithHash:(NSString *)hash forFile:(NSString *)filePath; -- (void)updateLocalStateWithObject:(ASIPithosObject *)object localFilePath:(NSString *)filePath; +- (void)updateLocalStateWithObject:(ASIPithosObject *)object + localFilePath:(NSString *)filePath + pithosContainer:(ASIPithosContainer *)pithosContainer; - (void)updateServerStateWithCurrentState:(PithosLocalObjectState *)currentState object:(ASIPithosObject *)object - localFilePath:(NSString *)filePath; + localFilePath:(NSString *)filePath + pithosContainer:(ASIPithosContainer *)pithosContainer; +- (void)listRequestFailed:(ASIPithosContainerRequest *)containerRequest; - (void)requestFailed:(ASIPithosRequest *)request; -- (void)increaseSyncOperationCount; -- (void)decreaseSyncOperationCount; +- (void)syncOperationStarted; +- (void)syncOperationFinishedWithSuccess:(BOOL)operationSuccessfull; @end @implementation PithosSyncDaemon -@synthesize blockHash, blockSize, lastModified, lastCompletedSync, remoteObjects, storedLocalObjectStates, currentLocalObjectStates; -@synthesize directoryPath, containerDirectoryPath, pithosStateFilePath, tempDownloadsDirPath, tempTrashDirPath; +@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 - containerName:(NSString *)aContainerName - timeInterval:(NSTimeInterval)aTimeInterval + pithosAccount:(PithosAccount *)aPithosAccount + containersDictionary:(NSDictionary *)aContainersDictionary resetLocalState:(BOOL)resetLocalState { if ((self = [super init])) { directoryPath = [aDirectoryPath copy]; - containerName = [aContainerName copy]; - timeInterval = aTimeInterval; + pithosAccount = [aPithosAccount retain]; + containersDictionary = [aContainersDictionary copy]; + self.pithos = pithosAccount.pithos; - syncOperationCount = 0; - newSyncRequested = NO; - containerDirectoryPath = [[directoryPath stringByAppendingPathComponent:containerName] copy]; + 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]; - NSFileManager *fileManager = [NSFileManager defaultManager]; - if (resetLocalState) { - NSError *error = nil; - if ([fileManager fileExistsAtPath:self.pithosStateFilePath] && - (![fileManager removeItemAtPath:self.pithosStateFilePath error:&error] || error)) - [PithosUtilities fileActionFailedAlertWithTitle:@"Remove File Error" - message:[NSString stringWithFormat:@"Cannot remove file at '%@'", self.pithosStateFilePath] - error:error]; - 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; - } - } - } - if ([[NSFileManager defaultManager] fileExistsAtPath:self.pithosStateFilePath]) { - NSData *data = [NSData dataWithContentsOfFile:self.pithosStateFilePath]; - NSKeyedUnarchiver *unarchiver = [[[NSKeyedUnarchiver alloc] initForReadingWithData:data] autorelease]; - self.storedLocalObjectStates = [unarchiver decodeObjectForKey:containerName]; - [unarchiver finishDecoding]; - } else { - self.storedLocalObjectStates = [NSMutableDictionary dictionary]; - } + if (resetLocalState) + [self resetLocalStateWithAll:YES]; + else + [self resetLocalStateWithAll:NO]; networkQueue = [[ASINetworkQueue alloc] init]; networkQueue.showAccurateProgress = YES; networkQueue.shouldCancelAllRequestsOnFailure = NO; - [networkQueue go]; +// 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]; - timer = [[NSTimer scheduledTimerWithTimeInterval:timeInterval target:self selector:@selector(sync) userInfo:nil repeats:YES] retain]; - [timer fire]; + syncOperationCount = 0; + + @synchronized(self) { + daemonActive = NO; } +} + +- (void)startDaemon { + @synchronized(self) { + if (daemonActive) + return; + } + + // In the improbable case of leftover operations + [networkQueue reset]; + [callbackQueue cancelAllOperations]; - return self; + 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]; - [networkQueue cancelAllOperations]; + [self resetDaemon]; + [callbackQueue release]; [networkQueue release]; - [timer invalidate]; - [timer release]; [tempTrashDirPath release]; [tempDownloadsDirPath release]; [pithosStateFilePath release]; - [containerDirectoryPath release]; [currentLocalObjectStates release]; [storedLocalObjectStates release]; + [previousRemoteObjects release]; [remoteObjects release]; [objects release]; [lastCompletedSync release]; - [lastModified release]; - [blockHash release]; - [containerName release]; + [pithosContainers release]; + [pithos release]; + [containersDictionary release]; + [pithosAccount release]; [directoryPath release]; [super dealloc]; } #pragma mark - -#pragma mark Background - -- (void)fileActionFailedAlert:(NSDictionary *)args { - [PithosUtilities fileActionFailedAlertWithTitle:[args objectForKey:@"title"] - message:[args objectForKey:@"message"] - error:[args objectForKey:@"error"]]; -} - -- (void)fileActionFailedAlertWithTitle:(NSString *)title - message:(NSString *)message - error:(NSError *)error { - NSMutableDictionary *args = [NSMutableDictionary dictionaryWithObjectsAndKeys: - title, @"title", - message, @"message", - nil]; - if (error) - [args setObject:error forKey:@"error"]; - [self performSelectorOnMainThread:@selector(fileActionFailedAlert:) - withObject:args - waitUntilDone:YES]; -} - -- (void)startAndEndActivity:(NSDictionary *)args { - [activityFacility startAndEndActivityWithType:[[args objectForKey:@"type"] intValue] message:[args objectForKey:@"message"]]; -} - -- (void)startAndEndActivityWithType:(PithosActivityType)type message:(NSString *)message { - NSDictionary *args = [NSDictionary dictionaryWithObjectsAndKeys: - [NSNumber numberWithInt:type], @"type", - message, @"message", - nil]; - [self performSelectorOnMainThread:@selector(startAndEndActivity:) - withObject:args - waitUntilDone:YES]; -} - -- (void)updateActivity:(NSDictionary *)args { - NSNumber *totalBytesNumber = [args objectForKey:@"totalBytes"]; - NSNumber *currentBytesNumber = [args objectForKey:@"currentBytes"]; - if (totalBytesNumber && currentBytesNumber) - [activityFacility updateActivity:[args objectForKey:@"activity"] - withMessage:[args objectForKey:@"message"] - totalBytes:[totalBytesNumber unsignedIntegerValue] - currentBytes:[currentBytesNumber unsignedIntegerValue]]; - else - [activityFacility updateActivity:[args objectForKey:@"activity"] - withMessage:[args objectForKey:@"message"]]; -} - -- (void)updateActivity:(PithosActivity *)activity - withMessage:(NSString *)message - totalBytes:(NSUInteger)totalBytes - currentBytes:(NSUInteger)currentBytes { - NSMutableDictionary *args = [NSMutableDictionary dictionaryWithObjectsAndKeys: - activity, @"activity", - [NSNumber numberWithUnsignedInteger:totalBytes], @"totalBytes", - [NSNumber numberWithUnsignedInteger:currentBytes], @"currentBytes", - nil]; - if (message) - [args setObject:message forKey:@"message"]; - [self performSelectorOnMainThread:@selector(updateActivity:) - withObject:args - waitUntilDone:YES]; -} - -- (void)updateActivity:(PithosActivity *)activity withMessage:(NSString *)message { - NSMutableDictionary *args = [NSMutableDictionary dictionaryWithObjectsAndKeys: - activity, @"activity", - nil]; - if (message) - [args setObject:message forKey:@"message"]; - [self performSelectorOnMainThread:@selector(updateActivity:) - withObject:args - waitUntilDone:YES]; -} - -- (void)endActivity:(NSDictionary *)args { - NSNumber *totalBytesNumber = [args objectForKey:@"totalBytes"]; - NSNumber *currentBytesNumber = [args objectForKey:@"currentBytes"]; - if (totalBytesNumber && currentBytesNumber) - [activityFacility endActivity:[args objectForKey:@"activity"] - withMessage:[args objectForKey:@"message"] - totalBytes:[totalBytesNumber unsignedIntegerValue] - currentBytes:[currentBytesNumber unsignedIntegerValue]]; - else - [activityFacility endActivity:[args objectForKey:@"activity"] - withMessage:[args objectForKey:@"message"]]; -} - -- (void)endActivity:(PithosActivity *)activity - withMessage:(NSString *)message - totalBytes:(NSUInteger)totalBytes - currentBytes:(NSUInteger)currentBytes { - NSMutableDictionary *args = [NSMutableDictionary dictionaryWithObjectsAndKeys: - activity, @"activity", - [NSNumber numberWithUnsignedInteger:totalBytes], @"totalBytes", - [NSNumber numberWithUnsignedInteger:currentBytes], @"currentBytes", - nil]; - if (message) - [args setObject:message forKey:@"message"]; - [self performSelectorOnMainThread:@selector(endActivity:) - withObject:args - waitUntilDone:YES]; -} - -- (void)endActivity:(PithosActivity *)activity withMessage:(NSString *)message { - NSMutableDictionary *args = [NSMutableDictionary dictionaryWithObjectsAndKeys: - activity, @"activity", - nil]; - if (message) - [args setObject:message forKey:@"message"]; - [self performSelectorOnMainThread:@selector(endActivity:) - withObject:args - waitUntilDone:YES]; -} - -#pragma mark - #pragma mark Observers - (void)applicationWillTerminate:(NSNotification *)notification { @@ -289,173 +297,245 @@ #pragma mark Properties - (NSString *)pithosStateFilePath { - if (!pithosStateFilePath) + if (!pithosStateFilePath) { pithosStateFilePath = [[[[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) objectAtIndex:0] stringByAppendingPathComponent:[[NSBundle mainBundle] bundleIdentifier]] - stringByAppendingPathComponent:@"PithosLocalObjectStates.archive"] retain]; + 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 { - NSFileManager *fileManager = [NSFileManager defaultManager]; - if (!tempDownloadsDirPath || ![fileManager fileExistsAtPath:tempDownloadsDirPath]) { - // Get the path from user defaults - NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults]; - tempDownloadsDirPath = [userDefaults stringForKey:@"PithosSyncTempDownloadsDirPath"]; - if (tempDownloadsDirPath) { - // Check if the path exists - 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; - } - if (!tempDownloadsDirPath) { - NSString *tempDirTemplate = [[[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) objectAtIndex:0] - stringByAppendingPathComponent:[[NSBundle mainBundle] bundleIdentifier]] - stringByAppendingPathComponent:@"Temp Downloads XXXXXX"]; - const char *tempDirTemplateCString = [tempDirTemplate fileSystemRepresentation]; - char *tempDirNameCString = (char *)malloc(strlen(tempDirTemplateCString) + 1); - strcpy(tempDirNameCString, tempDirTemplateCString); - tempDirNameCString = mkdtemp(tempDirNameCString); - if (tempDirNameCString != NULL) - tempDownloadsDirPath = [fileManager stringWithFileSystemRepresentation:tempDirNameCString length:strlen(tempDirNameCString)]; - free(tempDirNameCString); - } - if (tempDownloadsDirPath) - [userDefaults setObject:tempDownloadsDirPath forKey:@"PithosSyncTempDownloadsDirPath"]; - [tempDownloadsDirPath retain]; + 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 copy] autorelease]; + return tempDownloadsDirPath; } - (NSString *)tempTrashDirPath { - NSFileManager *fileManager = [NSFileManager defaultManager]; - if (!tempTrashDirPath || ![fileManager fileExistsAtPath:tempTrashDirPath]) { - // Get the path from user defaults - NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults]; - tempTrashDirPath = [userDefaults stringForKey:@"PithosSyncTempTrashDirPath"]; - if (tempTrashDirPath) { - // Check if the path exists - 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; - } - if (!tempTrashDirPath) { - NSString *tempDirTemplate = [[[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) objectAtIndex:0] - stringByAppendingPathComponent:[[NSBundle mainBundle] bundleIdentifier]] - stringByAppendingPathComponent:@"Temp Trash XXXXXX"]; - const char *tempDirTemplateCString = [tempDirTemplate fileSystemRepresentation]; - char *tempDirNameCString = (char *)malloc(strlen(tempDirTemplateCString) + 1); - strcpy(tempDirNameCString, tempDirTemplateCString); - tempDirNameCString = mkdtemp(tempDirNameCString); - if (tempDirNameCString != NULL) - tempTrashDirPath = [fileManager stringWithFileSystemRepresentation:tempDirNameCString length:strlen(tempDirNameCString)]; - free(tempDirNameCString); + 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]; } - if (tempTrashDirPath) - [userDefaults setObject:tempTrashDirPath forKey:@"PithosSyncTempTrashDirPath"]; - [tempTrashDirPath retain]; + [self resetLocalStateWithAll:NO]; } - return [[tempTrashDirPath copy] autorelease]; +} + +- (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)saveLocalState { - NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; - NSMutableData *data = [NSMutableData data]; - NSKeyedArchiver *archiver = [[[NSKeyedArchiver alloc] initForWritingWithMutableData:data] autorelease]; - [archiver encodeObject:storedLocalObjectStates forKey:containerName]; - [archiver finishEncoding]; - [data writeToFile:self.pithosStateFilePath atomically:YES]; - [pool drain]; -} - -- (void)increaseSyncOperationCount { +- (void)syncOperationStarted { @synchronized(self) { syncOperationCount++; } } -- (void)decreaseSyncOperationCount { +- (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) { + if (syncOperationCount == 0) { if (!syncIncomplete) { self.lastCompletedSync = [NSDate date]; - [self startAndEndActivityWithType:PithosActivityOther - message:[NSString stringWithFormat:@"Sync: Completed %@", lastCompletedSync]]; + 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 (syncOperationCount) { + if ([self isSyncing]) { // If at least one operation is running return newSyncRequested = YES; return; - } else { + } else if (daemonActive && containersCount) { // The first operation is the server listing - syncOperationCount = 1; + [self syncOperationStarted]; newSyncRequested = NO; syncIncomplete = NO; + syncLate = NO; + } else { + return; } } NSFileManager *fileManager = [NSFileManager defaultManager]; BOOL isDirectory; NSError *error = nil; - if (![fileManager fileExistsAtPath:containerDirectoryPath isDirectory:&isDirectory]) { - if (![fileManager createDirectoryAtPath:containerDirectoryPath withIntermediateDirectories:YES attributes:nil error:&error] || + 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 '%@'", containerDirectoryPath] + message:[NSString stringWithFormat:@"Cannot create local sync directory at '%@'", directoryPath] error:error]; - @synchronized(self) { - syncOperationCount = 0; - } + [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] + message:[NSString stringWithFormat:@"File already exists at the local sync directory path at '%@'", directoryPath] error:nil]; - @synchronized(self) { - syncOperationCount = 0; - } + [self syncOperationFinishedWithSuccess:NO]; return; } - - ASIPithosContainerRequest *containerRequest = [ASIPithosContainerRequest listObjectsRequestWithContainerName:containerName - limit:0 - marker:nil - prefix:nil - delimiter:nil - path:nil - meta:nil - shared:NO - until:nil - ifModifiedSince:lastModified]; + 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"]; + message:@"Sync: Getting server listing" + pithosAccount:pithosAccount]; containerRequest.userInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys: activity, @"activity", @"Sync: Getting server listing (stopped)", @"stoppedActivityMessage", @@ -478,9 +558,11 @@ // NSArray *subPaths = [[NSFileManager defaultManager] subpathsOfDirectoryAtPath:trashDirPath error:&error]; NSArray *subPaths = [fileManager contentsOfDirectoryAtPath:trashDirPath error:&error]; if (error) { - [self fileActionFailedAlertWithTitle:@"Directory Contents Error" - message:[NSString stringWithFormat:@"Cannot get contents of directory at '%@'", containerDirectoryPath] - error: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; } @@ -489,32 +571,35 @@ // for (NSString *subPath in subPaths) { // [subURLs addObject:[NSURL fileURLWithPath:[trashDirPath stringByAppendingPathComponent:subPath]]]; // } -// syncOperationCount = 1; +// [self syncOperationStarted]; // [[NSWorkspace sharedWorkspace] recycleURLs:subURLs completionHandler:^(NSDictionary *newURLs, NSError *error) { // if (error) { -// [self fileActionFailedAlertWithTitle:@"Move to Trash Error" -// message:@"Cannot move files to Trash" -// error:error]; +// dispatch_async(dispatch_get_main_queue(), ^{ +// [PithosUtilities fileActionFailedAlertWithTitle:@"Move to Trash Error" +// message:@"Cannot move files to Trash" +// error:error]; +// }); // } -// syncOperationCount = 0; +// [self syncOperationFinishedWithSuccess:YES]; // }]; for (NSString *subPath in subPaths) { NSString *subFilePath = [trashDirPath stringByAppendingPathComponent:subPath]; error = nil; if (![fileManager removeItemAtPath:subFilePath error:&error] || error) - [self fileActionFailedAlertWithTitle:@"Remove File Error" - message:[NSString stringWithFormat:@"Cannot remove file at '%@'", subFilePath] - 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 { +- (BOOL)moveToTempTrashFile:(NSString *)filePath pithosContainer:(ASIPithosContainer *)pithosContainer { NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; - NSString *trashDirPath = self.tempTrashDirPath; - if (!tempTrashDirPath) { + if (!self.tempTrashDirPath) { [pool drain]; return NO; } @@ -522,30 +607,36 @@ BOOL isDirectory; BOOL fileExists = [fileManager fileExistsAtPath:filePath isDirectory:&isDirectory]; NSError *error = nil; - NSString *newFilePath = [filePath stringByReplacingOccurrencesOfString:containerDirectoryPath - withString:trashDirPath]; + 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) { - [self fileActionFailedAlertWithTitle:@"Directory Contents Error" - message:[NSString stringWithFormat:@"Cannot get contents of directory at '%@'", containerDirectoryPath] - error: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) { - [self fileActionFailedAlertWithTitle:@"Create Directory Error" - message:[NSString stringWithFormat:@"Cannot create directory at '%@'", newDirPath] - 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) { - [self fileActionFailedAlertWithTitle:@"Move File Error" - message:[NSString stringWithFormat:@"Cannot move file at '%@' to '%@'", - filePath, newFilePath] - 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; } @@ -556,14 +647,14 @@ [currentLocalObjectStates removeObjectForKey:filePath]; } else { [currentLocalObjectStates setObject:[PithosLocalObjectState localObjectStateWithFile:newFilePath - blockHash:blockHash - blockSize:blockSize] + blockHash:pithosContainer.blockHash + blockSize:pithosContainer.blockSize] forKey:newFilePath]; } for (NSString *subPath in subPaths) { NSString *subFilePath = [filePath stringByAppendingPathComponent:subPath]; NSString *newSubFilePath = [subFilePath stringByReplacingOccurrencesOfString:containerDirectoryPath - withString:trashDirPath]; + withString:self.tempTrashDirPath]; currentState = [currentLocalObjectStates objectForKey:subFilePath]; if (currentState) { currentState.filePath = newSubFilePath; @@ -571,24 +662,28 @@ [currentLocalObjectStates removeObjectForKey:subFilePath]; } else { [currentLocalObjectStates setObject:[PithosLocalObjectState localObjectStateWithFile:newSubFilePath - blockHash:blockHash - blockSize:blockSize] + blockHash:pithosContainer.blockHash + blockSize:pithosContainer.blockSize] forKey:newSubFilePath]; } } } else if (fileExists && !isDirectory) { if (![fileManager createDirectoryAtPath:newDirPath withIntermediateDirectories:YES attributes:nil error:&error] || error) { - [self fileActionFailedAlertWithTitle:@"Create Directory Error" - message:[NSString stringWithFormat:@"Cannot create directory at '%@'", newDirPath] - 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) { - [self fileActionFailedAlertWithTitle:@"Move File Error" - message:[NSString stringWithFormat:@"Cannot move file at '%@' to '%@'", - filePath, newFilePath] - 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; } @@ -599,8 +694,8 @@ [currentLocalObjectStates removeObjectForKey:filePath]; } else { [currentLocalObjectStates setObject:[PithosLocalObjectState localObjectStateWithFile:newFilePath - blockHash:blockHash - blockSize:blockSize] + blockHash:pithosContainer.blockHash + blockSize:pithosContainer.blockSize] forKey:newFilePath]; } } @@ -609,36 +704,37 @@ } - (BOOL)findLocalCopyForObjectWithHash:(NSString *)hash forFile:(NSString *)filePath { - NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; - NSUInteger hashLength = [hash length]; - if ((hashLength != 32) && (hashLength != 64)) { - [pool drain]; + 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 allKeys]) { + for (NSString *localFilePath in currentLocalObjectStates) { localState = [currentLocalObjectStates objectForKey:localFilePath]; - if (!localState.isDirectory && ([hash isEqualToString:localState.md5] || [hash isEqualToString:localState.hashMapHash]) && + if (!localState.isDirectory && [hash isEqualToString:localState.hash] && [fileManager fileExistsAtPath:localFilePath isDirectory:&isDirectory] && !isDirectory) { - if ([localFilePath hasPrefix:containerDirectoryPath]) { + if ([localFilePath hasPrefix:directoryPath]) { if (![fileManager copyItemAtPath:localFilePath toPath:filePath error:&error] || error) { - [self fileActionFailedAlertWithTitle:@"Copy File Error" - message:[NSString stringWithFormat:@"Cannot copy file at '%@' to '%@'", - localFilePath, filePath] - 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) { - [self fileActionFailedAlertWithTitle:@"Move File Error" - message:[NSString stringWithFormat:@"Cannot move file at '%@' to '%@'", - localFilePath, filePath] - 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]; @@ -654,32 +750,40 @@ } - (void)updateLocalStateWithObject:(ASIPithosObject *)object - localFilePath:(NSString *)filePath { + 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]; - PithosLocalObjectState *storedState = [storedLocalObjectStates objectForKey:object.name]; + 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 objectExistsAtContainerName:containerName - objectName:object.name - error:&remoteError - isDirectory:&remoteIsDirectory - sharingAccount:nil]; - if (!object || !object.hash) { + 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]) { - [self startAndEndActivityWithType:PithosActivityOther - message:[NSString stringWithFormat:@"Sync: Deleting '%@' locally (finished)", object.name]]; - [storedLocalObjectStates removeObjectForKey:object.name]; + 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]) { @@ -692,15 +796,18 @@ } NSLog(@"Sync::create local directory object: %@", filePath); BOOL directoryCreated = NO; - if (!fileExists || (!isDirectory && [self moveToTempTrashFile:filePath])) { + 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) { - [self fileActionFailedAlertWithTitle:@"Create Directory Error" - message:[NSString stringWithFormat:@"Cannot create directory at '%@'", filePath] - 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]; } @@ -709,8 +816,12 @@ directoryCreated = YES; } if (directoryCreated) - [self startAndEndActivityWithType:PithosActivityOther - message:[NSString stringWithFormat:@"Sync: Creating directory '%@' locally (finished)", object.name]]; + 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) { @@ -721,16 +832,28 @@ } NSLog(@"Sync::create local zero length object: %@", filePath); BOOL fileCreated = NO; - if (!fileExists || ((isDirectory || [PithosUtilities bytesOfFile:filePath]) && [self moveToTempTrashFile:filePath])) { + 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]) { - [self fileActionFailedAlertWithTitle:@"Create File Error" - message:[NSString stringWithFormat:@"Cannot create file at '%@'", filePath] - error:error]; + 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.hash = object.hash; + storedState.filePath = filePath; + storedState.hash = object.objectHash; storedState.tmpFilePath = nil; [self saveLocalState]; } @@ -739,8 +862,12 @@ fileCreated = YES; } if (fileCreated) - [self startAndEndActivityWithType:PithosActivityOther - message:[NSString stringWithFormat:@"Sync: Downloading '%@' (100%%)", object.name]]; + 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) { @@ -749,33 +876,55 @@ [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.hash forFile:filePath]) { - [self startAndEndActivityWithType:PithosActivityOther - message:[NSString stringWithFormat:@"Sync: Downloading '%@' (100%%)", object.name]]; + 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 increaseSyncOperationCount]; - __block ASIPithosObjectRequest *objectRequest = [PithosUtilities objectBlockDataRequestWithContainerName:containerName - object:object - blockIndex:0 - blockSize:blockSize]; + [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%%)", object.name] + message:[NSString stringWithFormat:@"Sync: Downloading '%@/%@' (0%%)", + pithosContainer.name, object.name] totalBytes:object.bytes - currentBytes:0]; - [self updateActivity:activity withMessage:activity.message]; + 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)/(blockSize + 0.0)))], @"missingBlocks", + [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)", object.name], @"stoppedActivityMessage", - [NSString stringWithFormat:@"Sync: Downloading '%@' (failed)", object.name], @"failedActivityMessage", - [NSString stringWithFormat:@"Sync: Downloading '%@' (100%%)", object.name], @"finishedActivityMessage", + [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", @@ -783,8 +932,9 @@ nil]; [objectRequest setBytesReceivedBlock:^(unsigned long long size, unsigned long long total){ [activityFacility updateActivity:activity - withMessage:[NSString stringWithFormat:@"Sync: Downloading '%@' (%.0f%%)", - [[objectRequest.userInfo valueForKey:@"pithosObject"] name], + 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)]; @@ -799,32 +949,53 @@ [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.hash forFile:filePath]) { - [self startAndEndActivityWithType:PithosActivityOther - message:[NSString stringWithFormat:@"Sync: Downloading '%@' (100%%)", object.name]]; + if ([self findLocalCopyForObjectWithHash:object.objectHash forFile:filePath]) { + storedState.filePath = filePath; + storedState.hash = object.objectHash; // Delete incomplete temp download - error = nil; 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 increaseSyncOperationCount]; - ASIPithosObjectRequest *objectRequest = [ASIPithosObjectRequest objectHashmapRequestWithContainerName:containerName - objectName:object.name]; + [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%%)", object.name] + message:[NSString stringWithFormat:@"Sync: Downloading '%@/%@' (0%%)", + pithosContainer.name, object.name] totalBytes:object.bytes - currentBytes:0]; - [self updateActivity:activity withMessage:activity.message]; + 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)", object.name], @"stoppedActivityMessage", - [NSString stringWithFormat:@"Sync: Downloading '%@' (failed)", object.name], @"failedActivityMessage", - [NSString stringWithFormat:@"Sync: Downloading '%@' (100%%)", object.name], @"finishedActivityMessage", + [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", @@ -838,9 +1009,10 @@ -(void)updateServerStateWithCurrentState:(PithosLocalObjectState *)currentState object:(ASIPithosObject *)object - localFilePath:(NSString *)filePath { + localFilePath:(NSString *)filePath + pithosContainer:(ASIPithosContainer *)pithosContainer { NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; - [self increaseSyncOperationCount]; + [self syncOperationStarted]; NSFileManager *fileManager = [NSFileManager defaultManager]; BOOL isDirectory; BOOL fileExists = [fileManager fileExistsAtPath:filePath isDirectory:&isDirectory]; @@ -848,92 +1020,129 @@ // 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 - syncIncomplete = YES; - [self decreaseSyncOperationCount]; + [self syncOperationFinishedWithSuccess:NO]; [pool drain]; return; } - ASIPithosObjectRequest *objectRequest = [ASIPithosObjectRequest writeObjectDataRequestWithContainerName:containerName - objectName:object.name - eTag:nil - contentType:@"application/directory" - contentEncoding:nil - contentDisposition:nil - manifest:nil - sharing:nil - isPublic:ASIPithosObjectRequestPublicIgnore - metadata:nil - data:[NSData data]]; + 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 '%@'", object.name]]; - [self updateActivity:activity withMessage:activity.message]; + 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)", object.name], @"stoppedActivityMessage", - [NSString stringWithFormat:@"Sync: Creating directory '%@' (failed)", object.name], @"failedActivityMessage", - [NSString stringWithFormat:@"Sync: Creating directory '%@' (finished)", object.name], @"finishedActivityMessage", + [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) { + } 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; } - NSString *safeName; - if ([PithosUtilities isContentTypeDirectory:object.contentType]) - safeName = [PithosUtilities safeSubdirNameForContainerName:@"trash" - subdirName:object.name]; - else - safeName = [PithosUtilities safeObjectNameForContainerName:@"trash" - objectName:object.name]; - if (safeName) { - ASIPithosObjectRequest *objectRequest = [PithosUtilities moveObjectRequestWithContainerName:containerName - 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 '%@'", object.name]]; - [self updateActivity:activity withMessage:activity.message]; - objectRequest.userInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys: - object, @"pithosObject", - activity, @"activity", - [NSString stringWithFormat:@"Sync: Moving to trash '%@' (stopped)", object.name], @"stoppedActivityMessage", - [NSString stringWithFormat:@"Sync: Moving to trash '%@' (failed)", object.name], @"failedActivityMessage", - [NSString stringWithFormat:@"Sync: Moving to trash '%@' (finished)", 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]]; + 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 { - syncIncomplete = YES; - [self decreaseSyncOperationCount]; + [self syncOperationFinishedWithSuccess:NO]; } - } else { - syncIncomplete = YES; - [self decreaseSyncOperationCount]; } } 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 - syncIncomplete = YES; - [self decreaseSyncOperationCount]; + [self syncOperationFinishedWithSuccess:NO]; [pool drain]; return; } @@ -944,26 +1153,32 @@ if (error) NSLog(@"contentType detection error: %@", error); NSArray *hashes = nil; - ASIPithosObjectRequest *objectRequest = [PithosUtilities writeObjectDataRequestWithContainerName:containerName - objectName:object.name - contentType:object.contentType - blockSize:blockSize - blockHash:blockHash - forFile:filePath - checkIfExists:NO - hashes:&hashes - sharingAccount: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%%)", object.name] - totalBytes:[[objectRequest.userInfo valueForKey:@"bytes"] unsignedIntegerValue] - currentBytes:0]; - [self updateActivity:activity withMessage:activity.message]; + 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", @@ -979,8 +1194,7 @@ nil]]; [networkQueue addOperation:[PithosUtilities prepareRequest:objectRequest priority:NSOperationQueuePriorityNormal]]; } else { - syncIncomplete = YES; - [self decreaseSyncOperationCount]; + [self syncOperationFinishedWithSuccess:NO]; } } [pool drain]; @@ -990,17 +1204,59 @@ #pragma mark ASIHTTPRequestDelegate - (void)performRequestFinishedDelegateInBackground:(ASIPithosRequest *)request { - [self performSelectorInBackground:NSSelectorFromString([request.userInfo objectForKey:@"didFinishSelector"]) withObject: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 { - [self performSelectorInBackground:NSSelectorFromString([request.userInfo objectForKey:@"didFailSelector"]) withObject: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 ((containerRequest.responseStatusCode == 200) || (containerRequest.responseStatusCode == 304)) { + 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) { @@ -1009,218 +1265,398 @@ [objects addObjectsFromArray:someObjects]; } if ([someObjects count] < 10000) { - self.blockHash = [containerRequest blockHash]; - self.blockSize = [containerRequest blockSize]; - self.lastModified = [containerRequest lastModified]; - self.remoteObjects = [NSMutableDictionary dictionaryWithCapacity:[objects count]]; + 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) { - [remoteObjects setObject:object forKey:object.name]; + [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 listObjectsRequestWithContainerName:containerName - limit:0 - marker:[[someObjects lastObject] name] - prefix:nil - delimiter:nil - path:nil - meta:nil - shared:NO - until:nil - ifModifiedSince:lastModified]; + 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 = newContainerRequest.userInfo; + 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]; } - [self endActivity:[containerRequest.userInfo objectForKey:@"activity"] - withMessage:[containerRequest.userInfo objectForKey:@"finishedActivityMessage"]]; + 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]; - NSError *error = nil; - NSArray *subPaths = [fileManager subpathsOfDirectoryAtPath:containerDirectoryPath error:&error]; - if (error) { - [self fileActionFailedAlertWithTitle:@"Directory Contents Error" - message:[NSString stringWithFormat:@"Cannot get contents of directory at '%@'", containerDirectoryPath] - error:error]; - [self startAndEndActivityWithType:PithosActivityOther message:@"Sync: Failed to read contents of sync directory"]; - @synchronized(self) { - // Since the local listing failed, the operation finished and the sync cycle is completeted unsuccessfully - syncOperationCount = 0; - if (newSyncRequested) - [self sync]; + + // 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; } - self.currentLocalObjectStates = [NSMutableDictionary dictionaryWithCapacity:[subPaths count]]; - for (NSString *objectName in subPaths) { - if (![storedLocalObjectStates objectForKey:objectName]) { - [storedLocalObjectStates setObject:[PithosLocalObjectState localObjectState] forKey:objectName]; + + // 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]; } - NSString *filePath = [containerDirectoryPath stringByAppendingPathComponent:objectName]; - [currentLocalObjectStates setObject:[PithosLocalObjectState localObjectStateWithFile:filePath - blockHash:blockHash - blockSize:blockSize] - forKey:filePath]; + [self saveLocalState]; } - [self saveLocalState]; - for (NSString *objectName in remoteObjects) { - if (![storedLocalObjectStates objectForKey:objectName]) - [storedLocalObjectStates setObject:[PithosLocalObjectState localObjectState] forKey:objectName]; + if (operation.isCancelled) { + operation.completionBlock = nil; + [self syncOperationFinishedWithSuccess:NO]; + [pool drain]; + return; } - for (NSString *objectName in [[storedLocalObjectStates allKeys] sortedArrayUsingSelector:@selector(compare:)]) { - NSString *filePath = [containerDirectoryPath stringByAppendingPathComponent:objectName]; - if ([objectName hasSuffix:@"/"]) - filePath = [filePath stringByAppendingString:@":"]; - ASIPithosObject *object = [ASIPithosObject object]; - object.name = objectName; - NSLog(@"Sync::object name: %@", objectName); + // 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 = [storedLocalObjectStates objectForKey:object.name]; - PithosLocalObjectState *currentLocalObjectState = [currentLocalObjectStates objectForKey:filePath]; - if (!currentLocalObjectState) - currentLocalObjectState = [PithosLocalObjectState localObjectStateWithFile:filePath - blockHash:blockHash - blockSize:blockSize]; - if (currentLocalObjectState.isDirectory) - object.contentType = @"application/directory"; + 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 = [remoteObjects objectForKey:objectName]; - if (remoteObject) { - if ([PithosUtilities isContentTypeDirectory:remoteObject.contentType]) { - remoteObjectState.isDirectory = YES; - object.contentType = @"application/directory"; - } else { - remoteObjectState.hash = remoteObject.hash; + 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.hash = remoteObject.hash; - [self updateLocalStateWithObject:object localFilePath:filePath]; - } 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 - [storedLocalObjectStates removeObjectForKey:objectName]; - [self saveLocalState]; - } - } else { - // Local state has changed - if (!serverStateHasChanged) { - // Server state hasn't changed - [self updateServerStateWithCurrentState:currentLocalObjectState - object:object - localFilePath:filePath]; - } else { - // Server state has also changed - if (remoteObjectState.isDirectory && currentLocalObjectState.isDirectory) { - // Both did the same change (directory) - storedLocalObjectState.isDirectory = YES; + 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 if ([remoteObjectState isEqualToState:currentLocalObjectState]) { - // Both did the same change (object edit or delete) - if (!remoteObjectState.exists) - [storedLocalObjectStates removeObjectForKey:object.name]; + } + } else { + // Local state has changed + if (!serverStateHasChanged) { + // Server state hasn't changed + if (currentLocalObjectState.isDirectory) + object.contentType = @"application/directory"; else - storedLocalObjectState.hash = remoteObjectState.hash; - [self saveLocalState]; + object.objectHash = currentLocalObjectState.hash; + [self updateServerStateWithCurrentState:currentLocalObjectState + object:object + localFilePath:filePath + pithosContainer:pithosContainer]; } 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.", 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.", object.name]; - firstButtonText = @"Download file from server"; - secondButtonText = @"Delete file on server"; + // 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 { - informativeText = [NSString stringWithFormat:@"'%@' has been modifed both locally and on the server.", 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.hash = remoteObject.hash; - [self updateLocalStateWithObject:object localFilePath:filePath]; - } if (choice == NSAlertSecondButtonReturn) { - [self updateServerStateWithCurrentState:currentLocalObjectState - object:object - localFilePath:filePath]; + // 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]; + } } } } } } - @synchronized(self) { - [self decreaseSyncOperationCount]; - if (newSyncRequested && !syncOperationCount) - [self sync]; - } + [self syncOperationFinishedWithSuccess:YES]; } else { - 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 { - [self endActivity:[containerRequest.userInfo objectForKey:@"activity"] - withMessage:[containerRequest.userInfo objectForKey:@"failedActivityMessage"]]; - @synchronized(self) { - // Since the server listing failed in all retries, the operation finished and the sync cycle is completeted unsuccesfully - syncOperationCount = 0; - if (newSyncRequested) - [self sync]; - } - } + [self listRequestFailed:containerRequest]; } [pool drain]; } - (void)listRequestFailed:(ASIPithosContainerRequest *)containerRequest { NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; - if ([containerRequest isCancelled]) { - [self endActivity:[containerRequest.userInfo objectForKey:@"activity"] - withMessage:[containerRequest.userInfo objectForKey:@"stoppedActivityMessage"]]; + NSOperation *operation = [containerRequest.userInfo objectForKey:@"operation"]; + if (operation.isCancelled) { [objects release]; objects = nil; - @synchronized(self) { - syncOperationCount = 0; - } + [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; } @@ -1231,24 +1667,26 @@ [(NSMutableDictionary *)(newContainerRequest.userInfo)setObject:[NSNumber numberWithUnsignedInteger:(--retries)] forKey:@"retries"]; [networkQueue addOperation:[PithosUtilities prepareRequest:newContainerRequest priority:[[newContainerRequest.userInfo objectForKey:@"priority"] integerValue]]]; } else { - [self endActivity:[containerRequest.userInfo objectForKey:@"activity"] - withMessage:[containerRequest.userInfo objectForKey:@"failedActivityMessage"]]; + dispatch_async(dispatch_get_main_queue(), ^{ + [activityFacility endActivity:[containerRequest.userInfo objectForKey:@"activity"] + withMessage:[containerRequest.userInfo objectForKey:@"failedActivityMessage"]]; + }); [objects release]; objects = nil; - @synchronized(self) { - // Since the server listing failed in all retries, the operation finished and the sync cycle is completeted unsuccesfully - syncOperationCount = 0; - if (newSyncRequested) - [self sync]; - } + // 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 (objectRequest.responseStatusCode == 206) { + 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; @@ -1256,19 +1694,16 @@ NSString *downloadsDirPath = self.tempDownloadsDirPath; if (!downloadsDirPath) { - [self endActivity:activity - withMessage:[objectRequest.userInfo objectForKey:@"failedActivityMessage"]]; - @synchronized(self) { - syncIncomplete = YES; - [self decreaseSyncOperationCount]; - if (newSyncRequested && !syncOperationCount) - [self sync]; - } + dispatch_async(dispatch_get_main_queue(), ^{ + [activityFacility endActivity:activity + withMessage:[objectRequest.userInfo objectForKey:@"failedActivityMessage"]]; + }); + [self syncOperationFinishedWithSuccess:NO]; [pool drain]; return; } - PithosLocalObjectState *storedState = [storedLocalObjectStates objectForKey:object.name]; + 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]; @@ -1278,17 +1713,14 @@ NSString *tempFilePath = [fileManager stringWithFileSystemRepresentation:tempFileNameCString length:strlen(tempFileNameCString)]; free(tempFileNameCString); if (fileDescriptor == -1) { - [self fileActionFailedAlertWithTitle:@"Create Temporary File Error" - message:[NSString stringWithFormat:@"Cannot create temporary file at '%@'", tempFilePath] - error:nil]; - [self endActivity:activity - withMessage:[objectRequest.userInfo objectForKey:@"failedActivityMessage"]]; - @synchronized(self) { - syncIncomplete = YES; - [self decreaseSyncOperationCount]; - if (newSyncRequested && !syncOperationCount) - [self sync]; - } + 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; } @@ -1300,7 +1732,7 @@ NSUInteger missingBlockIndex = [[objectRequest.userInfo objectForKey:@"missingBlockIndex"] unsignedIntegerValue]; NSFileHandle *tempFileHandle = [NSFileHandle fileHandleForWritingAtPath:storedState.tmpFilePath]; - [tempFileHandle seekToFileOffset:missingBlockIndex*blockSize]; + [tempFileHandle seekToFileOffset:missingBlockIndex*pithosContainer.blockSize]; [tempFileHandle writeData:[objectRequest responseData]]; [tempFileHandle closeFile]; @@ -1309,15 +1741,12 @@ if (missingBlockIndex == NSNotFound) { NSString *filePath = [objectRequest.userInfo objectForKey:@"filePath"]; NSString *dirPath = [filePath stringByDeletingLastPathComponent]; - if ([fileManager fileExistsAtPath:filePath] && ![self moveToTempTrashFile:filePath]) { - [self endActivity:activity - withMessage:[objectRequest.userInfo objectForKey:@"failedActivityMessage"]]; - @synchronized(self) { - syncIncomplete = YES; - [self decreaseSyncOperationCount]; - if (newSyncRequested && !syncOperationCount) - [self sync]; - } + 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]) { @@ -1327,17 +1756,14 @@ error = nil; [fileManager createDirectoryAtPath:dirPath withIntermediateDirectories:YES attributes:nil error:nil]; if (error != nil) { - [self fileActionFailedAlertWithTitle:@"Create Directory Error" - message:[NSString stringWithFormat:@"Cannot create directory at '%@'", dirPath] - error:error]; - [self endActivity:[objectRequest.userInfo objectForKey:@"activity"] - withMessage:[objectRequest.userInfo objectForKey:@"failedActivityMessage"]]; - @synchronized(self) { - syncIncomplete = YES; - [self decreaseSyncOperationCount]; - if (newSyncRequested && !syncOperationCount) - [self sync]; - } + 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; } @@ -1346,53 +1772,40 @@ error = nil; [fileManager moveItemAtPath:storedState.tmpFilePath toPath:filePath error:&error]; if (error != nil) { - [self fileActionFailedAlertWithTitle:@"Move File Error" - message:[NSString stringWithFormat:@"Cannot move file at '%@' to '%@'", storedState.tmpFilePath, filePath] - error:error]; - [self endActivity:activity - withMessage:[objectRequest.userInfo objectForKey:@"failedActivityMessage"]]; - @synchronized(self) { - syncIncomplete = YES; - [self decreaseSyncOperationCount]; - if (newSyncRequested && !syncOperationCount) - [self sync]; - } + 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; } - [self endActivity:activity - withMessage:[objectRequest.userInfo objectForKey:@"finishedActivityMessage"] - totalBytes:activity.totalBytes - currentBytes:activity.totalBytes]; + dispatch_async(dispatch_get_main_queue(), ^{ + [activityFacility endActivity:activity + withMessage:[objectRequest.userInfo objectForKey:@"finishedActivityMessage"] + totalBytes:activity.totalBytes + currentBytes:activity.totalBytes]; + }); - storedState.hash = object.hash; + storedState.filePath = filePath; + storedState.hash = object.objectHash; storedState.tmpFilePath = nil; [self saveLocalState]; - - @synchronized(self) { - [self decreaseSyncOperationCount]; - if (newSyncRequested && !syncOperationCount) - [self sync]; - } + [self syncOperationFinishedWithSuccess:YES]; [pool drain]; return; } else { - if (newSyncRequested) { - [self endActivity:[objectRequest.userInfo objectForKey:@"activity"] - withMessage:[objectRequest.userInfo objectForKey:@"stoppedActivityMessage"]]; - @synchronized(self) { - syncIncomplete = YES; - [self decreaseSyncOperationCount]; - if (!syncOperationCount) - [self sync]; - } - [pool drain]; - return; + if (newSyncRequested || syncLate || operation.isCancelled) { + [self requestFailed:objectRequest]; } else { - __block ASIPithosObjectRequest *newObjectRequest = [PithosUtilities objectBlockDataRequestWithContainerName:containerName - object:object - blockIndex:missingBlockIndex - blockSize: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:); @@ -1401,8 +1814,8 @@ [(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%%)", - object.name, + 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)]; @@ -1412,14 +1825,11 @@ } } else if (objectRequest.responseStatusCode == 412) { // The object has changed on the server - [self endActivity:[objectRequest.userInfo objectForKey:@"activity"] - withMessage:[objectRequest.userInfo objectForKey:@"stoppedActivityMessage"]]; - @synchronized(self) { - syncIncomplete = YES; - [self decreaseSyncOperationCount]; - if (newSyncRequested && !syncOperationCount) - [self sync]; - } + dispatch_async(dispatch_get_main_queue(), ^{ + [activityFacility endActivity:[objectRequest.userInfo objectForKey:@"activity"] + withMessage:[objectRequest.userInfo objectForKey:@"stoppedActivityMessage"]]; + }); + [self syncOperationFinishedWithSuccess:NO]; } else { [self requestFailed:objectRequest]; } @@ -1428,41 +1838,39 @@ - (void)downloadObjectHashMapFinished:(ASIPithosObjectRequest *)objectRequest { NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; + NSOperation *operation = [objectRequest.userInfo objectForKey:@"operation"]; NSLog(@"Sync::download object hashmap finished: %@", objectRequest.url); - if (objectRequest.responseStatusCode == 200) { - if (newSyncRequested) { - [self endActivity:[objectRequest.userInfo objectForKey:@"activity"] - withMessage:[objectRequest.userInfo objectForKey:@"stoppedActivityMessage"]]; - @synchronized(self) { - syncIncomplete = YES; - [self decreaseSyncOperationCount]; - if (!syncOperationCount) - [self sync]; - } - [pool drain]; - return; + 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:object.name]; + 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:blockSize - blockHash:blockHash + blockSize:pithosContainer.blockSize + blockHash:pithosContainer.blockHash withHashes:[objectRequest hashes]]; NSUInteger missingBlockIndex = [missingBlocks firstIndex]; - [self endActivity:activity - withMessage:[NSString stringWithFormat:@"Sync: Downloading '%@' (%.0f%%)", - object.name, - (100*(activity.totalBytes - [missingBlocks count]*blockSize + 0.0)/(activity.totalBytes + 0.0))] - totalBytes:activity.totalBytes - currentBytes:(activity.totalBytes - [missingBlocks count]*blockSize)]; + 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 objectBlockDataRequestWithContainerName:containerName - object:object - blockIndex:missingBlockIndex - blockSize: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:); @@ -1473,8 +1881,8 @@ [(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%%)", - object.name, + 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)]; @@ -1489,18 +1897,20 @@ - (void)uploadDirectoryObjectFinished:(ASIPithosObjectRequest *)objectRequest { NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; + NSOperation *operation = [objectRequest.userInfo objectForKey:@"operation"]; NSLog(@"Sync::upload directory object finished: %@", objectRequest.url); - if (objectRequest.responseStatusCode == 201) { - PithosLocalObjectState *storedState = [storedLocalObjectStates objectForKey:[[objectRequest.userInfo objectForKey:@"pithosObject"] name]]; + 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]; - [self endActivity:[objectRequest.userInfo objectForKey:@"activity"] - withMessage:[objectRequest.userInfo objectForKey:@"finishedActivityMessage"]]; - @synchronized(self) { - [self decreaseSyncOperationCount]; - if (newSyncRequested && !syncOperationCount) - [self sync]; - } + dispatch_async(dispatch_get_main_queue(), ^{ + [activityFacility endActivity:[objectRequest.userInfo objectForKey:@"activity"] + withMessage:[objectRequest.userInfo objectForKey:@"finishedActivityMessage"]]; + }); + [self syncOperationFinishedWithSuccess:YES]; } else { [self requestFailed:objectRequest]; } @@ -1509,17 +1919,40 @@ - (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 (objectRequest.responseStatusCode == 201) { - [storedLocalObjectStates removeObjectForKey:[[objectRequest.userInfo objectForKey:@"pithosObject"] name]]; + 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]; - [self endActivity:[objectRequest.userInfo objectForKey:@"activity"] - withMessage:[objectRequest.userInfo objectForKey:@"finishedActivityMessage"]]; - @synchronized(self) { - [self decreaseSyncOperationCount]; - if (newSyncRequested && !syncOperationCount) - [self sync]; - } + 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]; } @@ -1528,63 +1961,61 @@ - (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:object.name]; + PithosLocalObjectState *storedState = [[storedLocalObjectStates objectForKey:pithosContainer.name] objectForKey:object.name]; PithosActivity *activity = [objectRequest.userInfo objectForKey:@"activity"]; NSUInteger totalBytes = activity.totalBytes; NSUInteger currentBytes = activity.currentBytes; - if (objectRequest.responseStatusCode == 201) { + if (operation.isCancelled) { + [self requestFailed:objectRequest]; + } else if (objectRequest.responseStatusCode == 201) { NSLog(@"Sync::object created: %@", objectRequest.url); - storedState.hash = [objectRequest eTag]; + storedState.filePath = [objectRequest.userInfo objectForKey:@"filePath"]; + storedState.hash = object.objectHash; [self saveLocalState]; - [self endActivity:activity - withMessage:[objectRequest.userInfo objectForKey:@"finishedActivityMessage"] - totalBytes:totalBytes - currentBytes:totalBytes]; - @synchronized(self) { - [self decreaseSyncOperationCount]; - if (newSyncRequested && !syncOperationCount) - [self sync]; - } + 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) { - [self endActivity:activity - withMessage:[objectRequest.userInfo objectForKey:@"stoppedActivityMessage"]]; - @synchronized(self) { - syncIncomplete = YES; - [self decreaseSyncOperationCount]; - if (!syncOperationCount) - [self sync]; - } - [pool drain]; - return; + 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); - [self endActivity:activity - withMessage:[objectRequest.userInfo objectForKey:@"stoppedActivityMessage"]]; - syncIncomplete = YES; - [self decreaseSyncOperationCount]; + 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"] - withMissingHashesResponse:[objectRequest responseString]]; - if (totalBytes >= [missingBlocks count]*blockSize) - currentBytes = totalBytes - [missingBlocks count]*blockSize; - [self updateActivity:activity - withMessage:[NSString stringWithFormat:@"Sync: Uploading '%@' (%.0f%%)", object.name, (100*(currentBytes + 0.0)/(totalBytes + 0.0))] - totalBytes:totalBytes - currentBytes:currentBytes]; + 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 updateContainerDataRequestWithContainerName:containerName - blockSize:blockSize - forFile:[objectRequest.userInfo objectForKey:@"filePath"] - missingBlockIndex:missingBlockIndex - sharingAccount:nil]; + __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:); @@ -1596,7 +2027,8 @@ [(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%%)", object.name, (100*(activity.currentBytes + size + 0.0)/(activity.totalBytes + 0.0))] + 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)]; }]; @@ -1610,24 +2042,31 @@ - (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 (containerRequest.responseStatusCode == 202) { + 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 (missingBlockIndex == NSNotFound) { + if (operation.isCancelled) { + [self requestFailed:containerRequest]; + } else if (missingBlockIndex == NSNotFound) { NSArray *hashes = [containerRequest.userInfo objectForKey:@"hashes"]; - ASIPithosObjectRequest *newObjectRequest = [PithosUtilities writeObjectDataRequestWithContainerName:containerName - objectName:object.name - contentType:object.contentType - blockSize:blockSize - blockHash:blockHash - forFile:[containerRequest.userInfo objectForKey:@"filePath"] - checkIfExists:NO - hashes:&hashes - sharingAccount:nil]; + 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:); @@ -1638,23 +2077,15 @@ [(NSMutableDictionary *)(newObjectRequest.userInfo) setObject:NSStringFromSelector(@selector(uploadObjectUsingHashMapFinished:)) forKey:@"didFinishSelector"]; [networkQueue addOperation:[PithosUtilities prepareRequest:newObjectRequest priority:[[newObjectRequest.userInfo objectForKey:@"priority"] integerValue]]]; } else { - if (newSyncRequested) { - [self endActivity:activity - withMessage:[containerRequest.userInfo objectForKey:@"stoppedActivityMessage"]]; - @synchronized(self) { - syncIncomplete = YES; - [self decreaseSyncOperationCount]; - if (!syncOperationCount) - [self sync]; - } - [pool drain]; - return; + if (newSyncRequested || syncLate || operation.isCancelled) { + [self requestFailed:containerRequest]; } else { - __block ASIPithosContainerRequest *newContainerRequest = [PithosUtilities updateContainerDataRequestWithContainerName:containerName - blockSize:blockSize - forFile:[containerRequest.userInfo objectForKey:@"filePath"] - missingBlockIndex:missingBlockIndex - sharingAccount:nil]; + __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:); @@ -1663,7 +2094,8 @@ [(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%%)", object.name, (100*(activity.currentBytes + size + 0.0)/(activity.totalBytes + 0.0))] + 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)]; }]; @@ -1678,23 +2110,18 @@ - (void)requestFailed:(ASIPithosRequest *)request { NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; - if ([request isCancelled]) { - [self endActivity:[request.userInfo objectForKey:@"activity"] - withMessage:[request.userInfo objectForKey:@"stoppedActivityMessage"]]; - syncIncomplete = YES; - [self decreaseSyncOperationCount]; + NSOperation *operation = [request.userInfo objectForKey:@"operation"]; + NSLog(@"Sync::request failed: %@", request.url); + if (operation.isCancelled) { [pool drain]; - return; + return; } - if (newSyncRequested) { - [self endActivity:[request.userInfo objectForKey:@"activity"] - withMessage:[request.userInfo objectForKey:@"stoppedActivityMessage"]]; - @synchronized(self) { - syncIncomplete = YES; - [self decreaseSyncOperationCount]; - if (!syncOperationCount) - [self sync]; - } + 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; } @@ -1704,13 +2131,13 @@ [(NSMutableDictionary *)(newRequest.userInfo)setObject:[NSNumber numberWithUnsignedInteger:(--retries)] forKey:@"retries"]; [networkQueue addOperation:[PithosUtilities prepareRequest:newRequest priority:[[newRequest.userInfo objectForKey:@"priority"] integerValue]]]; } else { - [self endActivity:[request.userInfo objectForKey:@"activity"] - withMessage:[request.userInfo objectForKey:@"failedActivityMessage"]]; - syncIncomplete = YES; - [self decreaseSyncOperationCount]; + dispatch_async(dispatch_get_main_queue(), ^{ + [activityFacility endActivity:[request.userInfo objectForKey:@"activity"] + withMessage:[request.userInfo objectForKey:@"failedActivityMessage"]]; + }); + [self syncOperationFinishedWithSuccess:NO]; } [pool drain]; } - @end