Support multiple accounts. Bug fixes. Improve concurrency.
[pithos-macos] / pithos-macos / PithosSyncDaemon.m
index 5366cd6..81efa3c 100644 (file)
@@ -36,6 +36,7 @@
 // or implied, of GRNET S.A.
 
 #import "PithosSyncDaemon.h"
+#import "PithosAccount.h"
 #import "PithosLocalObjectState.h"
 #import "PithosActivityFacility.h"
 #import "PithosUtilities.h"
@@ -47,6 +48,7 @@
 #import "ASIPithosObject.h"
 
 @interface PithosSyncDaemon (Private)
+- (void)resetLocalState;
 - (NSString *)pithosStateFilePath;
 - (void)saveLocalState;
 
 - (void)updateServerStateWithCurrentState:(PithosLocalObjectState *)currentState 
                                    object:(ASIPithosObject *)object 
                             localFilePath:(NSString *)filePath;
+- (void)listRequestFailed:(ASIPithosContainerRequest *)containerRequest;
 - (void)requestFailed:(ASIPithosRequest *)request;
 
-- (void)increaseSyncOperationCount;
-- (void)decreaseSyncOperationCount;
+- (void)syncOperationStarted;
+- (void)syncOperationFinishedWithSuccess:(BOOL)operationSuccessfull;
 
 @end
 
 @implementation PithosSyncDaemon
+@synthesize directoryPath, pithosAccount, containerName, pithos;
 @synthesize blockHash, blockSize, lastModified, lastCompletedSync, remoteObjects, storedLocalObjectStates, currentLocalObjectStates;
-@synthesize directoryPath, containerDirectoryPath, pithosStateFilePath, tempDownloadsDirPath, tempTrashDirPath;
+@synthesize containerDirectoryPath, pithosStateFilePath, tempDownloadsDirPath, tempTrashDirPath;
 
 #pragma mark -
 #pragma Object Lifecycle
 
 - (id)initWithDirectoryPath:(NSString *)aDirectoryPath 
-                     pithos:(ASIPithos *)aPithos 
+              pithosAccount:(PithosAccount *)aPithosAccount 
               containerName:(NSString *)aContainerName 
-               timeInterval:(NSTimeInterval)aTimeInterval 
             resetLocalState:(BOOL)resetLocalState {
     if ((self = [super init])) {
         directoryPath = [aDirectoryPath copy];
-        pithos = [aPithos retain];
+        pithosAccount = [aPithosAccount retain];
         containerName = [aContainerName copy];
-        timeInterval = aTimeInterval;
-        
-        syncOperationCount = 0;
-        newSyncRequested = NO;
-        containerDirectoryPath = [[directoryPath stringByAppendingPathComponent:containerName] copy];
+        self.pithos = pithosAccount.pithos;
         
         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 resetLocalState];
         
         networkQueue = [[ASINetworkQueue alloc] init];
         networkQueue.showAccurateProgress = YES;
         networkQueue.shouldCancelAllRequestsOnFailure = NO;
-        [networkQueue go];
+//        networkQueue.maxConcurrentOperationCount = 1;
         
-        queue = dispatch_queue_create("gr.grnet.pithos.SyncQueue", NULL);
+        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]];
+        
+        [self startDaemon];
+    }
+    return self;
+}
+
+- (void)resetLocalState {
+    self.lastCompletedSync = nil;
+    NSFileManager *fileManager = [NSFileManager defaultManager];
+    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;
+        }
+    }
+}
+
+- (void)resetDaemon {
+    @synchronized(self) {
+        if (!daemonActive)
+            return;
+    }
     
-        timer = [[NSTimer scheduledTimerWithTimeInterval:timeInterval target:self selector:@selector(sync) userInfo:nil repeats:YES] retain];
-        [timer fire];
+    [networkQueue reset];
+    [callbackQueue cancelAllOperations];
+    [callbackQueue setSuspended:YES];
+    [self emptyTempTrash];
+    
+    @synchronized(self) {            
+        daemonActive = NO;
+    }
+}
+
+- (void)startDaemon {
+    @synchronized(self) {
+        if (daemonActive)
+            return;
     }
     
-    return self;
+    syncOperationCount = 0;
+    newSyncRequested = NO;
+    syncIncomplete = NO;
+    syncLate = NO;
+
+    self.containerDirectoryPath = [directoryPath stringByAppendingPathComponent:containerName];
+            
+    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];
+    }
+
+    // In the improbable case of leftover operations
+    [networkQueue reset];
+    [callbackQueue cancelAllOperations];
+    
+    [networkQueue go];
+    [callbackQueue setSuspended:NO];
+    
+    @synchronized(self) {
+        daemonActive = YES;
+    }
 }
 
 - (void)dealloc {
     [[NSNotificationCenter defaultCenter] removeObserver:self];
-    dispatch_release(queue);
-    [networkQueue cancelAllOperations];
+    [self resetDaemon];
+    [callbackQueue release];
     [networkQueue release];
-    [timer invalidate];
-    [timer release];
     [tempTrashDirPath release];
     [tempDownloadsDirPath release];
     [pithosStateFilePath release];
     [lastCompletedSync release];
     [lastModified release];
     [blockHash release];
-    [containerName release];
     [pithos release];
+    [containerName release];
+    [pithosAccount release];
     [directoryPath release];
     [super dealloc];
 }
     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];
     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)
