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