Statistics
| Branch: | Tag: | Revision:

root / pithos-macos / PithosSyncDaemon.m @ 3812626f

History | View | Annotate | Download (79.5 kB)

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
#import "FileMD5Hash.h"
48
#import "HashMapHash.h"
49

    
50
#define DATA_MODEL_FILE @"localstate.archive"
51
#define ARCHIVE_KEY @"Data"
52

    
53
@interface PithosSyncDaemon (Private)
54
- (NSString *)pithosStateFilePath;
55
- (void)saveLocalState;
56
- (BOOL)localStateHasChanged:(PithosLocalObjectState *)storedState currentState:(PithosLocalObjectState *)currentState;
57
- (BOOL)serverStateHasChanged:(PithosLocalObjectState *)storedState 
58
             remoteObjectHash:(NSString *)remoteObjectHash 
59
      remoteObjectIsDirectory:(BOOL)remoteObjectIsDirectory;
60
- (void)updateLocalStateWithObject:(ASIPithosObject *)object localFilePath:(NSString *)filePath;
61
-(void)updateServerStateWithCurrentState:(PithosLocalObjectState *)currentState 
62
                                object:(ASIPithosObject *)object 
63
                         localFilePath:(NSString *)filePath;
64
- (void)requestFailed:(ASIPithosRequest *)request;
65

    
66
- (void)increaseSyncOperationCount;
67
- (void)decreaseSyncOperationCount;
68

    
69
@end
70

    
71
@implementation PithosSyncDaemon
72
@synthesize blockHash, blockSize, lastModified, lastCompletedSync, remoteObjects, storedLocalObjectStates;
73
@synthesize pithosStateFilePath, tempDownloadsDirPath;
74

    
75
#pragma mark -
76
#pragma Object Lifecycle
77

    
78
- (id)initWithDirectoryPath:(NSString *)aDirectoryPath 
79
              containerName:(NSString *)aContainerName 
80
               timeInterval:(NSTimeInterval)aTimeInterval {
81
    if ((self = [super init])) {
82
        directoryPath = [aDirectoryPath copy];
83
        containerName = [aContainerName copy];
84
        timeInterval = aTimeInterval;
85
        
86
        syncOperationCount = 0;
87
        newSyncRequested = NO;
88
        
89
        activityFacility = [PithosActivityFacility defaultPithosActivityFacility];
90
        
91
        if ([[NSFileManager defaultManager] fileExistsAtPath:self.pithosStateFilePath]) {
92
            NSData *data = [NSData dataWithContentsOfFile:self.pithosStateFilePath];
93
            NSKeyedUnarchiver *unarchiver = [[[NSKeyedUnarchiver alloc] initForReadingWithData:data] autorelease];
94
            self.storedLocalObjectStates = [unarchiver decodeObjectForKey:ARCHIVE_KEY];
95
            [unarchiver finishDecoding];
96
        } else {
97
            self.storedLocalObjectStates = [NSMutableDictionary dictionary];
98
        }
99
        
100
        queue = [[ASINetworkQueue alloc] init];
101
        queue.shouldCancelAllRequestsOnFailure = NO;
102
        [queue go];
103
        
104
        [[NSNotificationCenter defaultCenter] addObserver:self
105
                                                 selector:@selector(applicationWillTerminate:)
106
                                                     name:NSApplicationWillTerminateNotification
107
                                                   object:[NSApplication sharedApplication]];
108
    
109
        timer = [[NSTimer scheduledTimerWithTimeInterval:timeInterval target:self selector:@selector(sync) userInfo:nil repeats:YES] retain];
110
        [timer fire];
111
    }
112
    
113
    return self;
114
}
115

    
116
- (void)dealloc {
117
    [[NSNotificationCenter defaultCenter] removeObserver:self];
118
    [queue cancelAllOperations];
119
    [queue release];
120
    [timer invalidate];
121
    [timer release];
122
    [tempDownloadsDirPath release];
123
    [pithosStateFilePath release];
124
    [storedLocalObjectStates release];
125
    [remoteObjects release];
126
    [objects release];
127
    [lastCompletedSync release];
128
    [lastModified release];
129
    [blockHash release];
130
    [containerName release];
131
    [directoryPath release];
132
    [super dealloc];
133
}
134

    
135
#pragma mark -
136
#pragma mark Observers
137

    
138
- (void)applicationWillTerminate:(NSNotification *)notification {
139
    [self saveLocalState];
140
}
141

    
142
#pragma mark -
143
#pragma mark Properties
144

    
145
- (NSString *)pithosStateFilePath {
146
    if (!pithosStateFilePath) {
147
        pithosStateFilePath = [[[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent:DATA_MODEL_FILE] retain];
148
    }
149
    return [pithosStateFilePath copy];
150
}
151

    
152
- (NSString *)tempDownloadsDirPath {
153
    NSFileManager *fileManager = [NSFileManager defaultManager];
154
    if (!tempDownloadsDirPath || ![fileManager fileExistsAtPath:tempDownloadsDirPath]) {
155
        // Get the path from user defaults
156
        tempDownloadsDirPath = [[NSUserDefaults standardUserDefaults] stringForKey:@"PithosTempDownloadsDirPath"];
157
        if (tempDownloadsDirPath) {
158
            // Check if the path exists
159
            BOOL isDirectory;
160
            BOOL fileExists = [fileManager fileExistsAtPath:tempDownloadsDirPath isDirectory:&isDirectory];
161
            NSError *error = nil;
162
            if (fileExists && !isDirectory)
163
                [fileManager removeItemAtPath:tempDownloadsDirPath error:&error];
164
            if (!error & !fileExists)
165
                [fileManager createDirectoryAtPath:tempDownloadsDirPath withIntermediateDirectories:YES attributes:nil error:&error];
166
            if (error)
167
                tempDownloadsDirPath = nil;
168
            }
169
        if (!tempDownloadsDirPath) {
170
            NSString *tempDirTemplate = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:@"Temp Downloads XXXXXX"];
171
            const char *tempDirTemplateCString = [tempDirTemplate fileSystemRepresentation];
172
            char *tempDirNameCString = (char *)malloc(strlen(tempDirTemplateCString) + 1);
173
            strcpy(tempDirNameCString, tempDirTemplateCString);
174
            tempDirNameCString = mkdtemp(tempDirNameCString);
175
            if (tempDirNameCString != NULL)
176
                tempDownloadsDirPath = [fileManager stringWithFileSystemRepresentation:tempDirNameCString length:strlen(tempDirNameCString)];
177
            free(tempDirNameCString);
178
        }
179
        if (!tempDownloadsDirPath)
180
            [[NSUserDefaults standardUserDefaults] setObject:tempDownloadsDirPath forKey:@"PithosTempDownloadsDirPath"];
181
        [tempDownloadsDirPath retain];
182
    }
183
    return [tempDownloadsDirPath copy];
184
}
185

    
186
#pragma mark -
187
#pragma mark Sync
188

    
189
- (void)saveLocalState {
190
    NSMutableData *data = [NSMutableData data];
191
    NSKeyedArchiver *archiver = [[[NSKeyedArchiver alloc] initForWritingWithMutableData:data] autorelease];
192
    [archiver encodeObject:storedLocalObjectStates forKey:ARCHIVE_KEY];
193
    [archiver finishEncoding];
194
    [data writeToFile:self.pithosStateFilePath atomically:YES];
195
}
196

    
197
- (void)increaseSyncOperationCount {
198
    @synchronized(self) {
199
        syncOperationCount++;
200
    }
201
}
202

    
203
- (void)decreaseSyncOperationCount {
204
    @synchronized(self) {
205
        syncOperationCount--;
206
        if (!syncOperationCount && !syncIncomplete) {
207
            self.lastCompletedSync = [NSDate date];
208
            [activityFacility startAndEndActivityWithType:PithosActivityOther 
209
                                                  message:[NSString stringWithFormat:@"Sync: Completed %@", lastCompletedSync]];
210
        }
211
    }
212
}
213

    
214
- (void)sync {
215
    @synchronized(self) {
216
        if (syncOperationCount) {
217
            // If at least one operation is running return
218
            newSyncRequested = YES;
219
            return;
220
        } else {
221
            // The first operation is the server listing
222
            syncOperationCount = 1;
223
            newSyncRequested = NO;
224
            syncIncomplete = NO;
225
        }
226
    }
227

    
228
    NSString *containerDirectoryPath = [directoryPath stringByAppendingPathComponent:containerName];
229
    NSFileManager *fileManager = [NSFileManager defaultManager];
230
    BOOL isDirectory;
231
    NSError *error = nil;
232
    if (![fileManager fileExistsAtPath:containerDirectoryPath isDirectory:&isDirectory]) {
233
        if (![fileManager createDirectoryAtPath:containerDirectoryPath withIntermediateDirectories:YES attributes:nil error:&error] || 
234
            error) {
235
            [PithosUtilities fileActionFailedAlertWithTitle:@"Local Sync Directory Error" 
236
                                                    message:[NSString stringWithFormat:@"Cannot create local sync directory at '%@'", containerDirectoryPath] 
237
                                                      error:error];
238
            @synchronized(self) {
239
                syncOperationCount = 0;
240
            }
241
            return;
242
        }
243
    } else if (!isDirectory) {
244
        [PithosUtilities fileActionFailedAlertWithTitle:@"Local Sync Directory Error" 
245
                                                message:[NSString stringWithFormat:@"File already exists at the local sync directory path at '%@'", containerDirectoryPath] 
246
                                                  error:nil];
247
        @synchronized(self) {
248
            syncOperationCount = 0;
249
        }
250
        return;
251
    }
252
    
253
    ASIPithosContainerRequest *containerRequest = [ASIPithosContainerRequest listObjectsRequestWithContainerName:containerName 
254
                                                                                                           limit:0 
255
                                                                                                          marker:nil 
256
                                                                                                          prefix:nil 
257
                                                                                                       delimiter:nil 
258
                                                                                                            path:nil 
259
                                                                                                            meta:nil 
260
                                                                                                          shared:NO 
261
                                                                                                           until:nil 
262
                                                                                                 ifModifiedSince:lastModified];
263
    containerRequest.delegate = self;
264
    containerRequest.didFinishSelector = @selector(listRequestFinished:);
265
    containerRequest.didFailSelector = @selector(listRequestFailed:);
266
    PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityOther 
267
                                                               message:@"Sync: Getting server listing"];
