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