-            [userDefaults setObject:tempTrashDirPath forKey:@"PithosSyncTempTrashDirPath"];
-        [tempTrashDirPath retain];
+    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 copy] autorelease];
+    return tempTrashDirPath;
+}
+
+- (void)setDirectoryPath:(NSString *)aDirectoryPath {
+    if (aDirectoryPath && ![aDirectoryPath isEqualToString:directoryPath]) {
+        [self resetDaemon];
+        [self resetLocalState];
+        [directoryPath release];
+        directoryPath = [aDirectoryPath copy];
+    }
+}
+
+- (void)setContainerName:(NSString *)aContainerName {
+    if (aContainerName && ![aContainerName isEqualToString:containerName]) {
+        [self resetDaemon];
+        [self resetLocalState];
+        [containerName release];
+        containerName = [aContainerName copy];
+    }    
+}
+
+- (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 resetLocalState];
+        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 -
     [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
+            return;
         syncOperationCount--;
-        if (!syncOperationCount) {
+        if (syncOperationCount == 0) {
             if (!syncIncomplete) {
                 self.lastCompletedSync = [NSDate date];
                 dispatch_async(dispatch_get_main_queue(), ^{
                     [activityFacility startAndEndActivityWithType:PithosActivityOther 
-                                                          message:[NSString stringWithFormat:@"Sync: Completed %@", lastCompletedSync]];
-
+                                                          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) {
             // The first operation is the server listing
-            syncOperationCount = 1;
+            [self syncOperationStarted];
             newSyncRequested = NO;
             syncIncomplete = NO;
+            syncLate = NO;
+        } else {
+            return;
         }
     }
 
             [PithosUtilities fileActionFailedAlertWithTitle:@"Local Sync Directory Error" 
                                                     message:[NSString stringWithFormat:@"Cannot create local sync directory at '%@'", containerDirectoryPath] 
                                                       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] 
                                                   error:nil];
-        @synchronized(self) {
-            syncOperationCount = 0;
-        }
+        [self syncOperationFinishedWithSuccess:NO];
         return;
     }
     
     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", 
 //            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) {
 //                    dispatch_async(dispatch_get_main_queue(), ^{
 //                                                                  error:error];
 //                    });
 //                }
-//                syncOperationCount = 0;
+//                [self syncOperationFinishedWithSuccess:YES];
 //            }];
             for (NSString *subPath in subPaths) {
                 NSString *subFilePath = [trashDirPath stringByAppendingPathComponent:subPath];
         if (!fileExists || [self moveToTempTrashFile:filePath]) {
             dispatch_async(dispatch_get_main_queue(), ^{
                 [activityFacility startAndEndActivityWithType:PithosActivityOther 
-                                                      message:[NSString stringWithFormat:@"Sync: Deleting '%@' locally (finished)", object.name]];
+                                                      message:[NSString stringWithFormat:@"Sync: Deleting '%@' locally (finished)", object.name] 
+                                                pithosAccount:pithosAccount];
             });
             [storedLocalObjectStates removeObjectForKey:object.name];
             [self saveLocalState];
         if (directoryCreated)
             dispatch_async(dispatch_get_main_queue(), ^{
                 [activityFacility startAndEndActivityWithType:PithosActivityOther 
-                                                      message:[NSString stringWithFormat:@"Sync: Creating directory '%@' locally (finished)", object.name]];
+                                                      message:[NSString stringWithFormat:@"Sync: Creating directory '%@' locally (finished)", object.name] 
+                                                pithosAccount:pithosAccount];
             });
     } else if (object.bytes == 0) {
         // Create local object with zero length
         if (fileCreated)
             dispatch_async(dispatch_get_main_queue(), ^{
                 [activityFacility startAndEndActivityWithType:PithosActivityOther 
-                                                      message:[NSString stringWithFormat:@"Sync: Downloading '%@' (100%%)", object.name]];
+                                                      message:[NSString stringWithFormat:@"Sync: Downloading '%@' (100%%)", object.name] 
+                                                pithosAccount:pithosAccount];
             });
     } else if (storedState.tmpFilePath == nil) {
         // Create new local object
             [self saveLocalState];
             dispatch_async(dispatch_get_main_queue(), ^{
                 [activityFacility startAndEndActivityWithType:PithosActivityOther 
-                                                      message:[NSString stringWithFormat:@"Sync: Downloading '%@' (100%%)", object.name]];
+                                                      message:[NSString stringWithFormat:@"Sync: Downloading '%@' (100%%)", object.name] 
+                                                pithosAccount:pithosAccount];
             });
         } else {
-            [self increaseSyncOperationCount];
+            [self syncOperationStarted];
             __block ASIPithosObjectRequest *objectRequest = [PithosUtilities objectBlockDataRequestWithPithos:pithos 
                                                                                                 containerName:containerName 
                                                                                                        object:object 
             PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityDownload 
                                                                        message:[NSString stringWithFormat:@"Sync: Downloading '%@' (0%%)", object.name] 
                                                                     totalBytes:object.bytes 
-                                                                  currentBytes:0];
+                                                                  currentBytes:0 
+                                                                 pithosAccount:pithosAccount];
             dispatch_async(dispatch_get_main_queue(), ^{
-                [activityFacility updateActivity:activity withMessage:activity.message];  
+                [activityFacility updateActivity:activity withMessage:activity.message];
             });
             objectRequest.userInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys:
                                       object, @"pithosObject", 
             [self saveLocalState];
             dispatch_async(dispatch_get_main_queue(), ^{
                 [activityFacility startAndEndActivityWithType:PithosActivityOther 
-                                                      message:[NSString stringWithFormat:@"Sync: Downloading '%@' (100%%)", object.name]];
+                                                      message:[NSString stringWithFormat:@"Sync: Downloading '%@' (100%%)", object.name] 
+                                                pithosAccount:pithosAccount];
             });
         } else {
-            [self increaseSyncOperationCount];
+            [self syncOperationStarted];
             ASIPithosObjectRequest *objectRequest = [ASIPithosObjectRequest objectHashmapRequestWithPithos:pithos 
                                                                                              containerName:containerName 
                                                                                                 objectName:object.name];
             PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityDownload 
                                                                        message:[NSString stringWithFormat:@"Sync: Downloading '%@' (0%%)", object.name] 
                                                                     totalBytes:object.bytes 
-                                                                  currentBytes:0];
+                                                                  currentBytes:0 
+                                                                 pithosAccount:pithosAccount];
             dispatch_async(dispatch_get_main_queue(), ^{
                 [activityFacility updateActivity:activity withMessage:activity.message];
             });
                                   object:(ASIPithosObject *)object 
                            localFilePath:(NSString *)filePath {
     NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
-    [self increaseSyncOperationCount];
+    [self syncOperationStarted];
     NSFileManager *fileManager = [NSFileManager defaultManager];
     BOOL isDirectory;
     BOOL fileExists = [fileManager fileExistsAtPath:filePath isDirectory:&isDirectory];
         // Create remote directory object
         if (!fileExists || !isDirectory) {
             // Local directory object deleted or changed to a file in the meantime, mark the sync cycle as incomplete and skip
-            syncIncomplete = YES;
-            [self decreaseSyncOperationCount];
+            [self syncOperationFinishedWithSuccess:NO];
             [pool drain];
             return;
         }
         objectRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:);
         objectRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:);
         PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityCreateDirectory 
