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