268
    containerRequest.userInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys:
269
                                 activity, @"activity", 
270
                                 @"Sync: Getting server listing (stopped)", @"stoppedActivityMessage", 
271
                                 @"Sync: Getting server listing (failed)", @"failedActivityMessage", 
272
                                 @"Sync: Getting server listing (finished)", @"finishedActivityMessage", 
273
                                 [NSNumber numberWithInteger:NSOperationQueuePriorityVeryHigh], @"priority", 
274
                                 [NSNumber numberWithUnsignedInteger:10], @"retries", 
275
                                 nil];
276
    [queue addOperation:[PithosUtilities prepareRequest:containerRequest priority:NSOperationQueuePriorityVeryHigh]];
277
}
278

    
279
- (BOOL)localStateHasChanged:(PithosLocalObjectState *)storedState currentState:(PithosLocalObjectState *)currentState {
280
    if (currentState.isDirectory)
281
        // Currently a directory, check previous state
282
        return (!storedState.isDirectory);
283
    if (storedState.isDirectory)
284
        // Previously a directory, currently a file or doesn't exist, state has changed
285
        return YES;
286
    if ([currentState.md5 isEqualToString:@" "] && [currentState.hashMapHash isEqualToString:@" "] && 
287
        (![storedState.md5 isEqualToString:@" "] || ![storedState.hashMapHash isEqualToString:@" "]))
288
        // Currently doesn't exist, previously a file, state has changed
289
        return YES;
290
    if (![storedState.md5 isEqualToString:currentState.md5] && ![storedState.hashMapHash isEqualToString:currentState.hashMapHash])
291
        // Neither hash remained the same, different files, state has changed
292
        return YES;
293
    else
294
        // At least one hash remained the same (the other is either the same or emmpty), state hasn't changed
295
        return NO;
296
}
297

    
298
- (BOOL)serverStateHasChanged:(PithosLocalObjectState *)storedState 
299
             remoteObjectHash:(NSString *)remoteObjectHash 
300
      remoteObjectIsDirectory:(BOOL)remoteObjectIsDirectory {
301
    if (remoteObjectIsDirectory)
302
        // Remotely a directory, check previous state
303
        return (!storedState.isDirectory);
304
    if (storedState.isDirectory)
305
        // Previously a directory, remotely a file or doesn't exist, state has changed
306
        return YES;
307
    if ([remoteObjectHash length] == 32)
308
        // Remotely a file, check previous state
309
        return ![remoteObjectHash isEqualToString:storedState.md5];
310
    else if ([remoteObjectHash length] == 64)
311
        // Remotely a file, check previous state
312
        return ![remoteObjectHash isEqualToString:storedState.hashMapHash];
313
    else if ([remoteObjectHash isEqualToString:@" "]) {
314
        // Remotely doesn't exist
315
        if ([storedState.md5 isEqualToString:@" "] && [storedState.hashMapHash isEqualToString:@" "])
316
            // Previously didn't exist, state hasn't changed
317
            return NO;
318
        else
319
            // Previously did exist, state has changed
320
            return YES;
321
    }
322
    // Only if the server doesn't respond properly this will be reached, leave as is for now
323
    return NO;
324
}
325

    
326
- (void)updateLocalStateWithObject:(ASIPithosObject *)object
327
                     localFilePath:(NSString *)filePath {
328
    NSFileManager *fileManager = [NSFileManager defaultManager];
329
    NSError *error;
330
    BOOL isDirectory;
331
    BOOL fileExists = [fileManager fileExistsAtPath:filePath isDirectory:&isDirectory];
332
    PithosLocalObjectState *storedState = [storedLocalObjectStates objectForKey:object.name];
333
    if ([object.hash isEqualToString:@" "]) {
334
        // Delete local object
335
        // XXX move to local trash instead
336
        NSLog(@"Sync::delete local object: %@", filePath);
337
        BOOL deleteFailed = NO;
338
        if (fileExists) {
339
            error = nil;
340
            if (![fileManager removeItemAtPath:filePath error:&error] || error) {
341
                [PithosUtilities fileActionFailedAlertWithTitle:@"Remove File Error" 
342
                                                        message:[NSString stringWithFormat:@"Cannot remove file at '%@'", filePath] 
343
                                                          error:error];
344
                deleteFailed = YES;
345
            }
346
        }
347
        if (!deleteFailed) {
348
            [activityFacility startAndEndActivityWithType:PithosActivityOther 
349
                                                  message:[NSString stringWithFormat:@"Sync: Deleting '%@' locally (finished)", object.name]];
350
            [storedLocalObjectStates removeObjectForKey:object.name];
351
            [self saveLocalState];
352
        }
353
    } else if ([object.contentType isEqualToString:@"application/directory"]) {
354
        // Create local directory object
355
        NSLog(@"Sync::create local directory object: %@", filePath);
356
        BOOL directoryCreated = NO;
357
        if (fileExists && !isDirectory) {
358
            NSLog(@"Sync::delete local file object: %@", filePath);
359
            error = nil;
360
            if (![fileManager removeItemAtPath:filePath error:&error] || error) {
361
                [PithosUtilities fileActionFailedAlertWithTitle:@"Remove File Error" 
362
                                                        message:[NSString stringWithFormat:@"Cannot remove file at '%@'", filePath] 
363
                                                          error:error];
364
            }
365
        }
366
        if (![fileManager fileExistsAtPath:filePath]) {
367
            NSLog(@"Sync::local directory object doesn't exist: %@", filePath);
368
            error = nil;
369
            if (![fileManager createDirectoryAtPath:filePath withIntermediateDirectories:YES attributes:nil error:&error] || error) {
370
                [PithosUtilities fileActionFailedAlertWithTitle:@"Create Directory Error" 
371
                                                        message:[NSString stringWithFormat:@"Cannot create directory at '%@'", filePath] 
372
                                                          error:error];
373
            } else {
374
                directoryCreated = YES;
375
                storedState.isDirectory = YES;
376
                [self saveLocalState];
377
            }
378
        } else if (isDirectory) {
379
            NSLog(@"Sync::local directory object exists: %@", filePath);
380
            directoryCreated = YES;
381
        }
382
        if (directoryCreated)
383
            [activityFacility startAndEndActivityWithType:PithosActivityOther 
384
                                                  message:[NSString stringWithFormat:@"Sync: Creating directory '%@' locally (finished)", object.name]];
385
    } else if (object.bytes == 0) {
386
        // Create local object with zero length
387
        NSLog(@"Sync::create local zero length object: %@", filePath);
388
        BOOL fileCreated = NO;
389
        if (fileExists && (isDirectory || [PithosUtilities bytesOfFile:filePath])) {
390
            NSLog(@"Sync::delete local object: %@", filePath);
391
            error = nil;
392
            if (![fileManager removeItemAtPath:filePath error:&error] || error) {
393
                [PithosUtilities fileActionFailedAlertWithTitle:@"Remove File Error" 
394
                                                        message:[NSString stringWithFormat:@"Cannot remove file at '%@'", filePath] 
395
                                                          error:error];
396
            }
397
        }
398
        if (![fileManager fileExistsAtPath:filePath]) {
399
            NSLog(@"Sync::local zero length object doesn't exist: %@", filePath);
400
            error = nil;
401
            if (![fileManager createFileAtPath:filePath contents:nil attributes:nil]) {
402
                [PithosUtilities fileActionFailedAlertWithTitle:@"Create File Error" 
403
                                                        message:[NSString stringWithFormat:@"Cannot create file at '%@'", filePath] 
404
                                                          error:error];
405
            } else {
406
                fileCreated = YES;
407
                if ([object.hash length] == 32) {
408
                    storedState.md5 = object.hash;
409
                    storedState.hashMapHash = @" ";
410
                } else if([object.hash length] == 64) {
411
                    storedState.md5 = @" ";
412
                    storedState.hashMapHash = object.hash;
413
                } else {
414
                    storedState.md5 = @" ";
415
                    storedState.hashMapHash = @" ";
416
                }
417
                storedState.tmpDownloadFile = nil;
418
                [self saveLocalState];
419
            }
420
        } else if (!isDirectory && ![PithosUtilities bytesOfFile:filePath]) {
421
            NSLog(@"Sync::local zero length object exists: %@", filePath);
422
            fileCreated = YES;
423
        }
424
        if (fileCreated)
425
            [activityFacility startAndEndActivityWithType:PithosActivityOther 
426
                                                  message:[NSString stringWithFormat:@"Sync: Downloading '%@' (100%%)", object.name]];
427
    } else if (storedState.tmpDownloadFile == nil) {
428
        // Create new local object
429
        [self increaseSyncOperationCount];
430
        __block ASIPithosObjectRequest *objectRequest = [PithosUtilities objectBlockDataRequestWithContainerName:containerName 
431
                                                                                                          object:object 
432
                                                                                                      blockIndex:0 
433
                                                                                                       blockSize:blockSize];
434
        objectRequest.delegate = self;
435
        objectRequest.didFinishSelector = @selector(downloadObjectBlockFinished:);
436
        objectRequest.didFailSelector = @selector(requestFailed:);
437
        PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityDownload 
438
                                                                   message:[NSString stringWithFormat:@"Sync: Downloading '%@' (0%%)", object.name] 
439
                                                                totalBytes:object.bytes 
440
                                                              currentBytes:0];
441
        objectRequest.userInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys:
442
                                  object, @"pithosObject", 
443
                                  [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, (NSUInteger)ceil((object.bytes +0.0)/(blockSize + 0.0)))], @"missingBlocks", 