-                                                                   message:[NSString stringWithFormat:@"Sync: Creating directory '%@'", object.name]];
+                                                                   message:[NSString stringWithFormat:@"Sync: Creating directory '%@'", object.name] 
+                                                             pithosAccount:pithosAccount];
         dispatch_async(dispatch_get_main_queue(), ^{
             [activityFacility updateActivity:activity withMessage:activity.message];
         });
                 objectRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:);
                 objectRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:);
                 PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityDelete 
-                                                                           message:[NSString stringWithFormat:@"Sync: Moving to trash '%@'", object.name]];
+                                                                           message:[NSString stringWithFormat:@"Sync: Moving to trash '%@'", object.name] 
+                                                                     pithosAccount:pithosAccount];
                 dispatch_async(dispatch_get_main_queue(), ^{
                     [activityFacility updateActivity:activity withMessage:activity.message];
                 });
                                           nil];
                 [networkQueue addOperation:[PithosUtilities prepareRequest:objectRequest priority:NSOperationQueuePriorityHigh]];
             } else {
-                syncIncomplete = YES;
-                [self decreaseSyncOperationCount];
+                [self syncOperationFinishedWithSuccess:NO];
             }
         } else {
-            syncIncomplete = YES;
-            [self decreaseSyncOperationCount];
+            [self syncOperationFinishedWithSuccess:NO];
         }
     } else {
         // Upload file to remote object
         if (!fileExists || isDirectory) {
             // Local file object deleted or changed to a directory in the meantime, mark the sync cycle as incomplete and skip
-            syncIncomplete = YES;
-            [self decreaseSyncOperationCount];
+            [self syncOperationFinishedWithSuccess:NO];
             [pool drain];
             return;
         }
             PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityUpload 
                                                                        message:[NSString stringWithFormat:@"Sync: Uploading '%@' (0%%)", object.name]
                                                                     totalBytes:[[objectRequest.userInfo objectForKey:@"bytes"] unsignedIntegerValue]
-                                                                  currentBytes:0];
+                                                                  currentBytes:0 
+                                                                 pithosAccount:pithosAccount];
             dispatch_async(dispatch_get_main_queue(), ^{
                 [activityFacility updateActivity:activity withMessage:activity.message];
             });
               nil]];
             [networkQueue addOperation:[PithosUtilities prepareRequest:objectRequest priority:NSOperationQueuePriorityNormal]];
         } else {
-            syncIncomplete = YES;
-            [self decreaseSyncOperationCount];
+            [self syncOperationFinishedWithSuccess:NO];
         }
     }
     [pool drain];
 #pragma mark ASIHTTPRequestDelegate
 
 - (void)performRequestFinishedDelegateInBackground:(ASIPithosRequest *)request {
-    dispatch_async(queue, ^{
-        [self performSelector: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 {
-    dispatch_async(queue, ^{
-        [self performSelector: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) {
                 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;
             }
         }
+        
+        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"]];
                 [PithosUtilities fileActionFailedAlertWithTitle:@"Directory Contents Error" 
                                                         message:[NSString stringWithFormat:@"Cannot get contents of directory at '%@'", containerDirectoryPath] 
                                                           error:error];
-                [activityFacility startAndEndActivityWithType:PithosActivityOther message:@"Sync: Failed to read contents of sync directory"];
+                [activityFacility startAndEndActivityWithType:PithosActivityOther 
+                                                      message:@"Sync: Failed to read contents of sync directory" 
+                                                pithosAccount:pithosAccount];
             });
-            @synchronized(self) {
-                // Since the local listing failed, the operation finished and the sync cycle is completeted unsuccessfully
-                syncOperationCount = 0;
-                if (newSyncRequested)
-                    [self sync];
-            }
+            // Since the local listing failed, the operation finished and the sync cycle is completeted unsuccessfully
+            [self syncOperationFinishedWithSuccess:NO];
+            [pool drain];
+            return;
+        }
+        
+        if (operation.isCancelled) {
+            operation.completionBlock = nil;
+            [self syncOperationFinishedWithSuccess:NO];
             [pool drain];
             return;
         }
+
         self.currentLocalObjectStates = [NSMutableDictionary dictionaryWithCapacity:[subPaths count]];
         for (NSString *objectName in subPaths) {
+            if (operation.isCancelled) {
+                operation.completionBlock = nil;
+                [self saveLocalState];
+                [self syncOperationFinishedWithSuccess:NO];
+                [pool drain];
+                return;
+            }
+
             PithosLocalObjectState *storedLocalObjectState = [storedLocalObjectStates objectForKey:objectName];
             NSString *filePath = [containerDirectoryPath stringByAppendingPathComponent:objectName];
             if (!storedLocalObjectState || [storedLocalObjectState isModified]) {
         }
         [self saveLocalState];
 
+        if (operation.isCancelled) {
+            operation.completionBlock = nil;
+            [self syncOperationFinishedWithSuccess:NO];
+            [pool drain];
+            return;
+        }
+
         for (NSString *objectName in remoteObjects) {
+            if (operation.isCancelled) {
+                operation.completionBlock = nil;
+                [self saveLocalState];
+                [self syncOperationFinishedWithSuccess:NO];
+                [pool drain];
+                return;
+            }
+
             if (![storedLocalObjectStates objectForKey:objectName])
                 [storedLocalObjectStates setObject:[PithosLocalObjectState localObjectState] forKey:objectName];
         }
+        [self saveLocalState];
+
+        if (operation.isCancelled) {
+            operation.completionBlock = nil;
+            [self syncOperationFinishedWithSuccess:NO];
+            [pool drain];
+            return;
+        }
 
         for (NSString *objectName in [[storedLocalObjectStates 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:@":"];
                 }
             }
         }
-        @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 {
-            dispatch_async(dispatch_get_main_queue(), ^{
-                [activityFacility 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]) {
+    NSOperation *operation = [containerRequest.userInfo objectForKey:@"operation"];
+    if (operation.isCancelled) {
+        [objects release];
+        objects = nil;
+        [pool drain];
+        return;        
+    }
+    if (containerRequest.isCancelled) {
         dispatch_async(dispatch_get_main_queue(), ^{
             [activityFacility endActivity:[containerRequest.userInfo objectForKey:@"activity"] 
                               withMessage:[containerRequest.userInfo objectForKey:@"stoppedActivityMessage"]];
         });
         [objects release];
         objects = nil;
-        @synchronized(self) {
-            syncOperationCount = 0;
-        }
+        [self syncOperationFinishedWithSuccess:NO];
         [pool drain];
         return;
     }
         });
         [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) {
         ASIPithosObject *object = [objectRequest.userInfo objectForKey:@"pithosObject"];
         NSFileManager *fileManager = [NSFileManager defaultManager];
         NSError *error;
                 [activityFacility endActivity:activity 
                                   withMessage:[objectRequest.userInfo objectForKey:@"failedActivityMessage"]];
             });
-            @synchronized(self) {
-                syncIncomplete = YES;
-                [self decreaseSyncOperationCount];
-                if (newSyncRequested && !syncOperationCount)
-                    [self sync];
-            }
+            [self syncOperationFinishedWithSuccess:NO];
             [pool drain];
             return;
         }
                     [activityFacility endActivity:activity 
                                       withMessage:[objectRequest.userInfo objectForKey:@"failedActivityMessage"]];
                 });
