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