444
                                  [NSNumber numberWithUnsignedInteger:0], @"missingBlockIndex", 
445
                                  filePath, @"filePath", 
446
                                  activity, @"activity", 
447
                                  [NSString stringWithFormat:@"Sync: Downloading '%@' (stopped)", object.name], @"stoppedActivityMessage", 
448
                                  [NSString stringWithFormat:@"Sync: Downloading '%@' (failed)", object.name], @"failedActivityMessage", 
449
                                  [NSString stringWithFormat:@"Sync: Downloading '%@' (100%%)", object.name], @"finishedActivityMessage", 
450
                                  [NSNumber numberWithInteger:NSOperationQueuePriorityNormal], @"priority", 
451
                                  [NSNumber numberWithUnsignedInteger:10], @"retries", 
452
                                  nil];
453
        [objectRequest setBytesReceivedBlock:^(unsigned long long size, unsigned long long total){
454
            [activityFacility updateActivity:activity 
455
                                 withMessage:[NSString stringWithFormat:@"Downloading '%@' (%.0f%%)", 
456
                                              [[objectRequest.userInfo valueForKey:@"pithosObject"] name], 
457
                                              (100*(activity.currentBytes + size + 0.0)/(activity.totalBytes + 0.0))] 
458
                                  totalBytes:activity.totalBytes 
459
                                currentBytes:(activity.currentBytes + size)];
460
        }];
461
        [queue addOperation:[PithosUtilities prepareRequest:objectRequest priority:NSOperationQueuePriorityNormal]];
462
    } else {
463
        // Resume local object download
464
        [self increaseSyncOperationCount];
465
        ASIPithosObjectRequest *objectRequest = [ASIPithosObjectRequest objectHashmapRequestWithContainerName:containerName 
466
                                                                                                   objectName:object.name];
467
        objectRequest.delegate = self;
468
        objectRequest.didFinishSelector = @selector(downloadObjectHashMapFinished:);
469
        // The fail method for block download does exactly what we want
470
        objectRequest.didFailSelector = @selector(requestFailed:);
471
        PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityDownload 
472
                                                                   message:[NSString stringWithFormat:@"Sync: Downloading '%@' (0%%)", object.name] 
473
                                                                totalBytes:object.bytes 
474
                                                              currentBytes:0];
475
        objectRequest.userInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys:
476
                                  object, @"pithosObject", 
477
                                  filePath, @"filePath", 
478
                                  activity, @"activity", 
479
                                  [NSString stringWithFormat:@"Sync: Downloading '%@' (stopped)", object.name], @"stoppedActivityMessage", 
480
                                  [NSString stringWithFormat:@"Sync: Downloading '%@' (failed)", object.name], @"failedActivityMessage", 
481
                                  [NSString stringWithFormat:@"Sync: Downloading '%@' (100%%)", object.name], @"finishedActivityMessage", 
482
                                  [NSNumber numberWithInteger:NSOperationQueuePriorityNormal], @"priority", 
483
                                  [NSNumber numberWithUnsignedInteger:10], @"retries", 
484
                                  nil];
485
        [queue addOperation:[PithosUtilities prepareRequest:objectRequest priority:NSOperationQueuePriorityNormal]];
486
    }
487
}
488

    
489
-(void)updateServerStateWithCurrentState:(PithosLocalObjectState *)currentState 
490
                                  object:(ASIPithosObject *)object 
491
                           localFilePath:(NSString *)filePath {
492
    [self increaseSyncOperationCount];
493
    if (currentState.isDirectory) {
494
        // Create remote directory object
495
        ASIPithosObjectRequest *objectRequest = [ASIPithosObjectRequest writeObjectDataRequestWithContainerName:containerName 
496
                                                                                                     objectName:object.name 
497
                                                                                                           eTag:nil 
498
                                                                                                    contentType:@"application/directory" 
499
                                                                                                contentEncoding:nil 
500
                                                                                             contentDisposition:nil 
501
                                                                                                       manifest:nil 
502
                                                                                                        sharing:nil 
503
                                                                                                       isPublic:ASIPithosObjectRequestPublicIgnore 
504
                                                                                                       metadata:nil 
505
                                                                                                           data:[NSData data]];
506
        objectRequest.delegate = self;
507
        objectRequest.didFinishSelector = @selector(uploadDirectoryObjectFinished:);
508
        objectRequest.didFailSelector = @selector(requestFailed:);
509
        PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityCreateDirectory 
510
                                                                   message:[NSString stringWithFormat:@"Sync: Creating directory '%@'", object.name]];
511
        objectRequest.userInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys:
512
                                  object, @"pithosObject", 
513
                                  activity, @"activity", 
514
                                  [NSString stringWithFormat:@"Sync: Creating directory '%@' (stopped)", object.name], @"stoppedActivityMessage", 
515
                                  [NSString stringWithFormat:@"Sync: Creating directory '%@' (failed)", object.name], @"failedActivityMessage", 
516
                                  [NSString stringWithFormat:@"Sync: Creating directory '%@' (finished)", object.name], @"finishedActivityMessage", 
517
                                  [NSNumber numberWithInteger:NSOperationQueuePriorityHigh], @"priority", 
518
                                  [NSNumber numberWithUnsignedInteger:10], @"retries", 
519
                                  nil];
520
        [queue addOperation:[PithosUtilities prepareRequest:objectRequest priority:NSOperationQueuePriorityHigh]];
521
    } else if ([currentState.md5 isEqualToString:@" "] && [currentState.hashMapHash isEqualToString:@" "]) {
522
        // Delete remote object
523
        NSString *safeObjectName = [PithosUtilities safeObjectNameForContainerName:@"trash" 
524
                                                                        objectName:object.name];
525
        if (safeObjectName) {
526
            ASIPithosObjectRequest *objectRequest = [PithosUtilities moveObjectRequestWithContainerName:containerName 
527
                                                                                             objectName:object.name 
528
                                                                               destinationContainerName:@"trash" 
529
                                                                                  destinationObjectName:safeObjectName 
530
                                                                                          checkIfExists:NO];
531
            if (objectRequest) {
532
                objectRequest.delegate = self;
533
                objectRequest.didFinishSelector = @selector(moveObjectToTrashFinished:);
534
                objectRequest.didFailSelector = @selector(requestFailed:);
535
                PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityDelete 
536
                                                                           message:[NSString stringWithFormat:@"Sync: Moving to trash '%@'", object.name]];
537
                objectRequest.userInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys:
538
                                          object, @"pithosObject", 
539
                                          activity, @"activity", 
540
                                          [NSString stringWithFormat:@"Sync: Moving to trash '%@' (stopped)", object.name], @"stoppedActivityMessage", 
541
                                          [NSString stringWithFormat:@"Sync: Moving to trash '%@' (failed)", object.name], @"failedActivityMessage", 
542
                                          [NSString stringWithFormat:@"Sync: Moving to trash '%@' (finished)", object.name], @"finishedActivityMessage", 
543
                                          [NSNumber numberWithInteger:NSOperationQueuePriorityHigh], @"priority", 
544
                                          [NSNumber numberWithUnsignedInteger:10], @"retries", 
545
                                          nil];
546
                [queue addOperation:[PithosUtilities prepareRequest:objectRequest priority:NSOperationQueuePriorityHigh]];
547
            } else {
548
                syncIncomplete = YES;
549
                [self decreaseSyncOperationCount];
550
            }
551
        } else {
552
            syncIncomplete = YES;
553
            [self decreaseSyncOperationCount];
554
        }