-                @synchronized(self) {
-                    syncIncomplete = YES;
-                    [self decreaseSyncOperationCount];
-                    if (newSyncRequested && !syncOperationCount)
-                        [self sync];
-                }
+                [self syncOperationFinishedWithSuccess:NO];
                 [pool drain];
                 return;
             }
                     [activityFacility endActivity:activity 
                                       withMessage:[objectRequest.userInfo objectForKey:@"failedActivityMessage"]];
                 });
-                @synchronized(self) {
-                    syncIncomplete = YES;
-                    [self decreaseSyncOperationCount];
-                    if (newSyncRequested && !syncOperationCount)
-                        [self sync];
-                }
+                [self syncOperationFinishedWithSuccess:NO];
                 [pool drain];
                 return;
             } else if (![fileManager fileExistsAtPath:dirPath]) {
                         [activityFacility endActivity:[objectRequest.userInfo objectForKey:@"activity"] 
                                           withMessage:[objectRequest.userInfo objectForKey:@"failedActivityMessage"]];
                     });
-                    @synchronized(self) {
-                        syncIncomplete = YES;
-                        [self decreaseSyncOperationCount];
-                        if (newSyncRequested && !syncOperationCount)
-                            [self sync];
-                    }
+                    [self syncOperationFinishedWithSuccess:NO];
                     [pool drain];
                     return;
                 }
                     [activityFacility endActivity:activity 
                                       withMessage:[objectRequest.userInfo objectForKey:@"failedActivityMessage"]];
                 });
