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