5 // Copyright 2011 GRNET S.A. All rights reserved.
7 // Redistribution and use in source and binary forms, with or
8 // without modification, are permitted provided that the following
11 // 1. Redistributions of source code must retain the above
12 // copyright notice, this list of conditions and the following
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.
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.
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.
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"
48 #define DATA_MODEL_FILE @"localstate.archive"
49 #define ARCHIVE_KEY @"Data"
51 @interface PithosSyncDaemon (Private)
52 - (NSString *)pithosStateFilePath;
53 - (void)saveLocalState;
54 - (BOOL)moveToTempTrashFile:(NSString *)filePath;
55 - (BOOL)findLocalCopyForObjectWithHash:(NSString *)hash forFile:(NSString *)filePath;
56 - (void)updateLocalStateWithObject:(ASIPithosObject *)object localFilePath:(NSString *)filePath;
57 - (void)updateServerStateWithCurrentState:(PithosLocalObjectState *)currentState
58 object:(ASIPithosObject *)object
59 localFilePath:(NSString *)filePath;
60 - (void)requestFailed:(ASIPithosRequest *)request;
62 - (void)increaseSyncOperationCount;
63 - (void)decreaseSyncOperationCount;
67 @implementation PithosSyncDaemon
68 @synthesize blockHash, blockSize, lastModified, lastCompletedSync, remoteObjects, storedLocalObjectStates, currentLocalObjectStates;
69 @synthesize containerDirectoryPath, pithosStateFilePath, tempDownloadsDirPath, tempTrashDirPath;
72 #pragma Object Lifecycle
74 - (id)initWithDirectoryPath:(NSString *)aDirectoryPath
75 containerName:(NSString *)aContainerName
76 timeInterval:(NSTimeInterval)aTimeInterval {
77 if ((self = [super init])) {
78 directoryPath = [aDirectoryPath copy];
79 containerName = [aContainerName copy];
80 timeInterval = aTimeInterval;
82 syncOperationCount = 0;
83 newSyncRequested = NO;
84 containerDirectoryPath = [[directoryPath stringByAppendingPathComponent:containerName] copy];
86 activityFacility = [PithosActivityFacility defaultPithosActivityFacility];
88 if ([[NSFileManager defaultManager] fileExistsAtPath:self.pithosStateFilePath]) {
89 NSData *data = [NSData dataWithContentsOfFile:self.pithosStateFilePath];
90 NSKeyedUnarchiver *unarchiver = [[[NSKeyedUnarchiver alloc] initForReadingWithData:data] autorelease];
91 self.storedLocalObjectStates = [unarchiver decodeObjectForKey:ARCHIVE_KEY];
92 [unarchiver finishDecoding];
94 self.storedLocalObjectStates = [NSMutableDictionary dictionary];
97 queue = [[ASINetworkQueue alloc] init];
98 queue.shouldCancelAllRequestsOnFailure = NO;
101 [[NSNotificationCenter defaultCenter] addObserver:self
102 selector:@selector(applicationWillTerminate:)
103 name:NSApplicationWillTerminateNotification
104 object:[NSApplication sharedApplication]];
106 timer = [[NSTimer scheduledTimerWithTimeInterval:timeInterval target:self selector:@selector(sync) userInfo:nil repeats:YES] retain];
114 [[NSNotificationCenter defaultCenter] removeObserver:self];
115 [queue cancelAllOperations];
119 [tempTrashDirPath release];
120 [tempDownloadsDirPath release];
121 [pithosStateFilePath release];
122 [containerDirectoryPath release];
123 [currentLocalObjectStates release];
124 [storedLocalObjectStates release];
125 [remoteObjects release];
127 [lastCompletedSync release];
128 [lastModified release];
130 [containerName release];
131 [directoryPath release];
136 #pragma mark Observers
138 - (void)applicationWillTerminate:(NSNotification *)notification {
139 [self saveLocalState];
143 #pragma mark Properties
145 - (NSString *)pithosStateFilePath {
146 if (!pithosStateFilePath) {
147 pithosStateFilePath = [[[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent:DATA_MODEL_FILE] retain];
149 return [pithosStateFilePath copy];
152 - (NSString *)tempDownloadsDirPath {
153 NSFileManager *fileManager = [NSFileManager defaultManager];
154 if (!tempDownloadsDirPath || ![fileManager fileExistsAtPath:tempDownloadsDirPath]) {
155 // Get the path from user defaults
156 tempDownloadsDirPath = [[NSUserDefaults standardUserDefaults] stringForKey:@"PithosTempDownloadsDirPath"];
157 if (tempDownloadsDirPath) {
158 // Check if the path exists
160 BOOL fileExists = [fileManager fileExistsAtPath:tempDownloadsDirPath isDirectory:&isDirectory];
161 NSError *error = nil;
162 if (fileExists && !isDirectory)
163 [fileManager removeItemAtPath:tempDownloadsDirPath error:&error];
164 if (!error & !fileExists)
165 [fileManager createDirectoryAtPath:tempDownloadsDirPath withIntermediateDirectories:YES attributes:nil error:&error];
167 tempDownloadsDirPath = nil;
169 if (!tempDownloadsDirPath) {
170 NSString *tempDirTemplate = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:@"Temp Downloads XXXXXX"];
171 const char *tempDirTemplateCString = [tempDirTemplate fileSystemRepresentation];
172 char *tempDirNameCString = (char *)malloc(strlen(tempDirTemplateCString) + 1);
173 strcpy(tempDirNameCString, tempDirTemplateCString);
174 tempDirNameCString = mkdtemp(tempDirNameCString);
175 if (tempDirNameCString != NULL)
176 tempDownloadsDirPath = [fileManager stringWithFileSystemRepresentation:tempDirNameCString length:strlen(tempDirNameCString)];
177 free(tempDirNameCString);
179 if (!tempDownloadsDirPath)
180 [[NSUserDefaults standardUserDefaults] setObject:tempDownloadsDirPath forKey:@"PithosTempDownloadsDirPath"];
181 [tempDownloadsDirPath retain];
183 return [tempDownloadsDirPath copy];
186 - (NSString *)tempTrashDirPath {
187 NSFileManager *fileManager = [NSFileManager defaultManager];
188 if (!tempTrashDirPath || ![fileManager fileExistsAtPath:tempTrashDirPath]) {
189 // Get the path from user defaults
190 tempTrashDirPath = [[NSUserDefaults standardUserDefaults] stringForKey:@"PithosTempTrashDirPath"];
191 if (tempTrashDirPath) {
192 // Check if the path exists
194 BOOL fileExists = [fileManager fileExistsAtPath:tempTrashDirPath isDirectory:&isDirectory];
195 NSError *error = nil;
196 if (fileExists && !isDirectory)
197 [fileManager removeItemAtPath:tempTrashDirPath error:&error];
198 if (!error & !fileExists)
199 [fileManager createDirectoryAtPath:tempTrashDirPath withIntermediateDirectories:YES attributes:nil error:&error];
201 tempTrashDirPath = nil;
203 if (!tempTrashDirPath) {
204 NSString *tempDirTemplate = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:@"Temp Trash XXXXXX"];
205 const char *tempDirTemplateCString = [tempDirTemplate fileSystemRepresentation];
206 char *tempDirNameCString = (char *)malloc(strlen(tempDirTemplateCString) + 1);
207 strcpy(tempDirNameCString, tempDirTemplateCString);
208 tempDirNameCString = mkdtemp(tempDirNameCString);
209 if (tempDirNameCString != NULL)
210 tempTrashDirPath = [fileManager stringWithFileSystemRepresentation:tempDirNameCString length:strlen(tempDirNameCString)];
211 free(tempDirNameCString);
213 if (!tempTrashDirPath)
214 [[NSUserDefaults standardUserDefaults] setObject:tempTrashDirPath forKey:@"PithosTempTrashDirPath"];
215 [tempTrashDirPath retain];
217 return [tempTrashDirPath copy];
223 - (void)saveLocalState {
224 NSMutableData *data = [NSMutableData data];
225 NSKeyedArchiver *archiver = [[[NSKeyedArchiver alloc] initForWritingWithMutableData:data] autorelease];
226 [archiver encodeObject:storedLocalObjectStates forKey:ARCHIVE_KEY];
227 [archiver finishEncoding];
228 [data writeToFile:self.pithosStateFilePath atomically:YES];
231 - (void)increaseSyncOperationCount {
232 @synchronized(self) {
233 syncOperationCount++;
237 - (void)decreaseSyncOperationCount {
238 @synchronized(self) {
239 syncOperationCount--;
240 if (!syncOperationCount && !syncIncomplete) {
241 self.lastCompletedSync = [NSDate date];
242 [activityFacility startAndEndActivityWithType:PithosActivityOther
243 message:[NSString stringWithFormat:@"Sync: Completed %@", lastCompletedSync]];
244 // The sync cycle is completed and the final operation is to move the Trash Folder the contents of the Temp Trash
245 NSString *trashDirPath = self.tempTrashDirPath;
246 if (tempTrashDirPath) {
247 NSError *error = nil;
248 NSArray *subPaths = [[NSFileManager defaultManager] subpathsOfDirectoryAtPath:trashDirPath error:&error];
250 [PithosUtilities fileActionFailedAlertWithTitle:@"Directory Contents Error"
251 message:[NSString stringWithFormat:@"Cannot get contents of directory at '%@'", containerDirectoryPath]
255 if ([subPaths count]) {
256 NSMutableArray *subURLs = [NSMutableArray arrayWithCapacity:[subPaths count]];
257 for (NSString *subPath in subPaths) {
258 [subURLs addObject:[NSURL fileURLWithPath:[trashDirPath stringByAppendingPathComponent:subPath]]];
260 syncOperationCount = 1;
261 [[NSWorkspace sharedWorkspace] recycleURLs:subURLs completionHandler:^(NSDictionary *newURLs, NSError *error) {
263 [PithosUtilities fileActionFailedAlertWithTitle:@"Move to Trash Error"
264 message:@"Cannot move files to Trash"
267 syncOperationCount = 0;
276 @synchronized(self) {
277 if (syncOperationCount) {
278 // If at least one operation is running return
279 newSyncRequested = YES;
282 // The first operation is the server listing
283 syncOperationCount = 1;
284 newSyncRequested = NO;
289 NSFileManager *fileManager = [NSFileManager defaultManager];
291 NSError *error = nil;
292 if (![fileManager fileExistsAtPath:containerDirectoryPath isDirectory:&isDirectory]) {
293 if (![fileManager createDirectoryAtPath:containerDirectoryPath withIntermediateDirectories:YES attributes:nil error:&error] ||
295 [PithosUtilities fileActionFailedAlertWithTitle:@"Local Sync Directory Error"
296 message:[NSString stringWithFormat:@"Cannot create local sync directory at '%@'", containerDirectoryPath]
298 @synchronized(self) {
299 syncOperationCount = 0;
303 } else if (!isDirectory) {
304 [PithosUtilities fileActionFailedAlertWithTitle:@"Local Sync Directory Error"
305 message:[NSString stringWithFormat:@"File already exists at the local sync directory path at '%@'", containerDirectoryPath]
307 @synchronized(self) {
308 syncOperationCount = 0;
313 ASIPithosContainerRequest *containerRequest = [ASIPithosContainerRequest listObjectsRequestWithContainerName:containerName
322 ifModifiedSince:lastModified];
323 containerRequest.delegate = self;
324 containerRequest.didFinishSelector = @selector(listRequestFinished:);
325 containerRequest.didFailSelector = @selector(listRequestFailed:);
326 PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityOther
327 message:@"Sync: Getting server listing"];
328 containerRequest.userInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys:
329 activity, @"activity",
330 @"Sync: Getting server listing (stopped)", @"stoppedActivityMessage",
331 @"Sync: Getting server listing (failed)", @"failedActivityMessage",
332 @"Sync: Getting server listing (finished)", @"finishedActivityMessage",
333 [NSNumber numberWithInteger:NSOperationQueuePriorityVeryHigh], @"priority",
334 [NSNumber numberWithUnsignedInteger:10], @"retries",
336 [queue addOperation:[PithosUtilities prepareRequest:containerRequest priority:NSOperationQueuePriorityVeryHigh]];
339 - (BOOL)moveToTempTrashFile:(NSString *)filePath {
340 NSString *trashDirPath = self.tempTrashDirPath;
341 if (!tempTrashDirPath)
343 NSFileManager *fileManager = [NSFileManager defaultManager];
345 BOOL fileExists = [fileManager fileExistsAtPath:filePath isDirectory:&isDirectory];
346 NSError *error = nil;
347 NSString *newFilePath = [filePath stringByReplacingOccurrencesOfString:containerDirectoryPath
348 withString:trashDirPath];
349 NSString *newDirPath = [newFilePath stringByDeletingLastPathComponent];
350 if (fileExists && isDirectory) {
351 NSArray *subPaths = [fileManager subpathsOfDirectoryAtPath:filePath error:&error];
353 [PithosUtilities fileActionFailedAlertWithTitle:@"Directory Contents Error"
354 message:[NSString stringWithFormat:@"Cannot get contents of directory at '%@'", containerDirectoryPath]
358 if (![fileManager createDirectoryAtPath:newDirPath withIntermediateDirectories:YES attributes:nil error:&error] || error) {
359 [PithosUtilities fileActionFailedAlertWithTitle:@"Create Directory Error"
360 message:[NSString stringWithFormat:@"Cannot create directory at '%@'", newDirPath]
364 if (![fileManager moveItemAtPath:filePath toPath:newFilePath error:&error] || error) {
365 [PithosUtilities fileActionFailedAlertWithTitle:@"Move File Error"
366 message:[NSString stringWithFormat:@"Cannot move file at '%@' to '%@'",
367 filePath, newFilePath]
371 PithosLocalObjectState *currentState = [currentLocalObjectStates objectForKey:filePath];
373 currentState.filePath = newFilePath;
374 [currentLocalObjectStates setObject:currentState forKey:newFilePath];
375 [currentLocalObjectStates removeObjectForKey:filePath];
377 [currentLocalObjectStates setObject:[PithosLocalObjectState localObjectStateWithFile:newFilePath
382 for (NSString *subPath in subPaths) {
383 NSString *subFilePath = [filePath stringByAppendingPathComponent:subPath];
384 NSString *newSubFilePath = [subFilePath stringByReplacingOccurrencesOfString:containerDirectoryPath
385 withString:trashDirPath];
386 currentState = [currentLocalObjectStates objectForKey:subFilePath];
388 currentState.filePath = newSubFilePath;
389 [currentLocalObjectStates setObject:currentState forKey:newSubFilePath];
390 [currentLocalObjectStates removeObjectForKey:subFilePath];
392 [currentLocalObjectStates setObject:[PithosLocalObjectState localObjectStateWithFile:newSubFilePath
395 forKey:newSubFilePath];
398 } else if (fileExists && !isDirectory) {
399 if (![fileManager createDirectoryAtPath:newDirPath withIntermediateDirectories:YES attributes:nil error:&error] || error) {
400 [PithosUtilities fileActionFailedAlertWithTitle:@"Create Directory Error"
401 message:[NSString stringWithFormat:@"Cannot create directory at '%@'", newDirPath]
405 if (![fileManager moveItemAtPath:filePath toPath:newFilePath error:&error] || error) {
406 [PithosUtilities fileActionFailedAlertWithTitle:@"Move File Error"
407 message:[NSString stringWithFormat:@"Cannot move file at '%@' to '%@'",
408 filePath, newFilePath]
412 PithosLocalObjectState *currentState = [currentLocalObjectStates objectForKey:filePath];
414 currentState.filePath = newFilePath;
415 [currentLocalObjectStates setObject:currentState forKey:newFilePath];
416 [currentLocalObjectStates removeObjectForKey:filePath];
418 [currentLocalObjectStates setObject:[PithosLocalObjectState localObjectStateWithFile:newFilePath
427 - (BOOL)findLocalCopyForObjectWithHash:(NSString *)hash forFile:(NSString *)filePath {
428 NSUInteger hashLength = [hash length];
429 if ((hashLength != 32) && (hashLength != 64))
431 PithosLocalObjectState *localState;
432 NSFileManager *fileManager = [NSFileManager defaultManager];
434 NSError *error = nil;
435 for (NSString *localFilePath in [currentLocalObjectStates allKeys]) {
436 localState = [currentLocalObjectStates objectForKey:localFilePath];
437 if (!localState.isDirectory && ([hash isEqualToString:localState.md5] || [hash isEqualToString:localState.hashMapHash]) &&
438 [fileManager fileExistsAtPath:localFilePath isDirectory:&isDirectory] && !isDirectory) {
439 if ([localFilePath hasPrefix:containerDirectoryPath]) {
440 if (![fileManager copyItemAtPath:localFilePath toPath:filePath error:&error] || error) {
441 [PithosUtilities fileActionFailedAlertWithTitle:@"Copy File Error"
442 message:[NSString stringWithFormat:@"Cannot copy file at '%@' to '%@'",
443 localFilePath, filePath]
448 } else if (self.tempTrashDirPath && [localFilePath hasPrefix:self.tempTrashDirPath]) {
449 if (![fileManager moveItemAtPath:localFilePath toPath:filePath error:&error] || error) {
450 [PithosUtilities fileActionFailedAlertWithTitle:@"Move File Error"
451 message:[NSString stringWithFormat:@"Cannot move file at '%@' to '%@'",
452 localFilePath, filePath]
455 localState.filePath = filePath;
456 [currentLocalObjectStates setObject:localState forKey:filePath];
457 [currentLocalObjectStates removeObjectForKey:localFilePath];
466 - (void)updateLocalStateWithObject:(ASIPithosObject *)object
467 localFilePath:(NSString *)filePath {
468 NSFileManager *fileManager = [NSFileManager defaultManager];
471 BOOL fileExists = [fileManager fileExistsAtPath:filePath isDirectory:&isDirectory];
472 PithosLocalObjectState *storedState = [storedLocalObjectStates objectForKey:object.name];
473 if (!object || !object.hash) {
474 // Delete local object
475 NSLog(@"Sync::delete local object: %@", filePath);
476 if (!fileExists || [self moveToTempTrashFile:filePath]) {
477 [activityFacility startAndEndActivityWithType:PithosActivityOther
478 message:[NSString stringWithFormat:@"Sync: Deleting '%@' locally (finished)", object.name]];
479 [storedLocalObjectStates removeObjectForKey:object.name];
480 [self saveLocalState];
482 } else if ([object.contentType isEqualToString:@"application/directory"]) {
483 // Create local directory object
484 NSLog(@"Sync::create local directory object: %@", filePath);
485 BOOL directoryCreated = NO;
486 if (!fileExists || (!isDirectory && [self moveToTempTrashFile:filePath])) {
487 NSLog(@"Sync::local directory object doesn't exist: %@", filePath);
489 if (![fileManager createDirectoryAtPath:filePath withIntermediateDirectories:YES attributes:nil error:&error] || error) {
490 [PithosUtilities fileActionFailedAlertWithTitle:@"Create Directory Error"
491 message:[NSString stringWithFormat:@"Cannot create directory at '%@'", filePath]
494 directoryCreated = YES;
495 storedState.isDirectory = YES;
496 [self saveLocalState];
499 NSLog(@"Sync::local directory object exists: %@", filePath);
500 directoryCreated = YES;
502 if (directoryCreated)
503 [activityFacility startAndEndActivityWithType:PithosActivityOther
504 message:[NSString stringWithFormat:@"Sync: Creating directory '%@' locally (finished)", object.name]];
505 } else if (object.bytes == 0) {
506 // Create local object with zero length
507 NSLog(@"Sync::create local zero length object: %@", filePath);
508 BOOL fileCreated = NO;
509 if (!fileExists || ((isDirectory || [PithosUtilities bytesOfFile:filePath]) && [self moveToTempTrashFile:filePath])) {
510 NSLog(@"Sync::local zero length object doesn't exist: %@", filePath);
512 if (![fileManager createFileAtPath:filePath contents:nil attributes:nil]) {
513 [PithosUtilities fileActionFailedAlertWithTitle:@"Create File Error"
514 message:[NSString stringWithFormat:@"Cannot create file at '%@'", filePath]
518 storedState.hash = object.hash;
519 storedState.filePath = nil;
520 [self saveLocalState];
523 NSLog(@"Sync::local zero length object exists: %@", filePath);
527 [activityFacility startAndEndActivityWithType:PithosActivityOther
528 message:[NSString stringWithFormat:@"Sync: Downloading '%@' (100%%)", object.name]];
529 } else if (storedState.filePath == nil) {
530 // Create new local object
531 // Check first if a local copy exists
532 if ([self findLocalCopyForObjectWithHash:object.hash forFile:filePath]) {
533 [activityFacility startAndEndActivityWithType:PithosActivityOther
534 message:[NSString stringWithFormat:@"Sync: Downloading '%@' (100%%)", object.name]];
536 [self increaseSyncOperationCount];
537 __block ASIPithosObjectRequest *objectRequest = [PithosUtilities objectBlockDataRequestWithContainerName:containerName
540 blockSize:blockSize];
541 objectRequest.delegate = self;
542 objectRequest.didFinishSelector = @selector(downloadObjectBlockFinished:);
543 objectRequest.didFailSelector = @selector(requestFailed:);
544 PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityDownload
545 message:[NSString stringWithFormat:@"Sync: Downloading '%@' (0%%)", object.name]
546 totalBytes:object.bytes
548 objectRequest.userInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys:
549 object, @"pithosObject",
550 [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, (NSUInteger)ceil((object.bytes +0.0)/(blockSize + 0.0)))], @"missingBlocks",
551 [NSNumber numberWithUnsignedInteger:0], @"missingBlockIndex",
552 filePath, @"filePath",
553 activity, @"activity",
554 [NSString stringWithFormat:@"Sync: Downloading '%@' (stopped)", object.name], @"stoppedActivityMessage",
555 [NSString stringWithFormat:@"Sync: Downloading '%@' (failed)", object.name], @"failedActivityMessage",
556 [NSString stringWithFormat:@"Sync: Downloading '%@' (100%%)", object.name], @"finishedActivityMessage",
557 [NSNumber numberWithInteger:NSOperationQueuePriorityNormal], @"priority",
558 [NSNumber numberWithUnsignedInteger:10], @"retries",
560 [objectRequest setBytesReceivedBlock:^(unsigned long long size, unsigned long long total){
561 [activityFacility updateActivity:activity
562 withMessage:[NSString stringWithFormat:@"Downloading '%@' (%.0f%%)",
563 [[objectRequest.userInfo valueForKey:@"pithosObject"] name],
564 (100*(activity.currentBytes + size + 0.0)/(activity.totalBytes + 0.0))]
565 totalBytes:activity.totalBytes
566 currentBytes:(activity.currentBytes + size)];
568 [queue addOperation:[PithosUtilities prepareRequest:objectRequest priority:NSOperationQueuePriorityNormal]];
571 // Resume local object download
572 // Check first if a local copy exists
573 if ([self findLocalCopyForObjectWithHash:object.hash forFile:filePath]) {
574 [activityFacility startAndEndActivityWithType:PithosActivityOther
575 message:[NSString stringWithFormat:@"Sync: Downloading '%@' (100%%)", object.name]];
576 // Delete incomplete temp download
578 if (![fileManager removeItemAtPath:storedState.filePath error:&error] || error) {
579 [PithosUtilities fileActionFailedAlertWithTitle:@"Remove File Error"
580 message:[NSString stringWithFormat:@"Cannot remove file at '%@'", storedState.filePath]
584 [self increaseSyncOperationCount];
585 ASIPithosObjectRequest *objectRequest = [ASIPithosObjectRequest objectHashmapRequestWithContainerName:containerName
586 objectName:object.name];
587 objectRequest.delegate = self;
588 objectRequest.didFinishSelector = @selector(downloadObjectHashMapFinished:);
589 // The fail method for block download does exactly what we want
590 objectRequest.didFailSelector = @selector(requestFailed:);
591 PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityDownload
592 message:[NSString stringWithFormat:@"Sync: Downloading '%@' (0%%)", object.name]
593 totalBytes:object.bytes
595 objectRequest.userInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys:
596 object, @"pithosObject",
597 filePath, @"filePath",
598 activity, @"activity",
599 [NSString stringWithFormat:@"Sync: Downloading '%@' (stopped)", object.name], @"stoppedActivityMessage",
600 [NSString stringWithFormat:@"Sync: Downloading '%@' (failed)", object.name], @"failedActivityMessage",
601 [NSString stringWithFormat:@"Sync: Downloading '%@' (100%%)", object.name], @"finishedActivityMessage",
602 [NSNumber numberWithInteger:NSOperationQueuePriorityNormal], @"priority",
603 [NSNumber numberWithUnsignedInteger:10], @"retries",
605 [queue addOperation:[PithosUtilities prepareRequest:objectRequest priority:NSOperationQueuePriorityNormal]];
610 -(void)updateServerStateWithCurrentState:(PithosLocalObjectState *)currentState
611 object:(ASIPithosObject *)object
612 localFilePath:(NSString *)filePath {
613 [self increaseSyncOperationCount];
614 if (currentState.isDirectory) {
615 // Create remote directory object
616 ASIPithosObjectRequest *objectRequest = [ASIPithosObjectRequest writeObjectDataRequestWithContainerName:containerName
617 objectName:object.name
619 contentType:@"application/directory"
621 contentDisposition:nil
624 isPublic:ASIPithosObjectRequestPublicIgnore
627 objectRequest.delegate = self;
628 objectRequest.didFinishSelector = @selector(uploadDirectoryObjectFinished:);
629 objectRequest.didFailSelector = @selector(requestFailed:);
630 PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityCreateDirectory
631 message:[NSString stringWithFormat:@"Sync: Creating directory '%@'", object.name]];
632 objectRequest.userInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys:
633 object, @"pithosObject",
634 activity, @"activity",
635 [NSString stringWithFormat:@"Sync: Creating directory '%@' (stopped)", object.name], @"stoppedActivityMessage",
636 [NSString stringWithFormat:@"Sync: Creating directory '%@' (failed)", object.name], @"failedActivityMessage",
637 [NSString stringWithFormat:@"Sync: Creating directory '%@' (finished)", object.name], @"finishedActivityMessage",
638 [NSNumber numberWithInteger:NSOperationQueuePriorityHigh], @"priority",
639 [NSNumber numberWithUnsignedInteger:10], @"retries",
641 [queue addOperation:[PithosUtilities prepareRequest:objectRequest priority:NSOperationQueuePriorityHigh]];
642 } else if (!currentState.exists) {
643 // Delete remote object
644 NSString *safeObjectName = [PithosUtilities safeObjectNameForContainerName:@"trash"
645 objectName:object.name];
646 if (safeObjectName) {
647 ASIPithosObjectRequest *objectRequest = [PithosUtilities moveObjectRequestWithContainerName:containerName
648 objectName:object.name
649 destinationContainerName:@"trash"
650 destinationObjectName:safeObjectName
653 objectRequest.delegate = self;
654 objectRequest.didFinishSelector = @selector(moveObjectToTrashFinished:);
655 objectRequest.didFailSelector = @selector(requestFailed:);
656 PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityDelete
657 message:[NSString stringWithFormat:@"Sync: Moving to trash '%@'", object.name]];
658 objectRequest.userInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys:
659 object, @"pithosObject",
660 activity, @"activity",
661 [NSString stringWithFormat:@"Sync: Moving to trash '%@' (stopped)", object.name], @"stoppedActivityMessage",
662 [NSString stringWithFormat:@"Sync: Moving to trash '%@' (failed)", object.name], @"failedActivityMessage",
663 [NSString stringWithFormat:@"Sync: Moving to trash '%@' (finished)", object.name], @"finishedActivityMessage",
664 [NSNumber numberWithInteger:NSOperationQueuePriorityHigh], @"priority",
665 [NSNumber numberWithUnsignedInteger:10], @"retries",
667 [queue addOperation:[PithosUtilities prepareRequest:objectRequest priority:NSOperationQueuePriorityHigh]];
669 syncIncomplete = YES;
670 [self decreaseSyncOperationCount];
673 syncIncomplete = YES;
674 [self decreaseSyncOperationCount];
677 // Upload file to remote object
678 NSError *error = nil;
679 object.contentType = [PithosUtilities contentTypeOfFile:filePath error:&error];
680 if (object.contentType == nil)
681 object.contentType = @"application/octet-stream";
683 NSLog(@"contentType detection error: %@", error);
684 NSArray *hashes = nil;
685 ASIPithosObjectRequest *objectRequest = [PithosUtilities writeObjectDataRequestWithContainerName:containerName
686 objectName:object.name
687 contentType:object.contentType
695 objectRequest.delegate = self;
696 objectRequest.didFinishSelector = @selector(uploadObjectUsingHashMapFinished:);
697 objectRequest.didFailSelector = @selector(requestFailed:);
698 PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityUpload
699 message:[NSString stringWithFormat:@"Sync: Uploading '%@' (0%%)", object.name]
700 totalBytes:[[objectRequest.userInfo valueForKey:@"bytes"] unsignedIntegerValue]
702 [(NSMutableDictionary *)objectRequest.userInfo addEntriesFromDictionary:
703 [NSDictionary dictionaryWithObjectsAndKeys:
704 object, @"pithosObject",
705 filePath, @"filePath",
707 [NSNumber numberWithUnsignedInteger:10], @"iteration",
708 activity, @"activity",
709 [NSString stringWithFormat:@"Sync: Uploading '%@' (stopped)", object.name], @"stoppedActivityMessage",
710 [NSString stringWithFormat:@"Sync: Uploading '%@' (failed)", object.name], @"failedActivityMessage",
711 [NSString stringWithFormat:@"Sync: Uploading '%@' (100%%)", object.name], @"finishedActivityMessage",
712 [NSNumber numberWithInteger:NSOperationQueuePriorityNormal], @"priority",
713 [NSNumber numberWithUnsignedInteger:10], @"retries",
715 [queue addOperation:[PithosUtilities prepareRequest:objectRequest priority:NSOperationQueuePriorityNormal]];
717 syncIncomplete = YES;
718 [self decreaseSyncOperationCount];
725 #pragma mark ASIHTTPRequestDelegate
727 - (void)listRequestFinished:(ASIPithosContainerRequest *)containerRequest {
728 NSLog(@"Sync::list request finished: %@", containerRequest.url);
729 if ((containerRequest.responseStatusCode == 200) || (containerRequest.responseStatusCode == 304)) {
730 if (containerRequest.responseStatusCode == 200) {
731 NSArray *someObjects = [containerRequest objects];
732 if (objects == nil) {
733 objects = [[NSMutableArray alloc] initWithArray:someObjects];
735 [objects addObjectsFromArray:someObjects];
737 if ([someObjects count] < 10000) {
738 self.blockHash = [containerRequest blockHash];
739 self.blockSize = [containerRequest blockSize];
740 self.lastModified = [containerRequest lastModified];
741 self.remoteObjects = [NSMutableDictionary dictionaryWithCapacity:[objects count]];
742 for (ASIPithosObject *object in objects) {
743 [remoteObjects setObject:object forKey:object.name];
748 // Do an additional request to fetch more objects
749 ASIPithosContainerRequest *newContainerRequest = [ASIPithosContainerRequest listObjectsRequestWithContainerName:containerName
751 marker:[[someObjects lastObject] name]
758 ifModifiedSince:lastModified];
759 newContainerRequest.delegate = self;
760 newContainerRequest.didFinishSelector = @selector(listRequestFinished:);
761 newContainerRequest.didFailSelector = @selector(listRequestFailed:);
762 newContainerRequest.userInfo = newContainerRequest.userInfo;
763 [(NSMutableDictionary *)newContainerRequest.userInfo setObject:[NSNumber numberWithUnsignedInteger:10] forKey:@"retries"];
764 [queue addOperation:[PithosUtilities prepareRequest:newContainerRequest priority:[[newContainerRequest.userInfo objectForKey:@"priority"] integerValue]]];
768 [activityFacility endActivity:[containerRequest.userInfo objectForKey:@"activity"]
769 withMessage:[containerRequest.userInfo objectForKey:@"finishedActivityMessage"]];
770 NSFileManager *fileManager = [NSFileManager defaultManager];
771 NSError *error = nil;
772 NSArray *subPaths = [fileManager subpathsOfDirectoryAtPath:containerDirectoryPath error:&error];
774 [PithosUtilities fileActionFailedAlertWithTitle:@"Directory Contents Error"
775 message:[NSString stringWithFormat:@"Cannot get contents of directory at '%@'", containerDirectoryPath]
777 [activityFacility startAndEndActivityWithType:PithosActivityOther message:@"Sync: Failed to read contents of sync directory"];
778 @synchronized(self) {
779 // Since the local listing failed, the operation finished and the sync cycle is completeted unsuccesfully
780 syncOperationCount = 0;
781 if (newSyncRequested)
786 self.currentLocalObjectStates = [NSMutableDictionary dictionaryWithCapacity:[subPaths count]];
787 for (NSString *objectName in subPaths) {
788 if (![storedLocalObjectStates objectForKey:objectName]) {
789 [storedLocalObjectStates setObject:[PithosLocalObjectState localObjectState] forKey:objectName];
791 NSString *filePath = [containerDirectoryPath stringByAppendingPathComponent:objectName];
792 [currentLocalObjectStates setObject:[PithosLocalObjectState localObjectStateWithFile:filePath
797 [self saveLocalState];
799 for (NSString *objectName in remoteObjects) {
800 if (![storedLocalObjectStates objectForKey:objectName])
801 [storedLocalObjectStates setObject:[PithosLocalObjectState localObjectState] forKey:objectName];
804 for (NSString *objectName in [[storedLocalObjectStates allKeys] sortedArrayUsingSelector:@selector(compare:)]) {
805 NSString *filePath = [containerDirectoryPath stringByAppendingPathComponent:objectName];
806 if ([objectName hasSuffix:@"/"])
807 filePath = [filePath stringByAppendingString:@":"];
808 ASIPithosObject *object = [ASIPithosObject object];
809 object.name = objectName;
810 NSLog(@"Sync::object name: %@", objectName);
812 PithosLocalObjectState *storedLocalObjectState = [storedLocalObjectStates objectForKey:object.name];
813 PithosLocalObjectState *currentLocalObjectState = [currentLocalObjectStates objectForKey:filePath];
814 if (!currentLocalObjectState)
815 currentLocalObjectState = [PithosLocalObjectState localObjectStateWithFile:filePath
817 blockSize:blockSize];
818 if (currentLocalObjectState.isDirectory)
819 object.contentType = @"application/directory";
821 PithosLocalObjectState *remoteObjectState = [PithosLocalObjectState localObjectState];
822 ASIPithosObject *remoteObject = [remoteObjects objectForKey:objectName];
824 if ([remoteObject.contentType isEqualToString:@"application/directory"]) {
825 remoteObjectState.isDirectory = YES;
826 object.contentType = @"application/directory";
828 remoteObjectState.hash = remoteObject.hash;
832 BOOL localStateHasChanged = ![currentLocalObjectState isEqualToState:storedLocalObjectState];
833 BOOL serverStateHasChanged = ![remoteObjectState isEqualToState:storedLocalObjectState];
834 NSLog(@"Sync::localStateHasChanged: %d, serverStateHasChanged: %d", localStateHasChanged, serverStateHasChanged);
835 if (!localStateHasChanged) {
836 // Local state hasn't changed
837 if (serverStateHasChanged) {
838 // Server state has changed
839 // Update local state to match that of the server
840 object.bytes = remoteObject.bytes;
841 object.version = remoteObject.version;
842 object.contentType = remoteObject.contentType;
843 object.hash = remoteObject.hash;
844 [self updateLocalStateWithObject:object localFilePath:filePath];
845 } else if (!remoteObject && !currentLocalObjectState.exists) {
846 // Server state hasn't changed
847 // If the object doesn't exist neither in the server or locally, it should be removed from the stored local objects
848 [storedLocalObjectStates removeObjectForKey:objectName];
849 [self saveLocalState];
852 // Local state has changed
853 if (!serverStateHasChanged) {
854 // Server state hasn't changed
855 [self updateServerStateWithCurrentState:currentLocalObjectState
857 localFilePath:filePath];
859 // Server state has also changed
860 if (remoteObjectState.isDirectory && currentLocalObjectState.isDirectory) {
861 // Both did the same change (directory)
862 storedLocalObjectState.isDirectory = YES;
863 [self saveLocalState];
864 } else if ([remoteObjectState isEqualToState:currentLocalObjectState]) {
865 // Both did the same change (object edit or delete)
866 if (!remoteObjectState.exists)
867 [storedLocalObjectStates removeObjectForKey:object.name];
869 storedLocalObjectState.hash = remoteObjectState.hash;
870 [self saveLocalState];
872 // Conflict, we ask the user which change to keep
873 NSString *informativeText;
874 NSString *firstButtonText;
875 NSString *secondButtonText;
877 if (!remoteObjectState.exists) {
878 // Remote object has been deleted
879 informativeText = [NSString stringWithFormat:@"'%@' has been modified locally, while it has been deleted from server.", object.name ];
880 firstButtonText = @"Delete local file";
881 secondButtonText = @"Upload file to server";
882 } else if (!currentLocalObjectState.exists) {
883 informativeText = [NSString stringWithFormat:@"'%@' has been modified on the server, while it has been deleted locally.", object.name];
884 firstButtonText = @"Download file from server";
885 secondButtonText = @"Delete file on server";
887 informativeText = [NSString stringWithFormat:@"'%@' has been modifed both locally and on the server.", object.name];
888 firstButtonText = @"Keep server version";
889 secondButtonText = @"Keep local version";
891 NSAlert *alert = [[[NSAlert alloc] init] autorelease];
892 [alert setMessageText:@"Conflict"];
893 [alert setInformativeText:informativeText];
894 [alert addButtonWithTitle:firstButtonText];
895 [alert addButtonWithTitle:secondButtonText];
896 [alert addButtonWithTitle:@"Do nothing"];
897 NSInteger choice = [alert runModal];
898 if (choice == NSAlertFirstButtonReturn) {
899 object.bytes = remoteObject.bytes;
900 object.version = remoteObject.version;
901 object.contentType = remoteObject.contentType;
902 object.hash = remoteObject.hash;
903 [self updateLocalStateWithObject:object localFilePath:filePath];
904 } if (choice == NSAlertSecondButtonReturn) {
905 [self updateServerStateWithCurrentState:currentLocalObjectState
907 localFilePath:filePath];
913 @synchronized(self) {
914 [self decreaseSyncOperationCount];
915 if (newSyncRequested && !syncOperationCount)
919 NSUInteger retries = [[containerRequest.userInfo objectForKey:@"retries"] unsignedIntegerValue];
921 ASIPithosContainerRequest *newContainerRequest = (ASIPithosContainerRequest *)[PithosUtilities copyRequest:containerRequest];
922 [(NSMutableDictionary *)(newContainerRequest.userInfo)setObject:[NSNumber numberWithUnsignedInteger:(--retries)] forKey:@"retries"];
923 [queue addOperation:[PithosUtilities prepareRequest:newContainerRequest priority:[[newContainerRequest.userInfo objectForKey:@"priority"] integerValue]]];
925 [activityFacility endActivity:[containerRequest.userInfo objectForKey:@"activity"]
926 withMessage:[containerRequest.userInfo objectForKey:@"failedActivityMessage"]];
927 @synchronized(self) {
928 // Since the server listing failed in all retries, the operation finished and the sync cycle is completeted unsuccesfully
929 syncOperationCount = 0;
930 if (newSyncRequested)
937 - (void)listRequestFailed:(ASIPithosContainerRequest *)containerRequest {
938 if ([containerRequest isCancelled]) {
939 [activityFacility endActivity:[containerRequest.userInfo objectForKey:@"activity"]
940 withMessage:[containerRequest.userInfo objectForKey:@"stoppedActivityMessage"]];
943 @synchronized(self) {
944 syncOperationCount = 0;
948 // If the server listing fails, the sync should start over, so just retrying is enough
949 NSUInteger retries = [[containerRequest.userInfo objectForKey:@"retries"] unsignedIntegerValue];
951 ASIPithosContainerRequest *newContainerRequest = (ASIPithosContainerRequest *)[PithosUtilities copyRequest:containerRequest];
952 [(NSMutableDictionary *)(newContainerRequest.userInfo)setObject:[NSNumber numberWithUnsignedInteger:(--retries)] forKey:@"retries"];
953 [queue addOperation:[PithosUtilities prepareRequest:newContainerRequest priority:[[newContainerRequest.userInfo objectForKey:@"priority"] integerValue]]];
955 [activityFacility endActivity:[containerRequest.userInfo objectForKey:@"activity"]
956 withMessage:[containerRequest.userInfo objectForKey:@"failedActivityMessage"]];
959 @synchronized(self) {
960 // Since the server listing failed in all retries, the operation finished and the sync cycle is completeted unsuccesfully
961 syncOperationCount = 0;
962 if (newSyncRequested)
968 - (void)downloadObjectBlockFinished:(ASIPithosObjectRequest *)objectRequest {
969 NSLog(@"Sync::download object block finished: %@", objectRequest.url);
970 if (objectRequest.responseStatusCode == 206) {
971 ASIPithosObject *object = [objectRequest.userInfo objectForKey:@"pithosObject"];
972 NSFileManager *fileManager = [NSFileManager defaultManager];
974 PithosActivity *activity = [objectRequest.userInfo objectForKey:@"activity"];
976 NSString *downloadsDirPath = self.tempDownloadsDirPath;
977 if (!downloadsDirPath) {
978 [activityFacility endActivity:activity
979 withMessage:[objectRequest.userInfo objectForKey:@"failedActivityMessage"]];
980 @synchronized(self) {
981 syncIncomplete = YES;
982 [self decreaseSyncOperationCount];
983 if (newSyncRequested && !syncOperationCount)
989 PithosLocalObjectState *storedState = [storedLocalObjectStates objectForKey:object.name];
990 if ((storedState.filePath == nil) || ![fileManager fileExistsAtPath:storedState.filePath]) {
991 NSString *tempFileTemplate = [downloadsDirPath stringByAppendingPathComponent:@"download.XXXXXX"];
992 const char *tempFileTemplateCString = [tempFileTemplate fileSystemRepresentation];
993 char *tempFileNameCString = (char *)malloc(strlen(tempFileTemplateCString) + 1);
994 strcpy(tempFileNameCString, tempFileTemplateCString);
995 int fileDescriptor = mkstemp(tempFileNameCString);
996 NSString *tempFilePath = [fileManager stringWithFileSystemRepresentation:tempFileNameCString length:strlen(tempFileNameCString)];
997 free(tempFileNameCString);
998 if (fileDescriptor == -1) {
999 [PithosUtilities fileActionFailedAlertWithTitle:@"Create Temporary File Error"
1000 message:[NSString stringWithFormat:@"Cannot create temporary file at '%@'", storedState.filePath]
1002 [activityFacility endActivity:activity
1003 withMessage:[objectRequest.userInfo objectForKey:@"failedActivityMessage"]];
1004 @synchronized(self) {
1005 syncIncomplete = YES;
1006 [self decreaseSyncOperationCount];
1007 if (newSyncRequested && !syncOperationCount)
1012 close(fileDescriptor);
1013 storedState.filePath = tempFilePath;
1014 [self saveLocalState];
1018 NSUInteger missingBlockIndex = [[objectRequest.userInfo objectForKey:@"missingBlockIndex"] unsignedIntegerValue];
1019 NSFileHandle *tempFileHandle = [NSFileHandle fileHandleForWritingAtPath:storedState.filePath];
1020 [tempFileHandle seekToFileOffset:missingBlockIndex*blockSize];
1021 [tempFileHandle writeData:[objectRequest responseData]];
1022 [tempFileHandle closeFile];
1024 NSIndexSet *missingBlocks = [objectRequest.userInfo objectForKey:@"missingBlocks"];
1025 missingBlockIndex = [missingBlocks indexGreaterThanIndex:missingBlockIndex];
1026 if (missingBlockIndex == NSNotFound) {
1027 NSString *filePath = [objectRequest.userInfo objectForKey:@"filePath"];
1028 NSString *dirPath = [filePath stringByDeletingLastPathComponent];
1029 if ([fileManager fileExistsAtPath:filePath] && ![self moveToTempTrashFile:filePath]) {
1030 [activityFacility endActivity:activity
1031 withMessage:[objectRequest.userInfo objectForKey:@"failedActivityMessage"]];
1032 @synchronized(self) {
1033 syncIncomplete = YES;
1034 [self decreaseSyncOperationCount];
1035 if (newSyncRequested && !syncOperationCount)
1039 } else if (![fileManager fileExistsAtPath:dirPath]) {
1040 // File doesn't exist but also the containing directory doesn't exist
1041 // In most cases this should have been resolved as an update of the corresponding local object,
1042 // but it never hurts to check
1044 [fileManager createDirectoryAtPath:dirPath withIntermediateDirectories:YES attributes:nil error:nil];
1046 [PithosUtilities fileActionFailedAlertWithTitle:@"Create Directory Error"
1047 message:[NSString stringWithFormat:@"Cannot create directory at '%@'", dirPath]
1049 [activityFacility endActivity:[objectRequest.userInfo objectForKey:@"activity"]
1050 withMessage:[objectRequest.userInfo objectForKey:@"failedActivityMessage"]];
1051 @synchronized(self) {
1052 syncIncomplete = YES;
1053 [self decreaseSyncOperationCount];
1054 if (newSyncRequested && !syncOperationCount)
1060 // Move file from tmp download
1062 [fileManager moveItemAtPath:storedState.filePath toPath:filePath error:&error];
1064 [PithosUtilities fileActionFailedAlertWithTitle:@"Move File Error"
1065 message:[NSString stringWithFormat:@"Cannot move file at '%@' to '%@'", storedState.filePath, filePath]
1067 [activityFacility endActivity:activity
1068 withMessage:[objectRequest.userInfo objectForKey:@"failedActivityMessage"]];
1069 @synchronized(self) {
1070 syncIncomplete = YES;
1071 [self decreaseSyncOperationCount];
1072 if (newSyncRequested && !syncOperationCount)
1077 [activityFacility endActivity:activity
1078 withMessage:[objectRequest.userInfo objectForKey:@"finishedActivityMessage"]
1079 totalBytes:activity.totalBytes
1080 currentBytes:activity.totalBytes];
1082 storedState.hash = object.hash;
1083 storedState.filePath = nil;
1084 [self saveLocalState];
1086 @synchronized(self) {
1087 [self decreaseSyncOperationCount];
1088 if (newSyncRequested && !syncOperationCount)
1093 if (newSyncRequested) {
1094 [activityFacility endActivity:[objectRequest.userInfo objectForKey:@"activity"]
1095 withMessage:[objectRequest.userInfo objectForKey:@"stoppedActivityMessage"]];
1096 @synchronized(self) {
1097 syncIncomplete = YES;
1098 [self decreaseSyncOperationCount];
1099 if (!syncOperationCount)
1104 __block ASIPithosObjectRequest *newObjectRequest = [PithosUtilities objectBlockDataRequestWithContainerName:containerName
1106 blockIndex:missingBlockIndex
1107 blockSize:blockSize];
1108 newObjectRequest.delegate = self;
1109 newObjectRequest.didFinishSelector = @selector(downloadObjectBlockFinished:);
1110 newObjectRequest.didFailSelector = @selector(downloadObjectBlockOrHashMapFailed:);
1111 newObjectRequest.userInfo = objectRequest.userInfo;
1112 [(NSMutableDictionary *)(newObjectRequest.userInfo) setObject:[NSNumber numberWithUnsignedInteger:missingBlockIndex] forKey:@"missingBlockIndex"];
1113 [(NSMutableDictionary *)(newObjectRequest.userInfo) setObject:[NSNumber numberWithUnsignedInteger:10] forKey:@"retries"];
1114 [newObjectRequest setBytesReceivedBlock:^(unsigned long long size, unsigned long long total){
1115 [activityFacility updateActivity:activity
1116 withMessage:[NSString stringWithFormat:@"Downloading '%@' (%.0f%%)",
1118 (100*(activity.currentBytes + size + 0.0)/(activity.totalBytes + 0.0))]
1119 totalBytes:activity.totalBytes
1120 currentBytes:(activity.currentBytes + size)];
1122 [queue addOperation:[PithosUtilities prepareRequest:newObjectRequest priority:[[newObjectRequest.userInfo objectForKey:@"priority"] integerValue]]];
1125 } else if (objectRequest.responseStatusCode == 412) {
1126 // The object has changed on the server
1127 [activityFacility endActivity:[objectRequest.userInfo objectForKey:@"activity"]
1128 withMessage:[objectRequest.userInfo objectForKey:@"stoppedActivityMessage"]];
1129 @synchronized(self) {
1130 syncIncomplete = YES;
1131 [self decreaseSyncOperationCount];
1132 if (newSyncRequested && !syncOperationCount)
1136 [self requestFailed:objectRequest];
1140 - (void)downloadObjectHashMapFinished:(ASIPithosObjectRequest *)objectRequest {
1141 NSLog(@"Sync::download object hashmap finished: %@", objectRequest.url);
1142 if (objectRequest.responseStatusCode == 200) {
1143 if (newSyncRequested) {
1144 [activityFacility endActivity:[objectRequest.userInfo objectForKey:@"activity"]
1145 withMessage:[objectRequest.userInfo objectForKey:@"stoppedActivityMessage"]];
1146 @synchronized(self) {
1147 syncIncomplete = YES;
1148 [self decreaseSyncOperationCount];
1149 if (!syncOperationCount)
1154 ASIPithosObject *object = [objectRequest.userInfo objectForKey:@"pithosObject"];
1155 PithosLocalObjectState *storedState = [storedLocalObjectStates objectForKey:object.name];
1156 if ([PithosUtilities bytesOfFile:storedState.filePath] > object.bytes)
1157 [[NSFileHandle fileHandleForWritingAtPath:storedState.filePath] truncateFileAtOffset:object.bytes];
1158 PithosActivity *activity = [objectRequest.userInfo objectForKey:@"activity"];
1159 NSIndexSet *missingBlocks = [PithosUtilities missingBlocksForFile:storedState.filePath
1162 withHashes:[objectRequest hashes]];
1163 NSUInteger missingBlockIndex = [missingBlocks firstIndex];
1164 [activityFacility endActivity:activity
1165 withMessage:[NSString stringWithFormat:@"Sync: Downloading '%@' (%.0f%%)",
1167 (100*(activity.totalBytes - [missingBlocks count]*blockSize + 0.0)/(activity.totalBytes + 0.0))]
1168 totalBytes:activity.totalBytes
1169 currentBytes:(activity.totalBytes - [missingBlocks count]*blockSize)];
1171 __block ASIPithosObjectRequest *newObjectRequest = [PithosUtilities objectBlockDataRequestWithContainerName:containerName
1173 blockIndex:missingBlockIndex
1174 blockSize:blockSize];
1175 newObjectRequest.delegate = self;
1176 newObjectRequest.didFinishSelector = @selector(downloadObjectBlockFinished:);
1177 newObjectRequest.didFailSelector = @selector(downloadObjectBlockOrHashMapFailed:);
1178 newObjectRequest.userInfo = objectRequest.userInfo;
1179 [(NSMutableDictionary *)(newObjectRequest.userInfo) setObject:missingBlocks forKey:@"missingBlocks"];
1180 [(NSMutableDictionary *)(newObjectRequest.userInfo) setObject:[NSNumber numberWithUnsignedInteger:missingBlockIndex] forKey:@"missingBlockIndex"];
1181 [(NSMutableDictionary *)(newObjectRequest.userInfo) setObject:[NSNumber numberWithUnsignedInteger:10] forKey:@"retries"];
1182 [newObjectRequest setBytesReceivedBlock:^(unsigned long long size, unsigned long long total){
1183 [activityFacility updateActivity:activity
1184 withMessage:[NSString stringWithFormat:@"Downloading '%@' (%.0f%%)",
1186 (100*(activity.currentBytes + size + 0.0)/(activity.totalBytes + 0.0))]
1187 totalBytes:activity.totalBytes
1188 currentBytes:(activity.currentBytes + size)];
1190 [queue addOperation:[PithosUtilities prepareRequest:newObjectRequest priority:[[newObjectRequest.userInfo objectForKey:@"priority"] integerValue]]];
1193 [self requestFailed:objectRequest];
1197 - (void)uploadDirectoryObjectFinished:(ASIPithosObjectRequest *)objectRequest {
1198 NSLog(@"Sync::upload directory object finished: %@", objectRequest.url);
1199 if (objectRequest.responseStatusCode == 201) {
1200 PithosLocalObjectState *storedState = [storedLocalObjectStates objectForKey:[[objectRequest.userInfo objectForKey:@"pithosObject"] name]];
1201 storedState.isDirectory = YES;
1202 [self saveLocalState];
1203 [activityFacility endActivity:[objectRequest.userInfo objectForKey:@"activity"]
1204 withMessage:[objectRequest.userInfo objectForKey:@"finishedActivityMessage"]];
1205 @synchronized(self) {
1206 [self decreaseSyncOperationCount];
1207 if (newSyncRequested && !syncOperationCount)
1211 [self requestFailed:objectRequest];
1215 - (void)moveObjectToTrashFinished:(ASIPithosObjectRequest *)objectRequest {
1216 NSLog(@"Sync::move object to trash finished: %@", objectRequest.url);
1217 if (objectRequest.responseStatusCode == 201) {
1218 [storedLocalObjectStates removeObjectForKey:[[objectRequest.userInfo objectForKey:@"pithosObject"] name]];
1219 [self saveLocalState];
1220 [activityFacility endActivity:[objectRequest.userInfo objectForKey:@"activity"]
1221 withMessage:[objectRequest.userInfo objectForKey:@"finishedActivityMessage"]];
1222 @synchronized(self) {
1223 [self decreaseSyncOperationCount];
1224 if (newSyncRequested && !syncOperationCount)
1228 [self requestFailed:objectRequest];
1232 - (void)uploadObjectUsingHashMapFinished:(ASIPithosObjectRequest *)objectRequest {
1233 NSLog(@"Sync::upload using hashmap finished: %@", objectRequest.url);
1234 ASIPithosObject *object = [objectRequest.userInfo objectForKey:@"pithosObject"];
1235 PithosLocalObjectState *storedState = [storedLocalObjectStates objectForKey:object.name];
1236 PithosActivity *activity = [objectRequest.userInfo objectForKey:@"activity"];
1237 NSUInteger totalBytes = activity.totalBytes;
1238 NSUInteger currentBytes = activity.currentBytes;
1239 if (objectRequest.responseStatusCode == 201) {
1240 NSLog(@"Sync::object created: %@", objectRequest.url);
1241 storedState.hash = [objectRequest eTag];
1242 [self saveLocalState];
1243 [activityFacility endActivity:activity
1244 withMessage:[objectRequest.userInfo objectForKey:@"finishedActivityMessage"]
1245 totalBytes:totalBytes
1246 currentBytes:totalBytes];
1247 @synchronized(self) {
1248 [self decreaseSyncOperationCount];
1249 if (newSyncRequested && !syncOperationCount)
1252 } else if (objectRequest.responseStatusCode == 409) {
1253 if (newSyncRequested) {
1254 [activityFacility endActivity:activity
1255 withMessage:[objectRequest.userInfo objectForKey:@"stoppedActivityMessage"]];
1256 @synchronized(self) {
1257 syncIncomplete = YES;
1258 [self decreaseSyncOperationCount];
1259 if (!syncOperationCount)
1264 NSUInteger iteration = [[objectRequest.userInfo objectForKey:@"iteration"] unsignedIntegerValue];
1265 if (iteration == 0) {
1266 NSLog(@"Sync::upload iteration limit reached: %@", objectRequest.url);
1267 [activityFacility endActivity:activity
1268 withMessage:[objectRequest.userInfo objectForKey:@"stoppedActivityMessage"]];
1269 syncIncomplete = YES;
1270 [self decreaseSyncOperationCount];
1273 NSLog(@"Sync::object is missing hashes: %@", objectRequest.url);
1274 NSIndexSet *missingBlocks = [PithosUtilities missingBlocksForHashes:[objectRequest.userInfo objectForKey:@"hashes"]
1275 withMissingHashesResponse:[objectRequest responseString]];
1276 if (totalBytes >= [missingBlocks count]*blockSize)
1277 currentBytes = totalBytes - [missingBlocks count]*blockSize;
1278 [activityFacility updateActivity:activity
1279 withMessage:[NSString stringWithFormat:@"Uploading '%@' (%.0f%%)", object.name, (100*(currentBytes + 0.0)/(totalBytes + 0.0))]
1280 totalBytes:totalBytes
1281 currentBytes:currentBytes];
1282 NSUInteger missingBlockIndex = [missingBlocks firstIndex];
1283 __block ASIPithosContainerRequest *newContainerRequest = [PithosUtilities updateContainerDataRequestWithContainerName:containerName
1285 forFile:[objectRequest.userInfo objectForKey:@"filePath"]
1286 missingBlockIndex:missingBlockIndex
1287 sharingAccount:nil];
1288 newContainerRequest.delegate = self;
1289 newContainerRequest.didFinishSelector = @selector(uploadMissingBlockFinished:);
1290 newContainerRequest.didFailSelector = @selector(requestFailed:);
1291 newContainerRequest.userInfo = objectRequest.userInfo;
1292 [(NSMutableDictionary *)(newContainerRequest.userInfo) setObject:[NSNumber numberWithUnsignedInteger:(--iteration)] forKey:@"iteration"];
1293 [(NSMutableDictionary *)(newContainerRequest.userInfo) setObject:[NSNumber numberWithUnsignedInteger:10] forKey:@"retries"];
1294 [(NSMutableDictionary *)(newContainerRequest.userInfo) setObject:missingBlocks forKey:@"missingBlocks"];
1295 [(NSMutableDictionary *)(newContainerRequest.userInfo) setObject:[NSNumber numberWithUnsignedInteger:missingBlockIndex] forKey:@"missingBlockIndex"];
1296 [newContainerRequest setBytesSentBlock:^(unsigned long long size, unsigned long long total){
1297 [activityFacility updateActivity:activity
1298 withMessage:[NSString stringWithFormat:@"Uploading '%@' (%.0f%%)", object.name, (100*(activity.currentBytes + size + 0.0)/(activity.totalBytes + 0.0))]
1299 totalBytes:activity.totalBytes
1300 currentBytes:(activity.currentBytes + size)];
1302 [queue addOperation:[PithosUtilities prepareRequest:newContainerRequest priority:[[newContainerRequest.userInfo objectForKey:@"priority"] integerValue]]];
1305 [self requestFailed:objectRequest];
1309 - (void)uploadMissingBlockFinished:(ASIPithosContainerRequest *)containerRequest {
1310 NSLog(@"Sync::upload of missing block finished: %@", containerRequest.url);
1311 if (containerRequest.responseStatusCode == 202) {
1312 ASIPithosObject *object = [containerRequest.userInfo objectForKey:@"pithosObject"];
1313 PithosActivity *activity = [containerRequest.userInfo objectForKey:@"activity"];
1314 NSUInteger totalBytes = activity.totalBytes;
1315 NSUInteger currentBytes = activity.currentBytes + blockSize;
1316 if (currentBytes > totalBytes)
1317 currentBytes = totalBytes;
1318 [activityFacility updateActivity:activity
1319 withMessage:[NSString stringWithFormat:@"Uploading '%@' (%.0f%%)", object.name, (100*(currentBytes + 0.0)/(totalBytes + 0.0))]
1320 totalBytes:totalBytes
1321 currentBytes:currentBytes];
1322 NSIndexSet *missingBlocks = [containerRequest.userInfo objectForKey:@"missingBlocks"];
1323 NSUInteger missingBlockIndex = [[containerRequest.userInfo objectForKey:@"missingBlockIndex"] unsignedIntegerValue];
1324 missingBlockIndex = [missingBlocks indexGreaterThanIndex:missingBlockIndex];
1325 if (missingBlockIndex == NSNotFound) {
1326 NSArray *hashes = [containerRequest.userInfo objectForKey:@"hashes"];
1327 ASIPithosObjectRequest *newObjectRequest = [PithosUtilities writeObjectDataRequestWithContainerName:containerName
1328 objectName:object.name
1329 contentType:object.contentType
1332 forFile:[containerRequest.userInfo objectForKey:@"filePath"]
1335 sharingAccount:nil];
1336 newObjectRequest.delegate = self;
1337 newObjectRequest.didFinishSelector = @selector(uploadObjectUsingHashMapFinished:);
1338 newObjectRequest.didFailSelector = @selector(requestFailed:);
1339 newObjectRequest.userInfo = containerRequest.userInfo;
1340 [(NSMutableDictionary *)(newObjectRequest.userInfo) setObject:[NSNumber numberWithUnsignedInteger:10] forKey:@"retries"];
1341 [(NSMutableDictionary *)(newObjectRequest.userInfo) removeObjectForKey:@"missingBlocks"];
1342 [(NSMutableDictionary *)(newObjectRequest.userInfo) removeObjectForKey:@"missingBlockIndex"];
1343 [queue addOperation:[PithosUtilities prepareRequest:newObjectRequest priority:[[newObjectRequest.userInfo objectForKey:@"priority"] integerValue]]];
1345 if (newSyncRequested) {
1346 [activityFacility endActivity:activity
1347 withMessage:[containerRequest.userInfo objectForKey:@"stoppedActivityMessage"]];
1348 @synchronized(self) {
1349 syncIncomplete = YES;
1350 [self decreaseSyncOperationCount];
1351 if (!syncOperationCount)
1356 __block ASIPithosContainerRequest *newContainerRequest = [PithosUtilities updateContainerDataRequestWithContainerName:containerName
1358 forFile:[containerRequest.userInfo objectForKey:@"filePath"]
1359 missingBlockIndex:missingBlockIndex
1360 sharingAccount:nil];
1361 newContainerRequest.delegate = self;
1362 newContainerRequest.didFinishSelector = @selector(uploadMissingBlockFinished:);
1363 newContainerRequest.didFailSelector = @selector(requestFailed:);
1364 newContainerRequest.userInfo = containerRequest.userInfo;
1365 [(NSMutableDictionary *)(newContainerRequest.userInfo) setObject:[NSNumber numberWithUnsignedInteger:10] forKey:@"retries"];
1366 [(NSMutableDictionary *)(newContainerRequest.userInfo) setObject:[NSNumber numberWithUnsignedInteger:missingBlockIndex] forKey:@"missingBlockIndex"];
1367 [newContainerRequest setBytesSentBlock:^(unsigned long long size, unsigned long long total){
1368 [activityFacility updateActivity:activity
1369 withMessage:[NSString stringWithFormat:@"Uploading '%@' (%.0f%%)", object.name, (100*(activity.currentBytes + size + 0.0)/(activity.totalBytes + 0.0))]
1370 totalBytes:activity.totalBytes
1371 currentBytes:(activity.currentBytes + size)];
1373 [queue addOperation:[PithosUtilities prepareRequest:newContainerRequest priority:[[newContainerRequest.userInfo objectForKey:@"priority"] integerValue]]];
1377 [self requestFailed:containerRequest];
1381 - (void)requestFailed:(ASIPithosRequest *)request {
1382 if ([request isCancelled]) {
1383 [activityFacility endActivity:[request.userInfo objectForKey:@"activity"]
1384 withMessage:[request.userInfo objectForKey:@"stoppedActivityMessage"]];
1385 syncIncomplete = YES;
1386 [self decreaseSyncOperationCount];
1389 if (newSyncRequested) {
1390 [activityFacility endActivity:[request.userInfo objectForKey:@"activity"]
1391 withMessage:[request.userInfo objectForKey:@"stoppedActivityMessage"]];
1392 @synchronized(self) {
1393 syncIncomplete = YES;
1394 [self decreaseSyncOperationCount];
1395 if (!syncOperationCount)
1400 NSUInteger retries = [[request.userInfo objectForKey:@"retries"] unsignedIntegerValue];
1402 ASIPithosRequest *newRequest = (ASIPithosRequest *)[PithosUtilities copyRequest:request];
1403 [(NSMutableDictionary *)(newRequest.userInfo)setObject:[NSNumber numberWithUnsignedInteger:(--retries)] forKey:@"retries"];
1404 [queue addOperation:[PithosUtilities prepareRequest:newRequest priority:[[newRequest.userInfo objectForKey:@"priority"] integerValue]]];
1406 [activityFacility endActivity:[request.userInfo objectForKey:@"activity"]
1407 withMessage:[request.userInfo objectForKey:@"failedActivityMessage"]];
1408 syncIncomplete = YES;
1409 [self decreaseSyncOperationCount];