-                @synchronized(self) {
-                    syncIncomplete = YES;
-                    [self decreaseSyncOperationCount];
-                    if (newSyncRequested && !syncOperationCount)
-                        [self sync];
-                }
+                [self syncOperationFinishedWithSuccess:NO];
                 [pool drain];
                 return;
             }
             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) {
-                dispatch_async(dispatch_get_main_queue(), ^{
-                    [activityFacility 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 objectBlockDataRequestWithPithos:pithos 
                                                                                                        containerName:containerName 
             [activityFacility endActivity:[objectRequest.userInfo objectForKey:@"activity"] 
                               withMessage:[objectRequest.userInfo objectForKey:@"stoppedActivityMessage"]];
         });
-        @synchronized(self) {
-            syncIncomplete = YES;
-            [self decreaseSyncOperationCount];
-            if (newSyncRequested && !syncOperationCount)
-                [self sync];
-        }
+        [self syncOperationFinishedWithSuccess:NO];
     } else {
         [self requestFailed:objectRequest];
     }
 
 - (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) {
-            dispatch_async(dispatch_get_main_queue(), ^{
-                [activityFacility 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 {
             ASIPithosObject *object = [objectRequest.userInfo objectForKey:@"pithosObject"];
             PithosLocalObjectState *storedState = [storedLocalObjectStates objectForKey:object.name];
 
 - (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) {
+    if (operation.isCancelled) {
+        [self requestFailed:objectRequest];
+    } else if (objectRequest.responseStatusCode == 201) {
         PithosLocalObjectState *storedState = [storedLocalObjectStates objectForKey:[[objectRequest.userInfo objectForKey:@"pithosObject"] name]];
         storedState.isDirectory = YES;
         [self saveLocalState];
             [activityFacility endActivity:[objectRequest.userInfo objectForKey:@"activity"] 
                               withMessage:[objectRequest.userInfo objectForKey:@"finishedActivityMessage"]];
         });
-        @synchronized(self) {
-            [self decreaseSyncOperationCount];
-            if (newSyncRequested && !syncOperationCount)
-                [self sync];
-        }
+        [self syncOperationFinishedWithSuccess:YES];
     } else {
         [self requestFailed:objectRequest];
     }
 
 - (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) {
+    if (operation.isCancelled) {
+        [self requestFailed:objectRequest];
+    } else if (objectRequest.responseStatusCode == 201) {
         [storedLocalObjectStates 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"]];
         });
-        @synchronized(self) {
-            [self decreaseSyncOperationCount];
-            if (newSyncRequested && !syncOperationCount)
-                [self sync];
-        }
+        [self syncOperationFinishedWithSuccess:YES];
     } else {
         [self requestFailed:objectRequest];
     }
 
 - (void)uploadObjectUsingHashMapFinished:(ASIPithosObjectRequest *)objectRequest {
     NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
+    NSOperation *operation = [objectRequest.userInfo objectForKey:@"operation"];
     NSLog(@"Sync::upload using hashmap finished: %@", objectRequest.url);
     ASIPithosObject *object = [objectRequest.userInfo objectForKey:@"pithosObject"];
     PithosLocalObjectState *storedState = [storedLocalObjectStates 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 objectHash];
         [self saveLocalState];
                                totalBytes:totalBytes 
                              currentBytes:totalBytes];
         });
-        @synchronized(self) {
-            [self decreaseSyncOperationCount];
-            if (newSyncRequested && !syncOperationCount)
-                [self sync];
-        }
+        [self syncOperationFinishedWithSuccess:YES];
     } else if (objectRequest.responseStatusCode == 409) {
-        if (newSyncRequested) {
-            dispatch_async(dispatch_get_main_queue(), ^{
-                [activityFacility 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);
                 dispatch_async(dispatch_get_main_queue(), ^{
-                    [activityFacility endActivity:activity 
-                                      withMessage:[objectRequest.userInfo objectForKey:@"stoppedActivityMessage"]];
+                    [activityFacility endActivity:activity withMessage:[objectRequest.userInfo objectForKey:@"stoppedActivityMessage"]];
                 });
-                syncIncomplete = YES;
-                [self decreaseSyncOperationCount];
+                [self syncOperationFinishedWithSuccess:NO];
                 [pool drain];
                 return;
             }
 
 - (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) {
         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 writeObjectDataRequestWithPithos:pithos 
                                                                                            containerName:containerName 
             [(NSMutableDictionary *)(newObjectRequest.userInfo) setObject:NSStringFromSelector(@selector(uploadObjectUsingHashMapFinished:)) forKey:@"didFinishSelector"];
             [networkQueue addOperation:[PithosUtilities prepareRequest:newObjectRequest priority:[[newObjectRequest.userInfo objectForKey:@"priority"] integerValue]]];
         } else {
-            if (newSyncRequested) {
-                dispatch_async(dispatch_get_main_queue(), ^{
-                    [activityFacility 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 updateContainerDataRequestWithPithos:pithos 
                                                                                                                  containerName:containerName
 
 - (void)requestFailed:(ASIPithosRequest *)request {
     NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
-    if ([request isCancelled]) {
-        dispatch_async(dispatch_get_main_queue(), ^{
-            [activityFacility endActivity:[request.userInfo objectForKey:@"activity"] 
-                              withMessage:[request.userInfo objectForKey:@"stoppedActivityMessage"]];
-        });
-        syncIncomplete = YES;
-        [self decreaseSyncOperationCount];
+    NSOperation *operation = [request.userInfo objectForKey:@"operation"];
+    if (operation.isCancelled) {
         [pool drain];
-        return;
+        return;        
     }
-    if (newSyncRequested) {
+    if (request.isCancelled || newSyncRequested || syncLate) {
         dispatch_async(dispatch_get_main_queue(), ^{
             [activityFacility endActivity:[request.userInfo objectForKey:@"activity"] 
                               withMessage:[request.userInfo objectForKey:@"stoppedActivityMessage"]];
         });
-        @synchronized(self) {
-            syncIncomplete = YES;
-            [self decreaseSyncOperationCount];
-            if (!syncOperationCount)
-                [self sync];
-        }
+        [self syncOperationFinishedWithSuccess:NO];
         [pool drain];
         return;
     }
             [activityFacility endActivity:[request.userInfo objectForKey:@"activity"] 
                               withMessage:[request.userInfo objectForKey:@"failedActivityMessage"]];
         });
-        syncIncomplete = YES;
-        [self decreaseSyncOperationCount];
+        [self syncOperationFinishedWithSuccess:NO];
     }
     [pool drain];
 }