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