Use user catalog in sync
[pithos-macos] / pithos-macos / PithosSyncDaemon.m
1 //
2 //  PithosSyncDaemon.m
3 //  pithos-macos
4 //
5 // Copyright 2011-2012 GRNET S.A. All rights reserved.
6 //
7 // Redistribution and use in source and binary forms, with or
8 // without modification, are permitted provided that the following
9 // conditions are met:
10 // 
11 //   1. Redistributions of source code must retain the above
12 //      copyright notice, this list of conditions and the following
13 //      disclaimer.
14 // 
15 //   2. Redistributions in binary form must reproduce the above
16 //      copyright notice, this list of conditions and the following
17 //      disclaimer in the documentation and/or other materials
18 //      provided with the distribution.
19 // 
20 // THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
21 // OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
22 // WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
23 // PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
24 // CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
25 // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
26 // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
27 // USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
28 // AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
29 // LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
30 // ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
31 // POSSIBILITY OF SUCH DAMAGE.
32 // 
33 // The views and conclusions contained in the software and
34 // documentation are those of the authors and should not be
35 // interpreted as representing official policies, either expressed
36 // or implied, of GRNET S.A.
37
38 #import "PithosSyncDaemon.h"
39 #import "PithosAccount.h"
40 #import "PithosLocalObjectState.h"
41 #import "PithosActivityFacility.h"
42 #import "PithosUtilities.h"
43 #import "ASINetworkQueue.h"
44 #import "ASIPithosRequest.h"
45 #import "ASIPithos.h"
46 #import "ASIPithosContainer.h"
47 #import "ASIPithosContainerRequest.h"
48 #import "ASIPithosObjectRequest.h"
49 #import "ASIPithosObject.h"
50
51 @interface PithosSyncDaemon (Private)
52 - (void)loadLocalState;
53 - (void)resetLocalStateWithAll:(BOOL)all;
54 - (void)saveLocalState;
55
56 - (BOOL)createSyncDirectory:(NSString *)dirPath;
57 - (NSString *)dirPathForAccount:(NSString *)accountName container:(NSString *)containerName;
58 - (NSString *)relativeDirPathForAccount:(NSString *)accountName container:(NSString *)containerName;
59
60 - (BOOL)moveToTempTrashFile:(NSString *)filePath 
61                 accountName:(NSString *)accountName 
62             pithosContainer:(ASIPithosContainer *)pithosContainer;
63 - (void)emptyTempTrash;
64 - (BOOL)findLocalCopyForObjectWithHash:(NSString *)hash forFile:(NSString *)filePath;
65
66 - (void)updateLocalStateWithObject:(ASIPithosObject *)object 
67                      localFilePath:(NSString *)filePath 
68                        accountName:(NSString *)accountName 
69                    pithosContainer:(ASIPithosContainer *)pithosContainer;
70 - (void)updateServerStateWithCurrentState:(PithosLocalObjectState *)currentState 
71                                    object:(ASIPithosObject *)object 
72                             localFilePath:(NSString *)filePath 
73                               accountName:(NSString *)accountName 
74                           pithosContainer:(ASIPithosContainer *)pithosContainer;
75 - (void)listRequestFailed:(ASIPithosContainerRequest *)containerRequest;
76 - (void)requestFailed:(ASIPithosRequest *)request;
77
78 - (void)syncOperationStarted;
79 - (void)syncOperationFinishedWithSuccess:(BOOL)operationSuccessfull;
80
81 @end
82
83 @implementation PithosSyncDaemon
84 @synthesize directoryPath, accountsDictionary, skipHidden, pithos;
85 @synthesize userCatalog, accountsNames, accountsPithosContainers;
86 @synthesize lastCompletedSync, remoteObjects, previousRemoteObjects, storedLocalObjectStates, currentLocalObjectStates;
87 @synthesize userCatalogFilePath, pithosStateFilePath, tempDownloadsDirPath, tempTrashDirPath;
88
89 #pragma mark -
90 #pragma Object Lifecycle
91
92 - (id)initWithDirectoryPath:(NSString *)aDirectoryPath 
93               pithosAccount:(PithosAccount *)aPithosAccount 
94          accountsDictionary:(NSDictionary *)anAccountsDictionary 
95                  skipHidden:(BOOL)aSkipHidden 
96             resetLocalState:(BOOL)resetLocalState {
97     if ((self = [super init])) {
98         directoryPath = [aDirectoryPath copy];
99         pithosAccount = aPithosAccount;
100         self.accountsDictionary = anAccountsDictionary;
101         skipHidden = aSkipHidden;
102         self.pithos = pithosAccount.pithos;        
103         
104         activityFacility = [PithosActivityFacility defaultPithosActivityFacility];
105         
106         if (resetLocalState)
107             [self resetLocalStateWithAll:YES];
108         else
109             [self resetLocalStateWithAll:NO];
110         
111         // Load user catalog.
112         if ([[NSFileManager defaultManager] fileExistsAtPath:self.userCatalogFilePath])
113             self.userCatalog = [NSKeyedUnarchiver unarchiveObjectWithFile:self.userCatalogFilePath];
114         else
115             self.userCatalog = [NSMutableDictionary dictionary];
116         if (!userCatalog)
117             self.userCatalog = [NSMutableDictionary dictionary];
118         
119         networkQueue = [[ASINetworkQueue alloc] init];
120         networkQueue.showAccurateProgress = YES;
121         networkQueue.shouldCancelAllRequestsOnFailure = NO;
122 //        networkQueue.maxConcurrentOperationCount = 1;
123         
124         callbackQueue = [[NSOperationQueue alloc] init];
125         [callbackQueue setSuspended:YES];
126         callbackQueue.name = [NSString stringWithFormat:@"gr.grnet.pithos.SyncCallbackQueue.%@", pithosAccount.uniqueName];
127 //        callbackQueue.maxConcurrentOperationCount = 1;
128         
129         [[NSNotificationCenter defaultCenter] addObserver:self
130                                                  selector:@selector(applicationWillTerminate:)
131                                                      name:NSApplicationWillTerminateNotification
132                                                    object:[NSApplication sharedApplication]];
133     }
134     return self;
135 }
136
137 - (void)loadLocalState {
138     @autoreleasepool {
139         if ([[NSFileManager defaultManager] fileExistsAtPath:self.pithosStateFilePath])
140             self.storedLocalObjectStates = [NSKeyedUnarchiver unarchiveObjectWithFile:self.pithosStateFilePath];
141         else
142             self.storedLocalObjectStates = [NSMutableDictionary dictionary];
143         if (!storedLocalObjectStates)
144             self.storedLocalObjectStates = [NSMutableDictionary dictionary];
145         for (NSString *accountName in accountsNames) {
146             NSMutableDictionary *accountStoredLocalObjectStates = [storedLocalObjectStates objectForKey:accountName];
147             if (!accountStoredLocalObjectStates) {
148                 accountStoredLocalObjectStates = [NSMutableDictionary dictionary];
149                 [storedLocalObjectStates setObject:accountStoredLocalObjectStates forKey:accountName];
150             }
151             for (ASIPithosContainer *pithosContainer in [accountsPithosContainers objectForKey:accountName]) {
152                 if (![accountStoredLocalObjectStates objectForKey:pithosContainer.name])
153                     [accountStoredLocalObjectStates setObject:[NSMutableDictionary dictionary] forKey:pithosContainer.name];
154             }
155         }
156     }
157 }
158
159 - (void)resetLocalStateWithAll:(BOOL)all {
160     @autoreleasepool {
161         self.lastCompletedSync = nil;
162         if (all) {
163             self.storedLocalObjectStates = [NSMutableDictionary dictionary];
164             [self saveLocalState]; // Save an empty dictionary
165             [self loadLocalState]; // Load to populate with containers
166             [self saveLocalState]; // Save again
167             if (self.tempDownloadsDirPath)
168                 [PithosUtilities removeContentsAtPath:self.tempDownloadsDirPath];
169         } else {
170             // Remove containers that don't interest us anymore and save
171             if (!storedLocalObjectStates)
172                 [self loadLocalState];
173             for (NSString *accountName in [storedLocalObjectStates allKeys]) {
174                 NSMutableDictionary *accountStoredLocalObjectStates = [storedLocalObjectStates objectForKey:accountName];
175                 NSDictionary *containersDictionary = [accountsDictionary objectForKey:accountName];
176                 if (!containersDictionary) {
177                     if (self.tempDownloadsDirPath) {
178                         if ([accountName isEqualToString:@""]) {
179                             for (NSString *containerName in accountStoredLocalObjectStates) {
180                                 [PithosUtilities removeContentsAtPath:[self.tempDownloadsDirPath stringByAppendingPathComponent:containerName]
181                                                          andDirectory:YES];
182                             }
183                         } else {
184                             [PithosUtilities removeContentsAtPath:[[self.tempDownloadsDirPath stringByAppendingPathComponent:@"shared with me"]
185                                                                    stringByAppendingPathComponent:accountName]
186                                                      andDirectory:YES];
187                         }
188                     }
189                     [storedLocalObjectStates removeObjectForKey:accountName];
190                 } else {
191                     // Check the account's containers
192                     for (NSString *containerName in [accountStoredLocalObjectStates allKeys]) {
193                         if (![containersDictionary objectForKey:containerName]) {
194                             if ([accountName isEqualToString:@""])
195                                 [PithosUtilities removeContentsAtPath:[self.tempDownloadsDirPath stringByAppendingPathComponent:containerName]
196                                                          andDirectory:YES];
197                             else
198                                 [PithosUtilities removeContentsAtPath:[[[self.tempDownloadsDirPath stringByAppendingPathComponent:@"shared with me"]
199                                                                         stringByAppendingPathComponent:accountName] 
200                                                                        stringByAppendingPathComponent:containerName]
201                                                          andDirectory:YES];
202                             [accountStoredLocalObjectStates removeObjectForKey:containerName];
203                         }
204                     }
205                 }
206             }
207             [self saveLocalState];
208         }
209     }
210 }
211
212 - (void)saveLocalState {
213     @autoreleasepool {
214         [NSKeyedArchiver archiveRootObject:storedLocalObjectStates toFile:self.pithosStateFilePath];
215     }
216 }
217
218 - (void)resetDaemon {
219     @synchronized(self) {
220         if (!daemonActive)
221             return;
222     }
223     
224     [networkQueue reset];
225     [callbackQueue cancelAllOperations];
226     [callbackQueue setSuspended:YES];
227     [self emptyTempTrash];
228     
229     syncOperationCount = 0;
230     
231     @synchronized(self) {            
232         daemonActive = NO;
233     }
234 }
235
236 - (void)startDaemon {
237     @synchronized(self) {
238         if (daemonActive)
239             return;
240     }
241
242     // In the improbable case of leftover operations
243     [networkQueue reset];
244     [callbackQueue cancelAllOperations];
245     
246     syncOperationCount = 0;
247     newSyncRequested = NO;
248     syncIncomplete = NO;
249     syncLate = NO;
250     
251     [self loadLocalState];
252     
253     [networkQueue go];
254     [callbackQueue setSuspended:NO];
255     
256     @synchronized(self) {
257         daemonActive = YES;
258     }
259 }
260
261 - (void)dealloc {
262     [[NSNotificationCenter defaultCenter] removeObserver:self];
263     [self resetDaemon];
264 }
265
266 #pragma mark -
267 #pragma mark Observers
268
269 - (void)applicationWillTerminate:(NSNotification *)notification {
270     [self saveLocalState];
271 }
272
273 #pragma mark -
274 #pragma mark Properties
275
276 - (NSString *)userCatalogFilePath {
277     if (!userCatalogFilePath) {
278         userCatalogFilePath = [[[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) objectAtIndex:0]
279                                 stringByAppendingPathComponent:[[NSBundle mainBundle] bundleIdentifier]]
280                                stringByAppendingPathComponent:[NSString stringWithFormat:@"%@-SyncUserCatalog.archive",
281                                                                pithosAccount.uniqueName]];
282         NSString *userCatalogFileDirPath = [userCatalogFilePath stringByDeletingLastPathComponent];
283         NSFileManager *fileManager = [NSFileManager defaultManager];
284         BOOL isDirectory;
285         BOOL fileExists = [fileManager fileExistsAtPath:userCatalogFileDirPath isDirectory:&isDirectory];
286         NSError *error = nil;
287         if (fileExists && !isDirectory)
288             [fileManager removeItemAtPath:userCatalogFileDirPath error:&error];
289         if (!error && !fileExists)
290             [fileManager createDirectoryAtPath:userCatalogFileDirPath withIntermediateDirectories:YES attributes:nil error:&error];
291         //if (error)
292         //  userCatalogFilePath = nil;
293         // XXX create a dir using mktmps?
294     }
295     return [userCatalogFilePath copy];
296 }
297
298 - (NSString *)pithosStateFilePath {
299     if (!pithosStateFilePath) {
300         pithosStateFilePath = [[[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) objectAtIndex:0] 
301                                  stringByAppendingPathComponent:[[NSBundle mainBundle] bundleIdentifier]] 
302                                 stringByAppendingPathComponent:[NSString stringWithFormat:@"%@-PithosLocalObjectStates.archive", 
303                                                                 pithosAccount.uniqueName]];
304         NSString *pithosStateFileDirPath = [pithosStateFilePath stringByDeletingLastPathComponent];
305         NSFileManager *fileManager = [NSFileManager defaultManager];
306         BOOL isDirectory;
307         BOOL fileExists = [fileManager fileExistsAtPath:pithosStateFileDirPath isDirectory:&isDirectory];
308         NSError *error = nil;
309         if (fileExists && !isDirectory)
310             [fileManager removeItemAtPath:pithosStateFileDirPath error:&error];
311         if (!error && !fileExists)
312             [fileManager createDirectoryAtPath:pithosStateFileDirPath withIntermediateDirectories:YES attributes:nil error:&error];
313         //if (error)
314         //  pithosStateFilePath = nil;
315         // XXX create a dir using mktmps?
316     }
317     return [pithosStateFilePath copy];
318 }
319
320 - (NSString *)tempDownloadsDirPath {
321     if (!tempDownloadsDirPath) {
322         tempDownloadsDirPath = [[[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) objectAtIndex:0] 
323                                   stringByAppendingPathComponent:[[NSBundle mainBundle] bundleIdentifier]] 
324                                  stringByAppendingPathComponent:[NSString stringWithFormat:@"%@-TempDownloads", 
325                                                                  pithosAccount.uniqueName]];
326         NSFileManager *fileManager = [NSFileManager defaultManager];
327         BOOL isDirectory;
328         BOOL fileExists = [fileManager fileExistsAtPath:tempDownloadsDirPath isDirectory:&isDirectory];
329         NSError *error = nil;
330         if (fileExists && !isDirectory)
331             [fileManager removeItemAtPath:tempDownloadsDirPath error:&error];
332         if (!error && !fileExists)
333             [fileManager createDirectoryAtPath:tempDownloadsDirPath withIntermediateDirectories:YES attributes:nil error:&error];
334         //if (error)
335         //    tempDownloadsDirPath = nil;
336         // XXX create a dir using mktmps?
337     }
338     return tempDownloadsDirPath;    
339 }
340
341 - (NSString *)tempTrashDirPath {
342     if (!tempTrashDirPath) {
343         tempTrashDirPath = [[[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) objectAtIndex:0] 
344                                   stringByAppendingPathComponent:[[NSBundle mainBundle] bundleIdentifier]] 
345                                  stringByAppendingPathComponent:[NSString stringWithFormat:@"%@-TempTrash", 
346                                                                  pithosAccount.uniqueName]];
347         NSFileManager *fileManager = [NSFileManager defaultManager];
348         BOOL isDirectory;
349         BOOL fileExists = [fileManager fileExistsAtPath:tempTrashDirPath isDirectory:&isDirectory];
350         NSError *error = nil;
351         if (fileExists && !isDirectory)
352             [fileManager removeItemAtPath:tempTrashDirPath error:&error];
353         if (!error && !fileExists)
354             [fileManager createDirectoryAtPath:tempTrashDirPath withIntermediateDirectories:YES attributes:nil error:&error];
355         //if (error)
356         //    tempTrashDirPath = nil;
357         // XXX create a dir using mktmps?
358     }
359     return tempTrashDirPath;
360 }
361
362 - (void)setDirectoryPath:(NSString *)aDirectoryPath {
363     if (aDirectoryPath && ![aDirectoryPath isEqualToString:directoryPath]) {
364         [self resetDaemon];
365         [self resetLocalStateWithAll:YES];
366         directoryPath = [aDirectoryPath copy];
367     }
368 }
369
370 - (void)setAccountsDictionary:(NSDictionary *)anAccountsDictionary {
371     if (anAccountsDictionary && ![anAccountsDictionary isEqualToDictionary:accountsDictionary]) {
372         BOOL reset = (accountsDictionary != nil);
373         if (reset)
374             [self resetDaemon];
375         
376         accountsDictionary = [anAccountsDictionary copy];
377         
378         accountsCount = [accountsDictionary count];
379         self.accountsNames = [NSMutableArray arrayWithCapacity:accountsCount];
380         self.accountsPithosContainers = [NSMutableDictionary dictionaryWithCapacity:accountsCount];
381         NSDictionary *containersDictionary = [accountsDictionary objectForKey:@""];
382         if (containersDictionary && [containersDictionary count]) {
383             NSMutableArray *pithosContainers = [NSMutableArray arrayWithCapacity:[containersDictionary count]];
384             for (NSString *containerName in containersDictionary) {
385                 ASIPithosContainer *pithosContainer = [ASIPithosContainer container];
386                 pithosContainer.name = containerName;
387                 [pithosContainers addObject:pithosContainer];
388             }
389             [accountsNames addObject:@""];
390             [accountsPithosContainers setObject:pithosContainers forKey:@""];
391         }
392         for (NSString *accountName in accountsDictionary) {
393             NSDictionary *containersDictionary = [accountsDictionary objectForKey:accountName];
394             if (![accountsNames containsObject:accountName] && [containersDictionary count]) {
395                 NSMutableArray *pithosContainers = [NSMutableArray arrayWithCapacity:[containersDictionary count]];
396                 for (NSString *containerName in containersDictionary) {
397                     ASIPithosContainer *pithosContainer = [ASIPithosContainer container];
398                     pithosContainer.name = containerName;
399                     [pithosContainers addObject:pithosContainer];
400                 }
401                 [accountsNames addObject:accountName];
402                 [accountsPithosContainers setObject:pithosContainers forKey:accountName];
403             }
404         }
405
406         if (reset)
407             [self resetLocalStateWithAll:NO];
408     }
409 }
410
411 - (void)setPithos:(ASIPithos *)aPithos {
412     if (!pithos) {
413         pithos = [ASIPithos pithos];
414         pithos.authUser = [aPithos.authUser copy];
415         pithos.authToken = [aPithos.authToken copy];
416         pithos.storageURLPrefix = [aPithos.storageURLPrefix copy];
417         pithos.authURL = [aPithos.authURL copy];
418         pithos.publicURLPrefix = [aPithos.publicURLPrefix copy];
419         pithos.userCatalogURL = [aPithos.userCatalogURL copy];
420     }
421     if (aPithos && 
422         (![aPithos.authUser isEqualToString:pithos.authUser] || 
423          ![aPithos.authToken isEqualToString:pithos.authToken] || 
424          ![aPithos.storageURLPrefix isEqualToString:pithos.storageURLPrefix])) {
425         [self resetDaemon];
426         if (![aPithos.authUser isEqualToString:pithos.authUser] || 
427             ![aPithos.storageURLPrefix isEqualToString:pithos.storageURLPrefix])
428             [self resetLocalStateWithAll:YES];
429         pithos.authUser = [aPithos.authUser copy];
430         pithos.authToken = [aPithos.authToken copy];
431         pithos.storageURLPrefix = [aPithos.storageURLPrefix copy];
432         pithos.authURL = [aPithos.authURL copy];
433         pithos.publicURLPrefix = [aPithos.publicURLPrefix copy];
434         pithos.userCatalogURL = [aPithos.userCatalogURL copy];
435     }
436 }
437
438 #pragma mark -
439 #pragma mark Helper Methods
440
441 - (BOOL)createSyncDirectory:(NSString *)dirPath {
442     NSFileManager *fileManager = [NSFileManager defaultManager];
443     BOOL isDirectory;
444     NSError *error = nil;
445     if (![fileManager fileExistsAtPath:dirPath isDirectory:&isDirectory]) {
446         if (![fileManager createDirectoryAtPath:dirPath withIntermediateDirectories:YES attributes:nil error:&error] || error) {
447             [PithosUtilities fileActionFailedAlertWithTitle:@"Local Sync Directory Error" 
448                                                     message:[NSString stringWithFormat:@"Cannot create local sync directory at '%@'", 
449                                                              dirPath] 
450                                                       error:error];
451             return NO;
452         }
453     } else if (!isDirectory) {
454         [PithosUtilities fileActionFailedAlertWithTitle:@"Local Sync Directory Error" 
455                                                 message:[NSString stringWithFormat:@"File already exists at the local sync directory path at '%@'", 
456                                                          dirPath] 
457                                                   error:nil];
458         return NO;
459     }
460     return YES;
461 }
462
463 - (NSString *)relativeDirPathForAccount:(NSString *)accountName container:(NSString *)containerName {
464     if ([accountName isEqualToString:@""]) {
465         return containerName;
466     } else {
467         NSMutableDictionary *displaynameDictionary = [userCatalog objectForKey:accountName];
468         if ([[displaynameDictionary objectForKey:@"suffix"] unsignedIntegerValue] == 0) {
469             return [[@"shared with me" stringByAppendingPathComponent:[displaynameDictionary objectForKey:@"displayname"]]
470                     stringByAppendingPathComponent:containerName];
471         } else {
472             return [[@"shared with me" stringByAppendingPathComponent:[NSString stringWithFormat:@"%@ (%@)",
473                                                                        [displaynameDictionary objectForKey:@"displayname"],
474                                                                        [displaynameDictionary objectForKey:@"suffix"]]]
475                     stringByAppendingPathComponent:containerName];
476         }
477     }
478 }
479
480 - (NSString *)dirPathForAccount:(NSString *)accountName container:(NSString *)containerName {
481     return [directoryPath stringByAppendingPathComponent:[self relativeDirPathForAccount:accountName container:containerName]];
482 }
483
484 #pragma mark -
485 #pragma mark Sync
486
487 - (void)syncOperationStarted {
488     @synchronized(self) {
489         syncOperationCount++;
490     }
491 }
492
493 - (void)syncOperationFinishedWithSuccess:(BOOL)operationSuccessfull {
494     @synchronized(self) {
495         if (!operationSuccessfull)
496             syncIncomplete = YES;
497         if (syncOperationCount == 0) {
498             // XXX This shouldn't happen, maybe change operationCount to a BOOL as operationsPending in browser
499             DLog(@"Sync::WARNING: tried to decrease syncOperationCount when 0");
500             return;
501         }
502         syncOperationCount--;
503         if (syncOperationCount == 0) {
504             if (!syncIncomplete) {
505                 self.lastCompletedSync = [NSDate date];
506                 dispatch_async(dispatch_get_main_queue(), ^{
507                     [activityFacility startAndEndActivityWithType:PithosActivityOther 
508                                                           message:[NSString stringWithFormat:@"Sync: Completed %@", lastCompletedSync] 
509                                                     pithosAccount:pithosAccount];
510                     
511                 });
512             }
513             [self emptyTempTrash];
514             if (newSyncRequested && daemonActive)
515                 [self sync];
516         }
517     }
518 }
519
520 - (BOOL)isSyncing {
521     @synchronized(self) {
522         return ((syncOperationCount > 0) && daemonActive);
523     }
524 }
525
526 - (void)syncLate {
527     @synchronized(self) {
528         if ([self isSyncing])
529             syncLate = YES;
530     }
531 }
532
533 - (void)sync {
534     @synchronized(self) {
535         if ([self isSyncing]) {
536             // If at least one operation is running return
537             newSyncRequested = YES;
538             return;
539         } else if (daemonActive && accountsCount) {
540             // The first operation is the server listing
541             [self syncOperationStarted];
542             newSyncRequested = NO;
543             syncIncomplete = NO;
544             syncLate = NO;
545         } else {
546             return;
547         }
548     }
549
550     if (![self createSyncDirectory:directoryPath]) {
551         [self syncOperationFinishedWithSuccess:NO];
552         return;
553     }
554     // Update user catalog for accountsNames.
555     ASIPithosRequest *userCatalogRequest = [pithosAccount updateUserCatalogForForDisplaynames:nil UUIDs:accountsNames];
556     if (userCatalogRequest.error || ((userCatalogRequest.responseStatusCode != 200) && (userCatalogRequest.responseStatusCode != 404))) {
557         // Update failed try sync again later.
558         [self syncOperationFinishedWithSuccess:NO];
559         return;
560     }
561     for (NSString *accountName in accountsNames) {
562         if (![accountName isEqualToString:@""]) {
563             // If 404, displayname will be same as accountName.
564             NSString *displayname = [pithosAccount displaynameForUUID:accountName safe:YES];
565             NSMutableDictionary *displaynameDictionary = [userCatalog objectForKey:accountName];
566             if (!displaynameDictionary) {
567                 // New entry in the user catalog. Determine suffix discriminator (0 is for no suffix).
568                 NSNumber *suffix = [NSNumber numberWithInteger:0];
569                 for (NSMutableDictionary *otherDisplaynameDictionary in [userCatalog objectEnumerator]) {
570                     if ([[otherDisplaynameDictionary objectForKey:@"displayname"] isEqualToString:displayname] &&
571                         [[otherDisplaynameDictionary objectForKey:@"suffix"] isGreaterThanOrEqualTo:suffix]) {
572                         suffix = [NSNumber numberWithUnsignedInteger:([[otherDisplaynameDictionary objectForKey:@"suffix"] unsignedIntegerValue] + 1)];
573                     }
574                 }
575                 [userCatalog setObject:[NSMutableDictionary dictionaryWithObjectsAndKeys:
576                                         displayname, @"displayname",
577                                         suffix, @"suffix",
578                                         nil]
579                                 forKey:accountName];
580                 // Save user catalog.
581                 [NSKeyedArchiver archiveRootObject:userCatalog toFile:self.userCatalogFilePath];
582             } else if (![[displaynameDictionary objectForKey:@"displayname"] isEqualToString:displayname]) {
583                 // First determine new suffix.
584                 NSNumber *suffix = [NSNumber numberWithInteger:0];
585                 for (NSMutableDictionary *otherDisplaynameDictionary in [userCatalog objectEnumerator]) {
586                     if ([[otherDisplaynameDictionary objectForKey:@"displayname"] isEqualToString:displayname] &&
587                         [[otherDisplaynameDictionary objectForKey:@"suffix"] isGreaterThanOrEqualTo:suffix]) {
588                         suffix = [NSNumber numberWithUnsignedInteger:([[otherDisplaynameDictionary objectForKey:@"suffix"] unsignedIntegerValue] + 1)];
589                     }
590                 }
591                 // Compute old and new sync account dirPaths.
592                 NSString *accountDirPath = (([[displaynameDictionary objectForKey:@"suffix"] unsignedIntegerValue] == 0) ?
593                                             [[directoryPath stringByAppendingPathComponent:@"shared with me"]
594                                              stringByAppendingPathComponent:[displaynameDictionary objectForKey:@"displayname"]] :
595                                             [[directoryPath stringByAppendingPathComponent:@"shared with me"]
596                                              stringByAppendingPathComponent:[NSString stringWithFormat:@"%@ (%@)",
597                                                                              [displaynameDictionary objectForKey:@"displayname"],
598                                                                              [displaynameDictionary objectForKey:@"suffix"]]]);
599                 NSString *newAccountDirPath = (([suffix unsignedIntegerValue] == 0) ?
600                                                [[directoryPath stringByAppendingPathComponent:@"shared with me"]
601                                                 stringByAppendingPathComponent:displayname] :
602                                                [[directoryPath stringByAppendingPathComponent:@"shared with me"]
603                                                 stringByAppendingPathComponent:[NSString stringWithFormat:@"%@ (%@)",
604                                                                                 displayname, suffix]]);
605                 // Check if the old account sync directory exists.
606                 NSFileManager *fileManager = [NSFileManager defaultManager];
607                 BOOL isDirectory;
608                 NSError *error = nil;
609                 if ([fileManager fileExistsAtPath:accountDirPath isDirectory:&isDirectory] && isDirectory &&
610                     (![fileManager moveItemAtPath:accountDirPath toPath:newAccountDirPath error:&error] || error)) {
611                     // If so try to move it. We know that the containing directory "shared with me" exists.
612                     [PithosUtilities fileActionFailedAlertWithTitle:@"Move Directory Error"
613                                                             message:[NSString stringWithFormat:@"Cannot move directory at '%@' to '%@'",
614                                                                      accountDirPath, newAccountDirPath]
615                                                               error:error];
616                     [self syncOperationFinishedWithSuccess:NO];
617                     return;
618                 }
619                 // Else the new account sync directory will be created afterwards.
620                 // Fix filePaths of the stored local object states of the account.
621                 for (NSMutableDictionary *containerStoredLocalObjectStates in [[storedLocalObjectStates objectForKey:accountName] objectEnumerator]) {
622                     for (PithosLocalObjectState *storedLocalObjectState in [containerStoredLocalObjectStates objectEnumerator]) {
623                         if ([storedLocalObjectState.filePath hasPrefix:accountDirPath]) {
624                             storedLocalObjectState.filePath = [newAccountDirPath stringByAppendingString:
625                                                                [storedLocalObjectState.filePath substringFromIndex:accountDirPath.length]];
626                         }
627                     }
628                 }
629                 [self saveLocalState];
630                 // Finally update user catalog.
631                 [userCatalog setObject:[NSMutableDictionary dictionaryWithObjectsAndKeys:
632                                         displayname, @"displayname",
633                                         suffix, @"suffix",
634                                         nil]
635                                 forKey:accountName];
636                 // Save user catalog.
637                 [NSKeyedArchiver archiveRootObject:userCatalog toFile:self.userCatalogFilePath];
638             }
639             // Else no change is required.
640         }
641     }
642
643     for (NSString *accountName in accountsNames) {
644         for (ASIPithosContainer *pithosContainer in [accountsPithosContainers objectForKey:accountName]) {
645             if (![self createSyncDirectory:[self dirPathForAccount:accountName container:pithosContainer.name]]) {
646                 [self syncOperationFinishedWithSuccess:NO];
647                 return;
648             }
649         }
650     }
651
652     self.remoteObjects = [NSMutableDictionary dictionaryWithCapacity:accountsCount];
653     for (NSString *accountName in accountsNames) {
654         [remoteObjects setObject:[NSMutableDictionary dictionary] forKey:accountName];
655     }
656     accountsIndex = 0;
657     containersIndex = 0;
658     NSString *accountName = [accountsNames objectAtIndex:accountsIndex];
659     ASIPithosContainer *pithosContainer = [[accountsPithosContainers objectForKey:accountName] objectAtIndex:containersIndex];
660     ASIPithosContainerRequest *containerRequest = [ASIPithosContainerRequest listObjectsRequestWithPithos:pithos 
661                                                                                             containerName:pithosContainer.name 
662                                                                                                     limit:0 
663                                                                                                    marker:nil 
664                                                                                                    prefix:nil 
665                                                                                                 delimiter:nil 
666                                                                                                      path:nil 
667                                                                                                      meta:nil 
668                                                                                                    shared:NO 
669                                                                                                     until:nil 
670                                                                                           ifModifiedSince:pithosContainer.lastModified];
671     if (![accountName isEqualToString:@""])
672         [containerRequest setRequestUserFromDefaultTo:accountName withPithos:pithos];
673     containerRequest.delegate = self;
674     containerRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:);
675     containerRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:);
676     PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityOther 
677                                                                message:@"Sync: Getting server listing" 
678                                                          pithosAccount:pithosAccount];
679     containerRequest.userInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys:
680                                  activity, @"activity", 
681                                  @"Sync: Getting server listing (stopped)", @"stoppedActivityMessage", 
682                                  @"Sync: Getting server listing (failed)", @"failedActivityMessage", 
683                                  @"Sync: Getting server listing (finished)", @"finishedActivityMessage", 
684                                  [NSNumber numberWithInteger:NSOperationQueuePriorityHigh], @"priority", 
685                                  [NSNumber numberWithUnsignedInteger:10], @"retries", 
686                                  NSStringFromSelector(@selector(listRequestFinished:)), @"didFinishSelector", 
687                                  NSStringFromSelector(@selector(listRequestFailed:)), @"didFailSelector", 
688                                  nil];
689     [networkQueue addOperation:[PithosUtilities prepareRequest:containerRequest priority:NSOperationQueuePriorityHigh]];
690 }
691
692 - (void)emptyTempTrash {
693     @autoreleasepool {
694         NSString *trashDirPath = self.tempTrashDirPath;
695         if (trashDirPath) {
696             NSFileManager *fileManager = [NSFileManager defaultManager];
697             NSError *error = nil;
698 //          NSArray *subPaths = [[NSFileManager defaultManager] subpathsOfDirectoryAtPath:trashDirPath error:&error];
699             NSArray *subPaths = [fileManager contentsOfDirectoryAtPath:trashDirPath error:&error];
700             if (error) {
701                 [PithosUtilities fileActionFailedAlertWithTitle:@"Directory Contents Error" 
702                                                         message:[NSString stringWithFormat:@"Cannot get contents of directory at '%@'", trashDirPath] 
703                                                           error:error];
704                 return;
705             }
706             if ([subPaths count]) {
707 //              NSMutableArray *subURLs = [NSMutableArray arrayWithCapacity:[subPaths count]];
708 //              for (NSString *subPath in subPaths) {
709 //                  [subURLs addObject:[NSURL fileURLWithPath:[trashDirPath stringByAppendingPathComponent:subPath]]];
710 //              }
711 //              [self syncOperationStarted];
712 //              [[NSWorkspace sharedWorkspace] recycleURLs:subURLs completionHandler:^(NSDictionary *newURLs, NSError *error) {
713 //                  if (error) {
714 //                      [PithosUtilities fileActionFailedAlertWithTitle:@"Move to Trash Error"
715 //                                                              message:@"Cannot move files to Trash"
716 //                                                                  error:error];
717 //                  }
718 //                  [self syncOperationFinishedWithSuccess:YES];
719 //              }];
720                 for (NSString *subPath in subPaths) {
721                     NSString *subFilePath = [trashDirPath stringByAppendingPathComponent:subPath];
722                     error = nil;
723                     if (![fileManager removeItemAtPath:subFilePath error:&error] || error)
724                         [PithosUtilities fileActionFailedAlertWithTitle:@"Remove File Error" 
725                                                                 message:[NSString stringWithFormat:@"Cannot remove file at '%@'", subFilePath] 
726                                                                   error:error];
727                 }
728             }
729         }
730     }
731 }
732
733 - (BOOL)moveToTempTrashFile:(NSString *)filePath 
734                 accountName:(NSString *)accountName 
735             pithosContainer:(ASIPithosContainer *)pithosContainer {
736     @autoreleasepool {
737         if (!self.tempTrashDirPath)
738             return NO;
739         NSFileManager *fileManager = [NSFileManager defaultManager];
740         BOOL isDirectory;
741         BOOL fileExists = [fileManager fileExistsAtPath:filePath isDirectory:&isDirectory];
742         NSError *error = nil;
743         NSString *newFilePath = [filePath stringByReplacingOccurrencesOfString:directoryPath withString:self.tempTrashDirPath];
744         NSString *newDirPath = [newFilePath stringByDeletingLastPathComponent];
745         if (fileExists && isDirectory) {
746             NSArray *subPaths = [fileManager subpathsOfDirectoryAtPath:filePath error:&error];
747             if (error) {
748                 [PithosUtilities fileActionFailedAlertWithTitle:@"Directory Contents Error" 
749                                                         message:[NSString stringWithFormat:@"Cannot get contents of directory at '%@'", filePath] 
750                                                           error:error];
751                 return NO;
752             }
753             if (![fileManager createDirectoryAtPath:newDirPath withIntermediateDirectories:YES attributes:nil error:&error] || error) {
754                 [PithosUtilities fileActionFailedAlertWithTitle:@"Create Directory Error" 
755                                                         message:[NSString stringWithFormat:@"Cannot create directory at '%@'", newDirPath] 
756                                                           error:error];
757                 return NO;
758             }
759             if (![fileManager moveItemAtPath:filePath toPath:newFilePath error:&error] || error) {
760                 [PithosUtilities fileActionFailedAlertWithTitle:@"Move File Error" 
761                                                         message:[NSString stringWithFormat:@"Cannot move file at '%@' to '%@'", 
762                                                                  filePath, newFilePath] 
763                                                           error:error];
764                 return NO;
765             }
766             PithosLocalObjectState *currentState = [currentLocalObjectStates objectForKey:filePath];
767             if (currentState) {
768                 currentState.filePath = newFilePath;
769                 [currentLocalObjectStates setObject:currentState forKey:newFilePath];
770                 [currentLocalObjectStates removeObjectForKey:filePath];        
771             } else {
772                 [currentLocalObjectStates setObject:[PithosLocalObjectState localObjectStateWithFile:newFilePath 
773                                                                                            blockHash:pithosContainer.blockHash 
774                                                                                            blockSize:pithosContainer.blockSize] 
775                                              forKey:newFilePath];
776             }
777             for (NSString *subPath in subPaths) {
778                 NSString *subFilePath = [filePath stringByAppendingPathComponent:subPath];
779                 NSString *newSubFilePath = [subFilePath stringByReplacingOccurrencesOfString:directoryPath
780                                                                                   withString:self.tempTrashDirPath];
781                 currentState = [currentLocalObjectStates objectForKey:subFilePath];
782                 if (currentState) {
783                     currentState.filePath = newSubFilePath;
784                     [currentLocalObjectStates setObject:currentState forKey:newSubFilePath];
785                     [currentLocalObjectStates removeObjectForKey:subFilePath];
786                 } else {
787                     [currentLocalObjectStates setObject:[PithosLocalObjectState localObjectStateWithFile:newSubFilePath 
788                                                                                                blockHash:pithosContainer.blockHash 
789                                                                                                blockSize:pithosContainer.blockSize] 
790                                                  forKey:newSubFilePath];
791                 }        
792             }
793         } else if (fileExists && !isDirectory) {
794             if (![fileManager createDirectoryAtPath:newDirPath withIntermediateDirectories:YES attributes:nil error:&error] || error) {
795                 [PithosUtilities fileActionFailedAlertWithTitle:@"Create Directory Error" 
796                                                         message:[NSString stringWithFormat:@"Cannot create directory at '%@'", newDirPath] 
797                                                           error:error];
798                 return NO;
799             }
800             if (![fileManager moveItemAtPath:filePath toPath:newFilePath error:&error] || error) {
801                 [PithosUtilities fileActionFailedAlertWithTitle:@"Move File Error" 
802                                                         message:[NSString stringWithFormat:@"Cannot move file at '%@' to '%@'", 
803                                                                  filePath, newFilePath] 
804                                                           error:error];
805                 return NO;
806             }
807             PithosLocalObjectState *currentState = [currentLocalObjectStates objectForKey:filePath];
808             if (currentState) {
809                 currentState.filePath = newFilePath;
810                 [currentLocalObjectStates setObject:currentState forKey:newFilePath];
811                 [currentLocalObjectStates removeObjectForKey:filePath];        
812             } else {
813                 [currentLocalObjectStates setObject:[PithosLocalObjectState localObjectStateWithFile:newFilePath 
814                                                                                            blockHash:pithosContainer.blockHash 
815                                                                                            blockSize:pithosContainer.blockSize] 
816                                              forKey:newFilePath];
817             }
818         }
819     }
820     return YES;
821 }
822
823 - (BOOL)findLocalCopyForObjectWithHash:(NSString *)hash forFile:(NSString *)filePath {
824     if ([hash length] != 64)
825         return NO;
826     @autoreleasepool {
827         PithosLocalObjectState *localState;
828         NSFileManager *fileManager = [NSFileManager defaultManager];
829         BOOL isDirectory;
830         NSError *error = nil;
831         for (NSString *localFilePath in [currentLocalObjectStates allKeys]) {
832             localState = [currentLocalObjectStates objectForKey:localFilePath];
833             if (!localState.isDirectory && [hash isEqualToString:localState.hash] && 
834                 [fileManager fileExistsAtPath:localFilePath isDirectory:&isDirectory] && !isDirectory) {
835                 if ([localFilePath hasPrefix:directoryPath]) {
836                     if (![fileManager copyItemAtPath:localFilePath toPath:filePath error:&error] || error) {
837                         [PithosUtilities fileActionFailedAlertWithTitle:@"Copy File Error" 
838                                                                 message:[NSString stringWithFormat:@"Cannot copy file at '%@' to '%@'", 
839                                                                          localFilePath, filePath] 
840                                                                   error:error];
841                     } else {
842                         return YES;
843                     }
844                 } else if (self.tempTrashDirPath && [localFilePath hasPrefix:self.tempTrashDirPath]) {
845                     if (![fileManager moveItemAtPath:localFilePath toPath:filePath error:&error] || error) {
846                         [PithosUtilities fileActionFailedAlertWithTitle:@"Move File Error" 
847                                                                 message:[NSString stringWithFormat:@"Cannot move file at '%@' to '%@'", 
848                                                                          localFilePath, filePath] 
849                                                                   error:error];
850                     } else {
851                         localState.filePath = filePath;
852                         [currentLocalObjectStates setObject:localState forKey:filePath];
853                         [currentLocalObjectStates removeObjectForKey:localFilePath];
854                         return YES;
855                     }
856                 }
857             }
858         }
859     }
860     return NO;
861 }
862
863 - (void)updateLocalStateWithObject:(ASIPithosObject *)object
864                      localFilePath:(NSString *)filePath 
865                        accountName:(NSString *)accountName 
866                    pithosContainer:(ASIPithosContainer *)pithosContainer {
867     @autoreleasepool {
868         NSFileManager *fileManager = [NSFileManager defaultManager];
869         NSError *error;
870         BOOL isDirectory;
871         BOOL fileExists = [fileManager fileExistsAtPath:filePath isDirectory:&isDirectory];
872         NSString *fileDirectoryPath = [filePath stringByDeletingLastPathComponent];
873         NSMutableDictionary *containerStoredLocalObjectStates = [[storedLocalObjectStates objectForKey:accountName] 
874                                                                  objectForKey:pithosContainer.name];
875         PithosLocalObjectState *storedState = [containerStoredLocalObjectStates objectForKey:object.name];
876         // Remote updated info
877         NSError *remoteError;
878         BOOL remoteIsDirectory;
879         BOOL remoteObjectExists = [PithosUtilities objectExistsAtPithos:pithos 
880                                                           containerName:pithosContainer.name 
881                                                              objectName:object.name 
882                                                                   error:&remoteError 
883                                                             isDirectory:&remoteIsDirectory 
884                                                          sharingAccount:([accountName isEqualToString:@""] ? nil : accountName)];
885         if (!object || !object.objectHash) {
886             // Delete local object
887             if (![accountName isEqualToString:@""])
888                 // If "shared with me" skip
889                 return;
890             if (remoteObjectExists) {
891                 // Remote object created in the meantime, just mark the sync cycle as incomplete, but do delete the local object
892                 syncIncomplete = YES;
893             }
894             DLog(@"Sync::delete local object: %@", filePath);
895             if (!fileExists || [self moveToTempTrashFile:filePath accountName:accountName pithosContainer:pithosContainer]) {
896                 dispatch_async(dispatch_get_main_queue(), ^{
897                     [activityFacility startAndEndActivityWithType:PithosActivityOther 
898                                                           message:[NSString stringWithFormat:@"Sync: Deleting '%@/%@' locally (finished)", 
899                                                                    pithosContainer.name, object.name] 
900                                                     pithosAccount:pithosAccount];
901                 });
902                 [containerStoredLocalObjectStates removeObjectForKey:object.name];
903                 [self saveLocalState];
904             }
905         } else if ([PithosUtilities isContentTypeDirectory:object.contentType]) {
906             // Create local directory object
907             if (!remoteObjectExists || !remoteIsDirectory) {
908                 // Remote directory object deleted or changed to a file object in the meantime, mark the sync cycle as incomplete and skip
909                 syncIncomplete = YES;
910                 return;
911             }
912             DLog(@"Sync::create local directory object: %@", filePath);
913             BOOL directoryCreated = NO;
914             if (!fileExists || (!isDirectory && [self moveToTempTrashFile:filePath accountName:accountName pithosContainer:pithosContainer])) {
915                 DLog(@"Sync::local directory object doesn't exist: %@", filePath);
916                 error = nil;
917                 if (![fileManager createDirectoryAtPath:filePath withIntermediateDirectories:YES attributes:nil error:&error] || error) {
918                     [PithosUtilities fileActionFailedAlertWithTitle:@"Create Directory Error" 
919                                                             message:[NSString stringWithFormat:@"Cannot create directory at '%@'", filePath] 
920                                                               error:error];
921                 } else {
922                     directoryCreated = YES;
923                     storedState.filePath = filePath;
924                     storedState.isDirectory = YES;
925                     [self saveLocalState];
926                 }
927             } else {
928                 DLog(@"Sync::local directory object exists: %@", filePath);
929                 directoryCreated = YES;
930             }
931             if (directoryCreated)
932                 dispatch_async(dispatch_get_main_queue(), ^{
933                     [activityFacility startAndEndActivityWithType:PithosActivityOther 
934                                                           message:[NSString stringWithFormat:@"Sync: Creating directory '%@/%@' locally (finished)", 
935                                                                    [self relativeDirPathForAccount:accountName container:pithosContainer.name], 
936                                                                    object.name] 
937                                                     pithosAccount:pithosAccount];
938                 });
939         } else if (object.bytes == 0) {
940             // Create local object with zero length
941             if (!remoteObjectExists || remoteIsDirectory) {
942                 // Remote file object deleted or changed to a directory object in the meantime, mark the sync cycle as incomplete and skip
943                 syncIncomplete = YES;
944                 return;
945             }
946             DLog(@"Sync::create local zero length object: %@", filePath);
947             BOOL fileCreated = NO;
948             if (!fileExists || 
949                 ((isDirectory || [PithosUtilities bytesOfFile:filePath]) && 
950                  [self moveToTempTrashFile:filePath accountName:accountName pithosContainer:pithosContainer])) {
951                 DLog(@"Sync::local zero length object doesn't exist: %@", filePath);
952                 // Create directory of the file, if it doesn't exist
953                 error = nil;
954                 if (![PithosUtilities safeCreateDirectory:fileDirectoryPath error:&error]) {
955                     dispatch_async(dispatch_get_main_queue(), ^{
956                         [PithosUtilities fileActionFailedAlertWithTitle:@"Create Directory Error" 
957                                                                 message:[NSString stringWithFormat:@"Cannot create directory at '%@'", fileDirectoryPath] 
958                                                                   error:error];
959                     });
960                 }
961                 error = nil;
962                 if (![fileManager createFileAtPath:filePath contents:nil attributes:nil]) {
963                     [PithosUtilities fileActionFailedAlertWithTitle:@"Create File Error" 
964                                                             message:[NSString stringWithFormat:@"Cannot create file at '%@'", filePath] 
965                                                               error:error];
966                 } else {
967                     fileCreated = YES;
968                     storedState.filePath = filePath;
969                     storedState.hash = object.objectHash;
970                     storedState.tmpFilePath = nil;
971                     [self saveLocalState];
972                 }
973             } else {
974                 DLog(@"Sync::local zero length object exists: %@", filePath);
975                 fileCreated = YES;
976             }
977             if (fileCreated)
978                 dispatch_async(dispatch_get_main_queue(), ^{
979                     [activityFacility startAndEndActivityWithType:PithosActivityOther 
980                                                           message:[NSString stringWithFormat:@"Sync: Downloading '%@/%@' (100%%)", 
981                                                                    [self relativeDirPathForAccount:accountName container:pithosContainer.name], 
982                                                                    object.name] 
983                                                     pithosAccount:pithosAccount];
984                 });
985         } else if (storedState.tmpFilePath == nil) {
986             // Create new local object
987             if (!remoteObjectExists || remoteIsDirectory) {
988                 // Remote file object deleted or changed to a directory object in the meantime, mark the sync cycle as incomplete and skip
989                 syncIncomplete = YES;
990                 return;
991             }
992             // Create directory of the file, if it doesn't exist
993             error = nil;
994             if (![PithosUtilities safeCreateDirectory:fileDirectoryPath error:&error]) {
995                 [PithosUtilities fileActionFailedAlertWithTitle:@"Create Directory Error" 
996                                                         message:[NSString stringWithFormat:@"Cannot create directory at '%@'", fileDirectoryPath] 
997                                                           error:error];
998             }
999             // Check first if a local copy exists
1000             if ([self findLocalCopyForObjectWithHash:object.objectHash forFile:filePath]) {
1001                 storedState.filePath = filePath;
1002                 storedState.hash = object.objectHash;
1003                 [self saveLocalState];
1004                 dispatch_async(dispatch_get_main_queue(), ^{
1005                     [activityFacility startAndEndActivityWithType:PithosActivityOther 
1006                                                           message:[NSString stringWithFormat:@"Sync: Downloading '%@/%@' (100%%)", 
1007                                                                    [self relativeDirPathForAccount:accountName container:pithosContainer.name], 
1008                                                                    object.name] 
1009                                                     pithosAccount:pithosAccount];
1010                 });
1011             } else {
1012                 [self syncOperationStarted];
1013                 __block ASIPithosObjectRequest *objectRequest = [PithosUtilities objectBlockDataRequestWithPithos:pithos 
1014                                                                                                     containerName:pithosContainer.name 
1015                                                                                                            object:object 
1016                                                                                                        blockIndex:0 
1017                                                                                                         blockSize:pithosContainer.blockSize];
1018                 if (![accountName isEqualToString:@""])
1019                     [objectRequest setRequestUserFromDefaultTo:accountName withPithos:pithos];
1020                 objectRequest.delegate = self;
1021                 objectRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:);
1022                 objectRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:);
1023                 NSString *messagePrefix = [NSString stringWithFormat:@"Sync: Downloading '%@/%@'", 
1024                                            [self relativeDirPathForAccount:accountName container:pithosContainer.name], object.name];
1025                 PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityDownload 
1026                                                                            message:[messagePrefix stringByAppendingString:@" (0%%)"] 
1027                                                                         totalBytes:object.bytes 
1028                                                                       currentBytes:0 
1029                                                                      pithosAccount:pithosAccount];
1030                 dispatch_async(dispatch_get_main_queue(), ^{
1031                     [activityFacility updateActivity:activity withMessage:activity.message];
1032                 });
1033                 objectRequest.userInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys:
1034                                           accountName, @"accountName", 
1035                                           pithosContainer, @"pithosContainer", 
1036                                           object, @"pithosObject", 
1037                                           messagePrefix, @"messagePrefix", 
1038                                           [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, (NSUInteger)ceil((object.bytes +0.0)/(pithosContainer.blockSize + 0.0)))], @"missingBlocks", 
1039                                           [NSNumber numberWithUnsignedInteger:0], @"missingBlockIndex", 
1040                                           filePath, @"filePath", 
1041                                           activity, @"activity", 
1042                                           [messagePrefix stringByAppendingString:@" (stopped)"], @"stoppedActivityMessage", 
1043                                           [messagePrefix stringByAppendingString:@" (failed)"], @"failedActivityMessage", 
1044                                           [messagePrefix stringByAppendingString:@" (100%%)"], @"finishedActivityMessage", 
1045                                           [NSNumber numberWithInteger:NSOperationQueuePriorityNormal], @"priority", 
1046                                           [NSNumber numberWithUnsignedInteger:10], @"retries", 
1047                                           NSStringFromSelector(@selector(downloadObjectBlockFinished:)), @"didFinishSelector", 
1048                                           NSStringFromSelector(@selector(requestFailed:)), @"didFailSelector", 
1049                                           nil];
1050                 [objectRequest setBytesReceivedBlock:^(unsigned long long size, unsigned long long total){
1051                     [activityFacility updateActivity:activity 
1052                                          withMessage:[NSString stringWithFormat:@"%@ (%.0f%%)", 
1053                                                       messagePrefix, (100*(activity.currentBytes + size + 0.0)/(activity.totalBytes + 0.0))] 
1054                                           totalBytes:activity.totalBytes 
1055                                         currentBytes:(activity.currentBytes + size)];
1056                 }];
1057                 [networkQueue addOperation:[PithosUtilities prepareRequest:objectRequest priority:NSOperationQueuePriorityNormal]];
1058             }
1059         } else {
1060             // Resume local object download
1061             if (!remoteObjectExists || remoteIsDirectory) {
1062                 // Remote file object deleted or changed to a directory object in the meantime, mark the sync cycle as incomplete and skip
1063                 syncIncomplete = YES;
1064                 return;
1065             }
1066             // Create directory of the file, if it doesn't exist
1067             error = nil;
1068             if (![PithosUtilities safeCreateDirectory:fileDirectoryPath error:&error]) {
1069                 [PithosUtilities fileActionFailedAlertWithTitle:@"Create Directory Error" 
1070                                                         message:[NSString stringWithFormat:@"Cannot create directory at '%@'", fileDirectoryPath] 
1071                                                           error:error];
1072             }
1073             // Check first if a local copy exists
1074             if ([self findLocalCopyForObjectWithHash:object.objectHash forFile:filePath]) {
1075                 storedState.filePath = filePath;
1076                 storedState.hash = object.objectHash;
1077                 // Delete incomplete temp download
1078                 storedState.tmpFilePath = nil;
1079                 [self saveLocalState];
1080                 dispatch_async(dispatch_get_main_queue(), ^{
1081                     [activityFacility startAndEndActivityWithType:PithosActivityOther 
1082                                                           message:[NSString stringWithFormat:@"Sync: Downloading '%@/%@' (100%%)", 
1083                                                                    [self relativeDirPathForAccount:accountName container:pithosContainer.name], 
1084                                                                    object.name] 
1085                                                     pithosAccount:pithosAccount];
1086                 });
1087             } else {
1088                 [self syncOperationStarted];
1089                 ASIPithosObjectRequest *objectRequest = [ASIPithosObjectRequest objectHashmapRequestWithPithos:pithos 
1090                                                                                                  containerName:pithosContainer.name 
1091                                                                                                     objectName:object.name];
1092                 if (![accountName isEqualToString:@""])
1093                     [objectRequest setRequestUserFromDefaultTo:accountName withPithos:pithos];
1094                 objectRequest.delegate = self;
1095                 objectRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:);
1096                 objectRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:);
1097                 NSString *messagePrefix = [NSString stringWithFormat:@"Sync: Downloading '%@/%@'", 
1098                                            [self relativeDirPathForAccount:accountName container:pithosContainer.name], object.name];
1099                 PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityDownload 
1100                                                                            message:[messagePrefix stringByAppendingString:@" (0%%)"] 
1101                                                                         totalBytes:object.bytes 
1102                                                                       currentBytes:0 
1103                                                                      pithosAccount:pithosAccount];
1104                 dispatch_async(dispatch_get_main_queue(), ^{
1105                     [activityFacility updateActivity:activity withMessage:activity.message];
1106                 });
1107                 objectRequest.userInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys:
1108                                           accountName, @"accountName", 
1109                                           pithosContainer, @"pithosContainer", 
1110                                           object, @"pithosObject", 
1111                                           messagePrefix, @"messagePrefix", 
1112                                           filePath, @"filePath", 
1113                                           activity, @"activity", 
1114                                           [messagePrefix stringByAppendingString:@" (stopped)"], @"stoppedActivityMessage", 
1115                                           [messagePrefix stringByAppendingString:@" (failed)"], @"failedActivityMessage", 
1116                                           [messagePrefix stringByAppendingString:@" (100%%)"], @"finishedActivityMessage", 
1117                                           [NSNumber numberWithInteger:NSOperationQueuePriorityNormal], @"priority", 
1118                                           [NSNumber numberWithUnsignedInteger:10], @"retries", 
1119                                           NSStringFromSelector(@selector(downloadObjectHashMapFinished:)), @"didFinishSelector", 
1120                                           NSStringFromSelector(@selector(requestFailed:)), @"didFailSelector", 
1121                                           nil];
1122                 [networkQueue addOperation:[PithosUtilities prepareRequest:objectRequest priority:NSOperationQueuePriorityNormal]];
1123             }
1124         }
1125     }
1126 }
1127
1128 -(void)updateServerStateWithCurrentState:(PithosLocalObjectState *)currentState 
1129                                   object:(ASIPithosObject *)object 
1130                            localFilePath:(NSString *)filePath 
1131                              accountName:(NSString *)accountName 
1132                          pithosContainer:(ASIPithosContainer *)pithosContainer {
1133     @autoreleasepool {
1134         [self syncOperationStarted];
1135         NSFileManager *fileManager = [NSFileManager defaultManager];
1136         BOOL isDirectory;
1137         BOOL fileExists = [fileManager fileExistsAtPath:filePath isDirectory:&isDirectory];
1138         if (currentState.isDirectory) {
1139             // Create remote directory object
1140             if (![accountName isEqualToString:@""]) {
1141                 if (!object.allowedTo) {
1142                     NSMutableDictionary *containerRemoteObjects = [[remoteObjects objectForKey:accountName] objectForKey:pithosContainer.name];
1143                     NSString *objectAncestorName = [object.name stringByDeletingLastPathComponent];
1144                     while ([objectAncestorName length] && !object.allowedTo) {
1145                         object.allowedTo = [[containerRemoteObjects objectForKey:objectAncestorName] allowedTo];
1146                         objectAncestorName = [objectAncestorName stringByDeletingLastPathComponent];
1147                     }
1148                 }
1149                 if (![object.allowedTo isEqualToString:@"write"]) {
1150                     // If read-only "shared with me" skip
1151                     [self syncOperationFinishedWithSuccess:YES];
1152                     return;
1153                 }
1154             }
1155             if (!fileExists || !isDirectory) {
1156                 // Local directory object deleted or changed to a file in the meantime, mark the sync cycle as incomplete and skip
1157                 [self syncOperationFinishedWithSuccess:NO];
1158                 return;
1159             }
1160             ASIPithosObjectRequest *objectRequest = [ASIPithosObjectRequest writeObjectDataRequestWithPithos:pithos 
1161                                                                                                containerName:pithosContainer.name 
1162                                                                                                   objectName:object.name 
1163                                                                                                         eTag:nil 
1164                                                                                                  contentType:@"application/directory" 
1165                                                                                              contentEncoding:nil 
1166                                                                                           contentDisposition:nil 
1167                                                                                                     manifest:nil 
1168                                                                                                      sharing:nil 
1169                                                                                                     isPublic:ASIPithosObjectRequestPublicIgnore 
1170                                                                                                     metadata:nil 
1171                                                                                                         data:[NSData data]];
1172             if (![accountName isEqualToString:@""])
1173                 [objectRequest setRequestUserFromDefaultTo:accountName withPithos:pithos];
1174             objectRequest.delegate = self;
1175             objectRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:);
1176             objectRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:);
1177             NSString *messagePrefix = [NSString stringWithFormat:@"Sync: Creating directory '%@/%@'", 
1178                                        [self relativeDirPathForAccount:accountName container:pithosContainer.name], object.name];
1179             PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityCreateDirectory 
1180                                                                        message:messagePrefix 
1181                                                                  pithosAccount:pithosAccount];
1182             dispatch_async(dispatch_get_main_queue(), ^{
1183                 [activityFacility updateActivity:activity withMessage:activity.message];
1184             });
1185             objectRequest.userInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys:
1186                                       accountName, @"accountName", 
1187                                       pithosContainer, @"pithosContainer", 
1188                                       object, @"pithosObject", 
1189                                       messagePrefix, @"messagePrefix", 
1190                                       activity, @"activity", 
1191                                       [messagePrefix stringByAppendingString:@" (stopped)"], @"stoppedActivityMessage", 
1192                                       [messagePrefix stringByAppendingString:@" (failed)"], @"failedActivityMessage", 
1193                                       [messagePrefix stringByAppendingString:@" (finished)"], @"finishedActivityMessage", 
1194                                       [NSNumber numberWithInteger:NSOperationQueuePriorityHigh], @"priority", 
1195                                       [NSNumber numberWithUnsignedInteger:10], @"retries", 
1196                                       NSStringFromSelector(@selector(uploadDirectoryObjectFinished:)), @"didFinishSelector", 
1197                                       NSStringFromSelector(@selector(requestFailed:)), @"didFailSelector", 
1198                                       nil];
1199             [networkQueue addOperation:[PithosUtilities prepareRequest:objectRequest priority:NSOperationQueuePriorityHigh]];
1200         } else if (![currentState exists]) {
1201             // Delete remote object
1202             if (![accountName isEqualToString:@""]) {
1203                 // If "shared with me" skip
1204                 [self syncOperationFinishedWithSuccess:YES];
1205                 return;
1206             }
1207             if (fileExists) {
1208                 // Local object created in the meantime, just mark the sync cycle as incomplete, but do delete the server object
1209                 syncIncomplete = YES;
1210             }
1211             if ([pithosContainer.name isEqualToString:@"trash"]) {
1212                 // Delete
1213                 ASIPithosObjectRequest *objectRequest = [ASIPithosObjectRequest deleteObjectRequestWithPithos:pithos 
1214                                                                                                 containerName:pithosContainer.name 
1215                                                                                                    objectName:object.name];
1216                 objectRequest.delegate = self;
1217                 objectRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:);
1218                 objectRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:);
1219                 NSString *messagePrefix = [NSString stringWithFormat:@"Sync: Deleting '%@/%@'", 
1220                                            [self relativeDirPathForAccount:accountName container:pithosContainer.name], object.name];
1221                 PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityDelete 
1222                                                                            message:messagePrefix 
1223                                                                      pithosAccount:pithosAccount];
1224                 dispatch_async(dispatch_get_main_queue(), ^{
1225                     [activityFacility updateActivity:activity withMessage:activity.message];
1226                 });
1227                 objectRequest.userInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys:
1228                                           accountName, @"accountName", 
1229                                           pithosContainer, @"pithosContainer", 
1230                                           object, @"pithosObject", 
1231                                           messagePrefix, @"messagePrefix", 
1232                                           activity, @"activity", 
1233                                           [messagePrefix stringByAppendingString:@" (stopped)"], @"stoppedActivityMessage", 
1234                                           [messagePrefix stringByAppendingString:@" (failed)"], @"failedActivityMessage", 
1235                                           [messagePrefix stringByAppendingString:@" (finished)"], @"finishedActivityMessage", 
1236                                           [NSNumber numberWithInteger:NSOperationQueuePriorityHigh], @"priority", 
1237                                           [NSNumber numberWithUnsignedInteger:10], @"retries", 
1238                                           NSStringFromSelector(@selector(deleteObjectFinished:)), @"didFinishSelector", 
1239                                           NSStringFromSelector(@selector(requestFailed:)), @"didFailSelector", 
1240                                           nil];
1241                 [networkQueue addOperation:[PithosUtilities prepareRequest:objectRequest priority:NSOperationQueuePriorityHigh]];
1242             } else {
1243                 // Move to container trash
1244                 NSString *safeName;
1245                 if ([PithosUtilities isContentTypeDirectory:object.contentType])
1246                     safeName = [PithosUtilities safeSubdirNameForPithos:pithos containerName:@"trash" subdirName:object.name];
1247                 else
1248                     safeName = [PithosUtilities safeObjectNameForPithos:pithos containerName:@"trash" objectName:object.name];
1249                 if (safeName) {
1250                     ASIPithosObjectRequest *objectRequest = [PithosUtilities moveObjectRequestWithPithos:pithos 
1251                                                                                            containerName:pithosContainer.name 
1252                                                                                               objectName:object.name 
1253                                                                                 destinationContainerName:@"trash" 
1254                                                                                    destinationObjectName:safeName 
1255                                                                                            checkIfExists:NO];
1256                     if (objectRequest) {
1257                         objectRequest.delegate = self;
1258                         objectRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:);
1259                         objectRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:);
1260                         NSString *messagePrefix = [NSString stringWithFormat:@"Sync: Moving to trash '%@/%@'", 
1261                                                    [self relativeDirPathForAccount:accountName container:pithosContainer.name], object.name];
1262                         PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityDelete 
1263                                                                                    message:messagePrefix 
1264                                                                              pithosAccount:pithosAccount];
1265                         dispatch_async(dispatch_get_main_queue(), ^{
1266                             [activityFacility updateActivity:activity withMessage:activity.message];
1267                         });
1268                         objectRequest.userInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys:
1269                                                   accountName, @"accountName", 
1270                                                   pithosContainer, @"pithosContainer", 
1271                                                   object, @"pithosObject", 
1272                                                   messagePrefix, @"messagePrefix", 
1273                                                   activity, @"activity", 
1274                                                   [messagePrefix stringByAppendingString:@" (stopped)"], @"stoppedActivityMessage", 
1275                                                   [messagePrefix stringByAppendingString:@" (failed)"], @"failedActivityMessage", 
1276                                                   [messagePrefix stringByAppendingString:@" (finished)"], @"finishedActivityMessage", 
1277                                                   [NSNumber numberWithInteger:NSOperationQueuePriorityHigh], @"priority", 
1278                                                   [NSNumber numberWithUnsignedInteger:10], @"retries", 
1279                                                   NSStringFromSelector(@selector(moveObjectToTrashFinished:)), @"didFinishSelector", 
1280                                                   NSStringFromSelector(@selector(requestFailed:)), @"didFailSelector", 
1281                                                   nil];
1282                         [networkQueue addOperation:[PithosUtilities prepareRequest:objectRequest priority:NSOperationQueuePriorityHigh]];
1283                     } else {
1284                         [self syncOperationFinishedWithSuccess:NO];
1285                     }
1286                 } else {
1287                     [self syncOperationFinishedWithSuccess:NO];
1288                 }
1289             }
1290         } else {
1291             // Upload file to remote object
1292             if (![accountName isEqualToString:@""]) {
1293                 if (!object.allowedTo) {
1294                     NSMutableDictionary *containerRemoteObjects = [[remoteObjects objectForKey:accountName] objectForKey:pithosContainer.name];
1295                     NSString *objectAncestorName = [object.name stringByDeletingLastPathComponent];
1296                     while ([objectAncestorName length] && !object.allowedTo) {
1297                         object.allowedTo = [[containerRemoteObjects objectForKey:objectAncestorName] allowedTo];
1298                         objectAncestorName = [objectAncestorName stringByDeletingLastPathComponent];
1299                     }
1300                 }
1301                 if (![object.allowedTo isEqualToString:@"write"]) {
1302                     // If read-only "shared with me" skip
1303                     [self syncOperationFinishedWithSuccess:YES];
1304                     return;
1305                 }
1306             }
1307             if (!fileExists || isDirectory) {
1308                 // Local file object deleted or changed to a directory in the meantime, mark the sync cycle as incomplete and skip
1309                 [self syncOperationFinishedWithSuccess:NO];
1310                 return;
1311             }
1312             NSError *error = nil;
1313             object.contentType = [PithosUtilities contentTypeOfFile:filePath error:&error];
1314             if (object.contentType == nil)
1315                 object.contentType = @"application/octet-stream";
1316             #if DEBUG_PITHOS
1317             if (error)
1318                 DLog(@"contentType detection error: %@", error);
1319             #endif
1320             NSArray *hashes = nil;
1321             ASIPithosObjectRequest *objectRequest = [PithosUtilities writeObjectDataRequestWithPithos:pithos 
1322                                                                                         containerName:pithosContainer.name 
1323                                                                                            objectName:object.name 
1324                                                                                           contentType:object.contentType 
1325                                                                                             blockSize:pithosContainer.blockSize 
1326                                                                                             blockHash:pithosContainer.blockHash 
1327                                                                                               forFile:filePath 
1328                                                                                         checkIfExists:NO 
1329                                                                                                hashes:&hashes 
1330                                                                                        sharingAccount:([accountName isEqualToString:@""] ? nil : accountName)];
1331             if (objectRequest) {
1332                 objectRequest.delegate = self;
1333                 objectRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:);
1334                 objectRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:);
1335                 NSString *messagePrefix = [NSString stringWithFormat:@"Sync: Uploading '%@/%@'", 
1336                                            [self relativeDirPathForAccount:accountName container:pithosContainer.name], object.name];
1337                 PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityUpload 
1338                                                                            message:[messagePrefix stringByAppendingString:@" (0%%)"] 
1339                                                                         totalBytes:[[objectRequest.userInfo objectForKey:@"bytes"] unsignedIntegerValue]
1340                                                                       currentBytes:0 
1341                                                                      pithosAccount:pithosAccount];
1342                 dispatch_async(dispatch_get_main_queue(), ^{
1343                     [activityFacility updateActivity:activity withMessage:activity.message];
1344                 });
1345                 [(NSMutableDictionary *)objectRequest.userInfo addEntriesFromDictionary:
1346                  [NSDictionary dictionaryWithObjectsAndKeys:
1347                   accountName, @"accountName", 
1348                   pithosContainer, @"pithosContainer", 
1349                   object, @"pithosObject", 
1350                   messagePrefix, @"messagePrefix", 
1351                   filePath, @"filePath", 
1352                   hashes, @"hashes", 
1353                   [NSNumber numberWithUnsignedInteger:10], @"iteration", 
1354                   activity, @"activity", 
1355                   [messagePrefix stringByAppendingString:@" (stopped)"], @"stoppedActivityMessage", 
1356                   [messagePrefix stringByAppendingString:@" (failed)"], @"failedActivityMessage", 
1357                   [messagePrefix stringByAppendingString:@" (100%%)"], @"finishedActivityMessage", 
1358                   [NSNumber numberWithInteger:NSOperationQueuePriorityNormal], @"priority", 
1359                   [NSNumber numberWithUnsignedInteger:10], @"retries", 
1360                   NSStringFromSelector(@selector(uploadObjectUsingHashMapFinished:)), @"didFinishSelector", 
1361                   NSStringFromSelector(@selector(requestFailed:)), @"didFailSelector", 
1362                   nil]];
1363                 [networkQueue addOperation:[PithosUtilities prepareRequest:objectRequest priority:NSOperationQueuePriorityNormal]];
1364             } else {
1365                 [self syncOperationFinishedWithSuccess:NO];
1366             }
1367         }
1368     }
1369 }
1370
1371 #pragma mark -
1372 #pragma mark ASIHTTPRequestDelegate
1373
1374 - (void)performRequestFinishedDelegateInBackground:(ASIPithosRequest *)request {
1375     // Add an operation to the callbackQueue with a completionBlock for the case of cancellation
1376     NSInvocationOperation *operation = [[NSInvocationOperation alloc] initWithTarget:self 
1377                                                                              selector:NSSelectorFromString([request.userInfo objectForKey:@"didFinishSelector"]) 
1378                                                                                object:request];
1379     operation.completionBlock = ^{
1380         @autoreleasepool {
1381             if ([[request.userInfo objectForKey:@"operation"] isCancelled]) {
1382                 dispatch_async(dispatch_get_main_queue(), ^{
1383                     [activityFacility endActivity:[request.userInfo objectForKey:@"activity"] 
1384                                       withMessage:[request.userInfo objectForKey:@"stoppedActivityMessage"]];
1385                 });
1386                 [self syncOperationFinishedWithSuccess:NO];
1387             }
1388         }
1389     };
1390     [(NSMutableDictionary *)request.userInfo setObject:operation forKey:@"operation"];
1391     [callbackQueue addOperation:operation];
1392 }
1393
1394 - (void)performRequestFailedDelegateInBackground:(ASIPithosRequest *)request {
1395     if (request.isCancelled) {
1396         // Request has been cancelled 
1397         // The callbackQueue might be suspended so we call directly the callback method, since it does minimal work anyway
1398         [self performSelector:NSSelectorFromString([request.userInfo objectForKey:@"didFailSelector"]) 
1399                    withObject:request];
1400     } else {
1401         // Add an operation to the callbackQueue with a completionBlock for the case of cancellation
1402         NSInvocationOperation *operation = [[NSInvocationOperation alloc] initWithTarget:self 
1403                                                                                  selector:NSSelectorFromString([request.userInfo objectForKey:@"didFailSelector"]) 
1404                                                                                    object:request];
1405         operation.completionBlock = ^{
1406             @autoreleasepool {
1407                 if ([[request.userInfo objectForKey:@"operation"] isCancelled]) {
1408                     dispatch_async(dispatch_get_main_queue(), ^{
1409                         [activityFacility endActivity:[request.userInfo objectForKey:@"activity"] 
1410                                           withMessage:[request.userInfo objectForKey:@"stoppedActivityMessage"]];
1411                     });
1412                     [self syncOperationFinishedWithSuccess:NO];
1413                 }
1414             }
1415         };
1416         [(NSMutableDictionary *)request.userInfo setObject:operation forKey:@"operation"];
1417         [callbackQueue addOperation:operation];
1418     }
1419 }
1420
1421 - (void)listRequestFinished:(ASIPithosContainerRequest *)containerRequest {
1422     @autoreleasepool {
1423         NSOperation *operation = [containerRequest.userInfo objectForKey:@"operation"];
1424         DLog(@"Sync::list request finished: %@", containerRequest.url);
1425         if (operation.isCancelled) {
1426             [self listRequestFailed:containerRequest];
1427         } else if ((containerRequest.responseStatusCode == 200) || 
1428                    (containerRequest.responseStatusCode == 304) || 
1429                    (containerRequest.responseStatusCode == 403)) {
1430             NSString *accountName = [accountsNames objectAtIndex:accountsIndex];
1431             ASIPithosContainer *pithosContainer = [[accountsPithosContainers objectForKey:accountName] objectAtIndex:containersIndex];
1432             if (containerRequest.responseStatusCode == 200) {
1433                 NSArray *someObjects = [containerRequest objects];
1434                 if (objects == nil) {
1435                     objects = [[NSMutableArray alloc] initWithArray:someObjects];
1436                 } else {
1437                     [objects addObjectsFromArray:someObjects];
1438                 }
1439                 if ([someObjects count] < 10000) {
1440                     pithosContainer.blockHash = [containerRequest blockHash];
1441                     pithosContainer.blockSize = [containerRequest blockSize];
1442                     pithosContainer.lastModified = [containerRequest lastModified];
1443                     NSMutableDictionary *containerRemoteObjects = [NSMutableDictionary dictionaryWithCapacity:[objects count]];
1444                     for (ASIPithosObject *object in objects) {
1445                         [containerRemoteObjects setObject:object forKey:object.name];
1446                     }
1447                     [[remoteObjects objectForKey:accountName] setObject:containerRemoteObjects forKey:pithosContainer.name];
1448                     objects = nil;
1449                 } else {
1450                     // Do an additional request to fetch more objects
1451                     ASIPithosContainerRequest *newContainerRequest = [ASIPithosContainerRequest listObjectsRequestWithPithos:pithos 
1452                                                                                                                containerName:pithosContainer.name 
1453                                                                                                                        limit:0 
1454                                                                                                                       marker:[[someObjects lastObject] name] 
1455                                                                                                                       prefix:nil 
1456                                                                                                                    delimiter:nil 
1457                                                                                                                         path:nil 
1458                                                                                                                         meta:nil 
1459                                                                                                                       shared:NO 
1460                                                                                                                        until:nil 
1461                                                                                                              ifModifiedSince:pithosContainer.lastModified];
1462                     if (![accountName isEqualToString:@""])
1463                         [newContainerRequest setRequestUserFromDefaultTo:accountName withPithos:pithos];
1464                     newContainerRequest.delegate = self;
1465                     newContainerRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:);
1466                     newContainerRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:);
1467                     newContainerRequest.userInfo = containerRequest.userInfo;
1468                     [(NSMutableDictionary *)newContainerRequest.userInfo setObject:[NSNumber numberWithUnsignedInteger:10] forKey:@"retries"];
1469                     [networkQueue addOperation:[PithosUtilities prepareRequest:newContainerRequest priority:[[newContainerRequest.userInfo objectForKey:@"priority"] integerValue]]];
1470                     return;
1471                 }
1472             } else if (containerRequest.responseStatusCode == 304) {
1473                 NSMutableDictionary *containerRemoteObjects = [[previousRemoteObjects objectForKey:accountName] 
1474                                                                objectForKey:pithosContainer.name];
1475                 if (containerRemoteObjects) 
1476                     [[remoteObjects objectForKey:accountName] setObject:containerRemoteObjects forKey:pithosContainer.name];
1477             } else if (containerRequest.responseStatusCode == 403) {
1478                 [[remoteObjects objectForKey:accountName] setObject:[NSMutableDictionary dictionary] forKey:pithosContainer.name];
1479             }
1480             
1481             containersIndex++;
1482             if (containersIndex == [[accountsPithosContainers objectForKey:accountName] count]) {
1483                 accountsIndex++;
1484                 containersIndex = 0;
1485             }
1486             if (accountsIndex < accountsCount) {
1487                 accountName = [accountsNames objectAtIndex:accountsIndex];
1488                 pithosContainer = [[accountsPithosContainers objectForKey:accountName] objectAtIndex:containersIndex];
1489                 // Do a request for the next container
1490                 ASIPithosContainerRequest *newContainerRequest = [ASIPithosContainerRequest listObjectsRequestWithPithos:pithos 
1491                                                                                                            containerName:pithosContainer.name 
1492                                                                                                                    limit:0 
1493                                                                                                                   marker:nil 
1494                                                                                                                   prefix:nil 
1495                                                                                                                delimiter:nil 
1496                                                                                                                     path:nil 
1497                                                                                                                     meta:nil 
1498                                                                                                                   shared:NO 
1499                                                                                                                    until:nil 
1500                                                                                                          ifModifiedSince:pithosContainer.lastModified];
1501                 if (![accountName isEqualToString:@""])
1502                     [newContainerRequest setRequestUserFromDefaultTo:accountName withPithos:pithos];
1503                 newContainerRequest.delegate = self;
1504                 newContainerRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:);
1505                 newContainerRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:);
1506                 newContainerRequest.userInfo = containerRequest.userInfo;
1507                 [(NSMutableDictionary *)newContainerRequest.userInfo setObject:[NSNumber numberWithUnsignedInteger:10] forKey:@"retries"];
1508                 [networkQueue addOperation:[PithosUtilities prepareRequest:newContainerRequest priority:[[newContainerRequest.userInfo objectForKey:@"priority"] integerValue]]];
1509                 return;
1510             }
1511             self.previousRemoteObjects = remoteObjects;
1512             // remoteObjects contains all remote objects for the legal containers, without enforcing directory exclusions
1513             
1514             if (operation.isCancelled) {
1515                 [self listRequestFailed:containerRequest];
1516                 return;
1517             }
1518
1519             dispatch_async(dispatch_get_main_queue(), ^{
1520                 [activityFacility endActivity:[containerRequest.userInfo objectForKey:@"activity"] 
1521                                   withMessage:[containerRequest.userInfo objectForKey:@"finishedActivityMessage"]];
1522             });
1523             NSFileManager *fileManager = [NSFileManager defaultManager];
1524             
1525             // Compute current state of legal existing local objects 
1526             // and add an empty stored state for legal new local objects since last sync
1527             self.currentLocalObjectStates = [NSMutableDictionary dictionary];
1528             for (NSString *accountName in accountsNames) {
1529                 for (ASIPithosContainer *pithosContainer in [accountsPithosContainers objectForKey:accountName]) {
1530                     NSString *containerDirectoryPath = [self dirPathForAccount:accountName container:pithosContainer.name];
1531                     NSSet *containerExcludedDirectories = [[accountsDictionary objectForKey:accountName] objectForKey:pithosContainer.name];
1532                     BOOL containerExcludeRootFiles = [containerExcludedDirectories containsObject:@""];
1533                     NSMutableDictionary *containerStoredLocalObjectStates = [[storedLocalObjectStates objectForKey:accountName] 
1534                                                                              objectForKey:pithosContainer.name];
1535                     NSDirectoryEnumerator *dirEnumerator = [fileManager enumeratorAtPath:containerDirectoryPath];
1536                     for (__strong NSString *objectName in dirEnumerator) {
1537                         objectName = [objectName precomposedStringWithCanonicalMapping];
1538                         if (operation.isCancelled) {
1539                             operation.completionBlock = nil;
1540                             [self saveLocalState];
1541                             [self syncOperationFinishedWithSuccess:NO];
1542                             return;
1543                         }
1544
1545                         NSString *filePath = [containerDirectoryPath stringByAppendingPathComponent:objectName];
1546                         NSDictionary *attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:nil];
1547                         BOOL isDirectory;
1548                         BOOL fileExists = [fileManager fileExistsAtPath:filePath isDirectory:&isDirectory];
1549                         if ([[attributes fileType] isEqualToString:NSFileTypeSymbolicLink]) {
1550                             [PithosUtilities fileActionFailedAlertWithTitle:@"Sync Error" 
1551                                                                     message:[NSString stringWithFormat:@"Sync directory at '%@' contains symbolic links. Remove them and re-activate sync for account '%@'.", 
1552                                                                              containerDirectoryPath, pithosAccount.name] 
1553                                                                       error:nil];
1554                             pithosAccount.syncActive = NO;
1555                             return;
1556                         } else if (fileExists) {
1557                             if (skipHidden && [[objectName lastPathComponent] hasPrefix:@"."]) {
1558                                 // Skip hidden directory and its descendants, or hidden file
1559                                 if (isDirectory)
1560                                     [dirEnumerator skipDescendants];
1561                                 // Remove stored state if any
1562                                 [containerStoredLocalObjectStates removeObjectForKey:objectName];
1563                                 continue;
1564                             } else if ([[objectName pathComponents] count] == 1) {
1565                                 if ([containerExcludedDirectories containsObject:[objectName lowercaseString]]) {
1566                                     // Skip excluded directory and its descendants, or root file with same name
1567                                     if (isDirectory)
1568                                         [dirEnumerator skipDescendants];
1569                                     // Remove stored state if any
1570                                     [containerStoredLocalObjectStates removeObjectForKey:objectName];
1571                                     continue;
1572                                 } else if (!isDirectory && containerExcludeRootFiles) {
1573                                     // Skip excluded root file
1574                                     // Remove stored state if any
1575                                     [containerStoredLocalObjectStates removeObjectForKey:objectName];
1576                                     continue;
1577                                 }
1578                             }
1579                             // Include local object
1580                             PithosLocalObjectState *storedLocalObjectState = [containerStoredLocalObjectStates objectForKey:objectName];
1581                             if (!storedLocalObjectState || [storedLocalObjectState isModified]) {
1582                                 // New or modified existing local object, compute current state
1583                                 if (!storedLocalObjectState)
1584                                     // For new local object, also create empty stored state
1585                                     [containerStoredLocalObjectStates setObject:[PithosLocalObjectState localObjectState] forKey:objectName];
1586                                 [currentLocalObjectStates setObject:[PithosLocalObjectState localObjectStateWithFile:filePath 
1587                                                                                                            blockHash:pithosContainer.blockHash 
1588                                                                                                            blockSize:pithosContainer.blockSize] 
1589                                                              forKey:filePath];
1590                             } else {
1591                                 // Local object hasn't changed, set stored state also to current
1592                                 [currentLocalObjectStates setObject:storedLocalObjectState forKey:filePath];
1593                             }
1594                         }
1595                     }
1596                     [self saveLocalState];
1597                 }
1598             }
1599             
1600             if (operation.isCancelled) {
1601                 operation.completionBlock = nil;
1602                 [self syncOperationFinishedWithSuccess:NO];
1603                 return;
1604             }
1605
1606             // Add an empty stored state for legal new remote objects since last sync
1607             for (NSString *accountName in accountsNames) {
1608                 for (ASIPithosContainer *pithosContainer in [accountsPithosContainers objectForKey:accountName]) {
1609                     NSSet *containerExcludedDirectories = [[accountsDictionary objectForKey:accountName] objectForKey:pithosContainer.name];
1610                     BOOL containerExcludeRootFiles = [containerExcludedDirectories containsObject:@""];
1611                     NSMutableDictionary *containerStoredLocalObjectStates = [[storedLocalObjectStates objectForKey:accountName] 
1612                                                                              objectForKey:pithosContainer.name];
1613                     NSMutableDictionary *containerRemoteObjects = [[remoteObjects objectForKey:accountName] objectForKey:pithosContainer.name];
1614                     for (NSString *objectName in containerRemoteObjects) {
1615                         if (operation.isCancelled) {
1616                             operation.completionBlock = nil;
1617                             [self saveLocalState];
1618                             [self syncOperationFinishedWithSuccess:NO];
1619                             return;
1620                         }
1621
1622                         ASIPithosObject *object = [containerRemoteObjects objectForKey:objectName];
1623                         NSString *localObjectName;
1624                         if ([object.name hasSuffix:@"/"])
1625                             localObjectName = [NSString stringWithFormat:@"%@:", [object.name substringToIndex:([object.name length] -1)]];
1626                         else
1627                             localObjectName = [NSString stringWithString:object.name];
1628                         NSArray *pathComponents = [localObjectName pathComponents];
1629                         if (skipHidden) {
1630                             BOOL skipObject = NO;
1631                             for (NSString *pathComponent in pathComponents) {
1632                                 if ([pathComponent hasPrefix:@"."]) {
1633                                     // Skip hidden directory and its descendants, or hidden file
1634                                     // Remove stored state if any
1635                                     [containerStoredLocalObjectStates removeObjectForKey:objectName];
1636                                     skipObject = YES;
1637                                     break;
1638                                 }
1639                             }
1640                             if (skipObject)
1641                                 continue;
1642                         }
1643                         if ([containerExcludedDirectories containsObject:[[pathComponents objectAtIndex:0] lowercaseString]]) {
1644                             // Skip excluded directory object and its descendants, or root file object with same name
1645                             // Remove stored state if any
1646                             [containerStoredLocalObjectStates removeObjectForKey:object.name];
1647                             continue;
1648                         } else if (containerExcludeRootFiles && 
1649                                    ([pathComponents count] == 1) && 
1650                                    ![PithosUtilities isContentTypeDirectory:object.contentType]) {
1651                             // Skip root file object
1652                             // Remove stored state if any
1653                             [containerStoredLocalObjectStates removeObjectForKey:object.name];                    
1654                             continue;
1655                         }
1656                         if (![containerStoredLocalObjectStates objectForKey:object.name])
1657                             [containerStoredLocalObjectStates setObject:[PithosLocalObjectState localObjectState] forKey:object.name];
1658                     }
1659                     [self saveLocalState];
1660                 }
1661             }
1662
1663             if (operation.isCancelled) {
1664                 operation.completionBlock = nil;
1665                 [self syncOperationFinishedWithSuccess:NO];
1666                 return;
1667             }
1668
1669             // For each stored state compare with current and remote state
1670             // Stored states of local objects that have been deleted, 
1671             // haven't been checked for legality (only existing local remote objects)
1672             // These should be identified and skipped
1673             for (NSString *accountName in accountsNames) {
1674                 for (ASIPithosContainer *pithosContainer in [accountsPithosContainers objectForKey:accountName]) {
1675                     NSString *containerDirectoryPath = [self dirPathForAccount:accountName container:pithosContainer.name];
1676                     NSSet *containerExcludedDirectories = [[accountsDictionary objectForKey:accountName] objectForKey:pithosContainer.name];
1677                     BOOL containerExcludeRootFiles = [containerExcludedDirectories containsObject:@""];
1678                     NSMutableDictionary *containerStoredLocalObjectStates = [[storedLocalObjectStates objectForKey:accountName] 
1679                                                                              objectForKey:pithosContainer.name];
1680                     NSMutableDictionary *containerRemoteObjects = [[remoteObjects objectForKey:accountName] objectForKey:pithosContainer.name];
1681                     for (NSString *objectName in [[containerStoredLocalObjectStates allKeys] sortedArrayUsingSelector:@selector(compare:)]) {
1682                         if (operation.isCancelled) {
1683                             operation.completionBlock = nil;
1684                             [self syncOperationFinishedWithSuccess:NO];
1685                             return;
1686                         }
1687
1688                         NSString *filePath = [containerDirectoryPath stringByAppendingPathComponent:objectName];
1689                         if ([objectName hasSuffix:@"/"])
1690                             filePath = [filePath stringByAppendingString:@":"];
1691                         ASIPithosObject *object = [ASIPithosObject object];
1692                         object.name = objectName;
1693                         DLog(@"Sync::object name: %@", object.name);
1694
1695                         PithosLocalObjectState *storedLocalObjectState = [containerStoredLocalObjectStates objectForKey:object.name];
1696                         PithosLocalObjectState *currentLocalObjectState = [currentLocalObjectStates objectForKey:filePath];
1697                         if (!currentLocalObjectState) {
1698                             // The stored state corresponds to a remote or deleted local object, that's why there is no current state
1699                             // In the latter case it must be checked for legality, which can be determined by its stored state
1700                             // If it existed locally, but was deleted, state.exists is true, 
1701                             // else if the stored state is an empty state that was created due to the server object, state.exists is false
1702                             if (storedLocalObjectState.exists) {
1703                                 NSString *localObjectName;
1704                                 if ([object.name hasSuffix:@"/"])
1705                                     localObjectName = [NSString stringWithFormat:@"%@:", [object.name substringToIndex:([object.name length] -1)]];
1706                                 else
1707                                     localObjectName = [NSString stringWithString:object.name];
1708                                 NSArray *pathComponents = [localObjectName pathComponents];
1709                                 if (skipHidden) {
1710                                     BOOL skipObject = NO;
1711                                     for (NSString *pathComponent in pathComponents) {
1712                                         if ([pathComponent hasPrefix:@"."]) {
1713                                             // Skip hidden directory and its descendants, or hidden file
1714                                             // Remove stored state if any
1715                                             [containerStoredLocalObjectStates removeObjectForKey:objectName];
1716                                             [self saveLocalState];
1717                                             skipObject = YES;
1718                                             break;
1719                                         }
1720                                     }
1721                                     if (skipObject)
1722                                         continue;
1723                                 }
1724                                 if ([containerExcludedDirectories containsObject:[[pathComponents objectAtIndex:0] lowercaseString]]) {
1725                                     // Skip excluded directory object and its descendants, or root file object with same name
1726                                     // Remove stored state
1727                                     [containerStoredLocalObjectStates removeObjectForKey:object.name];
1728                                     [self saveLocalState];
1729                                     continue;
1730                                 } else if (containerExcludeRootFiles && ([pathComponents count] == 1) && !storedLocalObjectState.isDirectory) {
1731                                     // Skip root file object
1732                                     // Remove stored state
1733                                     [containerStoredLocalObjectStates removeObjectForKey:object.name];                    
1734                                     [self saveLocalState];
1735                                     continue;
1736                                 }
1737                             }
1738                             // There is also the off case that a local object has been created in the meantime
1739                             // This call works in any case, existent or non-existent local object 
1740                             currentLocalObjectState = [PithosLocalObjectState localObjectStateWithFile:filePath 
1741                                                                                              blockHash:pithosContainer.blockHash 
1742                                                                                              blockSize:pithosContainer.blockSize];
1743                         }
1744                 
1745                         PithosLocalObjectState *remoteObjectState = [PithosLocalObjectState localObjectState];
1746                         ASIPithosObject *remoteObject = [containerRemoteObjects objectForKey:object.name];
1747                         if (remoteObject) {
1748                             if ([PithosUtilities isContentTypeDirectory:remoteObject.contentType]) {
1749                                 remoteObjectState.isDirectory = YES;
1750                             } else {
1751                                 remoteObjectState.hash = remoteObject.objectHash;
1752                             }
1753                         }
1754
1755                         BOOL localStateHasChanged = ![currentLocalObjectState isEqualToState:storedLocalObjectState];
1756                         BOOL serverStateHasChanged = ![remoteObjectState isEqualToState:storedLocalObjectState];
1757                         DLog(@"Sync::localStateHasChanged: %d, serverStateHasChanged: %d", localStateHasChanged, serverStateHasChanged);
1758                         if (!localStateHasChanged) {
1759                             // Local state hasn't changed
1760                             if (serverStateHasChanged) {
1761                                 // Server state has changed
1762                                 // Update local state to match that of the server 
1763                                 object.bytes = remoteObject.bytes;
1764                                 object.version = remoteObject.version;
1765                                 object.contentType = remoteObject.contentType;
1766                                 object.objectHash = remoteObject.objectHash;
1767                                 [self updateLocalStateWithObject:object localFilePath:filePath 
1768                                                      accountName:accountName pithosContainer:pithosContainer];
1769                             } else if (!remoteObject && ![currentLocalObjectState exists]) {
1770                                 // Server state hasn't changed
1771                                 // If the object doesn't exist neither in the server or locally, it should be removed from the stored local objects
1772                                 [containerStoredLocalObjectStates removeObjectForKey:objectName];
1773                                 [self saveLocalState];
1774                             }
1775                     } else {
1776                         // Local state has changed
1777                         if (!serverStateHasChanged) {
1778                             // Server state hasn't changed
1779                             if (currentLocalObjectState.isDirectory)
1780                                 object.contentType = @"application/directory";
1781                             else
1782                                 object.objectHash = currentLocalObjectState.hash;
1783                             [self updateServerStateWithCurrentState:currentLocalObjectState object:object localFilePath:filePath 
1784                                                         accountName:accountName pithosContainer:pithosContainer];
1785                         } else {
1786                             // Server state has also changed
1787                             if (remoteObjectState.isDirectory && currentLocalObjectState.isDirectory) {
1788                                 // Both did the same change (directory)
1789                                 storedLocalObjectState.filePath = filePath;
1790                                 storedLocalObjectState.isDirectory = YES;
1791                                 [self saveLocalState];
1792                             } else if ([remoteObjectState isEqualToState:currentLocalObjectState]) {
1793                                 // Both did the same change (object edit or delete)
1794                                 if (![remoteObjectState exists]) {
1795                                     [containerStoredLocalObjectStates removeObjectForKey:object.name];
1796                                 } else {
1797                                     storedLocalObjectState.filePath = filePath;
1798                                     storedLocalObjectState.hash = remoteObjectState.hash;
1799                                 }
1800                                 [self saveLocalState];
1801                             } else {
1802                                 // Conflict, we ask the user which change to keep
1803                                 NSString *informativeText;
1804                                 NSString *firstButtonText;
1805                                 NSString *secondButtonText;
1806                                 
1807                                 if (![remoteObjectState exists]) {
1808                                     // Remote object has been deleted
1809                                     informativeText = [NSString stringWithFormat:@"'%@/%@' has been modified locally, while it has been deleted from server.", 
1810                                                        [self relativeDirPathForAccount:accountName container:pithosContainer.name], object.name ];
1811                                     firstButtonText = @"Delete local file";
1812                                     secondButtonText = @"Upload file to server";
1813                                 } else if (![currentLocalObjectState exists]) {
1814                                     informativeText = [NSString stringWithFormat:@"'%@/%@' has been modified on the server, while it has been deleted locally.", 
1815                                                        [self relativeDirPathForAccount:accountName container:pithosContainer.name], object.name];
1816                                     firstButtonText = @"Download file from server";
1817                                     secondButtonText = @"Delete file on server";
1818                                 } else {
1819                                     informativeText = [NSString stringWithFormat:@"'%@/%@' has been modified both locally and on the server.", 
1820                                                        [self relativeDirPathForAccount:accountName container:pithosContainer.name], object.name];
1821                                     firstButtonText = @"Keep server version";
1822                                     secondButtonText = @"Keep local version";
1823                                 }
1824                                 __block NSInteger choice;
1825                                 dispatch_sync(dispatch_get_main_queue(), ^{
1826                                     NSAlert *alert = [[NSAlert alloc] init];
1827                                     [alert setMessageText:@"Conflict"];
1828                                     [alert setInformativeText:informativeText];
1829                                     [alert addButtonWithTitle:firstButtonText];
1830                                     [alert addButtonWithTitle:secondButtonText];
1831                                     [alert addButtonWithTitle:@"Do nothing"];
1832                                     choice = [alert runModal];
1833                                 });
1834                                 if (choice == NSAlertFirstButtonReturn) {
1835                                     object.bytes = remoteObject.bytes;
1836                                     object.version = remoteObject.version;
1837                                     object.contentType = remoteObject.contentType;
1838                                     object.objectHash = remoteObject.objectHash;
1839                                     [self updateLocalStateWithObject:object localFilePath:filePath 
1840                                                      accountName:accountName pithosContainer:pithosContainer];
1841                                 } if (choice == NSAlertSecondButtonReturn) {
1842                                     if (currentLocalObjectState.isDirectory)
1843                                         object.contentType = @"application/directory";
1844                                     else
1845                                         object.objectHash = currentLocalObjectState.hash;
1846                                     [self updateServerStateWithCurrentState:currentLocalObjectState object:object localFilePath:filePath 
1847                                                                 accountName:accountName pithosContainer:pithosContainer];
1848                                 }
1849                             }
1850                         }
1851                     }
1852                 }
1853             }
1854             }
1855             [self syncOperationFinishedWithSuccess:YES];
1856         } else {
1857             [self listRequestFailed:containerRequest];
1858         }
1859     }
1860 }
1861
1862 - (void)listRequestFailed:(ASIPithosContainerRequest *)containerRequest {
1863     @autoreleasepool {
1864         NSOperation *operation = [containerRequest.userInfo objectForKey:@"operation"];
1865         if (operation.isCancelled) {
1866             objects = nil;
1867             return;        
1868         }
1869         if (containerRequest.isCancelled) {
1870             dispatch_async(dispatch_get_main_queue(), ^{
1871                 [activityFacility endActivity:[containerRequest.userInfo objectForKey:@"activity"] 
1872                                   withMessage:[containerRequest.userInfo objectForKey:@"stoppedActivityMessage"]];
1873             });
1874             objects = nil;
1875             [self syncOperationFinishedWithSuccess:NO];
1876             return;
1877         }
1878         // If the server listing fails, the sync should start over, so just retrying is enough
1879         NSUInteger retries = [[containerRequest.userInfo objectForKey:@"retries"] unsignedIntegerValue];
1880         if (retries > 0) {
1881             ASIPithosContainerRequest *newContainerRequest = (ASIPithosContainerRequest *)[PithosUtilities copyRequest:containerRequest];
1882             [(NSMutableDictionary *)(newContainerRequest.userInfo)setObject:[NSNumber numberWithUnsignedInteger:(--retries)] forKey:@"retries"];
1883             [networkQueue addOperation:[PithosUtilities prepareRequest:newContainerRequest priority:[[newContainerRequest.userInfo objectForKey:@"priority"] integerValue]]];
1884         } else {
1885             dispatch_async(dispatch_get_main_queue(), ^{
1886                 [activityFacility endActivity:[containerRequest.userInfo objectForKey:@"activity"] 
1887                                   withMessage:[containerRequest.userInfo objectForKey:@"failedActivityMessage"]];
1888             });
1889             objects = nil;
1890             // Since the server listing failed in all retries, the operation finished and the sync cycle is completeted unsuccesfully
1891             [self syncOperationFinishedWithSuccess:NO];
1892         }
1893     }
1894 }
1895
1896 - (void)downloadObjectBlockFinished:(ASIPithosObjectRequest *)objectRequest {
1897     @autoreleasepool {
1898         NSOperation *operation = [objectRequest.userInfo objectForKey:@"operation"];
1899         DLog(@"Sync::download object block finished: %@", objectRequest.url);
1900         if (operation.isCancelled) {
1901             [self requestFailed:objectRequest];
1902         } else if (objectRequest.responseStatusCode == 206) {
1903             NSString *accountName = [objectRequest.userInfo objectForKey:@"accountName"];
1904             ASIPithosContainer *pithosContainer = [objectRequest.userInfo objectForKey:@"pithosContainer"];
1905             ASIPithosObject *object = [objectRequest.userInfo objectForKey:@"pithosObject"];
1906             NSFileManager *fileManager = [NSFileManager defaultManager];
1907             NSError *error;
1908             PithosActivity *activity = [objectRequest.userInfo objectForKey:@"activity"];
1909             
1910             PithosLocalObjectState *storedState = [[[storedLocalObjectStates objectForKey:accountName]
1911                                                     objectForKey:pithosContainer.name] 
1912                                                    objectForKey:object.name];
1913             if ((storedState.tmpFilePath == nil) || ![fileManager fileExistsAtPath:storedState.tmpFilePath]) {
1914                 NSString *downloadsDirPath = self.tempDownloadsDirPath;
1915                 if (!downloadsDirPath) {
1916                     dispatch_async(dispatch_get_main_queue(), ^{
1917                         [activityFacility endActivity:activity
1918                                           withMessage:[objectRequest.userInfo objectForKey:@"failedActivityMessage"]];
1919                     });
1920                     [self syncOperationFinishedWithSuccess:NO];
1921                     return;
1922                 }
1923                 if ([accountName isEqualToString:@""]) {
1924                     downloadsDirPath = [downloadsDirPath stringByAppendingPathComponent:pithosContainer.name];
1925                 } else {
1926                     downloadsDirPath = [[[downloadsDirPath stringByAppendingPathComponent:@"shared with me"]
1927                                          stringByAppendingPathComponent:accountName]
1928                                         stringByAppendingPathComponent:pithosContainer.name];
1929                 }
1930                 BOOL isDirectory;
1931                 error = nil;
1932                 if (![fileManager fileExistsAtPath:downloadsDirPath isDirectory:&isDirectory]) {
1933                     if (![fileManager createDirectoryAtPath:downloadsDirPath withIntermediateDirectories:YES attributes:nil error:&error] || error) {
1934                         dispatch_async(dispatch_get_main_queue(), ^{
1935                             [activityFacility endActivity:activity
1936                                               withMessage:[objectRequest.userInfo objectForKey:@"failedActivityMessage"]];
1937                         });
1938                         [self syncOperationFinishedWithSuccess:NO];
1939                         return;
1940                     }
1941                 } else if (!isDirectory) {
1942                     dispatch_async(dispatch_get_main_queue(), ^{
1943                         [activityFacility endActivity:activity
1944                                           withMessage:[objectRequest.userInfo objectForKey:@"failedActivityMessage"]];
1945                     });
1946                     [self syncOperationFinishedWithSuccess:NO];
1947                     return;
1948                 }
1949                 
1950                 NSString *tempFileTemplate = [downloadsDirPath stringByAppendingPathComponent:@"download.XXXXXX"];
1951                 const char *tempFileTemplateCString = [tempFileTemplate fileSystemRepresentation];
1952                 char *tempFileNameCString = (char *)malloc(strlen(tempFileTemplateCString) + 1);
1953                 strcpy(tempFileNameCString, tempFileTemplateCString);
1954                 int fileDescriptor = mkstemp(tempFileNameCString);
1955                 NSString *tempFilePath = [fileManager stringWithFileSystemRepresentation:tempFileNameCString length:strlen(tempFileNameCString)];
1956                 free(tempFileNameCString);
1957                 if (fileDescriptor == -1) {
1958                     [PithosUtilities fileActionFailedAlertWithTitle:@"Create Temporary File Error" 
1959                                                             message:[NSString stringWithFormat:@"Cannot create temporary file at '%@'", tempFilePath] 
1960                                                               error:nil];
1961                     dispatch_async(dispatch_get_main_queue(), ^{
1962                         [activityFacility endActivity:activity 
1963                                           withMessage:[objectRequest.userInfo objectForKey:@"failedActivityMessage"]];
1964                     });
1965                     [self syncOperationFinishedWithSuccess:NO];
1966                     return;
1967                 }
1968                 close(fileDescriptor);
1969                 storedState.tmpFilePath = tempFilePath;
1970                 [self saveLocalState];
1971             }
1972
1973             NSUInteger missingBlockIndex = [[objectRequest.userInfo objectForKey:@"missingBlockIndex"] unsignedIntegerValue];
1974             NSFileHandle *tempFileHandle = [NSFileHandle fileHandleForWritingAtPath:storedState.tmpFilePath];
1975             [tempFileHandle seekToFileOffset:missingBlockIndex*pithosContainer.blockSize];
1976             [tempFileHandle writeData:[objectRequest responseData]];
1977             [tempFileHandle closeFile];
1978
1979             NSIndexSet *missingBlocks = [objectRequest.userInfo objectForKey:@"missingBlocks"];
1980             missingBlockIndex = [missingBlocks indexGreaterThanIndex:missingBlockIndex];
1981             if (missingBlockIndex == NSNotFound) {
1982                 NSString *filePath = [objectRequest.userInfo objectForKey:@"filePath"];
1983                 NSString *dirPath = [filePath stringByDeletingLastPathComponent];
1984                 if ([fileManager fileExistsAtPath:filePath] && ![self moveToTempTrashFile:filePath 
1985                                                                               accountName:accountName 
1986                                                                           pithosContainer:pithosContainer]) {
1987                     dispatch_async(dispatch_get_main_queue(), ^{
1988                         [activityFacility endActivity:activity 
1989                                           withMessage:[objectRequest.userInfo objectForKey:@"failedActivityMessage"]];
1990                     });
1991                     [self syncOperationFinishedWithSuccess:NO];
1992                     return;
1993                 } else if (![fileManager fileExistsAtPath:dirPath]) {
1994                     // File doesn't exist but also the containing directory doesn't exist
1995                     // In most cases this should have been resolved as an update of the corresponding local object,
1996                     // but it never hurts to check
1997                     error = nil;
1998                     [fileManager createDirectoryAtPath:dirPath withIntermediateDirectories:YES attributes:nil error:nil];
1999                     if (error != nil) {
2000                         [PithosUtilities fileActionFailedAlertWithTitle:@"Create Directory Error" 
2001                                                                 message:[NSString stringWithFormat:@"Cannot create directory at '%@'", dirPath] 
2002                                                                   error:error];
2003                         dispatch_async(dispatch_get_main_queue(), ^{
2004                             [activityFacility endActivity:[objectRequest.userInfo objectForKey:@"activity"] 
2005                                               withMessage:[objectRequest.userInfo objectForKey:@"failedActivityMessage"]];
2006                         });
2007                         [self syncOperationFinishedWithSuccess:NO];
2008                         return;
2009                     }
2010                 }
2011                 // Move file from tmp download
2012                 error = nil;
2013                 [fileManager moveItemAtPath:storedState.tmpFilePath toPath:filePath error:&error];
2014                 if (error != nil) {
2015                     [PithosUtilities fileActionFailedAlertWithTitle:@"Move File Error" 
2016                                                             message:[NSString stringWithFormat:@"Cannot move file at '%@' to '%@'", storedState.tmpFilePath, filePath] 
2017                                                               error:error];
2018                     dispatch_async(dispatch_get_main_queue(), ^{
2019                         [activityFacility endActivity:activity 
2020                                           withMessage:[objectRequest.userInfo objectForKey:@"failedActivityMessage"]];
2021                     });
2022                     [self syncOperationFinishedWithSuccess:NO];
2023                     return;
2024                 }
2025                 dispatch_async(dispatch_get_main_queue(), ^{
2026                     [activityFacility endActivity:activity 
2027                                       withMessage:[objectRequest.userInfo objectForKey:@"finishedActivityMessage"] 
2028                                        totalBytes:activity.totalBytes 
2029                                      currentBytes:activity.totalBytes];
2030                 });
2031
2032                 storedState.filePath = filePath;
2033                 storedState.hash = object.objectHash;
2034                 storedState.tmpFilePath = nil;
2035                 [self saveLocalState];
2036                 [self syncOperationFinishedWithSuccess:YES];
2037                 return;
2038             } else {
2039                 if (newSyncRequested || syncLate || operation.isCancelled) {
2040                     [self requestFailed:objectRequest];
2041                 } else {
2042                     __block ASIPithosObjectRequest *newObjectRequest = [PithosUtilities objectBlockDataRequestWithPithos:pithos
2043                                                                                                            containerName:pithosContainer.name 
2044                                                                                                                   object:object 
2045                                                                                                               blockIndex:missingBlockIndex 
2046                                                                                                                blockSize:pithosContainer.blockSize];
2047                     if (![accountName isEqualToString:@""])
2048                         [newObjectRequest setRequestUserFromDefaultTo:accountName withPithos:pithos];
2049                     newObjectRequest.delegate = self;
2050                     newObjectRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:);
2051                     newObjectRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:);
2052                     newObjectRequest.userInfo = objectRequest.userInfo;
2053                     [(NSMutableDictionary *)(newObjectRequest.userInfo) setObject:[NSNumber numberWithUnsignedInteger:missingBlockIndex] forKey:@"missingBlockIndex"];
2054                     [(NSMutableDictionary *)(newObjectRequest.userInfo) setObject:[NSNumber numberWithUnsignedInteger:10] forKey:@"retries"];
2055                     [newObjectRequest setBytesReceivedBlock:^(unsigned long long size, unsigned long long total){
2056                         [activityFacility updateActivity:activity 
2057                                              withMessage:[NSString stringWithFormat:@"%@ (%.0f%%)", 
2058                                                           [newObjectRequest.userInfo objectForKey:@"messagePrefix"], 
2059                                                           (100*(activity.currentBytes + size + 0.0)/(activity.totalBytes + 0.0))] 
2060                                               totalBytes:activity.totalBytes 
2061                                             currentBytes:(activity.currentBytes + size)];
2062                     }];
2063                     [networkQueue addOperation:[PithosUtilities prepareRequest:newObjectRequest priority:[[newObjectRequest.userInfo objectForKey:@"priority"] integerValue]]];
2064                 }
2065             }
2066         } else if (objectRequest.responseStatusCode == 412) {
2067             // The object has changed on the server
2068             dispatch_async(dispatch_get_main_queue(), ^{
2069                 [activityFacility endActivity:[objectRequest.userInfo objectForKey:@"activity"] 
2070                                   withMessage:[objectRequest.userInfo objectForKey:@"stoppedActivityMessage"]];
2071             });
2072             [self syncOperationFinishedWithSuccess:NO];
2073         } else {
2074             [self requestFailed:objectRequest];
2075         }
2076     }
2077 }
2078
2079 - (void)downloadObjectHashMapFinished:(ASIPithosObjectRequest *)objectRequest {
2080     @autoreleasepool {
2081         NSOperation *operation = [objectRequest.userInfo objectForKey:@"operation"];
2082         DLog(@"Sync::download object hashmap finished: %@", objectRequest.url);
2083         if (operation.isCancelled) {
2084             [self requestFailed:objectRequest];
2085         } else if (objectRequest.responseStatusCode == 200) {
2086             if (newSyncRequested || syncLate || operation.isCancelled) {
2087                 [self requestFailed:objectRequest];
2088             } else {
2089                 NSString *accountName = [objectRequest.userInfo objectForKey:@"accountName"];
2090                 ASIPithosContainer *pithosContainer = [objectRequest.userInfo objectForKey:@"pithosContainer"];
2091                 ASIPithosObject *object = [objectRequest.userInfo objectForKey:@"pithosObject"];
2092                 PithosLocalObjectState *storedState = [[[storedLocalObjectStates objectForKey:accountName] 
2093                                                         objectForKey:pithosContainer.name] 
2094                                                        objectForKey:object.name];
2095                 if ([PithosUtilities bytesOfFile:storedState.tmpFilePath] > object.bytes)
2096                     [[NSFileHandle fileHandleForWritingAtPath:storedState.tmpFilePath] truncateFileAtOffset:object.bytes];
2097                 PithosActivity *activity = [objectRequest.userInfo objectForKey:@"activity"];
2098                 NSIndexSet *missingBlocks = [PithosUtilities missingBlocksForFile:storedState.tmpFilePath 
2099                                                                         blockSize:pithosContainer.blockSize 
2100                                                                         blockHash:pithosContainer.blockHash 
2101                                                                        withHashes:[objectRequest hashes]];
2102                 NSUInteger missingBlockIndex = [missingBlocks firstIndex];
2103                 dispatch_async(dispatch_get_main_queue(), ^{
2104                     [activityFacility updateActivity:activity 
2105                                          withMessage:[NSString stringWithFormat:@"%@ (%.0f%%)", 
2106                                                       [objectRequest.userInfo objectForKey:@"messagePrefix"], 
2107                                                       (100*(activity.totalBytes - [missingBlocks count]*pithosContainer.blockSize + 0.0)/(activity.totalBytes + 0.0))] 
2108                                           totalBytes:activity.totalBytes 
2109                                         currentBytes:(activity.totalBytes - [missingBlocks count]*pithosContainer.blockSize)];
2110                 });
2111
2112                 __block ASIPithosObjectRequest *newObjectRequest = [PithosUtilities objectBlockDataRequestWithPithos:pithos
2113                                                                                                        containerName:pithosContainer.name 
2114                                                                                                               object:object 
2115                                                                                                           blockIndex:missingBlockIndex 
2116                                                                                                            blockSize:pithosContainer.blockSize];
2117                 if (![accountName isEqualToString:@""])
2118                     [newObjectRequest setRequestUserFromDefaultTo:accountName withPithos:pithos];
2119                 newObjectRequest.delegate = self;
2120                 newObjectRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:);
2121                 newObjectRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:);
2122                 newObjectRequest.userInfo = objectRequest.userInfo;
2123                 [(NSMutableDictionary *)(newObjectRequest.userInfo) setObject:missingBlocks forKey:@"missingBlocks"];
2124                 [(NSMutableDictionary *)(newObjectRequest.userInfo) setObject:[NSNumber numberWithUnsignedInteger:missingBlockIndex] forKey:@"missingBlockIndex"];
2125                 [(NSMutableDictionary *)(newObjectRequest.userInfo) setObject:[NSNumber numberWithUnsignedInteger:10] forKey:@"retries"];
2126                 [(NSMutableDictionary *)(newObjectRequest.userInfo) setObject:NSStringFromSelector(@selector(downloadObjectBlockFinished:)) forKey:@"didFinishSelector"];
2127                 [newObjectRequest setBytesReceivedBlock:^(unsigned long long size, unsigned long long total){
2128                     [activityFacility updateActivity:activity 
2129                                          withMessage:[NSString stringWithFormat:@"%@ (%.0f%%)", 
2130                                                       [newObjectRequest.userInfo objectForKey:@"messagePrefix"], 
2131                                                       (100*(activity.currentBytes + size + 0.0)/(activity.totalBytes + 0.0))] 
2132                                           totalBytes:activity.totalBytes 
2133                                         currentBytes:(activity.currentBytes + size)];
2134                 }];
2135                 [networkQueue addOperation:[PithosUtilities prepareRequest:newObjectRequest priority:[[newObjectRequest.userInfo objectForKey:@"priority"] integerValue]]];
2136             }
2137         } else {
2138             [self requestFailed:objectRequest];
2139         }
2140     }
2141 }
2142
2143 - (void)uploadDirectoryObjectFinished:(ASIPithosObjectRequest *)objectRequest {
2144     @autoreleasepool {
2145         NSOperation *operation = [objectRequest.userInfo objectForKey:@"operation"];
2146         DLog(@"Sync::upload directory object finished: %@", objectRequest.url);
2147         if (operation.isCancelled) {
2148             [self requestFailed:objectRequest];
2149         } else if (objectRequest.responseStatusCode == 201) {
2150             PithosLocalObjectState *storedState = [[[storedLocalObjectStates objectForKey:[objectRequest.userInfo objectForKey:@"accountName"]] 
2151                                                     objectForKey:[[objectRequest.userInfo objectForKey:@"pithosContainer"] name]] 
2152                                                    objectForKey:[[objectRequest.userInfo objectForKey:@"pithosObject"] name]];
2153             storedState.isDirectory = YES;
2154             [self saveLocalState];
2155             dispatch_async(dispatch_get_main_queue(), ^{
2156                 [activityFacility endActivity:[objectRequest.userInfo objectForKey:@"activity"] 
2157                                   withMessage:[objectRequest.userInfo objectForKey:@"finishedActivityMessage"]];
2158             });
2159             [self syncOperationFinishedWithSuccess:YES];
2160         } else {
2161             [self requestFailed:objectRequest];
2162         }
2163     }
2164 }
2165
2166 - (void)moveObjectToTrashFinished:(ASIPithosObjectRequest *)objectRequest {
2167     @autoreleasepool {
2168         NSOperation *operation = [objectRequest.userInfo objectForKey:@"operation"];
2169         DLog(@"Sync::move object to trash finished: %@", objectRequest.url);
2170         if (operation.isCancelled) {
2171             [self requestFailed:objectRequest];
2172         } else if (objectRequest.responseStatusCode == 201) {
2173             [[[storedLocalObjectStates objectForKey:[objectRequest.userInfo objectForKey:@"accountName"]] 
2174               objectForKey:[[objectRequest.userInfo objectForKey:@"pithosContainer"] name]] 
2175              removeObjectForKey:[[objectRequest.userInfo objectForKey:@"pithosObject"] name]];
2176             [self saveLocalState];
2177             dispatch_async(dispatch_get_main_queue(), ^{
2178                 [activityFacility endActivity:[objectRequest.userInfo objectForKey:@"activity"] 
2179                                   withMessage:[objectRequest.userInfo objectForKey:@"finishedActivityMessage"]];
2180             });
2181             [self syncOperationFinishedWithSuccess:YES];
2182         } else {
2183             [self requestFailed:objectRequest];
2184         }
2185     }
2186 }
2187
2188 - (void)deleteObjectFinished:(ASIPithosObjectRequest *)objectRequest {
2189     @autoreleasepool {
2190         NSOperation *operation = [objectRequest.userInfo objectForKey:@"operation"];
2191         DLog(@"Sync::delete object finished: %@", objectRequest.url);
2192         if (operation.isCancelled) {
2193             [self requestFailed:objectRequest];
2194         } else if (objectRequest.responseStatusCode == 204) {
2195             [[[storedLocalObjectStates objectForKey:[objectRequest.userInfo objectForKey:@"accountName"]] 
2196               objectForKey:[[objectRequest.userInfo objectForKey:@"pithosContainer"] name]] 
2197              removeObjectForKey:[[objectRequest.userInfo objectForKey:@"pithosObject"] name]];
2198             [self saveLocalState];
2199             dispatch_async(dispatch_get_main_queue(), ^{
2200                 [activityFacility endActivity:[objectRequest.userInfo objectForKey:@"activity"] 
2201                                   withMessage:[objectRequest.userInfo objectForKey:@"finishedActivityMessage"]];
2202             });
2203             [self syncOperationFinishedWithSuccess:YES];
2204         } else {
2205             [self requestFailed:objectRequest];
2206         }
2207     }
2208 }
2209
2210 - (void)uploadObjectUsingHashMapFinished:(ASIPithosObjectRequest *)objectRequest {
2211     @autoreleasepool {
2212         NSOperation *operation = [objectRequest.userInfo objectForKey:@"operation"];
2213         DLog(@"Sync::upload using hashmap finished: %@", objectRequest.url);
2214         NSString *accountName = [objectRequest.userInfo objectForKey:@"accountName"];
2215         ASIPithosContainer *pithosContainer = [objectRequest.userInfo objectForKey:@"pithosContainer"];
2216         ASIPithosObject *object = [objectRequest.userInfo objectForKey:@"pithosObject"];
2217         PithosLocalObjectState *storedState = [[[storedLocalObjectStates objectForKey:accountName] 
2218                                                 objectForKey:pithosContainer.name] 
2219                                                objectForKey:object.name];
2220         PithosActivity *activity = [objectRequest.userInfo objectForKey:@"activity"];
2221         NSUInteger totalBytes = activity.totalBytes;
2222         NSUInteger currentBytes = activity.currentBytes;
2223         if (operation.isCancelled) {
2224             [self requestFailed:objectRequest];
2225         } else if (objectRequest.responseStatusCode == 201) {
2226             DLog(@"Sync::object created: %@", objectRequest.url);
2227             storedState.filePath = [objectRequest.userInfo objectForKey:@"filePath"];
2228             storedState.hash = object.objectHash;
2229             [self saveLocalState];
2230             dispatch_async(dispatch_get_main_queue(), ^{
2231                 [activityFacility endActivity:activity 
2232                                   withMessage:[objectRequest.userInfo objectForKey:@"finishedActivityMessage"] 
2233                                    totalBytes:totalBytes 
2234                                  currentBytes:totalBytes];
2235             });
2236             [self syncOperationFinishedWithSuccess:YES];
2237         } else if (objectRequest.responseStatusCode == 409) {
2238             if (newSyncRequested || syncLate || operation.isCancelled) {
2239                 [self requestFailed:objectRequest];
2240             } else {
2241                 NSUInteger iteration = [[objectRequest.userInfo objectForKey:@"iteration"] unsignedIntegerValue];
2242                 if (iteration == 0) {
2243                     DLog(@"Sync::upload iteration limit reached: %@", objectRequest.url);
2244                     dispatch_async(dispatch_get_main_queue(), ^{
2245                         [activityFacility endActivity:activity withMessage:[objectRequest.userInfo objectForKey:@"stoppedActivityMessage"]];
2246                     });
2247                     [self syncOperationFinishedWithSuccess:NO];
2248                     return;
2249                 }
2250                 DLog(@"Sync::object is missing hashes: %@", objectRequest.url);
2251                 NSIndexSet *missingBlocks = [PithosUtilities missingBlocksForHashes:[objectRequest.userInfo objectForKey:@"hashes"]
2252                                                                   withMissingHashes:[objectRequest hashes]];
2253                 if (totalBytes >= [missingBlocks count]*pithosContainer.blockSize)
2254                     currentBytes = totalBytes - [missingBlocks count]*pithosContainer.blockSize;
2255                 dispatch_async(dispatch_get_main_queue(), ^{
2256                     [activityFacility updateActivity:activity 
2257                                          withMessage:[NSString stringWithFormat:@"%@ (%.0f%%)", 
2258                                                       [objectRequest.userInfo objectForKey:@"messagePrefix"], 
2259                                                       (100*(currentBytes + 0.0)/(totalBytes + 0.0))] 
2260                                           totalBytes:totalBytes 
2261                                         currentBytes:currentBytes];
2262                 });
2263                 NSUInteger missingBlockIndex = [missingBlocks firstIndex];
2264                 __block ASIPithosContainerRequest *newContainerRequest = [PithosUtilities updateContainerDataRequestWithPithos:pithos
2265                                                                                                                  containerName:pithosContainer.name 
2266                                                                                                                      blockSize:pithosContainer.blockSize 
2267                                                                                                                        forFile:[objectRequest.userInfo objectForKey:@"filePath"] 
2268                                                                                                              missingBlockIndex:missingBlockIndex 
2269                                                                                                                 sharingAccount:([accountName isEqualToString:@""] ? nil : accountName)];
2270                 newContainerRequest.delegate = self;
2271                 newContainerRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:);
2272                 newContainerRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:);
2273                 newContainerRequest.userInfo = objectRequest.userInfo;
2274                 [(NSMutableDictionary *)(newContainerRequest.userInfo) setObject:[NSNumber numberWithUnsignedInteger:(--iteration)] forKey:@"iteration"];
2275                 [(NSMutableDictionary *)(newContainerRequest.userInfo) setObject:[NSNumber numberWithUnsignedInteger:10] forKey:@"retries"];
2276                 [(NSMutableDictionary *)(newContainerRequest.userInfo) setObject:missingBlocks forKey:@"missingBlocks"];
2277                 [(NSMutableDictionary *)(newContainerRequest.userInfo) setObject:[NSNumber numberWithUnsignedInteger:missingBlockIndex] forKey:@"missingBlockIndex"];
2278                 [(NSMutableDictionary *)(newContainerRequest.userInfo) setObject:NSStringFromSelector(@selector(uploadMissingBlockFinished:)) forKey:@"didFinishSelector"];
2279                 [newContainerRequest setBytesSentBlock:^(unsigned long long size, unsigned long long total){
2280                     [activityFacility updateActivity:activity 
2281                                          withMessage:[NSString stringWithFormat:@"%@ (%.0f%%)", 
2282                                                       [newContainerRequest.userInfo objectForKey:@"messagePrefix"], 
2283                                                       (100*(activity.currentBytes + size + 0.0)/(activity.totalBytes + 0.0))] 
2284                                           totalBytes:activity.totalBytes 
2285                                         currentBytes:(activity.currentBytes + size)];
2286                 }];
2287                 [networkQueue addOperation:[PithosUtilities prepareRequest:newContainerRequest priority:[[newContainerRequest.userInfo objectForKey:@"priority"] integerValue]]];
2288             }
2289         } else {
2290             [self requestFailed:objectRequest];
2291         }
2292     }
2293 }
2294
2295 - (void)uploadMissingBlockFinished:(ASIPithosContainerRequest *)containerRequest {
2296     @autoreleasepool {
2297         NSOperation *operation = [containerRequest.userInfo objectForKey:@"operation"];
2298         DLog(@"Sync::upload of missing block finished: %@", containerRequest.url);
2299         if (operation.isCancelled) {
2300             [self requestFailed:containerRequest];
2301         } else if (containerRequest.responseStatusCode == 202) {
2302             NSString *accountName = [containerRequest.userInfo objectForKey:@"accountName"];
2303             ASIPithosContainer *pithosContainer = [containerRequest.userInfo objectForKey:@"pithosContainer"];
2304             ASIPithosObject *object = [containerRequest.userInfo objectForKey:@"pithosObject"];
2305             PithosActivity *activity = [containerRequest.userInfo objectForKey:@"activity"];
2306             NSIndexSet *missingBlocks = [containerRequest.userInfo objectForKey:@"missingBlocks"];
2307             NSUInteger missingBlockIndex = [[containerRequest.userInfo objectForKey:@"missingBlockIndex"] unsignedIntegerValue];
2308             missingBlockIndex = [missingBlocks indexGreaterThanIndex:missingBlockIndex];
2309             if (operation.isCancelled) {
2310                 [self requestFailed:containerRequest];
2311             } else if (missingBlockIndex == NSNotFound) {
2312                 NSArray *hashes = [containerRequest.userInfo objectForKey:@"hashes"];
2313                 ASIPithosObjectRequest *newObjectRequest = [PithosUtilities writeObjectDataRequestWithPithos:pithos 
2314                                                                                                containerName:pithosContainer.name 
2315                                                                                                   objectName:object.name 
2316                                                                                                  contentType:object.contentType 
2317                                                                                                    blockSize:pithosContainer.blockSize 
2318                                                                                                    blockHash:pithosContainer.blockHash
2319                                                                                                      forFile:[containerRequest.userInfo objectForKey:@"filePath"] 
2320                                                                                                checkIfExists:NO 
2321                                                                                                       hashes:&hashes 
2322                                                                                               sharingAccount:([accountName isEqualToString:@""] ? nil : accountName)];
2323                 newObjectRequest.delegate = self;
2324                 newObjectRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:);
2325                 newObjectRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:);
2326                 newObjectRequest.userInfo = containerRequest.userInfo;
2327                 [(NSMutableDictionary *)(newObjectRequest.userInfo) setObject:[NSNumber numberWithUnsignedInteger:10] forKey:@"retries"];
2328                 [(NSMutableDictionary *)(newObjectRequest.userInfo) removeObjectForKey:@"missingBlocks"];
2329                 [(NSMutableDictionary *)(newObjectRequest.userInfo) removeObjectForKey:@"missingBlockIndex"];
2330                 [(NSMutableDictionary *)(newObjectRequest.userInfo) setObject:NSStringFromSelector(@selector(uploadObjectUsingHashMapFinished:)) forKey:@"didFinishSelector"];
2331                 [networkQueue addOperation:[PithosUtilities prepareRequest:newObjectRequest priority:[[newObjectRequest.userInfo objectForKey:@"priority"] integerValue]]];
2332             } else {
2333                 if (newSyncRequested || syncLate || operation.isCancelled) {
2334                     [self requestFailed:containerRequest];
2335                 } else {
2336                     __block ASIPithosContainerRequest *newContainerRequest = [PithosUtilities updateContainerDataRequestWithPithos:pithos 
2337                                                                                                                      containerName:pithosContainer.name
2338                                                                                                                          blockSize:pithosContainer.blockSize
2339                                                                                                                            forFile:[containerRequest.userInfo objectForKey:@"filePath"] 
2340                                                                                                                  missingBlockIndex:missingBlockIndex 
2341                                                                                                                     sharingAccount:([accountName isEqualToString:@""] ? nil : accountName)];
2342                     newContainerRequest.delegate = self;
2343                     newContainerRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:);
2344                     newContainerRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:);
2345                     newContainerRequest.userInfo = containerRequest.userInfo;
2346                     [(NSMutableDictionary *)(newContainerRequest.userInfo) setObject:[NSNumber numberWithUnsignedInteger:10] forKey:@"retries"];
2347                     [(NSMutableDictionary *)(newContainerRequest.userInfo) setObject:[NSNumber numberWithUnsignedInteger:missingBlockIndex] forKey:@"missingBlockIndex"];
2348                     [newContainerRequest setBytesSentBlock:^(unsigned long long size, unsigned long long total){
2349                         [activityFacility updateActivity:activity 
2350                                              withMessage:[NSString stringWithFormat:@"%@ (%.0f%%)", 
2351                                                           [newContainerRequest.userInfo objectForKey:@"messagePrefix"], 
2352                                                           (100*(activity.currentBytes + size + 0.0)/(activity.totalBytes + 0.0))] 
2353                                               totalBytes:activity.totalBytes 
2354                                             currentBytes:(activity.currentBytes + size)];
2355                     }];
2356                     [networkQueue addOperation:[PithosUtilities prepareRequest:newContainerRequest priority:[[newContainerRequest.userInfo objectForKey:@"priority"] integerValue]]];
2357                 }
2358             }
2359         } else {
2360             [self requestFailed:containerRequest];
2361         }
2362     }
2363 }
2364
2365 - (void)requestFailed:(ASIPithosRequest *)request {
2366     @autoreleasepool {
2367         NSOperation *operation = [request.userInfo objectForKey:@"operation"];
2368         DLog(@"Sync::request failed: %@", request.url);
2369         if (operation.isCancelled)
2370             return;
2371         if (request.isCancelled || newSyncRequested || syncLate) {
2372             dispatch_async(dispatch_get_main_queue(), ^{
2373                 [activityFacility endActivity:[request.userInfo objectForKey:@"activity"] 
2374                                   withMessage:[request.userInfo objectForKey:@"stoppedActivityMessage"]];
2375             });
2376             [self syncOperationFinishedWithSuccess:NO];
2377             return;
2378         }
2379         NSUInteger retries = [[request.userInfo objectForKey:@"retries"] unsignedIntegerValue];
2380         if (retries > 0) {
2381             ASIPithosRequest *newRequest = (ASIPithosRequest *)[PithosUtilities copyRequest:request];
2382             [(NSMutableDictionary *)(newRequest.userInfo)setObject:[NSNumber numberWithUnsignedInteger:(--retries)] forKey:@"retries"];
2383             [networkQueue addOperation:[PithosUtilities prepareRequest:newRequest priority:[[newRequest.userInfo objectForKey:@"priority"] integerValue]]];
2384         } else {
2385             dispatch_async(dispatch_get_main_queue(), ^{
2386                 [activityFacility endActivity:[request.userInfo objectForKey:@"activity"] 
2387                                   withMessage:[request.userInfo objectForKey:@"failedActivityMessage"]];
2388             });
2389             [self syncOperationFinishedWithSuccess:NO];
2390         }
2391     }
2392 }
2393
2394 @end