555
    } else {
556
        // Upload file to remote object
557
        NSError *error = nil;
558
        object.contentType = [PithosUtilities contentTypeOfFile:filePath error:&error];
559
        if (object.contentType == nil)
560
            object.contentType = @"application/octet-stream";
561
        if (error)
562
            NSLog(@"contentType detection error: %@", error);
563
        NSArray *hashes = nil;
564
        ASIPithosObjectRequest *objectRequest = [PithosUtilities writeObjectDataRequestWithContainerName:containerName 
565
                                                                                              objectName:object.name 
566
                                                                                             contentType:object.contentType 
567
                                                                                               blockSize:blockSize 
568
                                                                                               blockHash:blockHash 
569
                                                                                                 forFile:filePath 
570
                                                                                           checkIfExists:NO 
571
                                                                                                  hashes:&hashes 
572
                                                                                          sharingAccount:nil];
573
        if (objectRequest) {
574
            objectRequest.delegate = self;
575
            objectRequest.didFinishSelector = @selector(uploadObjectUsingHashMapFinished:);
576
            objectRequest.didFailSelector = @selector(requestFailed:);
577
            PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityUpload 
578
                                                                       message:[NSString stringWithFormat:@"Sync: Uploading '%@' (0%%)", object.name]
579
                                                                    totalBytes:[[objectRequest.userInfo valueForKey:@"bytes"] unsignedIntegerValue]
580
                                                                  currentBytes:0];
581
            [(NSMutableDictionary *)objectRequest.userInfo addEntriesFromDictionary:
582
             [NSDictionary dictionaryWithObjectsAndKeys:
583
              object, @"pithosObject", 
584
              filePath, @"filePath", 
585
              hashes, @"hashes", 
586
              [NSNumber numberWithUnsignedInteger:10], @"iteration", 
587
              activity, @"activity", 
588
              [NSString stringWithFormat:@"Sync: Uploading '%@' (stopped)", object.name], @"stoppedActivityMessage", 
589
              [NSString stringWithFormat:@"Sync: Uploading '%@' (failed)", object.name], @"failedActivityMessage", 
590
              [NSString stringWithFormat:@"Sync: Uploading '%@' (100%%)", object.name], @"finishedActivityMessage", 
591
              [NSNumber numberWithInteger:NSOperationQueuePriorityNormal], @"priority", 
592
              [NSNumber numberWithUnsignedInteger:10], @"retries", 
593
              nil]];
594
            [queue addOperation:[PithosUtilities prepareRequest:objectRequest priority:NSOperationQueuePriorityNormal]];
595
        } else {
596
            syncIncomplete = YES;
597
            [self decreaseSyncOperationCount];
598
        }
599
    }
