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