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