600

    
601
}
602

    
603
#pragma mark -
604
#pragma mark ASIHTTPRequestDelegate
605

    
606
- (void)listRequestFinished:(ASIPithosContainerRequest *)containerRequest {
607
    NSLog(@"Sync::list request finished: %@", containerRequest.url);
608
    if ((containerRequest.responseStatusCode == 200) || (containerRequest.responseStatusCode == 304)) {
609
        if (containerRequest.responseStatusCode == 200) {
610
            NSArray *someObjects = [containerRequest objects];
611
            if (objects == nil) {
612
                objects = [[NSMutableArray alloc] initWithArray:someObjects];
613
            } else {
614
                [objects addObjectsFromArray:someObjects];
615
            }
616
            if ([someObjects count] < 10000) {
617
                self.blockHash = [containerRequest blockHash];
618
                self.blockSize = [containerRequest blockSize];
619
                self.lastModified = [containerRequest lastModified];
620
                self.remoteObjects = [NSMutableDictionary dictionaryWithCapacity:[objects count]];
621
                for (ASIPithosObject *object in objects) {
622
                    [remoteObjects setObject:object forKey:object.name];
623
                }
624
                [objects release];
625
                objects = nil;
626
            } else {
627
                // Do an additional request to fetch more objects
628
                ASIPithosContainerRequest *newContainerRequest = [ASIPithosContainerRequest listObjectsRequestWithContainerName:containerName 
629
                                                                                                                          limit:0 
630
                                                                                                                         marker:[[someObjects lastObject] name] 
631
                                                                                                                         prefix:nil 
632
                                                                                                                      delimiter:nil 
633
                                                                                                                           path:nil 
634
                                                                                                                           meta:nil 
635
                                                                                                                         shared:NO 
636
                                                                                                                          until:nil 
637
                                                                                                                ifModifiedSince:lastModified];
638
                newContainerRequest.delegate = self;
639
                newContainerRequest.didFinishSelector = @selector(listRequestFinished:);
640
                newContainerRequest.didFailSelector = @selector(listRequestFailed:);
641
                newContainerRequest.userInfo = newContainerRequest.userInfo;
642
                [(NSMutableDictionary *)newContainerRequest.userInfo setObject:[NSNumber numberWithUnsignedInteger:10] forKey:@"retries"];
643
                [queue addOperation:[PithosUtilities prepareRequest:newContainerRequest priority:[[newContainerRequest.userInfo objectForKey:@"priority"] integerValue]]];
644
                return;
645
            }
646
        }
647
        [activityFacility endActivity:[containerRequest.userInfo objectForKey:@"activity"] 
648
                          withMessage:[containerRequest.userInfo objectForKey:@"finishedActivityMessage"]];
649
        NSFileManager *fileManager = [NSFileManager defaultManager];
650
        NSString *containerDirectoryPath = [directoryPath stringByAppendingPathComponent:containerName];
651
        NSError *error = nil;
652
        NSArray *subPaths = [fileManager subpathsOfDirectoryAtPath:containerDirectoryPath error:&error];
653
        if (error) {
654
            [PithosUtilities fileActionFailedAlertWithTitle:@"Directory Contents Error" 
655
                                                    message:[NSString stringWithFormat:@"Cannot get contents of directory at '%@'", containerDirectoryPath] 
656
                                                      error:error];
657
            [activityFacility startAndEndActivityWithType:PithosActivityOther message:@"Sync: Failed to read contents of sync directory"];
658
            @synchronized(self) {
659
                // Since the local listing failed, the operation finished and the sync cycle is completeted unsuccesfully
660
                syncOperationCount = 0;
661
                if (newSyncRequested)
662
                    [self sync];
663
            }
664
            return;
665
        }
666
        for (NSString *objectName in subPaths) {
667
            if (![storedLocalObjectStates objectForKey:objectName]) {
668
                [storedLocalObjectStates setObject:[PithosLocalObjectState nullObjectState] forKey:objectName];
669
            }
670
        }
671
        [self saveLocalState];
672

    
673
        for (NSString *objectName in remoteObjects) {
674
            if (![storedLocalObjectStates objectForKey:objectName])
675
                [storedLocalObjectStates setObject:[PithosLocalObjectState nullObjectState] forKey:objectName];
676
        }
677

    
678
        for (NSString *objectName in [[storedLocalObjectStates allKeys] sortedArrayUsingSelector:@selector(compare:)]) {
679
            NSString *filePath = [containerDirectoryPath stringByAppendingPathComponent:objectName];
680
            if ([objectName hasSuffix:@"/"])
681
                filePath = [filePath stringByAppendingString:@":"];
682
            ASIPithosObject *object = [ASIPithosObject object];
683
            object.name = objectName;
684
            NSLog(@"Sync::object name: %@", objectName);
685
            
686
            PithosLocalObjectState *storedLocalObjectState = [storedLocalObjectStates objectForKey:object.name];
687
            PithosLocalObjectState *currentLocalObjectState = [PithosLocalObjectState nullObjectState];
688
            BOOL isDirectory;
689
            if ([fileManager fileExistsAtPath:filePath isDirectory:&isDirectory]) {
690
                if (isDirectory) {
691
                    currentLocalObjectState.isDirectory = YES;
692
                } else {
693
                    currentLocalObjectState.md5 = (NSString *)FileMD5HashCreateWithPath((CFStringRef)filePath, 
694
                                                                                        FileHashDefaultChunkSizeForReadingData);
695
                    currentLocalObjectState.hashMapHash = [HashMapHash calculateHashMapHash:[HashMapHash calculateObjectHashMap:filePath 
696
                                                                                                                  withBlockHash:blockHash 
697
                                                                                                                   andBlockSize:blockSize]];
698
                }
699
            }
700
            if (currentLocalObjectState.isDirectory)
701
                object.contentType = @"application/directory";
702
            
703
            NSString *remoteObjectHash = @" ";
704
            BOOL remoteObjectIsDirectory = NO;
705
            ASIPithosObject *remoteObject = [remoteObjects objectForKey:objectName];
706
            if (remoteObject) {
707
                remoteObjectHash = remoteObject.hash;
708
                if ([remoteObject.contentType isEqualToString:@"application/directory"]) {
709
                    remoteObjectIsDirectory = YES;
710
                    object.contentType = @"application/directory";
711
                }
712
            }
713
            NSLog(@"Sync::remote object is directory: %d", remoteObjectIsDirectory);
714
            
715
            BOOL localStateHasChanged = [self localStateHasChanged:storedLocalObjectState currentState:currentLocalObjectState];
716
            BOOL serverStateHasChanged = [self serverStateHasChanged:storedLocalObjectState 
717
                                                    remoteObjectHash:remoteObjectHash 
718
                                             remoteObjectIsDirectory:remoteObjectIsDirectory];
719
            NSLog(@"Sync::localStateHasChanged: %d, serverStateHasChanged: %d", localStateHasChanged, serverStateHasChanged);
720
            // XXX shouldn't we first do all the deletes? in order not to face a dir that becomes a file and vice versa
721
            if (!localStateHasChanged) {
722
                // Local state hasn't changed
723
                if (serverStateHasChanged) {
724
                    // Server state has changed
725
                    // Update local state to match that of the server 
726
                    object.bytes = [remoteObject bytes];
727
                    object.version = [remoteObject version];
728
                    object.contentType = [remoteObject contentType];
729
                    object.hash = remoteObjectHash;
730
                    [self updateLocalStateWithObject:object localFilePath:filePath];
731
                }
732
            } else {
733
                // Local state has changed
734
                if (!serverStateHasChanged) {
735
                    // Server state hasn't changed
736
                    [self updateServerStateWithCurrentState:currentLocalObjectState 
737
                                                     object:object 
738
                                              localFilePath:filePath];
739
                } else {
740
                    // Server state has also changed
741
                    if (remoteObjectIsDirectory && currentLocalObjectState.isDirectory) {
742
                        // Both did the same change (directory)
743
                        storedLocalObjectState.isDirectory = YES;
744
                        [self saveLocalState];
745
                    } else if ([remoteObjectHash isEqualToString:currentLocalObjectState.md5] || 
746
                               [remoteObjectHash isEqualToString:currentLocalObjectState.hashMapHash]) {
747
                        // Both did the same change (object edit or delete)
748
                        if ([remoteObjectHash length] == 32) 
749
                            storedLocalObjectState.md5 = remoteObjectHash;
750
                        else if ([remoteObjectHash length] == 64)
751
                            storedLocalObjectState.hashMapHash = remoteObjectHash;
752
                        else if ([remoteObjectHash isEqualToString:@" "])
753
                            [storedLocalObjectStates removeObjectForKey:object.name];
754
                        [self saveLocalState];
755
                    } else {
756
                        // Conflict, we ask the user which change to keep
757
                        NSString *informativeText;
758
                        NSString *firstButtonText;
759
                        NSString *secondButtonText;
760
                        
761
                        if ([remoteObjectHash isEqualToString:@" "]) {
762
                            // Remote object has been deleted
763
                            informativeText = [NSString stringWithFormat:@"'%@' has been modified locally, while it has been deleted from server.", object.name ];
764
                            firstButtonText = @"Delete local file";
765
                            secondButtonText = @"Upload file to server";
766
                        } else if ([currentLocalObjectState.md5 isEqualToString:@" "] && [currentLocalObjectState.hashMapHash isEqualToString:@" "]) {
767
                            informativeText = [NSString stringWithFormat:@"'%@' has been modified on the server, while it has been deleted locally.", object.name];
768
                            firstButtonText = @"Download file from server";
769
                            secondButtonText = @"Delete file on server";
770
                        } else {
771
                            informativeText = [NSString stringWithFormat:@"'%@' has been modifed both locally and on the server.", object.name];
772
                            firstButtonText = @"Keep server version";
773
                            secondButtonText = @"Keep local version";
774
                        }
775
                        NSAlert *alert = [[[NSAlert alloc] init] autorelease];
776
                        [alert setMessageText:@"Conflict"];
777
                        [alert setInformativeText:informativeText];
778
                        [alert addButtonWithTitle:firstButtonText];
779
                        [alert addButtonWithTitle:secondButtonText];
780
                        [alert addButtonWithTitle:@"Do nothing"];
781
                        NSInteger choice = [alert runModal];
782
                        if (choice == NSAlertFirstButtonReturn) {
783
                            object.bytes = [remoteObject bytes];
784
                            object.version = [remoteObject version];
785
                            object.contentType = [remoteObject contentType];
786
                            object.hash = remoteObjectHash;
787
                            [self updateLocalStateWithObject:object localFilePath:filePath];
788
                        } if (choice == NSAlertSecondButtonReturn) {
789
                            [self updateServerStateWithCurrentState:currentLocalObjectState 
790
                                                             object:object 
791
                                                      localFilePath:filePath];
792
                        }
793
                    }
794
                }
795
            }
796
        }
797
        @synchronized(self) {
798
            [self decreaseSyncOperationCount];
799
            if (newSyncRequested && !syncOperationCount)
800
                [self sync];
801
        }
802
    } else {
803
        NSUInteger retries = [[containerRequest.userInfo objectForKey:@"retries"] unsignedIntegerValue];
804
        if (retries > 0) {
805
            ASIPithosContainerRequest *newContainerRequest = (ASIPithosContainerRequest *)[PithosUtilities copyRequest:containerRequest];
806
            [(NSMutableDictionary *)(newContainerRequest.userInfo)setObject:[NSNumber numberWithUnsignedInteger:(--retries)] forKey:@"retries"];
807
            [queue addOperation:[PithosUtilities prepareRequest:newContainerRequest priority:[[newContainerRequest.userInfo objectForKey:@"priority"] integerValue]]];
808
        } else {
809
            [activityFacility endActivity:[containerRequest.userInfo objectForKey:@"activity"] 
810
                              withMessage:[containerRequest.userInfo objectForKey:@"failedActivityMessage"]];
811
            @synchronized(self) {
812
                // Since the server listing failed in all retries, the operation finished and the sync cycle is completeted unsuccesfully
813
                syncOperationCount = 0;
814
                if (newSyncRequested)
815
                    [self sync];
816
            }
817
        }
818
    }
819
}
820

    
821
- (void)listRequestFailed:(ASIPithosContainerRequest *)containerRequest {
822
    if ([containerRequest isCancelled]) {
823
        [activityFacility endActivity:[containerRequest.userInfo objectForKey:@"activity"] 
824
                          withMessage:[containerRequest.userInfo objectForKey:@"stoppedActivityMessage"]];
825
        [objects release];
826
        objects = nil;
827
        @synchronized(self) {
828
            syncOperationCount = 0;
829
        }
830
        return;
831
    }
832
    // If the server listing fails, the sync should start over, so just retrying is enough
833
    NSUInteger retries = [[containerRequest.userInfo objectForKey:@"retries"] unsignedIntegerValue];
834
    if (retries > 0) {
835
        ASIPithosContainerRequest *newContainerRequest = (ASIPithosContainerRequest *)[PithosUtilities copyRequest:containerRequest];
836
        [(NSMutableDictionary *)(newContainerRequest.userInfo)setObject:[NSNumber numberWithUnsignedInteger:(--retries)] forKey:@"retries"];
837
        [queue addOperation:[PithosUtilities prepareRequest:newContainerRequest priority:[[newContainerRequest.userInfo objectForKey:@"priority"] integerValue]]];
838
    } else {
839
        [activityFacility endActivity:[containerRequest.userInfo objectForKey:@"activity"] 
840
                          withMessage:[containerRequest.userInfo objectForKey:@"failedActivityMessage"]];
841
        [objects release];
842
        objects = nil;
843
        @synchronized(self) {
844
            // Since the server listing failed in all retries, the operation finished and the sync cycle is completeted unsuccesfully
845
            syncOperationCount = 0;
846
            if (newSyncRequested)
847
                [self sync];
848
        }
849
    }
850
}
851

    
852
- (void)downloadObjectBlockFinished:(ASIPithosObjectRequest *)objectRequest {
853
    NSLog(@"Sync::download object block finished: %@", objectRequest.url);
854
    if (objectRequest.responseStatusCode == 206) {
855
        ASIPithosObject *object = [objectRequest.userInfo objectForKey:@"pithosObject"];
856
        NSFileManager *fileManager = [NSFileManager defaultManager];
857
        NSError *error;
858
        PithosActivity *activity = [objectRequest.userInfo objectForKey:@"activity"];
859
        
860
        NSString *downloadsDirPath = self.tempDownloadsDirPath;
861
        if (!downloadsDirPath) {
862
            [activityFacility endActivity:activity 
863
                              withMessage:[objectRequest.userInfo objectForKey:@"failedActivityMessage"]];
864
            @synchronized(self) {
865
                syncIncomplete = YES;
866
                [self decreaseSyncOperationCount];
867
                if (newSyncRequested && !syncOperationCount)
868
                    [self sync];
869
            }
870
            return;
871
        }
872
        
873
        PithosLocalObjectState *storedState = [storedLocalObjectStates objectForKey:object.name];
874
        if ((storedState.tmpDownloadFile == nil) || ![fileManager fileExistsAtPath:storedState.tmpDownloadFile]) {
875
            NSString *tempFileTemplate = [downloadsDirPath stringByAppendingPathComponent:@"download.XXXXXX"];
876
            const char *tempFileTemplateCString = [tempFileTemplate fileSystemRepresentation];
877
            char *tempFileNameCString = (char *)malloc(strlen(tempFileTemplateCString) + 1);
878
            strcpy(tempFileNameCString, tempFileTemplateCString);
879
            int fileDescriptor = mkstemp(tempFileNameCString);
880
            NSString *tempFilePath = [fileManager stringWithFileSystemRepresentation:tempFileNameCString length:strlen(tempFileNameCString)];
881
            free(tempFileNameCString);
882
            if (fileDescriptor == -1) {
883
                [PithosUtilities fileActionFailedAlertWithTitle:@"Create Temporary File Error" 
884
                                                        message:[NSString stringWithFormat:@"Cannot create temporary file at '%@'", storedState.tmpDownloadFile] 
885
                                                          error:nil];
886
                [activityFacility endActivity:activity 
887
                                  withMessage:[objectRequest.userInfo objectForKey:@"failedActivityMessage"]];
888
                @synchronized(self) {
889
                    syncIncomplete = YES;
890
                    [self decreaseSyncOperationCount];
891
                    if (newSyncRequested && !syncOperationCount)
892
                        [self sync];
893
                }
894
                return;
895
            }
896
            close(fileDescriptor);
897
            storedState.tmpDownloadFile = tempFilePath;
898
            [self saveLocalState];
899
        }
900
        
901

    
902
        NSUInteger missingBlockIndex = [[objectRequest.userInfo objectForKey:@"missingBlockIndex"] unsignedIntegerValue];
903
        NSFileHandle *tempFileHandle = [NSFileHandle fileHandleForWritingAtPath:storedState.tmpDownloadFile];
904
        [tempFileHandle seekToFileOffset:missingBlockIndex*blockSize];
905
        [tempFileHandle writeData:[objectRequest responseData]];
906
        [tempFileHandle closeFile];
907

    
908
        NSIndexSet *missingBlocks = [objectRequest.userInfo objectForKey:@"missingBlocks"];
909
        missingBlockIndex = [missingBlocks indexGreaterThanIndex:missingBlockIndex];
910
        if (missingBlockIndex == NSNotFound) {
911
            NSString *filePath = [objectRequest.userInfo objectForKey:@"filePath"];
912
            NSString *dirPath = [filePath stringByDeletingLastPathComponent];
913
            if ([fileManager fileExistsAtPath:filePath]) {
914
                error = nil;
915
                // XXX don't delete but move to local trash instead
916
                [fileManager removeItemAtPath:filePath error:&error];
917
                if (error != nil) {
918
                    [PithosUtilities fileActionFailedAlertWithTitle:@"Remove File Error" 
919
                                                            message:[NSString stringWithFormat:@"Cannot remove file at '%@'", filePath] 
920
                                                              error:error];
921
                    [activityFacility endActivity:activity 
922
                                      withMessage:[objectRequest.userInfo objectForKey:@"failedActivityMessage"]];
923
                    @synchronized(self) {
924
                        syncIncomplete = YES;
925
                        [self decreaseSyncOperationCount];
926
                        if (newSyncRequested && !syncOperationCount)
927
                            [self sync];
928
                    }
929
                    return;
930
                }
931
            } else if (![fileManager fileExistsAtPath:dirPath]) {
932
                // File doesn't exist but also the containing directory doesn't exist
933
                // In most cases this should have been resolved as an update of the corresponding local object,
934
                // but it never hurts to check
935
                error = nil;
936
                [fileManager createDirectoryAtPath:dirPath withIntermediateDirectories:YES attributes:nil error:nil];
937
                if (error != nil) {
938
                    [PithosUtilities fileActionFailedAlertWithTitle:@"Create Directory Error" 
939
                                                            message:[NSString stringWithFormat:@"Cannot create directory at '%@'", dirPath] 
940
                                                              error:error];
941
                    [activityFacility endActivity:[objectRequest.userInfo objectForKey:@"activity"] 
942
                                      withMessage:[objectRequest.userInfo objectForKey:@"failedActivityMessage"]];
943
                    @synchronized(self) {
944
                        syncIncomplete = YES;
945
                        [self decreaseSyncOperationCount];
946
                        if (newSyncRequested && !syncOperationCount)
947
                            [self sync];
948
                    }
949
                    return;
950
                }
951
            }
952
            // Move file from tmp download
953
            error = nil;
954
            [fileManager moveItemAtPath:storedState.tmpDownloadFile toPath:filePath error:&error];
955
            if (error != nil) {
956
                [PithosUtilities fileActionFailedAlertWithTitle:@"Move File Error" 
957
                                                        message:[NSString stringWithFormat:@"Cannot move file at '%@' to '%@'", storedState.tmpDownloadFile, filePath] 
958
                                                          error:error];
959
                [activityFacility endActivity:activity 
960
                                  withMessage:[objectRequest.userInfo objectForKey:@"failedActivityMessage"]];
961
                @synchronized(self) {
962
                    syncIncomplete = YES;
963
                    [self decreaseSyncOperationCount];
964
                    if (newSyncRequested && !syncOperationCount)
965
                        [self sync];
966
                }
967
                return;
968
            }
969
            [activityFacility endActivity:activity 
970
                              withMessage:[objectRequest.userInfo objectForKey:@"finishedActivityMessage"] 
971
                               totalBytes:activity.totalBytes 
972
                             currentBytes:activity.totalBytes];
973

    
974
            if ([object.hash length] == 32) {
975
                storedState.md5 = object.hash;
976
                storedState.hashMapHash = @" ";
977
            } else if([object.hash length] == 64) {
978
                storedState.md5 = @" ";
979
                storedState.hashMapHash = object.hash;
980
            } else {
981
                storedState.md5 = @" ";
982
                storedState.hashMapHash = @" ";
983
            }
984
            
985
            storedState.tmpDownloadFile = nil;
986
            [self saveLocalState];
987
            
988
            @synchronized(self) {
989
                [self decreaseSyncOperationCount];
990
                if (newSyncRequested && !syncOperationCount)
991
                    [self sync];
992
            }
993
            return;
994
        } else {
995
            if (newSyncRequested) {
996
                [activityFacility endActivity:[objectRequest.userInfo objectForKey:@"activity"] 
997
                                  withMessage:[objectRequest.userInfo objectForKey:@"stoppedActivityMessage"]];
998
                @synchronized(self) {
999
                    syncIncomplete = YES;
1000
                    [self decreaseSyncOperationCount];
1001
                    if (!syncOperationCount)
1002
                        [self sync];
1003
                }
1004
                return;
1005
            } else {
1006
                __block ASIPithosObjectRequest *newObjectRequest = [PithosUtilities objectBlockDataRequestWithContainerName:containerName 
1007
                                                                                                                     object:object 
1008
                                                                                                                 blockIndex:missingBlockIndex 
1009
                                                                                                                  blockSize:blockSize];
1010
                newObjectRequest.delegate = self;
1011
                newObjectRequest.didFinishSelector = @selector(downloadObjectBlockFinished:);
1012
                newObjectRequest.didFailSelector = @selector(downloadObjectBlockOrHashMapFailed:);
1013
                newObjectRequest.userInfo = objectRequest.userInfo;
1014
                [(NSMutableDictionary *)(newObjectRequest.userInfo) setObject:[NSNumber numberWithUnsignedInteger:missingBlockIndex] forKey:@"missingBlockIndex"];
1015
                [(NSMutableDictionary *)(newObjectRequest.userInfo) setObject:[NSNumber numberWithUnsignedInteger:10] forKey:@"retries"];
1016
                [newObjectRequest setBytesReceivedBlock:^(unsigned long long size, unsigned long long total){
1017
                    [activityFacility updateActivity:activity 
1018
                                         withMessage:[NSString stringWithFormat:@"Downloading '%@' (%.0f%%)", 
1019
                                                      object.name, 
1020
                                                      (100*(activity.currentBytes + size + 0.0)/(activity.totalBytes + 0.0))] 
1021
                                          totalBytes:activity.totalBytes 
1022
                                        currentBytes:(activity.currentBytes + size)];
1023
                }];
1024
                [queue addOperation:[PithosUtilities prepareRequest:newObjectRequest priority:[[newObjectRequest.userInfo objectForKey:@"priority"] integerValue]]];
1025
            }
1026
        }
1027
    } else if (objectRequest.responseStatusCode == 412) {
1028
        // The object has changed on the server
1029
        [activityFacility endActivity:[objectRequest.userInfo objectForKey:@"activity"] 
1030
                          withMessage:[objectRequest.userInfo objectForKey:@"stoppedActivityMessage"]];
1031
        @synchronized(self) {
1032
            syncIncomplete = YES;
1033
            [self decreaseSyncOperationCount];
1034
            if (newSyncRequested && !syncOperationCount)
1035
                [self sync];
1036
        }
1037
    } else {
1038
        [self requestFailed:objectRequest];
1039
    }
1040
}
1041

    
1042
- (void)downloadObjectHashMapFinished:(ASIPithosObjectRequest *)objectRequest {
1043
    NSLog(@"Sync::download object hashmap finished: %@", objectRequest.url);
1044
    if (objectRequest.responseStatusCode == 200) {
1045
        if (newSyncRequested) {
1046
            [activityFacility endActivity:[objectRequest.userInfo objectForKey:@"activity"] 
1047
                              withMessage:[objectRequest.userInfo objectForKey:@"stoppedActivityMessage"]];
1048
            @synchronized(self) {
1049
                syncIncomplete = YES;
1050
                [self decreaseSyncOperationCount];
1051
                if (!syncOperationCount)
1052
                    [self sync];
1053
            }
1054
            return;
1055
        } else {
1056
            ASIPithosObject *object = [objectRequest.userInfo objectForKey:@"pithosObject"];
1057
            PithosLocalObjectState *storedState = [storedLocalObjectStates objectForKey:object.name];
1058
            if ([PithosUtilities bytesOfFile:storedState.tmpDownloadFile] > object.bytes)
1059
                [[NSFileHandle fileHandleForWritingAtPath:storedState.tmpDownloadFile] truncateFileAtOffset:object.bytes];
1060
            PithosActivity *activity = [objectRequest.userInfo objectForKey:@"activity"];
1061
            NSIndexSet *missingBlocks = [PithosUtilities missingBlocksForFile:storedState.tmpDownloadFile 
1062
                                                                    blockSize:blockSize 
1063
                                                                    blockHash:blockHash 
1064
                                                                   withHashes:[objectRequest hashes]];
1065
            NSUInteger missingBlockIndex = [missingBlocks firstIndex];
1066
            [activityFacility endActivity:activity 
1067
                              withMessage:[NSString stringWithFormat:@"Sync: Downloading '%@' (%.0f%%)", 
1068
                                           object.name, 
1069
                                           (100*(activity.totalBytes - [missingBlocks count]*blockSize + 0.0)/(activity.totalBytes + 0.0))] 
1070
                               totalBytes:activity.totalBytes 
1071
                             currentBytes:(activity.totalBytes - [missingBlocks count]*blockSize)];
1072

    
1073
            __block ASIPithosObjectRequest *newObjectRequest = [PithosUtilities objectBlockDataRequestWithContainerName:containerName 
1074
                                                                                                                 object:object 
1075
                                                                                                             blockIndex:missingBlockIndex 
1076
                                                                                                              blockSize:blockSize];
1077
            newObjectRequest.delegate = self;
1078
            newObjectRequest.didFinishSelector = @selector(downloadObjectBlockFinished:);
1079
            newObjectRequest.didFailSelector = @selector(downloadObjectBlockOrHashMapFailed:);
1080
            newObjectRequest.userInfo = objectRequest.userInfo;
1081
            [(NSMutableDictionary *)(newObjectRequest.userInfo) setObject:missingBlocks forKey:@"missingBlocks"];
1082
            [(NSMutableDictionary *)(newObjectRequest.userInfo) setObject:[NSNumber numberWithUnsignedInteger:missingBlockIndex] forKey:@"missingBlockIndex"];
1083
            [(NSMutableDictionary *)(newObjectRequest.userInfo) setObject:[NSNumber numberWithUnsignedInteger:10] forKey:@"retries"];
1084
            [newObjectRequest setBytesReceivedBlock:^(unsigned long long size, unsigned long long total){
1085
                [activityFacility updateActivity:activity 
1086
                                     withMessage:[NSString stringWithFormat:@"Downloading '%@' (%.0f%%)", 
1087
                                                  object.name, 
1088
                                                  (100*(activity.currentBytes + size + 0.0)/(activity.totalBytes + 0.0))] 
1089
                                      totalBytes:activity.totalBytes 
1090
                                    currentBytes:(activity.currentBytes + size)];
1091
            }];
1092
            [queue addOperation:[PithosUtilities prepareRequest:newObjectRequest priority:[[newObjectRequest.userInfo objectForKey:@"priority"] integerValue]]];
1093
        }
1094
    } else {
1095
        [self requestFailed:objectRequest];
1096
    }
1097
}
1098

    
1099
- (void)uploadDirectoryObjectFinished:(ASIPithosObjectRequest *)objectRequest {
1100
    NSLog(@"Sync::upload directory object finished: %@", objectRequest.url);
1101
    if (objectRequest.responseStatusCode == 201) {
1102
        PithosLocalObjectState *storedState = [storedLocalObjectStates objectForKey:[[objectRequest.userInfo objectForKey:@"pithosObject"] name]];
1103
        storedState.isDirectory = YES;
1104
        [self saveLocalState];
1105
        [activityFacility endActivity:[objectRequest.userInfo objectForKey:@"activity"] 
1106
                          withMessage:[objectRequest.userInfo objectForKey:@"finishedActivityMessage"]];
1107
        @synchronized(self) {
1108
            [self decreaseSyncOperationCount];
1109
            if (newSyncRequested && !syncOperationCount)
1110
                [self sync];
1111
        }
1112
    } else {
1113
        [self requestFailed:objectRequest];
1114
    }
1115
}
1116

    
1117
- (void)moveObjectToTrashFinished:(ASIPithosObjectRequest *)objectRequest {
1118
    NSLog(@"Sync::move object to trash finished: %@", objectRequest.url);
1119
    if (objectRequest.responseStatusCode == 201) {
1120
        [storedLocalObjectStates removeObjectForKey:[[objectRequest.userInfo objectForKey:@"pithosObject"] name]];
1121
        [self saveLocalState];
1122
        [activityFacility endActivity:[objectRequest.userInfo objectForKey:@"activity"] 
1123
                          withMessage:[objectRequest.userInfo objectForKey:@"finishedActivityMessage"]];
1124
        @synchronized(self) {
1125
            [self decreaseSyncOperationCount];
1126
            if (newSyncRequested && !syncOperationCount)
1127
                [self sync];
1128
        }
1129
    } else {
1130
        [self requestFailed:objectRequest];
1131
    }
1132
}
1133

    
1134
- (void)uploadObjectUsingHashMapFinished:(ASIPithosObjectRequest *)objectRequest {
1135
    NSLog(@"Sync::upload using hashmap finished: %@", objectRequest.url);
1136
    ASIPithosObject *object = [objectRequest.userInfo objectForKey:@"pithosObject"];
1137
    PithosLocalObjectState *storedState = [storedLocalObjectStates objectForKey:object.name];
1138
    PithosActivity *activity = [objectRequest.userInfo objectForKey:@"activity"];
1139
    NSUInteger totalBytes = activity.totalBytes;
1140
    NSUInteger currentBytes = activity.currentBytes;
1141
    if (objectRequest.responseStatusCode == 201) {
1142
        NSLog(@"Sync::object created: %@", objectRequest.url);
1143
        NSString *eTag = [objectRequest eTag];
1144
        if ([eTag length] == 32)
1145
            storedState.md5 = eTag;
1146
        else if([eTag length] == 64)
1147
            storedState.hashMapHash = eTag;
1148
        [self saveLocalState];
1149
        [activityFacility endActivity:activity 
1150
                          withMessage:[objectRequest.userInfo objectForKey:@"finishedActivityMessage"] 
1151
                           totalBytes:totalBytes 
1152
                         currentBytes:totalBytes];
1153
        @synchronized(self) {
1154
            [self decreaseSyncOperationCount];
1155
            if (newSyncRequested && !syncOperationCount)
1156
                [self sync];
1157
        }
1158
    } else if (objectRequest.responseStatusCode == 409) {
1159
        if (newSyncRequested) {
1160
            [activityFacility endActivity:activity 
1161
                              withMessage:[objectRequest.userInfo objectForKey:@"stoppedActivityMessage"]];
1162
            @synchronized(self) {
1163
                syncIncomplete = YES;
1164
                [self decreaseSyncOperationCount];
1165
                if (!syncOperationCount)
1166
                    [self sync];
1167
            }
1168
            return;
1169
        } else {
1170
            NSUInteger iteration = [[objectRequest.userInfo objectForKey:@"iteration"] unsignedIntegerValue];
1171
            if (iteration == 0) {
1172
                NSLog(@"Sync::upload iteration limit reached: %@", objectRequest.url);
1173
                [activityFacility endActivity:activity 
1174
                                  withMessage:[objectRequest.userInfo objectForKey:@"stoppedActivityMessage"]];
1175
                syncIncomplete = YES;
1176
                [self decreaseSyncOperationCount];
1177
                return;
1178
            }
1179
            NSLog(@"Sync::object is missing hashes: %@", objectRequest.url);
1180
            NSIndexSet *missingBlocks = [PithosUtilities missingBlocksForHashes:[objectRequest.userInfo objectForKey:@"hashes"]
1181
                                                      withMissingHashesResponse:[objectRequest responseString]];
1182
            if (totalBytes >= [missingBlocks count]*blockSize)
1183
                currentBytes = totalBytes - [missingBlocks count]*blockSize;
1184
            [activityFacility updateActivity:activity 
1185
                                 withMessage:[NSString stringWithFormat:@"Uploading '%@' (%.0f%%)", object.name, (100*(currentBytes + 0.0)/(totalBytes + 0.0))] 
1186
                                  totalBytes:totalBytes 
1187
                                currentBytes:currentBytes];
1188
            NSUInteger missingBlockIndex = [missingBlocks firstIndex];
1189
            __block ASIPithosContainerRequest *newContainerRequest = [PithosUtilities updateContainerDataRequestWithContainerName:containerName 
1190
                                                                                                                        blockSize:blockSize 
1191
                                                                                                                          forFile:[objectRequest.userInfo objectForKey:@"filePath"] 
1192
                                                                                                                missingBlockIndex:missingBlockIndex 
1193
                                                                                                                   sharingAccount:nil];
1194
            newContainerRequest.delegate = self;
1195
            newContainerRequest.didFinishSelector = @selector(uploadMissingBlockFinished:);
1196
            newContainerRequest.didFailSelector = @selector(requestFailed:);
1197
            newContainerRequest.userInfo = objectRequest.userInfo;
1198
            [(NSMutableDictionary *)(newContainerRequest.userInfo) setObject:[NSNumber numberWithUnsignedInteger:(--iteration)] forKey:@"iteration"];
1199
            [(NSMutableDictionary *)(newContainerRequest.userInfo) setObject:[NSNumber numberWithUnsignedInteger:10] forKey:@"retries"];
1200
            [(NSMutableDictionary *)(newContainerRequest.userInfo) setObject:missingBlocks forKey:@"missingBlocks"];
1201
            [(NSMutableDictionary *)(newContainerRequest.userInfo) setObject:[NSNumber numberWithUnsignedInteger:missingBlockIndex] forKey:@"missingBlockIndex"];
1202
            [newContainerRequest setBytesSentBlock:^(unsigned long long size, unsigned long long total){
1203
                [activityFacility updateActivity:activity 
1204
                                     withMessage:[NSString stringWithFormat:@"Uploading '%@' (%.0f%%)", object.name, (100*(activity.currentBytes + size + 0.0)/(activity.totalBytes + 0.0))] 
1205
                                      totalBytes:activity.totalBytes 
1206
                                    currentBytes:(activity.currentBytes + size)];
1207
            }];
1208
            [queue addOperation:[PithosUtilities prepareRequest:newContainerRequest priority:[[newContainerRequest.userInfo objectForKey:@"priority"] integerValue]]];
1209
        }
1210
    } else {
1211
        [self requestFailed:objectRequest];
1212
    }
1213
}
1214

    
1215
- (void)uploadMissingBlockFinished:(ASIPithosContainerRequest *)containerRequest {
1216
    NSLog(@"Sync::upload of missing block finished: %@", containerRequest.url);
1217
    if (containerRequest.responseStatusCode == 202) {
1218
        ASIPithosObject *object = [containerRequest.userInfo objectForKey:@"pithosObject"];
1219
        PithosActivity *activity = [containerRequest.userInfo objectForKey:@"activity"];
1220
        NSUInteger totalBytes = activity.totalBytes;
1221
        NSUInteger currentBytes = activity.currentBytes + blockSize;
1222
        if (currentBytes > totalBytes)
1223
            currentBytes = totalBytes;
1224
        [activityFacility updateActivity:activity 
1225
                             withMessage:[NSString stringWithFormat:@"Uploading '%@' (%.0f%%)", object.name, (100*(currentBytes + 0.0)/(totalBytes + 0.0))] 
1226
                              totalBytes:totalBytes 
1227
                            currentBytes:currentBytes];
1228
        NSIndexSet *missingBlocks = [containerRequest.userInfo objectForKey:@"missingBlocks"];
1229
        NSUInteger missingBlockIndex = [[containerRequest.userInfo objectForKey:@"missingBlockIndex"] unsignedIntegerValue];
1230
        missingBlockIndex = [missingBlocks indexGreaterThanIndex:missingBlockIndex];
1231
        if (missingBlockIndex == NSNotFound) {
1232
            NSArray *hashes = [containerRequest.userInfo objectForKey:@"hashes"];
1233
            ASIPithosObjectRequest *newObjectRequest = [PithosUtilities writeObjectDataRequestWithContainerName:containerName 
1234
                                                                                                     objectName:object.name 
1235
                                                                                                    contentType:object.contentType 
1236
                                                                                                      blockSize:blockSize 
1237
                                                                                                      blockHash:blockHash
1238
                                                                                                        forFile:[containerRequest.userInfo objectForKey:@"filePath"] 
1239
                                                                                                  checkIfExists:NO 
1240
                                                                                                         hashes:&hashes 
1241
                                                                                                 sharingAccount:nil];
1242
            newObjectRequest.delegate = self;
1243
            newObjectRequest.didFinishSelector = @selector(uploadObjectUsingHashMapFinished:);
1244
            newObjectRequest.didFailSelector = @selector(requestFailed:);
1245
            newObjectRequest.userInfo = containerRequest.userInfo;
1246
            [(NSMutableDictionary *)(newObjectRequest.userInfo) setObject:[NSNumber numberWithUnsignedInteger:10] forKey:@"retries"];
1247
            [(NSMutableDictionary *)(newObjectRequest.userInfo) removeObjectForKey:@"missingBlocks"];
1248
            [(NSMutableDictionary *)(newObjectRequest.userInfo) removeObjectForKey:@"missingBlockIndex"];
1249
            [queue addOperation:[PithosUtilities prepareRequest:newObjectRequest priority:[[newObjectRequest.userInfo objectForKey:@"priority"] integerValue]]];
1250
        } else {
1251
            if (newSyncRequested) {
1252
                [activityFacility endActivity:activity 
1253
                                  withMessage:[containerRequest.userInfo objectForKey:@"stoppedActivityMessage"]];
1254
                @synchronized(self) {
1255
                    syncIncomplete = YES;
1256
                    [self decreaseSyncOperationCount];
1257
                    if (!syncOperationCount)
1258
                        [self sync];
1259
                }
1260
                return;
1261
            } else {
1262
                __block ASIPithosContainerRequest *newContainerRequest = [PithosUtilities updateContainerDataRequestWithContainerName:containerName
1263
                                                                                                                            blockSize:blockSize
1264
                                                                                                                              forFile:[containerRequest.userInfo objectForKey:@"filePath"] 
1265
                                                                                                                    missingBlockIndex:missingBlockIndex 
1266
                                                                                                                       sharingAccount:nil];
1267
                newContainerRequest.delegate = self;
1268
                newContainerRequest.didFinishSelector = @selector(uploadMissingBlockFinished:);
1269
                newContainerRequest.didFailSelector = @selector(requestFailed:);
1270
                newContainerRequest.userInfo = containerRequest.userInfo;
1271
                [(NSMutableDictionary *)(newContainerRequest.userInfo) setObject:[NSNumber numberWithUnsignedInteger:10] forKey:@"retries"];
1272
                [(NSMutableDictionary *)(newContainerRequest.userInfo) setObject:[NSNumber numberWithUnsignedInteger:missingBlockIndex] forKey:@"missingBlockIndex"];
1273
                [newContainerRequest setBytesSentBlock:^(unsigned long long size, unsigned long long total){
1274
                    [activityFacility updateActivity:activity 
1275
                                         withMessage:[NSString stringWithFormat:@"Uploading '%@' (%.0f%%)", object.name, (100*(activity.currentBytes + size + 0.0)/(activity.totalBytes + 0.0))] 
1276
                                          totalBytes:activity.totalBytes 
1277
                                        currentBytes:(activity.currentBytes + size)];
1278
                }];
1279
                [queue addOperation:[PithosUtilities prepareRequest:newContainerRequest priority:[[newContainerRequest.userInfo objectForKey:@"priority"] integerValue]]];
1280
            }
1281
        }
1282
    } else {
1283
        [self requestFailed:containerRequest];
1284
    }
1285
}
1286

    
1287
- (void)requestFailed:(ASIPithosRequest *)request {
1288
    if ([request isCancelled]) {
1289
        [activityFacility endActivity:[request.userInfo objectForKey:@"activity"] 
1290
                          withMessage:[request.userInfo objectForKey:@"stoppedActivityMessage"]];
1291
        syncIncomplete = YES;
1292
        [self decreaseSyncOperationCount];
1293
        return;
1294
    }
1295
    if (newSyncRequested) {
1296
        [activityFacility endActivity:[request.userInfo objectForKey:@"activity"] 
1297
                          withMessage:[request.userInfo objectForKey:@"stoppedActivityMessage"]];
1298
        @synchronized(self) {
1299
            syncIncomplete = YES;
1300
            [self decreaseSyncOperationCount];
1301
            if (!syncOperationCount)
1302
                [self sync];
1303
        }
1304
        return;
1305
    }
1306
    NSUInteger retries = [[request.userInfo objectForKey:@"retries"] unsignedIntegerValue];
1307
    if (retries > 0) {
1308
        ASIPithosRequest *newRequest = (ASIPithosRequest *)[PithosUtilities copyRequest:request];
1309
        [(NSMutableDictionary *)(newRequest.userInfo)setObject:[NSNumber numberWithUnsignedInteger:(--retries)] forKey:@"retries"];
1310
        [queue addOperation:[PithosUtilities prepareRequest:newRequest priority:[[newRequest.userInfo objectForKey:@"priority"] integerValue]]];
1311
    } else {
1312
        [activityFacility endActivity:[request.userInfo objectForKey:@"activity"] 
1313
                          withMessage:[request.userInfo objectForKey:@"failedActivityMessage"]];
1314
        syncIncomplete = YES;
1315
        [self decreaseSyncOperationCount];
1316
    }
1317
}
1318

    
1319

    
1320
@end