Statistics
| Branch: | Tag: | Revision:

root / pithos-macos / PithosSyncDaemon.m @ e5cbe7aa

History | View | Annotate | Download (155.3 kB)

1
//
2
//  PithosSyncDaemon.m
3
//  pithos-macos
4
//
5
// Copyright 2011-2012 GRNET S.A. All rights reserved.
6
//
7
// Redistribution and use in source and binary forms, with or
8
// without modification, are permitted provided that the following
9
// conditions are met:
10
// 
11
//   1. Redistributions of source code must retain the above
12
//      copyright notice, this list of conditions and the following
13
//      disclaimer.
14
// 
15
//   2. Redistributions in binary form must reproduce the above
16
//      copyright notice, this list of conditions and the following
17
//      disclaimer in the documentation and/or other materials
18
//      provided with the distribution.
19
// 
20
// THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
21
// OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
22
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
23
// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
24
// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
25
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
26
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
27
// USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
28
// AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
29
// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
30
// ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
31
// POSSIBILITY OF SUCH DAMAGE.
32
// 
33
// The views and conclusions contained in the software and
34
// documentation are those of the authors and should not be
35
// interpreted as representing official policies, either expressed
36
// or implied, of GRNET S.A.
37

    
38
#import "PithosSyncDaemon.h"
39
#import "PithosAccount.h"
40
#import "PithosLocalObjectState.h"
41
#import "PithosActivityFacility.h"
42
#import "PithosUtilities.h"
43
#import "ASINetworkQueue.h"
44
#import "ASIPithosRequest.h"
45
#import "ASIPithos.h"
46
#import "ASIPithosContainer.h"
47
#import "ASIPithosContainerRequest.h"
48
#import "ASIPithosObjectRequest.h"
49
#import "ASIPithosObject.h"
50

    
51
@interface PithosSyncDaemon (Private)
52
- (void)loadLocalState;
53
- (void)resetLocalStateWithAll:(BOOL)all;
54
- (void)saveLocalState;
55

    
56
- (BOOL)createSyncDirectory:(NSString *)dirPath;
57
- (NSString *)dirPathForAccount:(NSString *)accountName container:(NSString *)containerName;
58
- (NSString *)relativeDirPathForAccount:(NSString *)accountName container:(NSString *)containerName;
59

    
60
- (BOOL)moveToTempTrashFile:(NSString *)filePath 
61
                accountName:(NSString *)accountName 
62
            pithosContainer:(ASIPithosContainer *)pithosContainer;
63
- (void)emptyTempTrash;
64
- (BOOL)findLocalCopyForObjectWithHash:(NSString *)hash forFile:(NSString *)filePath;
65

    
66
- (void)updateLocalStateWithObject:(ASIPithosObject *)object 
67
                     localFilePath:(NSString *)filePath 
68
                       accountName:(NSString *)accountName 
69
                   pithosContainer:(ASIPithosContainer *)pithosContainer;
70
- (void)updateServerStateWithCurrentState:(PithosLocalObjectState *)currentState 
71
                                   object:(ASIPithosObject *)object 
72
                            localFilePath:(NSString *)filePath 
73
                              accountName:(NSString *)accountName 
74
                          pithosContainer:(ASIPithosContainer *)pithosContainer;
75
- (void)listRequestFailed:(ASIPithosContainerRequest *)containerRequest;
76
- (void)requestFailed:(ASIPithosRequest *)request;
77

    
78
- (void)syncOperationStarted;
79
- (void)syncOperationFinishedWithSuccess:(BOOL)operationSuccessfull;
80

    
81
@end
82

    
83
@implementation PithosSyncDaemon
84
@synthesize directoryPath, accountsDictionary, skipHidden, pithos;
85
@synthesize userCatalog, accountsNames, accountsPithosContainers;
86
@synthesize lastCompletedSync, remoteObjects, previousRemoteObjects, storedLocalObjectStates, currentLocalObjectStates;
87
@synthesize userCatalogFilePath, pithosStateFilePath, tempDownloadsDirPath, tempTrashDirPath;
88

    
89
#pragma mark -
90
#pragma Object Lifecycle
91

    
92
- (id)initWithDirectoryPath:(NSString *)aDirectoryPath 
93
              pithosAccount:(PithosAccount *)aPithosAccount 
94
         accountsDictionary:(NSDictionary *)anAccountsDictionary 
95
                 skipHidden:(BOOL)aSkipHidden 
96
            resetLocalState:(BOOL)resetLocalState {
97
    if ((self = [super init])) {
98
        directoryPath = [aDirectoryPath copy];
99
        pithosAccount = aPithosAccount;
100
        self.accountsDictionary = anAccountsDictionary;
101
        skipHidden = aSkipHidden;
102
        self.pithos = pithosAccount.pithos;        
103
        
104
        activityFacility = [PithosActivityFacility defaultPithosActivityFacility];
105
        
106
        if (resetLocalState)
107
            [self resetLocalStateWithAll:YES];
108
        else
109
            [self resetLocalStateWithAll:NO];
110
        
111
        // Load user catalog.
112
        if ([[NSFileManager defaultManager] fileExistsAtPath:self.userCatalogFilePath])
113
            self.userCatalog = [NSKeyedUnarchiver unarchiveObjectWithFile:self.userCatalogFilePath];
114
        else
115
            self.userCatalog = [NSMutableDictionary dictionary];
116
        if (!userCatalog)
117
            self.userCatalog = [NSMutableDictionary dictionary];
118
        
119
        networkQueue = [[ASINetworkQueue alloc] init];
120
        networkQueue.showAccurateProgress = YES;
121
        networkQueue.shouldCancelAllRequestsOnFailure = NO;
122
//        networkQueue.maxConcurrentOperationCount = 1;
123
        
124
        callbackQueue = [[NSOperationQueue alloc] init];
125
        [callbackQueue setSuspended:YES];
126
        callbackQueue.name = [NSString stringWithFormat:@"gr.grnet.pithos.SyncCallbackQueue.%@", pithosAccount.uniqueName];
127
//        callbackQueue.maxConcurrentOperationCount = 1;
128
        
129
        [[NSNotificationCenter defaultCenter] addObserver:self
130
                                                 selector:@selector(applicationWillTerminate:)
131
                                                     name:NSApplicationWillTerminateNotification
132
                                                   object:[NSApplication sharedApplication]];
133
    }
134
    return self;
135
}
136

    
137
- (void)loadLocalState {
138
    @autoreleasepool {
139
        if ([[NSFileManager defaultManager] fileExistsAtPath:self.pithosStateFilePath])
140
            self.storedLocalObjectStates = [NSKeyedUnarchiver unarchiveObjectWithFile:self.pithosStateFilePath];
141
        else
142
            self.storedLocalObjectStates = [NSMutableDictionary dictionary];
143
        if (!storedLocalObjectStates)
144
            self.storedLocalObjectStates = [NSMutableDictionary dictionary];
145
        for (NSString *accountName in accountsNames) {
146
            NSMutableDictionary *accountStoredLocalObjectStates = [storedLocalObjectStates objectForKey:accountName];
147
            if (!accountStoredLocalObjectStates) {
148
                accountStoredLocalObjectStates = [NSMutableDictionary dictionary];
149
                [storedLocalObjectStates setObject:accountStoredLocalObjectStates forKey:accountName];
150
            }
151
            for (ASIPithosContainer *pithosContainer in [accountsPithosContainers objectForKey:accountName]) {
152
                if (![accountStoredLocalObjectStates objectForKey:pithosContainer.name])
153
                    [accountStoredLocalObjectStates setObject:[NSMutableDictionary dictionary] forKey:pithosContainer.name];
154
            }
155
        }
156
    }
157
}
158

    
159
- (void)resetLocalStateWithAll:(BOOL)all {
160
    @autoreleasepool {
161
        self.lastCompletedSync = nil;
162
        if (all) {
163
            self.storedLocalObjectStates = [NSMutableDictionary dictionary];
164
            [self saveLocalState]; // Save an empty dictionary
165
            [self loadLocalState]; // Load to populate with containers
166
            [self saveLocalState]; // Save again
167
            if (self.tempDownloadsDirPath)
168
                [PithosUtilities removeContentsAtPath:self.tempDownloadsDirPath];
169
        } else {
170
            // Remove containers that don't interest us anymore and save
171
            if (!storedLocalObjectStates)
172
                [self loadLocalState];
173
            for (NSString *accountName in [storedLocalObjectStates allKeys]) {
174
                NSMutableDictionary *accountStoredLocalObjectStates = [storedLocalObjectStates objectForKey:accountName];
175
                NSDictionary *containersDictionary = [accountsDictionary objectForKey:accountName];
176
                if (!containersDictionary) {
177
                    if (self.tempDownloadsDirPath) {
178
                        if ([accountName isEqualToString:@""]) {
179
                            for (NSString *containerName in accountStoredLocalObjectStates) {
180
                                [PithosUtilities removeContentsAtPath:[self.tempDownloadsDirPath stringByAppendingPathComponent:containerName]
181
                                                         andDirectory:YES];
182
                            }
183
                        } else {
184
                            [PithosUtilities removeContentsAtPath:[[self.tempDownloadsDirPath stringByAppendingPathComponent:@"shared with me"]
185
                                                                   stringByAppendingPathComponent:accountName]
186
                                                     andDirectory:YES];
187
                        }
188
                    }
189
                    [storedLocalObjectStates removeObjectForKey:accountName];
190
                } else {
191
                    // Check the account's containers
192
                    for (NSString *containerName in [accountStoredLocalObjectStates allKeys]) {
193
                        if (![containersDictionary objectForKey:containerName]) {
194
                            if ([accountName isEqualToString:@""])
195
                                [PithosUtilities removeContentsAtPath:[self.tempDownloadsDirPath stringByAppendingPathComponent:containerName]
196
                                                         andDirectory:YES];
197
                            else
198
                                [PithosUtilities removeContentsAtPath:[[[self.tempDownloadsDirPath stringByAppendingPathComponent:@"shared with me"]
199
                                                                        stringByAppendingPathComponent:accountName] 
200
                                                                       stringByAppendingPathComponent:containerName]
201
                                                         andDirectory:YES];
202
                            [accountStoredLocalObjectStates removeObjectForKey:containerName];
203
                        }
204
                    }
205
                }
206
            }
207
            [self saveLocalState];
208
        }
209
    }
210
}
211

    
212
- (void)saveLocalState {
213
    @autoreleasepool {
214
        [NSKeyedArchiver archiveRootObject:storedLocalObjectStates toFile:self.pithosStateFilePath];
215
    }
216
}
217

    
218
- (void)resetDaemon {
219
    @synchronized(self) {
220
        if (!daemonActive)
221
            return;
222
    }
223
    
224
    [networkQueue reset];
225
    [callbackQueue cancelAllOperations];
226
    [callbackQueue setSuspended:YES];
227
    [self emptyTempTrash];
228
    
229
    syncOperationCount = 0;
230
    
231
    @synchronized(self) {            
232
        daemonActive = NO;
233
    }
234
}
235

    
236
- (void)startDaemon {
237
    @synchronized(self) {
238
        if (daemonActive)
239
            return;
240
    }
241

    
242
    // In the improbable case of leftover operations
243
    [networkQueue reset];
244
    [callbackQueue cancelAllOperations];
245
    
246
    syncOperationCount = 0;
247
    newSyncRequested = NO;
248
    syncIncomplete = NO;
249
    syncLate = NO;
250
    
251
    [self loadLocalState];
252
    
253
    [networkQueue go];
254
    [callbackQueue setSuspended:NO];
255
    
256
    @synchronized(self) {
257
        daemonActive = YES;
258
    }
259
}
260

    
261
- (void)dealloc {
262
    [[NSNotificationCenter defaultCenter] removeObserver:self];
263
    [self resetDaemon];
264
}
265

    
266
#pragma mark -
267
#pragma mark Observers
268

    
269
- (void)applicationWillTerminate:(NSNotification *)notification {
270
    [self saveLocalState];
271
}
272

    
273
#pragma mark -
274
#pragma mark Properties
275

    
276
- (NSString *)userCatalogFilePath {
277
    if (!userCatalogFilePath) {
278
        userCatalogFilePath = [[[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) objectAtIndex:0]
279
                                stringByAppendingPathComponent:[[NSBundle mainBundle] bundleIdentifier]]
280
                               stringByAppendingPathComponent:[NSString stringWithFormat:@"%@-SyncUserCatalog.archive",
281
                                                               pithosAccount.uniqueName]];
282
        NSString *userCatalogFileDirPath = [userCatalogFilePath stringByDeletingLastPathComponent];
283
        NSFileManager *fileManager = [NSFileManager defaultManager];
284
        BOOL isDirectory;
285
        BOOL fileExists = [fileManager fileExistsAtPath:userCatalogFileDirPath isDirectory:&isDirectory];
286
        NSError *error = nil;
287
        if (fileExists && !isDirectory)
288
            [fileManager removeItemAtPath:userCatalogFileDirPath error:&error];
289
        if (!error && !fileExists)
290
            [fileManager createDirectoryAtPath:userCatalogFileDirPath withIntermediateDirectories:YES attributes:nil error:&error];
291
        //if (error)
292
        //  userCatalogFilePath = nil;
293
        // XXX create a dir using mktmps?
294
    }
295
    return [userCatalogFilePath copy];
296
}
297

    
298
- (NSString *)pithosStateFilePath {
299
    if (!pithosStateFilePath) {
300
        pithosStateFilePath = [[[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) objectAtIndex:0] 
301
                                 stringByAppendingPathComponent:[[NSBundle mainBundle] bundleIdentifier]] 
302
                                stringByAppendingPathComponent:[NSString stringWithFormat:@"%@-PithosLocalObjectStates.archive", 
303
                                                                pithosAccount.uniqueName]];
304
        NSString *pithosStateFileDirPath = [pithosStateFilePath stringByDeletingLastPathComponent];
305
        NSFileManager *fileManager = [NSFileManager defaultManager];
306
        BOOL isDirectory;
307
        BOOL fileExists = [fileManager fileExistsAtPath:pithosStateFileDirPath isDirectory:&isDirectory];
308
        NSError *error = nil;
309
        if (fileExists && !isDirectory)
310
            [fileManager removeItemAtPath:pithosStateFileDirPath error:&error];
311
        if (!error && !fileExists)
312
            [fileManager createDirectoryAtPath:pithosStateFileDirPath withIntermediateDirectories:YES attributes:nil error:&error];
313
        //if (error)
314
        //  pithosStateFilePath = nil;
315
        // XXX create a dir using mktmps?
316
    }
317
    return [pithosStateFilePath copy];
318
}
319

    
320
- (NSString *)tempDownloadsDirPath {
321
    if (!tempDownloadsDirPath) {
322
        tempDownloadsDirPath = [[[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) objectAtIndex:0] 
323
                                  stringByAppendingPathComponent:[[NSBundle mainBundle] bundleIdentifier]] 
324
                                 stringByAppendingPathComponent:[NSString stringWithFormat:@"%@-TempDownloads", 
325
                                                                 pithosAccount.uniqueName]];
326
        NSFileManager *fileManager = [NSFileManager defaultManager];
327
        BOOL isDirectory;
328
        BOOL fileExists = [fileManager fileExistsAtPath:tempDownloadsDirPath isDirectory:&isDirectory];
329
        NSError *error = nil;
330
        if (fileExists && !isDirectory)
331
            [fileManager removeItemAtPath:tempDownloadsDirPath error:&error];
332
        if (!error && !fileExists)
333
            [fileManager createDirectoryAtPath:tempDownloadsDirPath withIntermediateDirectories:YES attributes:nil error:&error];
334
        //if (error)
335
        //    tempDownloadsDirPath = nil;
336
        // XXX create a dir using mktmps?
337
    }
338
    return tempDownloadsDirPath;    
339
}
340

    
341
- (NSString *)tempTrashDirPath {
342
    if (!tempTrashDirPath) {
343
        tempTrashDirPath = [[[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) objectAtIndex:0] 
344
                                  stringByAppendingPathComponent:[[NSBundle mainBundle] bundleIdentifier]] 
345
                                 stringByAppendingPathComponent:[NSString stringWithFormat:@"%@-TempTrash", 
346
                                                                 pithosAccount.uniqueName]];
347
        NSFileManager *fileManager = [NSFileManager defaultManager];
348
        BOOL isDirectory;
349
        BOOL fileExists = [fileManager fileExistsAtPath:tempTrashDirPath isDirectory:&isDirectory];
350
        NSError *error = nil;
351
        if (fileExists && !isDirectory)
352
            [fileManager removeItemAtPath:tempTrashDirPath error:&error];
353
        if (!error && !fileExists)
354
            [fileManager createDirectoryAtPath:tempTrashDirPath withIntermediateDirectories:YES attributes:nil error:&error];
355
        //if (error)
356
        //    tempTrashDirPath = nil;
357
        // XXX create a dir using mktmps?
358
    }
359
    return tempTrashDirPath;
360
}
361

    
362
- (void)setDirectoryPath:(NSString *)aDirectoryPath {
363
    if (aDirectoryPath && ![aDirectoryPath isEqualToString:directoryPath]) {
364
        [self resetDaemon];
365
        [self resetLocalStateWithAll:YES];
366
        directoryPath = [aDirectoryPath copy];
367
    }
368
}
369

    
370
- (void)setAccountsDictionary:(NSDictionary *)anAccountsDictionary {
371
    if (anAccountsDictionary && ![anAccountsDictionary isEqualToDictionary:accountsDictionary]) {
372
        BOOL reset = (accountsDictionary != nil);
373
        if (reset)
374
            [self resetDaemon];
375
        
376
        accountsDictionary = [anAccountsDictionary copy];
377
        
378
        accountsCount = [accountsDictionary count];
379
        self.accountsNames = [NSMutableArray arrayWithCapacity:accountsCount];
380
        self.accountsPithosContainers = [NSMutableDictionary dictionaryWithCapacity:accountsCount];
381
        NSDictionary *containersDictionary = [accountsDictionary objectForKey:@""];
382
        if (containersDictionary && [containersDictionary count]) {
383
            NSMutableArray *pithosContainers = [NSMutableArray arrayWithCapacity:[containersDictionary count]];
384
            for (NSString *containerName in containersDictionary) {
385
                ASIPithosContainer *pithosContainer = [ASIPithosContainer container];
386
                pithosContainer.name = containerName;
387
                [pithosContainers addObject:pithosContainer];
388
            }
389
            [accountsNames addObject:@""];
390
            [accountsPithosContainers setObject:pithosContainers forKey:@""];
391
        }
392
        for (NSString *accountName in accountsDictionary) {
393
            NSDictionary *containersDictionary = [accountsDictionary objectForKey:accountName];
394
            if (![accountsNames containsObject:accountName] && [containersDictionary count]) {
395
                NSMutableArray *pithosContainers = [NSMutableArray arrayWithCapacity:[containersDictionary count]];
396
                for (NSString *containerName in containersDictionary) {
397
                    ASIPithosContainer *pithosContainer = [ASIPithosContainer container];
398
                    pithosContainer.name = containerName;
399
                    [pithosContainers addObject:pithosContainer];
400
                }
401
                [accountsNames addObject:accountName];
402
                [accountsPithosContainers setObject:pithosContainers forKey:accountName];
403
            }
404
        }
405

    
406
        if (reset)
407
            [self resetLocalStateWithAll:NO];
408
    }
409
}
410

    
411
- (void)setPithos:(ASIPithos *)aPithos {
412
    if (!aPithos) {
413
        return;
414
    } else if (!pithos) {
415
        pithos = [ASIPithos pithos];
416
    } else if (![aPithos.tokensURL isEqualToString:pithos.tokensURL] || ![aPithos.authUser isEqualToString:pithos.authUser]) {
417
        // If the authURL or the authUser actually changed, reset daemon and local state.
418
        [self resetDaemon];
419
        [self resetLocalStateWithAll:YES];
420
    }
421
    pithos.authToken = [aPithos.authToken copy];
422
    pithos.authUser = [aPithos.authUser copy];
423
    pithos.ignoreSSLErrors = aPithos.ignoreSSLErrors;
424
    pithos.tokensURL = [aPithos.tokensURL copy];
425
    pithos.storageURLPrefix = [aPithos.storageURLPrefix copy];
426
    pithos.publicURLPrefix = [aPithos.publicURLPrefix copy];
427
    pithos.userCatalogURL = [aPithos.userCatalogURL copy];
428
}
429

    
430
#pragma mark -
431
#pragma mark Helper Methods
432

    
433
- (BOOL)createSyncDirectory:(NSString *)dirPath {
434
    NSFileManager *fileManager = [NSFileManager defaultManager];
435
    BOOL isDirectory;
436
    NSError *error = nil;
437
    if (![fileManager fileExistsAtPath:dirPath isDirectory:&isDirectory]) {
438
        if (![fileManager createDirectoryAtPath:dirPath withIntermediateDirectories:YES attributes:nil error:&error] || error) {
439
            [PithosUtilities fileActionFailedAlertWithTitle:@"Local Sync Directory Error" 
440
                                                    message:[NSString stringWithFormat:@"Cannot create local sync directory at '%@'", 
441
                                                             dirPath] 
442
                                                      error:error];
443
            return NO;
444
        }
445
    } else if (!isDirectory) {
446
        [PithosUtilities fileActionFailedAlertWithTitle:@"Local Sync Directory Error" 
447
                                                message:[NSString stringWithFormat:@"File already exists at the local sync directory path at '%@'", 
448
                                                         dirPath] 
449
                                                  error:nil];
450
        return NO;
451
    }
452
    return YES;
453
}
454

    
455
- (NSString *)relativeDirPathForAccount:(NSString *)accountName container:(NSString *)containerName {
456
    if ([accountName isEqualToString:@""]) {
457
        return containerName;
458
    } else {
459
        NSMutableDictionary *displaynameDictionary = [userCatalog objectForKey:accountName];
460
        if ([[displaynameDictionary objectForKey:@"suffix"] unsignedIntegerValue] == 0) {
461
            return [[@"shared with me" stringByAppendingPathComponent:[displaynameDictionary objectForKey:@"displayname"]]
462
                    stringByAppendingPathComponent:containerName];
463
        } else {
464
            return [[@"shared with me" stringByAppendingPathComponent:[NSString stringWithFormat:@"%@ (%@)",
465
                                                                       [displaynameDictionary objectForKey:@"displayname"],
466
                                                                       [displaynameDictionary objectForKey:@"suffix"]]]
467
                    stringByAppendingPathComponent:containerName];
468
        }
469
    }
470
}
471

    
472
- (NSString *)dirPathForAccount:(NSString *)accountName container:(NSString *)containerName {
473
    return [directoryPath stringByAppendingPathComponent:[self relativeDirPathForAccount:accountName container:containerName]];
474
}
475

    
476
#pragma mark -
477
#pragma mark Sync
478

    
479
- (void)syncOperationStarted {
480
    @synchronized(self) {
481
        syncOperationCount++;
482
    }
483
}
484

    
485
- (void)syncOperationFinishedWithSuccess:(BOOL)operationSuccessfull {
486
    @synchronized(self) {
487
        if (!operationSuccessfull)
488
            syncIncomplete = YES;
489
        if (syncOperationCount == 0) {
490
            // XXX This shouldn't happen, maybe change operationCount to a BOOL as operationsPending in browser
491
            DLog(@"Sync::WARNING: tried to decrease syncOperationCount when 0");
492
            return;
493
        }
494
        syncOperationCount--;
495
        if (syncOperationCount == 0) {
496
            if (!syncIncomplete) {
497
                self.lastCompletedSync = [NSDate date];
498
                dispatch_async(dispatch_get_main_queue(), ^{
499
                    [activityFacility startAndEndActivityWithType:PithosActivityOther 
500
                                                          message:[NSString stringWithFormat:@"Sync: Completed %@", lastCompletedSync] 
501
                                                    pithosAccount:pithosAccount];
502
                    
503
                });
504
            }
505
            [self emptyTempTrash];
506
            if (newSyncRequested && daemonActive)
507
                [self sync];
508
        }
509
    }
510
}
511

    
512
- (BOOL)isSyncing {
513
    @synchronized(self) {
514
        return ((syncOperationCount > 0) && daemonActive);
515
    }
516
}
517

    
518
- (void)syncLate {
519
    @synchronized(self) {
520
        if ([self isSyncing])
521
            syncLate = YES;
522
    }
523
}
524

    
525
- (void)sync {
526
    @synchronized(self) {
527
        if ([self isSyncing]) {
528
            // If at least one operation is running return
529
            newSyncRequested = YES;
530
            return;
531
        } else if (daemonActive && accountsCount) {
532
            // The first operation is the server listing
533
            [self syncOperationStarted];
534
            newSyncRequested = NO;
535
            syncIncomplete = NO;
536
            syncLate = NO;
537
        } else {
538
            return;
539
        }
540
    }
541

    
542
    if (![self createSyncDirectory:directoryPath]) {
543
        [self syncOperationFinishedWithSuccess:NO];
544
        return;
545
    }
546
    // Update user catalog for accountsNames.
547
    ASIPithosRequest *userCatalogRequest = [pithosAccount updateUserCatalogForDisplaynames:nil UUIDs:accountsNames];
548
    if (userCatalogRequest.error || ((userCatalogRequest.responseStatusCode != 200) && (userCatalogRequest.responseStatusCode != 404))) {
549
        // Update failed try sync again later.
550
        [self syncOperationFinishedWithSuccess:NO];
551
        return;
552
    }
553
    for (NSString *accountName in accountsNames) {
554
        if (![accountName isEqualToString:@""]) {
555
            // If 404, displayname will be same as accountName.
556
            NSString *displayname = [pithosAccount displaynameForUUID:accountName safe:YES];
557
            NSMutableDictionary *displaynameDictionary = [userCatalog objectForKey:accountName];
558
            if (!displaynameDictionary) {
559
                // New entry in the user catalog. Determine suffix discriminator (0 is for no suffix).
560
                NSNumber *suffix = [NSNumber numberWithInteger:0];
561
                for (NSMutableDictionary *otherDisplaynameDictionary in [userCatalog objectEnumerator]) {
562
                    if ([[otherDisplaynameDictionary objectForKey:@"displayname"] isEqualToString:displayname] &&
563
                        [[otherDisplaynameDictionary objectForKey:@"suffix"] isGreaterThanOrEqualTo:suffix]) {
564
                        suffix = [NSNumber numberWithUnsignedInteger:([[otherDisplaynameDictionary objectForKey:@"suffix"] unsignedIntegerValue] + 1)];
565
                    }
566
                }
567
                [userCatalog setObject:[NSMutableDictionary dictionaryWithObjectsAndKeys:
568
                                        displayname, @"displayname",
569
                                        suffix, @"suffix",
570
                                        nil]
571
                                forKey:accountName];
572
                // Save user catalog.
573
                [NSKeyedArchiver archiveRootObject:userCatalog toFile:self.userCatalogFilePath];
574
            } else if (![[displaynameDictionary objectForKey:@"displayname"] isEqualToString:displayname]) {
575
                // First determine new suffix.
576
                NSNumber *suffix = [NSNumber numberWithInteger:0];
577
                for (NSMutableDictionary *otherDisplaynameDictionary in [userCatalog objectEnumerator]) {
578
                    if ([[otherDisplaynameDictionary objectForKey:@"displayname"] isEqualToString:displayname] &&
579
                        [[otherDisplaynameDictionary objectForKey:@"suffix"] isGreaterThanOrEqualTo:suffix]) {
580
                        suffix = [NSNumber numberWithUnsignedInteger:([[otherDisplaynameDictionary objectForKey:@"suffix"] unsignedIntegerValue] + 1)];
581
                    }
582
                }
583
                // Compute old and new sync account dirPaths.
584
                NSString *accountDirPath = (([[displaynameDictionary objectForKey:@"suffix"] unsignedIntegerValue] == 0) ?
585
                                            [[directoryPath stringByAppendingPathComponent:@"shared with me"]
586
                                             stringByAppendingPathComponent:[displaynameDictionary objectForKey:@"displayname"]] :
587
                                            [[directoryPath stringByAppendingPathComponent:@"shared with me"]
588
                                             stringByAppendingPathComponent:[NSString stringWithFormat:@"%@ (%@)",
589
                                                                             [displaynameDictionary objectForKey:@"displayname"],
590
                                                                             [displaynameDictionary objectForKey:@"suffix"]]]);
591
                NSString *newAccountDirPath = (([suffix unsignedIntegerValue] == 0) ?
592
                                               [[directoryPath stringByAppendingPathComponent:@"shared with me"]
593
                                                stringByAppendingPathComponent:displayname] :
594
                                               [[directoryPath stringByAppendingPathComponent:@"shared with me"]
595
                                                stringByAppendingPathComponent:[NSString stringWithFormat:@"%@ (%@)",
596
                                                                                displayname, suffix]]);
597
                // Check if the old account sync directory exists.
598
                NSFileManager *fileManager = [NSFileManager defaultManager];
599
                BOOL isDirectory;
600
                NSError *error = nil;
601
                if ([fileManager fileExistsAtPath:accountDirPath isDirectory:&isDirectory] && isDirectory &&
602
                    (![fileManager moveItemAtPath:accountDirPath toPath:newAccountDirPath error:&error] || error)) {
603
                    // If so try to move it. We know that the containing directory "shared with me" exists.
604
                    [PithosUtilities fileActionFailedAlertWithTitle:@"Move Directory Error"
605
                                                            message:[NSString stringWithFormat:@"Cannot move directory at '%@' to '%@'",
606
                                                                     accountDirPath, newAccountDirPath]
607
                                                              error:error];
608
                    [self syncOperationFinishedWithSuccess:NO];
609
                    return;
610
                }
611
                // Else the new account sync directory will be created afterwards.
612
                // Fix filePaths of the stored local object states of the account.
613
                for (NSMutableDictionary *containerStoredLocalObjectStates in [[storedLocalObjectStates objectForKey:accountName] objectEnumerator]) {
614
                    for (PithosLocalObjectState *storedLocalObjectState in [containerStoredLocalObjectStates objectEnumerator]) {
615
                        if ([storedLocalObjectState.filePath hasPrefix:accountDirPath]) {
616
                            storedLocalObjectState.filePath = [newAccountDirPath stringByAppendingString:
617
                                                               [storedLocalObjectState.filePath substringFromIndex:accountDirPath.length]];
618
                        }
619
                    }
620
                }
621
                [self saveLocalState];
622
                // Finally update user catalog.
623
                [userCatalog setObject:[NSMutableDictionary dictionaryWithObjectsAndKeys:
624
                                        displayname, @"displayname",
625
                                        suffix, @"suffix",
626
                                        nil]
627
                                forKey:accountName];
628
                // Save user catalog.
629
                [NSKeyedArchiver archiveRootObject:userCatalog toFile:self.userCatalogFilePath];
630
            }
631
            // Else no change is required.
632
        }
633
    }
634

    
635
    for (NSString *accountName in accountsNames) {
636
        for (ASIPithosContainer *pithosContainer in [accountsPithosContainers objectForKey:accountName]) {
637
            if (![self createSyncDirectory:[self dirPathForAccount:accountName container:pithosContainer.name]]) {
638
                [self syncOperationFinishedWithSuccess:NO];
639
                return;
640
            }
641
        }
642
    }
643

    
644
    self.remoteObjects = [NSMutableDictionary dictionaryWithCapacity:accountsCount];
645
    for (NSString *accountName in accountsNames) {
646
        [remoteObjects setObject:[NSMutableDictionary dictionary] forKey:accountName];
647
    }
648
    accountsIndex = 0;
649
    containersIndex = 0;
650
    NSString *accountName = [accountsNames objectAtIndex:accountsIndex];
651
    ASIPithosContainer *pithosContainer = [[accountsPithosContainers objectForKey:accountName] objectAtIndex:containersIndex];
652
    ASIPithosContainerRequest *containerRequest = [ASIPithosContainerRequest listObjectsRequestWithPithos:pithos 
653
                                                                                            containerName:pithosContainer.name 
654
                                                                                                    limit:0 
655
                                                                                                   marker:nil 
656
                                                                                                   prefix:nil 
657
                                                                                                delimiter:nil 
658
                                                                                                     path:nil 
659
                                                                                                     meta:nil 
660
                                                                                                   shared:NO 
661
                                                                                                    until:nil 
662
                                                                                          ifModifiedSince:pithosContainer.lastModified];
663
    if (![accountName isEqualToString:@""])
664
        [containerRequest setRequestUserFromDefaultTo:accountName withPithos:pithos];
665
    containerRequest.delegate = self;
666
    containerRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:);
667
    containerRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:);
668
    PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityOther 
669
                                                               message:@"Sync: Getting server listing" 
670
                                                         pithosAccount:pithosAccount];
671
    containerRequest.userInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys:
672
                                 activity, @"activity", 
673
                                 @"Sync: Getting server listing (stopped)", @"stoppedActivityMessage", 
674
                                 @"Sync: Getting server listing (failed)", @"failedActivityMessage", 
675
                                 @"Sync: Getting server listing (finished)", @"finishedActivityMessage", 
676
                                 [NSNumber numberWithInteger:NSOperationQueuePriorityHigh], @"priority", 
677
                                 [NSNumber numberWithUnsignedInteger:10], @"retries", 
678
                                 NSStringFromSelector(@selector(listRequestFinished:)), @"didFinishSelector", 
679
                                 NSStringFromSelector(@selector(listRequestFailed:)), @"didFailSelector", 
680
                                 nil];
681
    [networkQueue addOperation:[PithosUtilities prepareRequest:containerRequest priority:NSOperationQueuePriorityHigh]];
682
}
683

    
684
- (void)emptyTempTrash {
685
    @autoreleasepool {
686
        NSString *trashDirPath = self.tempTrashDirPath;
687
        if (trashDirPath) {
688
            NSFileManager *fileManager = [NSFileManager defaultManager];
689
            NSError *error = nil;
690
//          NSArray *subPaths = [[NSFileManager defaultManager] subpathsOfDirectoryAtPath:trashDirPath error:&error];
691
            NSArray *subPaths = [fileManager contentsOfDirectoryAtPath:trashDirPath error:&error];
692
            if (error) {
693
                [PithosUtilities fileActionFailedAlertWithTitle:@"Directory Contents Error" 
694
                                                        message:[NSString stringWithFormat:@"Cannot get contents of directory at '%@'", trashDirPath] 
695
                                                          error:error];
696
                return;
697
            }
698
            if ([subPaths count]) {
699
//              NSMutableArray *subURLs = [NSMutableArray arrayWithCapacity:[subPaths count]];
700
//              for (NSString *subPath in subPaths) {
701
//                  [subURLs addObject:[NSURL fileURLWithPath:[trashDirPath stringByAppendingPathComponent:subPath]]];
702
//              }
703
//              [self syncOperationStarted];
704
//              [[NSWorkspace sharedWorkspace] recycleURLs:subURLs completionHandler:^(NSDictionary *newURLs, NSError *error) {
705
//                  if (error) {
706
//                      [PithosUtilities fileActionFailedAlertWithTitle:@"Move to Trash Error"
707
//                                                              message:@"Cannot move files to Trash"
708
//                                                                  error:error];
709
//                  }
710
//                  [self syncOperationFinishedWithSuccess:YES];
711
//              }];
712
                for (NSString *subPath in subPaths) {
713
                    NSString *subFilePath = [trashDirPath stringByAppendingPathComponent:subPath];
714
                    error = nil;
715
                    if (![fileManager removeItemAtPath:subFilePath error:&error] || error)
716
                        [PithosUtilities fileActionFailedAlertWithTitle:@"Remove File Error" 
717
                                                                message:[NSString stringWithFormat:@"Cannot remove file at '%@'", subFilePath] 
718
                                                                  error:error];
719
                }
720
            }
721
        }
722
    }
723
}
724

    
725
- (BOOL)moveToTempTrashFile:(NSString *)filePath 
726
                accountName:(NSString *)accountName 
727
            pithosContainer:(ASIPithosContainer *)pithosContainer {
728
    @autoreleasepool {
729
        if (!self.tempTrashDirPath)
730
            return NO;
731
        NSFileManager *fileManager = [NSFileManager defaultManager];
732
        BOOL isDirectory;
733
        BOOL fileExists = [fileManager fileExistsAtPath:filePath isDirectory:&isDirectory];
734
        NSError *error = nil;
735
        NSString *newFilePath = [filePath stringByReplacingOccurrencesOfString:directoryPath withString:self.tempTrashDirPath];
736
        NSString *newDirPath = [newFilePath stringByDeletingLastPathComponent];
737
        if (fileExists && isDirectory) {
738
            NSArray *subPaths = [fileManager subpathsOfDirectoryAtPath:filePath error:&error];
739
            if (error) {
740
                [PithosUtilities fileActionFailedAlertWithTitle:@"Directory Contents Error" 
741
                                                        message:[NSString stringWithFormat:@"Cannot get contents of directory at '%@'", filePath] 
742
                                                          error:error];
743
                return NO;
744
            }
745
            if (![fileManager createDirectoryAtPath:newDirPath withIntermediateDirectories:YES attributes:nil error:&error] || error) {
746
                [PithosUtilities fileActionFailedAlertWithTitle:@"Create Directory Error" 
747
                                                        message:[NSString stringWithFormat:@"Cannot create directory at '%@'", newDirPath] 
748
                                                          error:error];
749
                return NO;
750
            }
751
            if (![fileManager moveItemAtPath:filePath toPath:newFilePath error:&error] || error) {
752
                [PithosUtilities fileActionFailedAlertWithTitle:@"Move File Error" 
753
                                                        message:[NSString stringWithFormat:@"Cannot move file at '%@' to '%@'", 
754
                                                                 filePath, newFilePath] 
755
                                                          error:error];
756
                return NO;
757
            }
758
            PithosLocalObjectState *currentState = [currentLocalObjectStates objectForKey:filePath];
759
            if (currentState) {
760
                currentState.filePath = newFilePath;
761
                [currentLocalObjectStates setObject:currentState forKey:newFilePath];
762
                [currentLocalObjectStates removeObjectForKey:filePath];        
763
            } else {
764
                [currentLocalObjectStates setObject:[PithosLocalObjectState localObjectStateWithFile:newFilePath 
765
                                                                                           blockHash:pithosContainer.blockHash 
766
                                                                                           blockSize:pithosContainer.blockSize] 
767
                                             forKey:newFilePath];
768
            }
769
            for (NSString *subPath in subPaths) {
770
                NSString *subFilePath = [filePath stringByAppendingPathComponent:subPath];
771
                NSString *newSubFilePath = [subFilePath stringByReplacingOccurrencesOfString:directoryPath
772
                                                                                  withString:self.tempTrashDirPath];
773
                currentState = [currentLocalObjectStates objectForKey:subFilePath];
774
                if (currentState) {
775
                    currentState.filePath = newSubFilePath;
776
                    [currentLocalObjectStates setObject:currentState forKey:newSubFilePath];
777
                    [currentLocalObjectStates removeObjectForKey:subFilePath];
778
                } else {
779
                    [currentLocalObjectStates setObject:[PithosLocalObjectState localObjectStateWithFile:newSubFilePath 
780
                                                                                               blockHash:pithosContainer.blockHash 
781
                                                                                               blockSize:pithosContainer.blockSize] 
782
                                                 forKey:newSubFilePath];
783
                }        
784
            }
785
        } else if (fileExists && !isDirectory) {
786
            if (![fileManager createDirectoryAtPath:newDirPath withIntermediateDirectories:YES attributes:nil error:&error] || error) {
787
                [PithosUtilities fileActionFailedAlertWithTitle:@"Create Directory Error" 
788
                                                        message:[NSString stringWithFormat:@"Cannot create directory at '%@'", newDirPath] 
789
                                                          error:error];
790
                return NO;
791
            }
792
            if (![fileManager moveItemAtPath:filePath toPath:newFilePath error:&error] || error) {
793
                [PithosUtilities fileActionFailedAlertWithTitle:@"Move File Error" 
794
                                                        message:[NSString stringWithFormat:@"Cannot move file at '%@' to '%@'", 
795
                                                                 filePath, newFilePath] 
796
                                                          error:error];
797
                return NO;
798
            }
799
            PithosLocalObjectState *currentState = [currentLocalObjectStates objectForKey:filePath];
800
            if (currentState) {
801
                currentState.filePath = newFilePath;
802
                [currentLocalObjectStates setObject:currentState forKey:newFilePath];
803
                [currentLocalObjectStates removeObjectForKey:filePath];        
804
            } else {
805
                [currentLocalObjectStates setObject:[PithosLocalObjectState localObjectStateWithFile:newFilePath 
806
                                                                                           blockHash:pithosContainer.blockHash 
807
                                                                                           blockSize:pithosContainer.blockSize] 
808
                                             forKey:newFilePath];
809
            }
810
        }
811
    }
812
    return YES;
813
}
814

    
815
- (BOOL)findLocalCopyForObjectWithHash:(NSString *)hash forFile:(NSString *)filePath {
816
    if ([hash length] != 64)
817
        return NO;
818
    @autoreleasepool {
819
        PithosLocalObjectState *localState;
820
        NSFileManager *fileManager = [NSFileManager defaultManager];
821
        BOOL isDirectory;
822
        NSError *error = nil;
823
        for (NSString *localFilePath in [currentLocalObjectStates allKeys]) {
824
            localState = [currentLocalObjectStates objectForKey:localFilePath];
825
            if (!localState.isDirectory && [hash isEqualToString:localState.hash] && 
826
                [fileManager fileExistsAtPath:localFilePath isDirectory:&isDirectory] && !isDirectory) {
827
                if ([localFilePath hasPrefix:directoryPath]) {
828
                    if (![fileManager copyItemAtPath:localFilePath toPath:filePath error:&error] || error) {
829
                        [PithosUtilities fileActionFailedAlertWithTitle:@"Copy File Error" 
830
                                                                message:[NSString stringWithFormat:@"Cannot copy file at '%@' to '%@'", 
831
                                                                         localFilePath, filePath] 
832
                                                                  error:error];
833
                    } else {
834
                        return YES;
835
                    }
836
                } else if (self.tempTrashDirPath && [localFilePath hasPrefix:self.tempTrashDirPath]) {
837
                    if (![fileManager moveItemAtPath:localFilePath toPath:filePath error:&error] || error) {
838
                        [PithosUtilities fileActionFailedAlertWithTitle:@"Move File Error" 
839
                                                                message:[NSString stringWithFormat:@"Cannot move file at '%@' to '%@'", 
840
                                                                         localFilePath, filePath] 
841
                                                                  error:error];
842
                    } else {
843
                        localState.filePath = filePath;
844
                        [currentLocalObjectStates setObject:localState forKey:filePath];
845
                        [currentLocalObjectStates removeObjectForKey:localFilePath];
846
                        return YES;
847
                    }
848
                }
849
            }
850
        }
851
    }
852
    return NO;
853
}
854

    
855
- (void)updateLocalStateWithObject:(ASIPithosObject *)object
856
                     localFilePath:(NSString *)filePath 
857
                       accountName:(NSString *)accountName 
858
                   pithosContainer:(ASIPithosContainer *)pithosContainer {
859
    @autoreleasepool {
860
        NSFileManager *fileManager = [NSFileManager defaultManager];
861
        NSError *error;
862
        BOOL isDirectory;
863
        BOOL fileExists = [fileManager fileExistsAtPath:filePath isDirectory:&isDirectory];
864
        NSString *fileDirectoryPath = [filePath stringByDeletingLastPathComponent];
865
        NSMutableDictionary *containerStoredLocalObjectStates = [[storedLocalObjectStates objectForKey:accountName] 
866
                                                                 objectForKey:pithosContainer.name];
867
        PithosLocalObjectState *storedState = [containerStoredLocalObjectStates objectForKey:object.name];
868
        // Remote updated info
869
        NSError *remoteError;
870
        BOOL remoteIsDirectory;
871
        BOOL remoteObjectExists = [PithosUtilities objectExistsAtPithos:pithos 
872
                                                          containerName:pithosContainer.name 
873
                                                             objectName:object.name 
874
                                                                  error:&remoteError 
875
                                                            isDirectory:&remoteIsDirectory 
876
                                                         sharingAccount:([accountName isEqualToString:@""] ? nil : accountName)];
877
        if (!object || !object.objectHash) {
878
            // Delete local object
879
            if (![accountName isEqualToString:@""])
880
                // If "shared with me" skip
881
                return;
882
            if (remoteObjectExists) {
883
                // Remote object created in the meantime, just mark the sync cycle as incomplete, but do delete the local object
884
                syncIncomplete = YES;
885
            }
886
            DLog(@"Sync::delete local object: %@", filePath);
887
            if (!fileExists || [self moveToTempTrashFile:filePath accountName:accountName pithosContainer:pithosContainer]) {
888
                dispatch_async(dispatch_get_main_queue(), ^{
889
                    [activityFacility startAndEndActivityWithType:PithosActivityOther 
890
                                                          message:[NSString stringWithFormat:@"Sync: Deleting '%@/%@' locally (finished)", 
891
                                                                   pithosContainer.name, object.name] 
892
                                                    pithosAccount:pithosAccount];
893
                });
894
                [containerStoredLocalObjectStates removeObjectForKey:object.name];
895
                [self saveLocalState];
896
            }
897
        } else if ([PithosUtilities isContentTypeDirectory:object.contentType]) {
898
            // Create local directory object
899
            if (!remoteObjectExists || !remoteIsDirectory) {
900
                // Remote directory object deleted or changed to a file object in the meantime, mark the sync cycle as incomplete and skip
901
                syncIncomplete = YES;
902
                return;
903
            }
904
            DLog(@"Sync::create local directory object: %@", filePath);
905
            BOOL directoryCreated = NO;
906
            if (!fileExists || (!isDirectory && [self moveToTempTrashFile:filePath accountName:accountName pithosContainer:pithosContainer])) {
907
                DLog(@"Sync::local directory object doesn't exist: %@", filePath);
908
                error = nil;
909
                if (![fileManager createDirectoryAtPath:filePath withIntermediateDirectories:YES attributes:nil error:&error] || error) {
910
                    [PithosUtilities fileActionFailedAlertWithTitle:@"Create Directory Error" 
911
                                                            message:[NSString stringWithFormat:@"Cannot create directory at '%@'", filePath] 
912
                                                              error:error];
913
                } else {
914
                    directoryCreated = YES;
915
                    storedState.filePath = filePath;
916
                    storedState.isDirectory = YES;
917
                    [self saveLocalState];
918
                }
919
            } else {
920
                DLog(@"Sync::local directory object exists: %@", filePath);
921
                directoryCreated = YES;
922
            }
923
            if (directoryCreated)
924
                dispatch_async(dispatch_get_main_queue(), ^{
925
                    [activityFacility startAndEndActivityWithType:PithosActivityOther 
926
                                                          message:[NSString stringWithFormat:@"Sync: Creating directory '%@/%@' locally (finished)", 
927
                                                                   [self relativeDirPathForAccount:accountName container:pithosContainer.name], 
928
                                                                   object.name] 
929
                                                    pithosAccount:pithosAccount];
930
                });
931
        } else if (object.bytes == 0) {
932
            // Create local object with zero length
933
            if (!remoteObjectExists || remoteIsDirectory) {
934
                // Remote file object deleted or changed to a directory object in the meantime, mark the sync cycle as incomplete and skip
935
                syncIncomplete = YES;
936
                return;
937
            }
938
            DLog(@"Sync::create local zero length object: %@", filePath);
939
            BOOL fileCreated = NO;
940
            if (!fileExists || 
941
                ((isDirectory || [PithosUtilities bytesOfFile:filePath]) && 
942
                 [self moveToTempTrashFile:filePath accountName:accountName pithosContainer:pithosContainer])) {
943
                DLog(@"Sync::local zero length object doesn't exist: %@", filePath);
944
                // Create directory of the file, if it doesn't exist
945
                error = nil;
946
                if (![PithosUtilities safeCreateDirectory:fileDirectoryPath error:&error]) {
947
                    dispatch_async(dispatch_get_main_queue(), ^{
948
                        [PithosUtilities fileActionFailedAlertWithTitle:@"Create Directory Error" 
949
                                                                message:[NSString stringWithFormat:@"Cannot create directory at '%@'", fileDirectoryPath] 
950
                                                                  error:error];
951
                    });
952
                }
953
                error = nil;
954
                if (![fileManager createFileAtPath:filePath contents:nil attributes:nil]) {
955
                    [PithosUtilities fileActionFailedAlertWithTitle:@"Create File Error" 
956
                                                            message:[NSString stringWithFormat:@"Cannot create file at '%@'", filePath] 
957
                                                              error:error];
958
                } else {
959
                    fileCreated = YES;
960
                    storedState.filePath = filePath;
961
                    storedState.hash = object.objectHash;
962
                    storedState.tmpFilePath = nil;
963
                    [self saveLocalState];
964
                }
965
            } else {
966
                DLog(@"Sync::local zero length object exists: %@", filePath);
967
                fileCreated = YES;
968
            }
969
            if (fileCreated)
970
                dispatch_async(dispatch_get_main_queue(), ^{
971
                    [activityFacility startAndEndActivityWithType:PithosActivityOther 
972
                                                          message:[NSString stringWithFormat:@"Sync: Downloading '%@/%@' (100%%)", 
973
                                                                   [self relativeDirPathForAccount:accountName container:pithosContainer.name], 
974
                                                                   object.name] 
975
                                                    pithosAccount:pithosAccount];
976
                });
977
        } else if (storedState.tmpFilePath == nil) {
978
            // Create new local object
979
            if (!remoteObjectExists || remoteIsDirectory) {
980
                // Remote file object deleted or changed to a directory object in the meantime, mark the sync cycle as incomplete and skip
981
                syncIncomplete = YES;
982
                return;
983
            }
984
            // Create directory of the file, if it doesn't exist
985
            error = nil;
986
            if (![PithosUtilities safeCreateDirectory:fileDirectoryPath error:&error]) {
987
                [PithosUtilities fileActionFailedAlertWithTitle:@"Create Directory Error" 
988
                                                        message:[NSString stringWithFormat:@"Cannot create directory at '%@'", fileDirectoryPath] 
989
                                                          error:error];
990
            }
991
            // Check first if a local copy exists
992
            if ([self findLocalCopyForObjectWithHash:object.objectHash forFile:filePath]) {
993
                storedState.filePath = filePath;
994
                storedState.hash = object.objectHash;
995
                [self saveLocalState];
996
                dispatch_async(dispatch_get_main_queue(), ^{
997
                    [activityFacility startAndEndActivityWithType:PithosActivityOther 
998
                                                          message:[NSString stringWithFormat:@"Sync: Downloading '%@/%@' (100%%)", 
999
                                                                   [self relativeDirPathForAccount:accountName container:pithosContainer.name], 
1000
                                                                   object.name] 
1001
                                                    pithosAccount:pithosAccount];
1002
                });
1003
            } else {
1004
                [self syncOperationStarted];
1005
                __block ASIPithosObjectRequest *objectRequest = [PithosUtilities objectBlockDataRequestWithPithos:pithos 
1006
                                                                                                    containerName:pithosContainer.name 
1007
                                                                                                           object:object 
1008
                                                                                                       blockIndex:0 
1009
                                                                                                        blockSize:pithosContainer.blockSize];
1010
                if (![accountName isEqualToString:@""])
1011
                    [objectRequest setRequestUserFromDefaultTo:accountName withPithos:pithos];
1012
                objectRequest.delegate = self;
1013
                objectRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:);
1014
                objectRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:);
1015
                NSString *messagePrefix = [NSString stringWithFormat:@"Sync: Downloading '%@/%@'", 
1016
                                           [self relativeDirPathForAccount:accountName container:pithosContainer.name], object.name];
1017
                PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityDownload 
1018
                                                                           message:[messagePrefix stringByAppendingString:@" (0%%)"] 
1019
                                                                        totalBytes:object.bytes 
1020
                                                                      currentBytes:0 
1021
                                                                     pithosAccount:pithosAccount];
1022
                dispatch_async(dispatch_get_main_queue(), ^{
1023
                    [activityFacility updateActivity:activity withMessage:activity.message];
1024
                });
1025
                objectRequest.userInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys:
1026
                                          accountName, @"accountName", 
1027
                                          pithosContainer, @"pithosContainer", 
1028
                                          object, @"pithosObject", 
1029
                                          messagePrefix, @"messagePrefix", 
1030
                                          [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, (NSUInteger)ceil((object.bytes +0.0)/(pithosContainer.blockSize + 0.0)))], @"missingBlocks", 
1031
                                          [NSNumber numberWithUnsignedInteger:0], @"missingBlockIndex", 
1032
                                          filePath, @"filePath", 
1033
                                          activity, @"activity", 
1034
                                          [messagePrefix stringByAppendingString:@" (stopped)"], @"stoppedActivityMessage", 
1035
                                          [messagePrefix stringByAppendingString:@" (failed)"], @"failedActivityMessage", 
1036
                                          [messagePrefix stringByAppendingString:@" (100%%)"], @"finishedActivityMessage", 
1037
                                          [NSNumber numberWithInteger:NSOperationQueuePriorityNormal], @"priority", 
1038
                                          [NSNumber numberWithUnsignedInteger:10], @"retries", 
1039
                                          NSStringFromSelector(@selector(downloadObjectBlockFinished:)), @"didFinishSelector", 
1040
                                          NSStringFromSelector(@selector(requestFailed:)), @"didFailSelector", 
1041
                                          nil];
1042
                [objectRequest setBytesReceivedBlock:^(unsigned long long size, unsigned long long total){
1043
                    [activityFacility updateActivity:activity 
1044
                                         withMessage:[NSString stringWithFormat:@"%@ (%.0f%%)", 
1045
                                                      messagePrefix, (100*(activity.currentBytes + size + 0.0)/(activity.totalBytes + 0.0))] 
1046
                                          totalBytes:activity.totalBytes 
1047
                                        currentBytes:(activity.currentBytes + size)];
1048
                }];
1049
                [networkQueue addOperation:[PithosUtilities prepareRequest:objectRequest priority:NSOperationQueuePriorityNormal]];
1050
            }
1051
        } else {
1052
            // Resume local object download
1053
            if (!remoteObjectExists || remoteIsDirectory) {
1054
                // Remote file object deleted or changed to a directory object in the meantime, mark the sync cycle as incomplete and skip
1055
                syncIncomplete = YES;
1056
                return;
1057
            }
1058
            // Create directory of the file, if it doesn't exist
1059
            error = nil;
1060
            if (![PithosUtilities safeCreateDirectory:fileDirectoryPath error:&error]) {
1061
                [PithosUtilities fileActionFailedAlertWithTitle:@"Create Directory Error" 
1062
                                                        message:[NSString stringWithFormat:@"Cannot create directory at '%@'", fileDirectoryPath] 
1063
                                                          error:error];
1064
            }
1065
            // Check first if a local copy exists
1066
            if ([self findLocalCopyForObjectWithHash:object.objectHash forFile:filePath]) {
1067
                storedState.filePath = filePath;
1068
                storedState.hash = object.objectHash;
1069
                // Delete incomplete temp download
1070
                storedState.tmpFilePath = nil;
1071
                [self saveLocalState];
1072
                dispatch_async(dispatch_get_main_queue(), ^{
1073
                    [activityFacility startAndEndActivityWithType:PithosActivityOther 
1074
                                                          message:[NSString stringWithFormat:@"Sync: Downloading '%@/%@' (100%%)", 
1075
                                                                   [self relativeDirPathForAccount:accountName container:pithosContainer.name], 
1076
                                                                   object.name] 
1077
                                                    pithosAccount:pithosAccount];
1078
                });
1079
            } else {
1080
                [self syncOperationStarted];
1081
                ASIPithosObjectRequest *objectRequest = [ASIPithosObjectRequest objectHashmapRequestWithPithos:pithos 
1082
                                                                                                 containerName:pithosContainer.name 
1083
                                                                                                    objectName:object.name];
1084
                if (![accountName isEqualToString:@""])
1085
                    [objectRequest setRequestUserFromDefaultTo:accountName withPithos:pithos];
1086
                objectRequest.delegate = self;
1087
                objectRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:);
1088
                objectRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:);
1089
                NSString *messagePrefix = [NSString stringWithFormat:@"Sync: Downloading '%@/%@'", 
1090
                                           [self relativeDirPathForAccount:accountName container:pithosContainer.name], object.name];
1091
                PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityDownload 
1092
                                                                           message:[messagePrefix stringByAppendingString:@" (0%%)"] 
1093
                                                                        totalBytes:object.bytes 
1094
                                                                      currentBytes:0 
1095
                                                                     pithosAccount:pithosAccount];
1096
                dispatch_async(dispatch_get_main_queue(), ^{
1097
                    [activityFacility updateActivity:activity withMessage:activity.message];
1098
                });
1099
                objectRequest.userInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys:
1100
                                          accountName, @"accountName", 
1101
                                          pithosContainer, @"pithosContainer", 
1102
                                          object, @"pithosObject", 
1103
                                          messagePrefix, @"messagePrefix", 
1104
                                          filePath, @"filePath", 
1105
                                          activity, @"activity", 
1106
                                          [messagePrefix stringByAppendingString:@" (stopped)"], @"stoppedActivityMessage", 
1107
                                          [messagePrefix stringByAppendingString:@" (failed)"], @"failedActivityMessage", 
1108
                                          [messagePrefix stringByAppendingString:@" (100%%)"], @"finishedActivityMessage", 
1109
                                          [NSNumber numberWithInteger:NSOperationQueuePriorityNormal], @"priority", 
1110
                                          [NSNumber numberWithUnsignedInteger:10], @"retries", 
1111
                                          NSStringFromSelector(@selector(downloadObjectHashMapFinished:)), @"didFinishSelector", 
1112
                                          NSStringFromSelector(@selector(requestFailed:)), @"didFailSelector", 
1113
                                          nil];
1114
                [networkQueue addOperation:[PithosUtilities prepareRequest:objectRequest priority:NSOperationQueuePriorityNormal]];
1115
            }
1116
        }
1117
    }
1118
}
1119

    
1120
-(void)updateServerStateWithCurrentState:(PithosLocalObjectState *)currentState 
1121
                                  object:(ASIPithosObject *)object 
1122
                           localFilePath:(NSString *)filePath 
1123
                             accountName:(NSString *)accountName 
1124
                         pithosContainer:(ASIPithosContainer *)pithosContainer {
1125
    @autoreleasepool {
1126
        [self syncOperationStarted];
1127
        NSFileManager *fileManager = [NSFileManager defaultManager];
1128
        BOOL isDirectory;
1129
        BOOL fileExists = [fileManager fileExistsAtPath:filePath isDirectory:&isDirectory];
1130
        if (currentState.isDirectory) {
1131
            // Create remote directory object
1132
            if (![accountName isEqualToString:@""]) {
1133
                if (!object.allowedTo) {
1134
                    NSMutableDictionary *containerRemoteObjects = [[remoteObjects objectForKey:accountName] objectForKey:pithosContainer.name];
1135
                    NSString *objectAncestorName = [object.name stringByDeletingLastPathComponent];
1136
                    while ([objectAncestorName length] && !object.allowedTo) {
1137
                        object.allowedTo = [[containerRemoteObjects objectForKey:objectAncestorName] allowedTo];
1138
                        objectAncestorName = [objectAncestorName stringByDeletingLastPathComponent];
1139
                    }
1140
                }
1141
                if (![object.allowedTo isEqualToString:@"write"]) {
1142
                    // If read-only "shared with me" skip
1143
                    [self syncOperationFinishedWithSuccess:YES];
1144
                    return;
1145
                }
1146
            }
1147
            if (!fileExists || !isDirectory) {
1148
                // Local directory object deleted or changed to a file in the meantime, mark the sync cycle as incomplete and skip
1149
                [self syncOperationFinishedWithSuccess:NO];
1150
                return;
1151
            }
1152
            ASIPithosObjectRequest *objectRequest = [ASIPithosObjectRequest writeObjectDataRequestWithPithos:pithos 
1153
                                                                                               containerName:pithosContainer.name 
1154
                                                                                                  objectName:object.name 
1155
                                                                                                        eTag:nil 
1156
                                                                                                 contentType:@"application/directory" 
1157
                                                                                             contentEncoding:nil 
1158
                                                                                          contentDisposition:nil 
1159
                                                                                                    manifest:nil 
1160
                                                                                                     sharing:nil 
1161
                                                                                                    isPublic:ASIPithosObjectRequestPublicIgnore 
1162
                                                                                                    metadata:nil 
1163
                                                                                                        data:[NSData data]];
1164
            if (![accountName isEqualToString:@""])
1165
                [objectRequest setRequestUserFromDefaultTo:accountName withPithos:pithos];
1166
            objectRequest.delegate = self;
1167
            objectRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:);
1168
            objectRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:);
1169
            NSString *messagePrefix = [NSString stringWithFormat:@"Sync: Creating directory '%@/%@'", 
1170
                                       [self relativeDirPathForAccount:accountName container:pithosContainer.name], object.name];
1171
            PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityCreateDirectory 
1172
                                                                       message:messagePrefix 
1173
                                                                 pithosAccount:pithosAccount];
1174
            dispatch_async(dispatch_get_main_queue(), ^{
1175
                [activityFacility updateActivity:activity withMessage:activity.message];
1176
            });
1177
            objectRequest.userInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys:
1178
                                      accountName, @"accountName", 
1179
                                      pithosContainer, @"pithosContainer", 
1180
                                      object, @"pithosObject", 
1181
                                      messagePrefix, @"messagePrefix", 
1182
                                      activity, @"activity", 
1183
                                      [messagePrefix stringByAppendingString:@" (stopped)"], @"stoppedActivityMessage", 
1184
                                      [messagePrefix stringByAppendingString:@" (failed)"], @"failedActivityMessage", 
1185
                                      [messagePrefix stringByAppendingString:@" (finished)"], @"finishedActivityMessage", 
1186
                                      [NSNumber numberWithInteger:NSOperationQueuePriorityHigh], @"priority", 
1187
                                      [NSNumber numberWithUnsignedInteger:10], @"retries", 
1188
                                      NSStringFromSelector(@selector(uploadDirectoryObjectFinished:)), @"didFinishSelector", 
1189
                                      NSStringFromSelector(@selector(requestFailed:)), @"didFailSelector", 
1190
                                      nil];
1191
            [networkQueue addOperation:[PithosUtilities prepareRequest:objectRequest priority:NSOperationQueuePriorityHigh]];
1192
        } else if (![currentState exists]) {
1193
            // Delete remote object
1194
            if (![accountName isEqualToString:@""]) {
1195
                // If "shared with me" skip
1196
                [self syncOperationFinishedWithSuccess:YES];
1197
                return;
1198
            }
1199
            if (fileExists) {
1200
                // Local object created in the meantime, just mark the sync cycle as incomplete, but do delete the server object
1201
                syncIncomplete = YES;
1202
            }
1203
            if ([pithosContainer.name isEqualToString:@"trash"]) {
1204
                // Delete
1205
                ASIPithosObjectRequest *objectRequest = [ASIPithosObjectRequest deleteObjectRequestWithPithos:pithos 
1206
                                                                                                containerName:pithosContainer.name 
1207
                                                                                                   objectName:object.name];
1208
                objectRequest.delegate = self;
1209
                objectRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:);
1210
                objectRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:);
1211
                NSString *messagePrefix = [NSString stringWithFormat:@"Sync: Deleting '%@/%@'", 
1212
                                           [self relativeDirPathForAccount:accountName container:pithosContainer.name], object.name];
1213
                PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityDelete 
1214
                                                                           message:messagePrefix 
1215
                                                                     pithosAccount:pithosAccount];
1216
                dispatch_async(dispatch_get_main_queue(), ^{
1217
                    [activityFacility updateActivity:activity withMessage:activity.message];
1218
                });
1219
                objectRequest.userInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys:
1220
                                          accountName, @"accountName", 
1221
                                          pithosContainer, @"pithosContainer", 
1222
                                          object, @"pithosObject", 
1223
                                          messagePrefix, @"messagePrefix", 
1224
                                          activity, @"activity", 
1225
                                          [messagePrefix stringByAppendingString:@" (stopped)"], @"stoppedActivityMessage", 
1226
                                          [messagePrefix stringByAppendingString:@" (failed)"], @"failedActivityMessage", 
1227
                                          [messagePrefix stringByAppendingString:@" (finished)"], @"finishedActivityMessage", 
1228
                                          [NSNumber numberWithInteger:NSOperationQueuePriorityHigh], @"priority", 
1229
                                          [NSNumber numberWithUnsignedInteger:10], @"retries", 
1230
                                          NSStringFromSelector(@selector(deleteObjectFinished:)), @"didFinishSelector", 
1231
                                          NSStringFromSelector(@selector(requestFailed:)), @"didFailSelector", 
1232
                                          nil];
1233
                [networkQueue addOperation:[PithosUtilities prepareRequest:objectRequest priority:NSOperationQueuePriorityHigh]];
1234
            } else {
1235
                // Move to container trash
1236
                NSString *safeName;
1237
                if ([PithosUtilities isContentTypeDirectory:object.contentType])
1238
                    safeName = [PithosUtilities safeSubdirNameForPithos:pithos containerName:@"trash" subdirName:object.name];
1239
                else
1240
                    safeName = [PithosUtilities safeObjectNameForPithos:pithos containerName:@"trash" objectName:object.name];
1241
                if (safeName) {
1242
                    ASIPithosObjectRequest *objectRequest = [PithosUtilities moveObjectRequestWithPithos:pithos 
1243
                                                                                           containerName:pithosContainer.name 
1244
                                                                                              objectName:object.name 
1245
                                                                                destinationContainerName:@"trash" 
1246
                                                                                   destinationObjectName:safeName 
1247
                                                                                           checkIfExists:NO];
1248
                    if (objectRequest) {
1249
                        objectRequest.delegate = self;
1250
                        objectRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:);
1251
                        objectRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:);
1252
                        NSString *messagePrefix = [NSString stringWithFormat:@"Sync: Moving to trash '%@/%@'", 
1253
                                                   [self relativeDirPathForAccount:accountName container:pithosContainer.name], object.name];
1254
                        PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityDelete 
1255
                                                                                   message:messagePrefix 
1256
                                                                             pithosAccount:pithosAccount];
1257
                        dispatch_async(dispatch_get_main_queue(), ^{
1258
                            [activityFacility updateActivity:activity withMessage:activity.message];
1259
                        });
1260
                        objectRequest.userInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys:
1261
                                                  accountName, @"accountName", 
1262
                                                  pithosContainer, @"pithosContainer", 
1263
                                                  object, @"pithosObject", 
1264
                                                  messagePrefix, @"messagePrefix", 
1265
                                                  activity, @"activity", 
1266
                                                  [messagePrefix stringByAppendingString:@" (stopped)"], @"stoppedActivityMessage", 
1267
                                                  [messagePrefix stringByAppendingString:@" (failed)"], @"failedActivityMessage", 
1268
                                                  [messagePrefix stringByAppendingString:@" (finished)"], @"finishedActivityMessage", 
1269
                                                  [NSNumber numberWithInteger:NSOperationQueuePriorityHigh], @"priority", 
1270
                                                  [NSNumber numberWithUnsignedInteger:10], @"retries", 
1271
                                                  NSStringFromSelector(@selector(moveObjectToTrashFinished:)), @"didFinishSelector", 
1272
                                                  NSStringFromSelector(@selector(requestFailed:)), @"didFailSelector", 
1273
                                                  nil];
1274
                        [networkQueue addOperation:[PithosUtilities prepareRequest:objectRequest priority:NSOperationQueuePriorityHigh]];
1275
                    } else {
1276
                        [self syncOperationFinishedWithSuccess:NO];
1277
                    }
1278
                } else {
1279
                    [self syncOperationFinishedWithSuccess:NO];
1280
                }
1281
            }
1282
        } else {
1283
            // Upload file to remote object
1284
            if (![accountName isEqualToString:@""]) {
1285
                if (!object.allowedTo) {
1286
                    NSMutableDictionary *containerRemoteObjects = [[remoteObjects objectForKey:accountName] objectForKey:pithosContainer.name];
1287
                    NSString *objectAncestorName = [object.name stringByDeletingLastPathComponent];
1288
                    while ([objectAncestorName length] && !object.allowedTo) {
1289
                        object.allowedTo = [[containerRemoteObjects objectForKey:objectAncestorName] allowedTo];
1290
                        objectAncestorName = [objectAncestorName stringByDeletingLastPathComponent];
1291
                    }
1292
                }
1293
                if (![object.allowedTo isEqualToString:@"write"]) {
1294
                    // If read-only "shared with me" skip
1295
                    [self syncOperationFinishedWithSuccess:YES];
1296
                    return;
1297
                }
1298
            }
1299
            if (!fileExists || isDirectory) {
1300
                // Local file object deleted or changed to a directory in the meantime, mark the sync cycle as incomplete and skip
1301
                [self syncOperationFinishedWithSuccess:NO];
1302
                return;
1303
            }
1304
            NSError *error = nil;
1305
            object.contentType = [PithosUtilities contentTypeOfFile:filePath error:&error];
1306
            if (object.contentType == nil)
1307
                object.contentType = @"application/octet-stream";
1308
            #if DEBUG_PITHOS
1309
            if (error)
1310
                DLog(@"contentType detection error: %@", error);
1311
            #endif
1312
            NSArray *hashes = nil;
1313
            ASIPithosObjectRequest *objectRequest = [PithosUtilities writeObjectDataRequestWithPithos:pithos 
1314
                                                                                        containerName:pithosContainer.name 
1315
                                                                                           objectName:object.name 
1316
                                                                                          contentType:object.contentType 
1317
                                                                                            blockSize:pithosContainer.blockSize 
1318
                                                                                            blockHash:pithosContainer.blockHash 
1319
                                                                                              forFile:filePath 
1320
                                                                                        checkIfExists:NO 
1321
                                                                                               hashes:&hashes 
1322
                                                                                       sharingAccount:([accountName isEqualToString:@""] ? nil : accountName)];
1323
            if (objectRequest) {
1324
                objectRequest.delegate = self;
1325
                objectRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:);
1326
                objectRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:);
1327
                NSString *messagePrefix = [NSString stringWithFormat:@"Sync: Uploading '%@/%@'", 
1328
                                           [self relativeDirPathForAccount:accountName container:pithosContainer.name], object.name];
1329
                PithosActivity *activity = [activityFacility startActivityWithType:PithosActivityUpload 
1330
                                                                           message:[messagePrefix stringByAppendingString:@" (0%%)"] 
1331
                                                                        totalBytes:[[objectRequest.userInfo objectForKey:@"bytes"] unsignedIntegerValue]
1332
                                                                      currentBytes:0 
1333
                                                                     pithosAccount:pithosAccount];
1334
                dispatch_async(dispatch_get_main_queue(), ^{
1335
                    [activityFacility updateActivity:activity withMessage:activity.message];
1336
                });
1337
                [(NSMutableDictionary *)objectRequest.userInfo addEntriesFromDictionary:
1338
                 [NSDictionary dictionaryWithObjectsAndKeys:
1339
                  accountName, @"accountName", 
1340
                  pithosContainer, @"pithosContainer", 
1341
                  object, @"pithosObject", 
1342
                  messagePrefix, @"messagePrefix", 
1343
                  filePath, @"filePath", 
1344
                  hashes, @"hashes", 
1345
                  [NSNumber numberWithUnsignedInteger:10], @"iteration", 
1346
                  activity, @"activity", 
1347
                  [messagePrefix stringByAppendingString:@" (stopped)"], @"stoppedActivityMessage", 
1348
                  [messagePrefix stringByAppendingString:@" (failed)"], @"failedActivityMessage", 
1349
                  [messagePrefix stringByAppendingString:@" (100%%)"], @"finishedActivityMessage", 
1350
                  [NSNumber numberWithInteger:NSOperationQueuePriorityNormal], @"priority", 
1351
                  [NSNumber numberWithUnsignedInteger:10], @"retries", 
1352
                  NSStringFromSelector(@selector(uploadObjectUsingHashMapFinished:)), @"didFinishSelector", 
1353
                  NSStringFromSelector(@selector(requestFailed:)), @"didFailSelector", 
1354
                  nil]];
1355
                [networkQueue addOperation:[PithosUtilities prepareRequest:objectRequest priority:NSOperationQueuePriorityNormal]];
1356
            } else {
1357
                [self syncOperationFinishedWithSuccess:NO];
1358
            }
1359
        }
1360
    }
1361
}
1362

    
1363
#pragma mark -
1364
#pragma mark ASIHTTPRequestDelegate
1365

    
1366
- (void)performRequestFinishedDelegateInBackground:(ASIPithosRequest *)request {
1367
    // Add an operation to the callbackQueue with a completionBlock for the case of cancellation
1368
    NSInvocationOperation *operation = [[NSInvocationOperation alloc] initWithTarget:self 
1369
                                                                             selector:NSSelectorFromString([request.userInfo objectForKey:@"didFinishSelector"]) 
1370
                                                                               object:request];
1371
    operation.completionBlock = ^{
1372
        @autoreleasepool {
1373
            if ([[request.userInfo objectForKey:@"operation"] isCancelled]) {
1374
                dispatch_async(dispatch_get_main_queue(), ^{
1375
                    [activityFacility endActivity:[request.userInfo objectForKey:@"activity"] 
1376
                                      withMessage:[request.userInfo objectForKey:@"stoppedActivityMessage"]];
1377
                });
1378
                [self syncOperationFinishedWithSuccess:NO];
1379
            }
1380
        }
1381
    };
1382
    [(NSMutableDictionary *)request.userInfo setObject:operation forKey:@"operation"];
1383
    [callbackQueue addOperation:operation];
1384
}
1385

    
1386
- (void)performRequestFailedDelegateInBackground:(ASIPithosRequest *)request {
1387
    if (request.isCancelled) {
1388
        // Request has been cancelled 
1389
        // The callbackQueue might be suspended so we call directly the callback method, since it does minimal work anyway
1390
        [self performSelector:NSSelectorFromString([request.userInfo objectForKey:@"didFailSelector"]) 
1391
                   withObject:request];
1392
    } else {
1393
        // Add an operation to the callbackQueue with a completionBlock for the case of cancellation
1394
        NSInvocationOperation *operation = [[NSInvocationOperation alloc] initWithTarget:self 
1395
                                                                                 selector:NSSelectorFromString([request.userInfo objectForKey:@"didFailSelector"]) 
1396
                                                                                   object:request];
1397
        operation.completionBlock = ^{
1398
            @autoreleasepool {
1399
                if ([[request.userInfo objectForKey:@"operation"] isCancelled]) {
1400
                    dispatch_async(dispatch_get_main_queue(), ^{
1401
                        [activityFacility endActivity:[request.userInfo objectForKey:@"activity"] 
1402
                                          withMessage:[request.userInfo objectForKey:@"stoppedActivityMessage"]];
1403
                    });
1404
                    [self syncOperationFinishedWithSuccess:NO];
1405
                }
1406
            }
1407
        };
1408
        [(NSMutableDictionary *)request.userInfo setObject:operation forKey:@"operation"];
1409
        [callbackQueue addOperation:operation];
1410
    }
1411
}
1412

    
1413
- (void)listRequestFinished:(ASIPithosContainerRequest *)containerRequest {
1414
    @autoreleasepool {
1415
        NSOperation *operation = [containerRequest.userInfo objectForKey:@"operation"];
1416
        DLog(@"Sync::list request finished: %@", containerRequest.url);
1417
        if (operation.isCancelled) {
1418
            [self listRequestFailed:containerRequest];
1419
        } else if ((containerRequest.responseStatusCode == 200) || 
1420
                   (containerRequest.responseStatusCode == 304) || 
1421
                   (containerRequest.responseStatusCode == 403)) {
1422
            NSString *accountName = [accountsNames objectAtIndex:accountsIndex];
1423
            ASIPithosContainer *pithosContainer = [[accountsPithosContainers objectForKey:accountName] objectAtIndex:containersIndex];
1424
            if (containerRequest.responseStatusCode == 200) {
1425
                NSArray *someObjects = [containerRequest objects];
1426
                if (objects == nil) {
1427
                    objects = [[NSMutableArray alloc] initWithArray:someObjects];
1428
                } else {
1429
                    [objects addObjectsFromArray:someObjects];
1430
                }
1431
                if ([someObjects count] < 10000) {
1432
                    pithosContainer.blockHash = [containerRequest blockHash];
1433
                    pithosContainer.blockSize = [containerRequest blockSize];
1434
                    pithosContainer.lastModified = [containerRequest lastModified];
1435
                    NSMutableDictionary *containerRemoteObjects = [NSMutableDictionary dictionaryWithCapacity:[objects count]];
1436
                    for (ASIPithosObject *object in objects) {
1437
                        [containerRemoteObjects setObject:object forKey:object.name];
1438
                    }
1439
                    [[remoteObjects objectForKey:accountName] setObject:containerRemoteObjects forKey:pithosContainer.name];
1440
                    objects = nil;
1441
                } else {
1442
                    // Do an additional request to fetch more objects
1443
                    ASIPithosContainerRequest *newContainerRequest = [ASIPithosContainerRequest listObjectsRequestWithPithos:pithos 
1444
                                                                                                               containerName:pithosContainer.name 
1445
                                                                                                                       limit:0 
1446
                                                                                                                      marker:[[someObjects lastObject] name] 
1447
                                                                                                                      prefix:nil 
1448
                                                                                                                   delimiter:nil 
1449
                                                                                                                        path:nil 
1450
                                                                                                                        meta:nil 
1451
                                                                                                                      shared:NO 
1452
                                                                                                                       until:nil 
1453
                                                                                                             ifModifiedSince:pithosContainer.lastModified];
1454
                    if (![accountName isEqualToString:@""])
1455
                        [newContainerRequest setRequestUserFromDefaultTo:accountName withPithos:pithos];
1456
                    newContainerRequest.delegate = self;
1457
                    newContainerRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:);
1458
                    newContainerRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:);
1459
                    newContainerRequest.userInfo = containerRequest.userInfo;
1460
                    [(NSMutableDictionary *)newContainerRequest.userInfo setObject:[NSNumber numberWithUnsignedInteger:10] forKey:@"retries"];
1461
                    [networkQueue addOperation:[PithosUtilities prepareRequest:newContainerRequest priority:[[newContainerRequest.userInfo objectForKey:@"priority"] integerValue]]];
1462
                    return;
1463
                }
1464
            } else if (containerRequest.responseStatusCode == 304) {
1465
                NSMutableDictionary *containerRemoteObjects = [[previousRemoteObjects objectForKey:accountName] 
1466
                                                               objectForKey:pithosContainer.name];
1467
                if (containerRemoteObjects) 
1468
                    [[remoteObjects objectForKey:accountName] setObject:containerRemoteObjects forKey:pithosContainer.name];
1469
            } else if (containerRequest.responseStatusCode == 403) {
1470
                [[remoteObjects objectForKey:accountName] setObject:[NSMutableDictionary dictionary] forKey:pithosContainer.name];
1471
            }
1472
            
1473
            containersIndex++;
1474
            if (containersIndex == [[accountsPithosContainers objectForKey:accountName] count]) {
1475
                accountsIndex++;
1476
                containersIndex = 0;
1477
            }
1478
            if (accountsIndex < accountsCount) {
1479
                accountName = [accountsNames objectAtIndex:accountsIndex];
1480
                pithosContainer = [[accountsPithosContainers objectForKey:accountName] objectAtIndex:containersIndex];
1481
                // Do a request for the next container
1482
                ASIPithosContainerRequest *newContainerRequest = [ASIPithosContainerRequest listObjectsRequestWithPithos:pithos 
1483
                                                                                                           containerName:pithosContainer.name 
1484
                                                                                                                   limit:0 
1485
                                                                                                                  marker:nil 
1486
                                                                                                                  prefix:nil 
1487
                                                                                                               delimiter:nil 
1488
                                                                                                                    path:nil 
1489
                                                                                                                    meta:nil 
1490
                                                                                                                  shared:NO 
1491
                                                                                                                   until:nil 
1492
                                                                                                         ifModifiedSince:pithosContainer.lastModified];
1493
                if (![accountName isEqualToString:@""])
1494
                    [newContainerRequest setRequestUserFromDefaultTo:accountName withPithos:pithos];
1495
                newContainerRequest.delegate = self;
1496
                newContainerRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:);
1497
                newContainerRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:);
1498
                newContainerRequest.userInfo = containerRequest.userInfo;
1499
                [(NSMutableDictionary *)newContainerRequest.userInfo setObject:[NSNumber numberWithUnsignedInteger:10] forKey:@"retries"];
1500
                [networkQueue addOperation:[PithosUtilities prepareRequest:newContainerRequest priority:[[newContainerRequest.userInfo objectForKey:@"priority"] integerValue]]];
1501
                return;
1502
            }
1503
            self.previousRemoteObjects = remoteObjects;
1504
            // remoteObjects contains all remote objects for the legal containers, without enforcing directory exclusions
1505
            
1506
            if (operation.isCancelled) {
1507
                [self listRequestFailed:containerRequest];
1508
                return;
1509
            }
1510

    
1511
            dispatch_async(dispatch_get_main_queue(), ^{
1512
                [activityFacility endActivity:[containerRequest.userInfo objectForKey:@"activity"] 
1513
                                  withMessage:[containerRequest.userInfo objectForKey:@"finishedActivityMessage"]];
1514
            });
1515
            NSFileManager *fileManager = [NSFileManager defaultManager];
1516
            
1517
            // Compute current state of legal existing local objects 
1518
            // and add an empty stored state for legal new local objects since last sync
1519
            self.currentLocalObjectStates = [NSMutableDictionary dictionary];
1520
            for (NSString *accountName in accountsNames) {
1521
                for (ASIPithosContainer *pithosContainer in [accountsPithosContainers objectForKey:accountName]) {
1522
                    NSString *containerDirectoryPath = [self dirPathForAccount:accountName container:pithosContainer.name];
1523
                    NSSet *containerExcludedDirectories = [[accountsDictionary objectForKey:accountName] objectForKey:pithosContainer.name];
1524
                    BOOL containerExcludeRootFiles = [containerExcludedDirectories containsObject:@""];
1525
                    NSMutableDictionary *containerStoredLocalObjectStates = [[storedLocalObjectStates objectForKey:accountName] 
1526
                                                                             objectForKey:pithosContainer.name];
1527
                    NSDirectoryEnumerator *dirEnumerator = [fileManager enumeratorAtPath:containerDirectoryPath];
1528
                    for (__strong NSString *objectName in dirEnumerator) {
1529
                        objectName = [objectName precomposedStringWithCanonicalMapping];
1530
                        if (operation.isCancelled) {
1531
                            operation.completionBlock = nil;
1532
                            [self saveLocalState];
1533
                            [self syncOperationFinishedWithSuccess:NO];
1534
                            return;
1535
                        }
1536

    
1537
                        NSString *filePath = [containerDirectoryPath stringByAppendingPathComponent:objectName];
1538
                        NSDictionary *attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:nil];
1539
                        BOOL isDirectory;
1540
                        BOOL fileExists = [fileManager fileExistsAtPath:filePath isDirectory:&isDirectory];
1541
                        if ([[attributes fileType] isEqualToString:NSFileTypeSymbolicLink]) {
1542
                            [PithosUtilities fileActionFailedAlertWithTitle:@"Sync Error" 
1543
                                                                    message:[NSString stringWithFormat:@"Sync directory at '%@' contains symbolic links. Remove them and re-activate sync for account '%@'.", 
1544
                                                                             containerDirectoryPath, pithosAccount.name] 
1545
                                                                      error:nil];
1546
                            pithosAccount.syncActive = NO;
1547
                            return;
1548
                        } else if (fileExists) {
1549
                            if (skipHidden && [[objectName lastPathComponent] hasPrefix:@"."]) {
1550
                                // Skip hidden directory and its descendants, or hidden file
1551
                                if (isDirectory)
1552
                                    [dirEnumerator skipDescendants];
1553
                                // Remove stored state if any
1554
                                [containerStoredLocalObjectStates removeObjectForKey:objectName];
1555
                                continue;
1556
                            } else if ([[objectName pathComponents] count] == 1) {
1557
                                if ([containerExcludedDirectories containsObject:[objectName lowercaseString]]) {
1558
                                    // Skip excluded directory and its descendants, or root file with same name
1559
                                    if (isDirectory)
1560
                                        [dirEnumerator skipDescendants];
1561
                                    // Remove stored state if any
1562
                                    [containerStoredLocalObjectStates removeObjectForKey:objectName];
1563
                                    continue;
1564
                                } else if (!isDirectory && containerExcludeRootFiles) {
1565
                                    // Skip excluded root file
1566
                                    // Remove stored state if any
1567
                                    [containerStoredLocalObjectStates removeObjectForKey:objectName];
1568
                                    continue;
1569
                                }
1570
                            }
1571
                            // Include local object
1572
                            PithosLocalObjectState *storedLocalObjectState = [containerStoredLocalObjectStates objectForKey:objectName];
1573
                            if (!storedLocalObjectState || [storedLocalObjectState isModified]) {
1574
                                // New or modified existing local object, compute current state
1575
                                if (!storedLocalObjectState)
1576
                                    // For new local object, also create empty stored state
1577
                                    [containerStoredLocalObjectStates setObject:[PithosLocalObjectState localObjectState] forKey:objectName];
1578
                                [currentLocalObjectStates setObject:[PithosLocalObjectState localObjectStateWithFile:filePath 
1579
                                                                                                           blockHash:pithosContainer.blockHash 
1580
                                                                                                           blockSize:pithosContainer.blockSize] 
1581
                                                             forKey:filePath];
1582
                            } else {
1583
                                // Local object hasn't changed, set stored state also to current
1584
                                [currentLocalObjectStates setObject:storedLocalObjectState forKey:filePath];
1585
                            }
1586
                        }
1587
                    }
1588
                    [self saveLocalState];
1589
                }
1590
            }
1591
            
1592
            if (operation.isCancelled) {
1593
                operation.completionBlock = nil;
1594
                [self syncOperationFinishedWithSuccess:NO];
1595
                return;
1596
            }
1597

    
1598
            // Add an empty stored state for legal new remote objects since last sync
1599
            for (NSString *accountName in accountsNames) {
1600
                for (ASIPithosContainer *pithosContainer in [accountsPithosContainers objectForKey:accountName]) {
1601
                    NSSet *containerExcludedDirectories = [[accountsDictionary objectForKey:accountName] objectForKey:pithosContainer.name];
1602
                    BOOL containerExcludeRootFiles = [containerExcludedDirectories containsObject:@""];
1603
                    NSMutableDictionary *containerStoredLocalObjectStates = [[storedLocalObjectStates objectForKey:accountName] 
1604
                                                                             objectForKey:pithosContainer.name];
1605
                    NSMutableDictionary *containerRemoteObjects = [[remoteObjects objectForKey:accountName] objectForKey:pithosContainer.name];
1606
                    for (NSString *objectName in containerRemoteObjects) {
1607
                        if (operation.isCancelled) {
1608
                            operation.completionBlock = nil;
1609
                            [self saveLocalState];
1610
                            [self syncOperationFinishedWithSuccess:NO];
1611
                            return;
1612
                        }
1613

    
1614
                        ASIPithosObject *object = [containerRemoteObjects objectForKey:objectName];
1615
                        NSString *localObjectName;
1616
                        if ([object.name hasSuffix:@"/"])
1617
                            localObjectName = [NSString stringWithFormat:@"%@:", [object.name substringToIndex:([object.name length] -1)]];
1618
                        else
1619
                            localObjectName = [NSString stringWithString:object.name];
1620
                        NSArray *pathComponents = [localObjectName pathComponents];
1621
                        if (skipHidden) {
1622
                            BOOL skipObject = NO;
1623
                            for (NSString *pathComponent in pathComponents) {
1624
                                if ([pathComponent hasPrefix:@"."]) {
1625
                                    // Skip hidden directory and its descendants, or hidden file
1626
                                    // Remove stored state if any
1627
                                    [containerStoredLocalObjectStates removeObjectForKey:objectName];
1628
                                    skipObject = YES;
1629
                                    break;
1630
                                }
1631
                            }
1632
                            if (skipObject)
1633
                                continue;
1634
                        }
1635
                        if ([containerExcludedDirectories containsObject:[[pathComponents objectAtIndex:0] lowercaseString]]) {
1636
                            // Skip excluded directory object and its descendants, or root file object with same name
1637
                            // Remove stored state if any
1638
                            [containerStoredLocalObjectStates removeObjectForKey:object.name];
1639
                            continue;
1640
                        } else if (containerExcludeRootFiles && 
1641
                                   ([pathComponents count] == 1) && 
1642
                                   ![PithosUtilities isContentTypeDirectory:object.contentType]) {
1643
                            // Skip root file object
1644
                            // Remove stored state if any
1645
                            [containerStoredLocalObjectStates removeObjectForKey:object.name];                    
1646
                            continue;
1647
                        }
1648
                        if (![containerStoredLocalObjectStates objectForKey:object.name])
1649
                            [containerStoredLocalObjectStates setObject:[PithosLocalObjectState localObjectState] forKey:object.name];
1650
                    }
1651
                    [self saveLocalState];
1652
                }
1653
            }
1654

    
1655
            if (operation.isCancelled) {
1656
                operation.completionBlock = nil;
1657
                [self syncOperationFinishedWithSuccess:NO];
1658
                return;
1659
            }
1660

    
1661
            // For each stored state compare with current and remote state
1662
            // Stored states of local objects that have been deleted, 
1663
            // haven't been checked for legality (only existing local remote objects)
1664
            // These should be identified and skipped
1665
            for (NSString *accountName in accountsNames) {
1666
                for (ASIPithosContainer *pithosContainer in [accountsPithosContainers objectForKey:accountName]) {
1667
                    NSString *containerDirectoryPath = [self dirPathForAccount:accountName container:pithosContainer.name];
1668
                    NSSet *containerExcludedDirectories = [[accountsDictionary objectForKey:accountName] objectForKey:pithosContainer.name];
1669
                    BOOL containerExcludeRootFiles = [containerExcludedDirectories containsObject:@""];
1670
                    NSMutableDictionary *containerStoredLocalObjectStates = [[storedLocalObjectStates objectForKey:accountName] 
1671
                                                                             objectForKey:pithosContainer.name];
1672
                    NSMutableDictionary *containerRemoteObjects = [[remoteObjects objectForKey:accountName] objectForKey:pithosContainer.name];
1673
                    for (NSString *objectName in [[containerStoredLocalObjectStates allKeys] sortedArrayUsingSelector:@selector(compare:)]) {
1674
                        if (operation.isCancelled) {
1675
                            operation.completionBlock = nil;
1676
                            [self syncOperationFinishedWithSuccess:NO];
1677
                            return;
1678
                        }
1679

    
1680
                        NSString *filePath = [containerDirectoryPath stringByAppendingPathComponent:objectName];
1681
                        if ([objectName hasSuffix:@"/"])
1682
                            filePath = [filePath stringByAppendingString:@":"];
1683
                        ASIPithosObject *object = [ASIPithosObject object];
1684
                        object.name = objectName;
1685
                        DLog(@"Sync::object name: %@", object.name);
1686

    
1687
                        PithosLocalObjectState *storedLocalObjectState = [containerStoredLocalObjectStates objectForKey:object.name];
1688
                        PithosLocalObjectState *currentLocalObjectState = [currentLocalObjectStates objectForKey:filePath];
1689
                        if (!currentLocalObjectState) {
1690
                            // The stored state corresponds to a remote or deleted local object, that's why there is no current state
1691
                            // In the latter case it must be checked for legality, which can be determined by its stored state
1692
                            // If it existed locally, but was deleted, state.exists is true, 
1693
                            // else if the stored state is an empty state that was created due to the server object, state.exists is false
1694
                            if (storedLocalObjectState.exists) {
1695
                                NSString *localObjectName;
1696
                                if ([object.name hasSuffix:@"/"])
1697
                                    localObjectName = [NSString stringWithFormat:@"%@:", [object.name substringToIndex:([object.name length] -1)]];
1698
                                else
1699
                                    localObjectName = [NSString stringWithString:object.name];
1700
                                NSArray *pathComponents = [localObjectName pathComponents];
1701
                                if (skipHidden) {
1702
                                    BOOL skipObject = NO;
1703
                                    for (NSString *pathComponent in pathComponents) {
1704
                                        if ([pathComponent hasPrefix:@"."]) {
1705
                                            // Skip hidden directory and its descendants, or hidden file
1706
                                            // Remove stored state if any
1707
                                            [containerStoredLocalObjectStates removeObjectForKey:objectName];
1708
                                            [self saveLocalState];
1709
                                            skipObject = YES;
1710
                                            break;
1711
                                        }
1712
                                    }
1713
                                    if (skipObject)
1714
                                        continue;
1715
                                }
1716
                                if ([containerExcludedDirectories containsObject:[[pathComponents objectAtIndex:0] lowercaseString]]) {
1717
                                    // Skip excluded directory object and its descendants, or root file object with same name
1718
                                    // Remove stored state
1719
                                    [containerStoredLocalObjectStates removeObjectForKey:object.name];
1720
                                    [self saveLocalState];
1721
                                    continue;
1722
                                } else if (containerExcludeRootFiles && ([pathComponents count] == 1) && !storedLocalObjectState.isDirectory) {
1723
                                    // Skip root file object
1724
                                    // Remove stored state
1725
                                    [containerStoredLocalObjectStates removeObjectForKey:object.name];                    
1726
                                    [self saveLocalState];
1727
                                    continue;
1728
                                }
1729
                            }
1730
                            // There is also the off case that a local object has been created in the meantime
1731
                            // This call works in any case, existent or non-existent local object 
1732
                            currentLocalObjectState = [PithosLocalObjectState localObjectStateWithFile:filePath 
1733
                                                                                             blockHash:pithosContainer.blockHash 
1734
                                                                                             blockSize:pithosContainer.blockSize];
1735
                        }
1736
                
1737
                        PithosLocalObjectState *remoteObjectState = [PithosLocalObjectState localObjectState];
1738
                        ASIPithosObject *remoteObject = [containerRemoteObjects objectForKey:object.name];
1739
                        if (remoteObject) {
1740
                            if ([PithosUtilities isContentTypeDirectory:remoteObject.contentType]) {
1741
                                remoteObjectState.isDirectory = YES;
1742
                            } else {
1743
                                remoteObjectState.hash = remoteObject.objectHash;
1744
                            }
1745
                        }
1746

    
1747
                        BOOL localStateHasChanged = ![currentLocalObjectState isEqualToState:storedLocalObjectState];
1748
                        BOOL serverStateHasChanged = ![remoteObjectState isEqualToState:storedLocalObjectState];
1749
                        DLog(@"Sync::localStateHasChanged: %d, serverStateHasChanged: %d", localStateHasChanged, serverStateHasChanged);
1750
                        if (!localStateHasChanged) {
1751
                            // Local state hasn't changed
1752
                            if (serverStateHasChanged) {
1753
                                // Server state has changed
1754
                                // Update local state to match that of the server 
1755
                                object.bytes = remoteObject.bytes;
1756
                                object.version = remoteObject.version;
1757
                                object.contentType = remoteObject.contentType;
1758
                                object.objectHash = remoteObject.objectHash;
1759
                                [self updateLocalStateWithObject:object localFilePath:filePath 
1760
                                                     accountName:accountName pithosContainer:pithosContainer];
1761
                            } else if (!remoteObject && ![currentLocalObjectState exists]) {
1762
                                // Server state hasn't changed
1763
                                // If the object doesn't exist neither in the server or locally, it should be removed from the stored local objects
1764
                                [containerStoredLocalObjectStates removeObjectForKey:objectName];
1765
                                [self saveLocalState];
1766
                            }
1767
                    } else {
1768
                        // Local state has changed
1769
                        if (!serverStateHasChanged) {
1770
                            // Server state hasn't changed
1771
                            if (currentLocalObjectState.isDirectory)
1772
                                object.contentType = @"application/directory";
1773
                            else
1774
                                object.objectHash = currentLocalObjectState.hash;
1775
                            [self updateServerStateWithCurrentState:currentLocalObjectState object:object localFilePath:filePath 
1776
                                                        accountName:accountName pithosContainer:pithosContainer];
1777
                        } else {
1778
                            // Server state has also changed
1779
                            if (remoteObjectState.isDirectory && currentLocalObjectState.isDirectory) {
1780
                                // Both did the same change (directory)
1781
                                storedLocalObjectState.filePath = filePath;
1782
                                storedLocalObjectState.isDirectory = YES;
1783
                                [self saveLocalState];
1784
                            } else if ([remoteObjectState isEqualToState:currentLocalObjectState]) {
1785
                                // Both did the same change (object edit or delete)
1786
                                if (![remoteObjectState exists]) {
1787
                                    [containerStoredLocalObjectStates removeObjectForKey:object.name];
1788
                                } else {
1789
                                    storedLocalObjectState.filePath = filePath;
1790
                                    storedLocalObjectState.hash = remoteObjectState.hash;
1791
                                }
1792
                                [self saveLocalState];
1793
                            } else {
1794
                                // Conflict, we ask the user which change to keep
1795
                                NSString *informativeText;
1796
                                NSString *firstButtonText;
1797
                                NSString *secondButtonText;
1798
                                
1799
                                if (![remoteObjectState exists]) {
1800
                                    // Remote object has been deleted
1801
                                    informativeText = [NSString stringWithFormat:@"'%@/%@' has been modified locally, while it has been deleted from server.", 
1802
                                                       [self relativeDirPathForAccount:accountName container:pithosContainer.name], object.name ];
1803
                                    firstButtonText = @"Delete local file";
1804
                                    secondButtonText = @"Upload file to server";
1805
                                } else if (![currentLocalObjectState exists]) {
1806
                                    informativeText = [NSString stringWithFormat:@"'%@/%@' has been modified on the server, while it has been deleted locally.", 
1807
                                                       [self relativeDirPathForAccount:accountName container:pithosContainer.name], object.name];
1808
                                    firstButtonText = @"Download file from server";
1809
                                    secondButtonText = @"Delete file on server";
1810
                                } else {
1811
                                    informativeText = [NSString stringWithFormat:@"'%@/%@' has been modified both locally and on the server.", 
1812
                                                       [self relativeDirPathForAccount:accountName container:pithosContainer.name], object.name];
1813
                                    firstButtonText = @"Keep server version";
1814
                                    secondButtonText = @"Keep local version";
1815
                                }
1816
                                __block NSInteger choice;
1817
                                dispatch_sync(dispatch_get_main_queue(), ^{
1818
                                    NSAlert *alert = [[NSAlert alloc] init];
1819
                                    [alert setMessageText:@"Conflict"];
1820
                                    [alert setInformativeText:informativeText];
1821
                                    [alert addButtonWithTitle:firstButtonText];
1822
                                    [alert addButtonWithTitle:secondButtonText];
1823
                                    [alert addButtonWithTitle:@"Do nothing"];
1824
                                    choice = [alert runModal];
1825
                                });
1826
                                if (choice == NSAlertFirstButtonReturn) {
1827
                                    object.bytes = remoteObject.bytes;
1828
                                    object.version = remoteObject.version;
1829
                                    object.contentType = remoteObject.contentType;
1830
                                    object.objectHash = remoteObject.objectHash;
1831
                                    [self updateLocalStateWithObject:object localFilePath:filePath 
1832
                                                     accountName:accountName pithosContainer:pithosContainer];
1833
                                } if (choice == NSAlertSecondButtonReturn) {
1834
                                    if (currentLocalObjectState.isDirectory)
1835
                                        object.contentType = @"application/directory";
1836
                                    else
1837
                                        object.objectHash = currentLocalObjectState.hash;
1838
                                    [self updateServerStateWithCurrentState:currentLocalObjectState object:object localFilePath:filePath 
1839
                                                                accountName:accountName pithosContainer:pithosContainer];
1840
                                }
1841
                            }
1842
                        }
1843
                    }
1844
                }
1845
            }
1846
            }
1847
            [self syncOperationFinishedWithSuccess:YES];
1848
        } else {
1849
            [self listRequestFailed:containerRequest];
1850
        }
1851
    }
1852
}
1853

    
1854
- (void)listRequestFailed:(ASIPithosContainerRequest *)containerRequest {
1855
    @autoreleasepool {
1856
        NSOperation *operation = [containerRequest.userInfo objectForKey:@"operation"];
1857
        if (operation.isCancelled) {
1858
            objects = nil;
1859
            return;        
1860
        }
1861
        if (containerRequest.isCancelled) {
1862
            dispatch_async(dispatch_get_main_queue(), ^{
1863
                [activityFacility endActivity:[containerRequest.userInfo objectForKey:@"activity"] 
1864
                                  withMessage:[containerRequest.userInfo objectForKey:@"stoppedActivityMessage"]];
1865
            });
1866
            objects = nil;
1867
            [self syncOperationFinishedWithSuccess:NO];
1868
            return;
1869
        }
1870
        // If the server listing fails, the sync should start over, so just retrying is enough
1871
        NSUInteger retries = [[containerRequest.userInfo objectForKey:@"retries"] unsignedIntegerValue];
1872
        if (retries > 0) {
1873
            ASIPithosContainerRequest *newContainerRequest = (ASIPithosContainerRequest *)[PithosUtilities retryWithUpdatedURLRequest:containerRequest
1874
                                                                                                              andPithosAccountManager:pithosAccount];
1875
            [(NSMutableDictionary *)(newContainerRequest.userInfo)setObject:[NSNumber numberWithUnsignedInteger:(--retries)] forKey:@"retries"];
1876
            [networkQueue addOperation:[PithosUtilities prepareRequest:newContainerRequest priority:[[newContainerRequest.userInfo objectForKey:@"priority"] integerValue]]];
1877
        } else {
1878
            dispatch_async(dispatch_get_main_queue(), ^{
1879
                [activityFacility endActivity:[containerRequest.userInfo objectForKey:@"activity"] 
1880
                                  withMessage:[containerRequest.userInfo objectForKey:@"failedActivityMessage"]];
1881
            });
1882
            objects = nil;
1883
            // Since the server listing failed in all retries, the operation finished and the sync cycle is completeted unsuccesfully
1884
            [self syncOperationFinishedWithSuccess:NO];
1885
        }
1886
    }
1887
}
1888

    
1889
- (void)downloadObjectBlockFinished:(ASIPithosObjectRequest *)objectRequest {
1890
    @autoreleasepool {
1891
        NSOperation *operation = [objectRequest.userInfo objectForKey:@"operation"];
1892
        DLog(@"Sync::download object block finished: %@", objectRequest.url);
1893
        if (operation.isCancelled) {
1894
            [self requestFailed:objectRequest];
1895
        } else if (objectRequest.responseStatusCode == 206) {
1896
            NSString *accountName = [objectRequest.userInfo objectForKey:@"accountName"];
1897
            ASIPithosContainer *pithosContainer = [objectRequest.userInfo objectForKey:@"pithosContainer"];
1898
            ASIPithosObject *object = [objectRequest.userInfo objectForKey:@"pithosObject"];
1899
            NSFileManager *fileManager = [NSFileManager defaultManager];
1900
            NSError *error;
1901
            PithosActivity *activity = [objectRequest.userInfo objectForKey:@"activity"];
1902
            
1903
            PithosLocalObjectState *storedState = [[[storedLocalObjectStates objectForKey:accountName]
1904
                                                    objectForKey:pithosContainer.name] 
1905
                                                   objectForKey:object.name];
1906
            if ((storedState.tmpFilePath == nil) || ![fileManager fileExistsAtPath:storedState.tmpFilePath]) {
1907
                NSString *downloadsDirPath = self.tempDownloadsDirPath;
1908
                if (!downloadsDirPath) {
1909
                    dispatch_async(dispatch_get_main_queue(), ^{
1910
                        [activityFacility endActivity:activity
1911
                                          withMessage:[objectRequest.userInfo objectForKey:@"failedActivityMessage"]];
1912
                    });
1913
                    [self syncOperationFinishedWithSuccess:NO];
1914
                    return;
1915
                }
1916
                if ([accountName isEqualToString:@""]) {
1917
                    downloadsDirPath = [downloadsDirPath stringByAppendingPathComponent:pithosContainer.name];
1918
                } else {
1919
                    downloadsDirPath = [[[downloadsDirPath stringByAppendingPathComponent:@"shared with me"]
1920
                                         stringByAppendingPathComponent:accountName]
1921
                                        stringByAppendingPathComponent:pithosContainer.name];
1922
                }
1923
                BOOL isDirectory;
1924
                error = nil;
1925
                if (![fileManager fileExistsAtPath:downloadsDirPath isDirectory:&isDirectory]) {
1926
                    if (![fileManager createDirectoryAtPath:downloadsDirPath withIntermediateDirectories:YES attributes:nil error:&error] || error) {
1927
                        dispatch_async(dispatch_get_main_queue(), ^{
1928
                            [activityFacility endActivity:activity
1929
                                              withMessage:[objectRequest.userInfo objectForKey:@"failedActivityMessage"]];
1930
                        });
1931
                        [self syncOperationFinishedWithSuccess:NO];
1932
                        return;
1933
                    }
1934
                } else if (!isDirectory) {
1935
                    dispatch_async(dispatch_get_main_queue(), ^{
1936
                        [activityFacility endActivity:activity
1937
                                          withMessage:[objectRequest.userInfo objectForKey:@"failedActivityMessage"]];
1938
                    });
1939
                    [self syncOperationFinishedWithSuccess:NO];
1940
                    return;
1941
                }
1942
                
1943
                NSString *tempFileTemplate = [downloadsDirPath stringByAppendingPathComponent:@"download.XXXXXX"];
1944
                const char *tempFileTemplateCString = [tempFileTemplate fileSystemRepresentation];
1945
                char *tempFileNameCString = (char *)malloc(strlen(tempFileTemplateCString) + 1);
1946
                strcpy(tempFileNameCString, tempFileTemplateCString);
1947
                int fileDescriptor = mkstemp(tempFileNameCString);
1948
                NSString *tempFilePath = [fileManager stringWithFileSystemRepresentation:tempFileNameCString length:strlen(tempFileNameCString)];
1949
                free(tempFileNameCString);
1950
                if (fileDescriptor == -1) {
1951
                    [PithosUtilities fileActionFailedAlertWithTitle:@"Create Temporary File Error" 
1952
                                                            message:[NSString stringWithFormat:@"Cannot create temporary file at '%@'", tempFilePath] 
1953
                                                              error:nil];
1954
                    dispatch_async(dispatch_get_main_queue(), ^{
1955
                        [activityFacility endActivity:activity 
1956
                                          withMessage:[objectRequest.userInfo objectForKey:@"failedActivityMessage"]];
1957
                    });
1958
                    [self syncOperationFinishedWithSuccess:NO];
1959
                    return;
1960
                }
1961
                close(fileDescriptor);
1962
                storedState.tmpFilePath = tempFilePath;
1963
                [self saveLocalState];
1964
            }
1965

    
1966
            NSUInteger missingBlockIndex = [[objectRequest.userInfo objectForKey:@"missingBlockIndex"] unsignedIntegerValue];
1967
            NSFileHandle *tempFileHandle = [NSFileHandle fileHandleForWritingAtPath:storedState.tmpFilePath];
1968
            [tempFileHandle seekToFileOffset:missingBlockIndex*pithosContainer.blockSize];
1969
            [tempFileHandle writeData:[objectRequest responseData]];
1970
            [tempFileHandle closeFile];
1971

    
1972
            NSIndexSet *missingBlocks = [objectRequest.userInfo objectForKey:@"missingBlocks"];
1973
            missingBlockIndex = [missingBlocks indexGreaterThanIndex:missingBlockIndex];
1974
            if (missingBlockIndex == NSNotFound) {
1975
                NSString *filePath = [objectRequest.userInfo objectForKey:@"filePath"];
1976
                NSString *dirPath = [filePath stringByDeletingLastPathComponent];
1977
                if ([fileManager fileExistsAtPath:filePath] && ![self moveToTempTrashFile:filePath 
1978
                                                                              accountName:accountName 
1979
                                                                          pithosContainer:pithosContainer]) {
1980
                    dispatch_async(dispatch_get_main_queue(), ^{
1981
                        [activityFacility endActivity:activity 
1982
                                          withMessage:[objectRequest.userInfo objectForKey:@"failedActivityMessage"]];
1983
                    });
1984
                    [self syncOperationFinishedWithSuccess:NO];
1985
                    return;
1986
                } else if (![fileManager fileExistsAtPath:dirPath]) {
1987
                    // File doesn't exist but also the containing directory doesn't exist
1988
                    // In most cases this should have been resolved as an update of the corresponding local object,
1989
                    // but it never hurts to check
1990
                    error = nil;
1991
                    [fileManager createDirectoryAtPath:dirPath withIntermediateDirectories:YES attributes:nil error:nil];
1992
                    if (error != nil) {
1993
                        [PithosUtilities fileActionFailedAlertWithTitle:@"Create Directory Error" 
1994
                                                                message:[NSString stringWithFormat:@"Cannot create directory at '%@'", dirPath] 
1995
                                                                  error:error];
1996
                        dispatch_async(dispatch_get_main_queue(), ^{
1997
                            [activityFacility endActivity:[objectRequest.userInfo objectForKey:@"activity"] 
1998
                                              withMessage:[objectRequest.userInfo objectForKey:@"failedActivityMessage"]];
1999
                        });
2000
                        [self syncOperationFinishedWithSuccess:NO];
2001
                        return;
2002
                    }
2003
                }
2004
                // Move file from tmp download
2005
                error = nil;
2006
                [fileManager moveItemAtPath:storedState.tmpFilePath toPath:filePath error:&error];
2007
                if (error != nil) {
2008
                    [PithosUtilities fileActionFailedAlertWithTitle:@"Move File Error" 
2009
                                                            message:[NSString stringWithFormat:@"Cannot move file at '%@' to '%@'", storedState.tmpFilePath, filePath] 
2010
                                                              error:error];
2011
                    dispatch_async(dispatch_get_main_queue(), ^{
2012
                        [activityFacility endActivity:activity 
2013
                                          withMessage:[objectRequest.userInfo objectForKey:@"failedActivityMessage"]];
2014
                    });
2015
                    [self syncOperationFinishedWithSuccess:NO];
2016
                    return;
2017
                }
2018
                dispatch_async(dispatch_get_main_queue(), ^{
2019
                    [activityFacility endActivity:activity 
2020
                                      withMessage:[objectRequest.userInfo objectForKey:@"finishedActivityMessage"] 
2021
                                       totalBytes:activity.totalBytes 
2022
                                     currentBytes:activity.totalBytes];
2023
                });
2024

    
2025
                storedState.filePath = filePath;
2026
                storedState.hash = object.objectHash;
2027
                storedState.tmpFilePath = nil;
2028
                [self saveLocalState];
2029
                [self syncOperationFinishedWithSuccess:YES];
2030
                return;
2031
            } else {
2032
                if (newSyncRequested || syncLate || operation.isCancelled) {
2033
                    [self requestFailed:objectRequest];
2034
                } else {
2035
                    __block ASIPithosObjectRequest *newObjectRequest = [PithosUtilities objectBlockDataRequestWithPithos:pithos
2036
                                                                                                           containerName:pithosContainer.name 
2037
                                                                                                                  object:object 
2038
                                                                                                              blockIndex:missingBlockIndex 
2039
                                                                                                               blockSize:pithosContainer.blockSize];
2040
                    if (![accountName isEqualToString:@""])
2041
                        [newObjectRequest setRequestUserFromDefaultTo:accountName withPithos:pithos];
2042
                    newObjectRequest.delegate = self;
2043
                    newObjectRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:);
2044
                    newObjectRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:);
2045
                    newObjectRequest.userInfo = objectRequest.userInfo;
2046
                    [(NSMutableDictionary *)(newObjectRequest.userInfo) setObject:[NSNumber numberWithUnsignedInteger:missingBlockIndex] forKey:@"missingBlockIndex"];
2047
                    [(NSMutableDictionary *)(newObjectRequest.userInfo) setObject:[NSNumber numberWithUnsignedInteger:10] forKey:@"retries"];
2048
                    [newObjectRequest setBytesReceivedBlock:^(unsigned long long size, unsigned long long total){
2049
                        [activityFacility updateActivity:activity 
2050
                                             withMessage:[NSString stringWithFormat:@"%@ (%.0f%%)", 
2051
                                                          [newObjectRequest.userInfo objectForKey:@"messagePrefix"], 
2052
                                                          (100*(activity.currentBytes + size + 0.0)/(activity.totalBytes + 0.0))] 
2053
                                              totalBytes:activity.totalBytes 
2054
                                            currentBytes:(activity.currentBytes + size)];
2055
                    }];
2056
                    [networkQueue addOperation:[PithosUtilities prepareRequest:newObjectRequest priority:[[newObjectRequest.userInfo objectForKey:@"priority"] integerValue]]];
2057
                }
2058
            }
2059
        } else if (objectRequest.responseStatusCode == 412) {
2060
            // The object has changed on the server
2061
            dispatch_async(dispatch_get_main_queue(), ^{
2062
                [activityFacility endActivity:[objectRequest.userInfo objectForKey:@"activity"] 
2063
                                  withMessage:[objectRequest.userInfo objectForKey:@"stoppedActivityMessage"]];
2064
            });
2065
            [self syncOperationFinishedWithSuccess:NO];
2066
        } else {
2067
            [self requestFailed:objectRequest];
2068
        }
2069
    }
2070
}
2071

    
2072
- (void)downloadObjectHashMapFinished:(ASIPithosObjectRequest *)objectRequest {
2073
    @autoreleasepool {
2074
        NSOperation *operation = [objectRequest.userInfo objectForKey:@"operation"];
2075
        DLog(@"Sync::download object hashmap finished: %@", objectRequest.url);
2076
        if (operation.isCancelled) {
2077
            [self requestFailed:objectRequest];
2078
        } else if (objectRequest.responseStatusCode == 200) {
2079
            if (newSyncRequested || syncLate || operation.isCancelled) {
2080
                [self requestFailed:objectRequest];
2081
            } else {
2082
                NSString *accountName = [objectRequest.userInfo objectForKey:@"accountName"];
2083
                ASIPithosContainer *pithosContainer = [objectRequest.userInfo objectForKey:@"pithosContainer"];
2084
                ASIPithosObject *object = [objectRequest.userInfo objectForKey:@"pithosObject"];
2085
                PithosLocalObjectState *storedState = [[[storedLocalObjectStates objectForKey:accountName] 
2086
                                                        objectForKey:pithosContainer.name] 
2087
                                                       objectForKey:object.name];
2088
                if ([PithosUtilities bytesOfFile:storedState.tmpFilePath] > object.bytes)
2089
                    [[NSFileHandle fileHandleForWritingAtPath:storedState.tmpFilePath] truncateFileAtOffset:object.bytes];
2090
                PithosActivity *activity = [objectRequest.userInfo objectForKey:@"activity"];
2091
                NSIndexSet *missingBlocks = [PithosUtilities missingBlocksForFile:storedState.tmpFilePath 
2092
                                                                        blockSize:pithosContainer.blockSize 
2093
                                                                        blockHash:pithosContainer.blockHash 
2094
                                                                       withHashes:[objectRequest hashes]];
2095
                NSUInteger missingBlockIndex = [missingBlocks firstIndex];
2096
                dispatch_async(dispatch_get_main_queue(), ^{
2097
                    [activityFacility updateActivity:activity 
2098
                                         withMessage:[NSString stringWithFormat:@"%@ (%.0f%%)", 
2099
                                                      [objectRequest.userInfo objectForKey:@"messagePrefix"], 
2100
                                                      (100*(activity.totalBytes - [missingBlocks count]*pithosContainer.blockSize + 0.0)/(activity.totalBytes + 0.0))] 
2101
                                          totalBytes:activity.totalBytes 
2102
                                        currentBytes:(activity.totalBytes - [missingBlocks count]*pithosContainer.blockSize)];
2103
                });
2104

    
2105
                __block ASIPithosObjectRequest *newObjectRequest = [PithosUtilities objectBlockDataRequestWithPithos:pithos
2106
                                                                                                       containerName:pithosContainer.name 
2107
                                                                                                              object:object 
2108
                                                                                                          blockIndex:missingBlockIndex 
2109
                                                                                                           blockSize:pithosContainer.blockSize];
2110
                if (![accountName isEqualToString:@""])
2111
                    [newObjectRequest setRequestUserFromDefaultTo:accountName withPithos:pithos];
2112
                newObjectRequest.delegate = self;
2113
                newObjectRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:);
2114
                newObjectRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:);
2115
                newObjectRequest.userInfo = objectRequest.userInfo;
2116
                [(NSMutableDictionary *)(newObjectRequest.userInfo) setObject:missingBlocks forKey:@"missingBlocks"];
2117
                [(NSMutableDictionary *)(newObjectRequest.userInfo) setObject:[NSNumber numberWithUnsignedInteger:missingBlockIndex] forKey:@"missingBlockIndex"];
2118
                [(NSMutableDictionary *)(newObjectRequest.userInfo) setObject:[NSNumber numberWithUnsignedInteger:10] forKey:@"retries"];
2119
                [(NSMutableDictionary *)(newObjectRequest.userInfo) setObject:NSStringFromSelector(@selector(downloadObjectBlockFinished:)) forKey:@"didFinishSelector"];
2120
                [newObjectRequest setBytesReceivedBlock:^(unsigned long long size, unsigned long long total){
2121
                    [activityFacility updateActivity:activity 
2122
                                         withMessage:[NSString stringWithFormat:@"%@ (%.0f%%)", 
2123
                                                      [newObjectRequest.userInfo objectForKey:@"messagePrefix"], 
2124
                                                      (100*(activity.currentBytes + size + 0.0)/(activity.totalBytes + 0.0))] 
2125
                                          totalBytes:activity.totalBytes 
2126
                                        currentBytes:(activity.currentBytes + size)];
2127
                }];
2128
                [networkQueue addOperation:[PithosUtilities prepareRequest:newObjectRequest priority:[[newObjectRequest.userInfo objectForKey:@"priority"] integerValue]]];
2129
            }
2130
        } else {
2131
            [self requestFailed:objectRequest];
2132
        }
2133
    }
2134
}
2135

    
2136
- (void)uploadDirectoryObjectFinished:(ASIPithosObjectRequest *)objectRequest {
2137
    @autoreleasepool {
2138
        NSOperation *operation = [objectRequest.userInfo objectForKey:@"operation"];
2139
        DLog(@"Sync::upload directory object finished: %@", objectRequest.url);
2140
        if (operation.isCancelled) {
2141
            [self requestFailed:objectRequest];
2142
        } else if (objectRequest.responseStatusCode == 201) {
2143
            PithosLocalObjectState *storedState = [[[storedLocalObjectStates objectForKey:[objectRequest.userInfo objectForKey:@"accountName"]] 
2144
                                                    objectForKey:[[objectRequest.userInfo objectForKey:@"pithosContainer"] name]] 
2145
                                                   objectForKey:[[objectRequest.userInfo objectForKey:@"pithosObject"] name]];
2146
            storedState.isDirectory = YES;
2147
            [self saveLocalState];
2148
            dispatch_async(dispatch_get_main_queue(), ^{
2149
                [activityFacility endActivity:[objectRequest.userInfo objectForKey:@"activity"] 
2150
                                  withMessage:[objectRequest.userInfo objectForKey:@"finishedActivityMessage"]];
2151
            });
2152
            [self syncOperationFinishedWithSuccess:YES];
2153
        } else {
2154
            [self requestFailed:objectRequest];
2155
        }
2156
    }
2157
}
2158

    
2159
- (void)moveObjectToTrashFinished:(ASIPithosObjectRequest *)objectRequest {
2160
    @autoreleasepool {
2161
        NSOperation *operation = [objectRequest.userInfo objectForKey:@"operation"];
2162
        DLog(@"Sync::move object to trash finished: %@", objectRequest.url);
2163
        if (operation.isCancelled) {
2164
            [self requestFailed:objectRequest];
2165
        } else if (objectRequest.responseStatusCode == 201) {
2166
            [[[storedLocalObjectStates objectForKey:[objectRequest.userInfo objectForKey:@"accountName"]] 
2167
              objectForKey:[[objectRequest.userInfo objectForKey:@"pithosContainer"] name]] 
2168
             removeObjectForKey:[[objectRequest.userInfo objectForKey:@"pithosObject"] name]];
2169
            [self saveLocalState];
2170
            dispatch_async(dispatch_get_main_queue(), ^{
2171
                [activityFacility endActivity:[objectRequest.userInfo objectForKey:@"activity"] 
2172
                                  withMessage:[objectRequest.userInfo objectForKey:@"finishedActivityMessage"]];
2173
            });
2174
            [self syncOperationFinishedWithSuccess:YES];
2175
        } else {
2176
            [self requestFailed:objectRequest];
2177
        }
2178
    }
2179
}
2180

    
2181
- (void)deleteObjectFinished:(ASIPithosObjectRequest *)objectRequest {
2182
    @autoreleasepool {
2183
        NSOperation *operation = [objectRequest.userInfo objectForKey:@"operation"];
2184
        DLog(@"Sync::delete object finished: %@", objectRequest.url);
2185
        if (operation.isCancelled) {
2186
            [self requestFailed:objectRequest];
2187
        } else if (objectRequest.responseStatusCode == 204) {
2188
            [[[storedLocalObjectStates objectForKey:[objectRequest.userInfo objectForKey:@"accountName"]] 
2189
              objectForKey:[[objectRequest.userInfo objectForKey:@"pithosContainer"] name]] 
2190
             removeObjectForKey:[[objectRequest.userInfo objectForKey:@"pithosObject"] name]];
2191
            [self saveLocalState];
2192
            dispatch_async(dispatch_get_main_queue(), ^{
2193
                [activityFacility endActivity:[objectRequest.userInfo objectForKey:@"activity"] 
2194
                                  withMessage:[objectRequest.userInfo objectForKey:@"finishedActivityMessage"]];
2195
            });
2196
            [self syncOperationFinishedWithSuccess:YES];
2197
        } else {
2198
            [self requestFailed:objectRequest];
2199
        }
2200
    }
2201
}
2202

    
2203
- (void)uploadObjectUsingHashMapFinished:(ASIPithosObjectRequest *)objectRequest {
2204
    @autoreleasepool {
2205
        NSOperation *operation = [objectRequest.userInfo objectForKey:@"operation"];
2206
        DLog(@"Sync::upload using hashmap finished: %@", objectRequest.url);
2207
        NSString *accountName = [objectRequest.userInfo objectForKey:@"accountName"];
2208
        ASIPithosContainer *pithosContainer = [objectRequest.userInfo objectForKey:@"pithosContainer"];
2209
        ASIPithosObject *object = [objectRequest.userInfo objectForKey:@"pithosObject"];
2210
        PithosLocalObjectState *storedState = [[[storedLocalObjectStates objectForKey:accountName] 
2211
                                                objectForKey:pithosContainer.name] 
2212
                                               objectForKey:object.name];
2213
        PithosActivity *activity = [objectRequest.userInfo objectForKey:@"activity"];
2214
        NSUInteger totalBytes = activity.totalBytes;
2215
        NSUInteger currentBytes = activity.currentBytes;
2216
        if (operation.isCancelled) {
2217
            [self requestFailed:objectRequest];
2218
        } else if (objectRequest.responseStatusCode == 201) {
2219
            DLog(@"Sync::object created: %@", objectRequest.url);
2220
            storedState.filePath = [objectRequest.userInfo objectForKey:@"filePath"];
2221
            storedState.hash = object.objectHash;
2222
            [self saveLocalState];
2223
            dispatch_async(dispatch_get_main_queue(), ^{
2224
                [activityFacility endActivity:activity 
2225
                                  withMessage:[objectRequest.userInfo objectForKey:@"finishedActivityMessage"] 
2226
                                   totalBytes:totalBytes 
2227
                                 currentBytes:totalBytes];
2228
            });
2229
            [self syncOperationFinishedWithSuccess:YES];
2230
        } else if (objectRequest.responseStatusCode == 409) {
2231
            if (newSyncRequested || syncLate || operation.isCancelled) {
2232
                [self requestFailed:objectRequest];
2233
            } else {
2234
                NSUInteger iteration = [[objectRequest.userInfo objectForKey:@"iteration"] unsignedIntegerValue];
2235
                if (iteration == 0) {
2236
                    DLog(@"Sync::upload iteration limit reached: %@", objectRequest.url);
2237
                    dispatch_async(dispatch_get_main_queue(), ^{
2238
                        [activityFacility endActivity:activity withMessage:[objectRequest.userInfo objectForKey:@"stoppedActivityMessage"]];
2239
                    });
2240
                    [self syncOperationFinishedWithSuccess:NO];
2241
                    return;
2242
                }
2243
                DLog(@"Sync::object is missing hashes: %@", objectRequest.url);
2244
                NSIndexSet *missingBlocks = [PithosUtilities missingBlocksForHashes:[objectRequest.userInfo objectForKey:@"hashes"]
2245
                                                                  withMissingHashes:[objectRequest hashes]];
2246
                if (totalBytes >= [missingBlocks count]*pithosContainer.blockSize)
2247
                    currentBytes = totalBytes - [missingBlocks count]*pithosContainer.blockSize;
2248
                dispatch_async(dispatch_get_main_queue(), ^{
2249
                    [activityFacility updateActivity:activity 
2250
                                         withMessage:[NSString stringWithFormat:@"%@ (%.0f%%)", 
2251
                                                      [objectRequest.userInfo objectForKey:@"messagePrefix"], 
2252
                                                      (100*(currentBytes + 0.0)/(totalBytes + 0.0))] 
2253
                                          totalBytes:totalBytes 
2254
                                        currentBytes:currentBytes];
2255
                });
2256
                NSUInteger missingBlockIndex = [missingBlocks firstIndex];
2257
                __block ASIPithosContainerRequest *newContainerRequest = [PithosUtilities updateContainerDataRequestWithPithos:pithos
2258
                                                                                                                 containerName:pithosContainer.name 
2259
                                                                                                                     blockSize:pithosContainer.blockSize 
2260
                                                                                                                       forFile:[objectRequest.userInfo objectForKey:@"filePath"] 
2261
                                                                                                             missingBlockIndex:missingBlockIndex 
2262
                                                                                                                sharingAccount:([accountName isEqualToString:@""] ? nil : accountName)];
2263
                newContainerRequest.delegate = self;
2264
                newContainerRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:);
2265
                newContainerRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:);
2266
                newContainerRequest.userInfo = objectRequest.userInfo;
2267
                [(NSMutableDictionary *)(newContainerRequest.userInfo) setObject:[NSNumber numberWithUnsignedInteger:(--iteration)] forKey:@"iteration"];
2268
                [(NSMutableDictionary *)(newContainerRequest.userInfo) setObject:[NSNumber numberWithUnsignedInteger:10] forKey:@"retries"];
2269
                [(NSMutableDictionary *)(newContainerRequest.userInfo) setObject:missingBlocks forKey:@"missingBlocks"];
2270
                [(NSMutableDictionary *)(newContainerRequest.userInfo) setObject:[NSNumber numberWithUnsignedInteger:missingBlockIndex] forKey:@"missingBlockIndex"];
2271
                [(NSMutableDictionary *)(newContainerRequest.userInfo) setObject:NSStringFromSelector(@selector(uploadMissingBlockFinished:)) forKey:@"didFinishSelector"];
2272
                [newContainerRequest setBytesSentBlock:^(unsigned long long size, unsigned long long total){
2273
                    [activityFacility updateActivity:activity 
2274
                                         withMessage:[NSString stringWithFormat:@"%@ (%.0f%%)", 
2275
                                                      [newContainerRequest.userInfo objectForKey:@"messagePrefix"], 
2276
                                                      (100*(activity.currentBytes + size + 0.0)/(activity.totalBytes + 0.0))] 
2277
                                          totalBytes:activity.totalBytes 
2278
                                        currentBytes:(activity.currentBytes + size)];
2279
                }];
2280
                [networkQueue addOperation:[PithosUtilities prepareRequest:newContainerRequest priority:[[newContainerRequest.userInfo objectForKey:@"priority"] integerValue]]];
2281
            }
2282
        } else {
2283
            [self requestFailed:objectRequest];
2284
        }
2285
    }
2286
}
2287

    
2288
- (void)uploadMissingBlockFinished:(ASIPithosContainerRequest *)containerRequest {
2289
    @autoreleasepool {
2290
        NSOperation *operation = [containerRequest.userInfo objectForKey:@"operation"];
2291
        DLog(@"Sync::upload of missing block finished: %@", containerRequest.url);
2292
        if (operation.isCancelled) {
2293
            [self requestFailed:containerRequest];
2294
        } else if (containerRequest.responseStatusCode == 202) {
2295
            NSString *accountName = [containerRequest.userInfo objectForKey:@"accountName"];
2296
            ASIPithosContainer *pithosContainer = [containerRequest.userInfo objectForKey:@"pithosContainer"];
2297
            ASIPithosObject *object = [containerRequest.userInfo objectForKey:@"pithosObject"];
2298
            PithosActivity *activity = [containerRequest.userInfo objectForKey:@"activity"];
2299
            NSIndexSet *missingBlocks = [containerRequest.userInfo objectForKey:@"missingBlocks"];
2300
            NSUInteger missingBlockIndex = [[containerRequest.userInfo objectForKey:@"missingBlockIndex"] unsignedIntegerValue];
2301
            missingBlockIndex = [missingBlocks indexGreaterThanIndex:missingBlockIndex];
2302
            if (operation.isCancelled) {
2303
                [self requestFailed:containerRequest];
2304
            } else if (missingBlockIndex == NSNotFound) {
2305
                NSArray *hashes = [containerRequest.userInfo objectForKey:@"hashes"];
2306
                ASIPithosObjectRequest *newObjectRequest = [PithosUtilities writeObjectDataRequestWithPithos:pithos 
2307
                                                                                               containerName:pithosContainer.name 
2308
                                                                                                  objectName:object.name 
2309
                                                                                                 contentType:object.contentType 
2310
                                                                                                   blockSize:pithosContainer.blockSize 
2311
                                                                                                   blockHash:pithosContainer.blockHash
2312
                                                                                                     forFile:[containerRequest.userInfo objectForKey:@"filePath"] 
2313
                                                                                               checkIfExists:NO 
2314
                                                                                                      hashes:&hashes 
2315
                                                                                              sharingAccount:([accountName isEqualToString:@""] ? nil : accountName)];
2316
                newObjectRequest.delegate = self;
2317
                newObjectRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:);
2318
                newObjectRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:);
2319
                newObjectRequest.userInfo = containerRequest.userInfo;
2320
                [(NSMutableDictionary *)(newObjectRequest.userInfo) setObject:[NSNumber numberWithUnsignedInteger:10] forKey:@"retries"];
2321
                [(NSMutableDictionary *)(newObjectRequest.userInfo) removeObjectForKey:@"missingBlocks"];
2322
                [(NSMutableDictionary *)(newObjectRequest.userInfo) removeObjectForKey:@"missingBlockIndex"];
2323
                [(NSMutableDictionary *)(newObjectRequest.userInfo) setObject:NSStringFromSelector(@selector(uploadObjectUsingHashMapFinished:)) forKey:@"didFinishSelector"];
2324
                [networkQueue addOperation:[PithosUtilities prepareRequest:newObjectRequest priority:[[newObjectRequest.userInfo objectForKey:@"priority"] integerValue]]];
2325
            } else {
2326
                if (newSyncRequested || syncLate || operation.isCancelled) {
2327
                    [self requestFailed:containerRequest];
2328
                } else {
2329
                    __block ASIPithosContainerRequest *newContainerRequest = [PithosUtilities updateContainerDataRequestWithPithos:pithos 
2330
                                                                                                                     containerName:pithosContainer.name
2331
                                                                                                                         blockSize:pithosContainer.blockSize
2332
                                                                                                                           forFile:[containerRequest.userInfo objectForKey:@"filePath"] 
2333
                                                                                                                 missingBlockIndex:missingBlockIndex 
2334
                                                                                                                    sharingAccount:([accountName isEqualToString:@""] ? nil : accountName)];
2335
                    newContainerRequest.delegate = self;
2336
                    newContainerRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:);
2337
                    newContainerRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:);
2338
                    newContainerRequest.userInfo = containerRequest.userInfo;
2339
                    [(NSMutableDictionary *)(newContainerRequest.userInfo) setObject:[NSNumber numberWithUnsignedInteger:10] forKey:@"retries"];
2340
                    [(NSMutableDictionary *)(newContainerRequest.userInfo) setObject:[NSNumber numberWithUnsignedInteger:missingBlockIndex] forKey:@"missingBlockIndex"];
2341
                    [newContainerRequest setBytesSentBlock:^(unsigned long long size, unsigned long long total){
2342
                        [activityFacility updateActivity:activity 
2343
                                             withMessage:[NSString stringWithFormat:@"%@ (%.0f%%)", 
2344
                                                          [newContainerRequest.userInfo objectForKey:@"messagePrefix"], 
2345
                                                          (100*(activity.currentBytes + size + 0.0)/(activity.totalBytes + 0.0))] 
2346
                                              totalBytes:activity.totalBytes 
2347
                                            currentBytes:(activity.currentBytes + size)];
2348
                    }];
2349
                    [networkQueue addOperation:[PithosUtilities prepareRequest:newContainerRequest priority:[[newContainerRequest.userInfo objectForKey:@"priority"] integerValue]]];
2350
                }
2351
            }
2352
        } else {
2353
            [self requestFailed:containerRequest];
2354
        }
2355
    }
2356
}
2357

    
2358
- (void)requestFailed:(ASIPithosRequest *)request {
2359
    @autoreleasepool {
2360
        NSOperation *operation = [request.userInfo objectForKey:@"operation"];
2361
        DLog(@"Sync::request failed: %@", request.url);
2362
        if (operation.isCancelled)
2363
            return;
2364
        if (request.isCancelled || newSyncRequested || syncLate) {
2365
            dispatch_async(dispatch_get_main_queue(), ^{
2366
                [activityFacility endActivity:[request.userInfo objectForKey:@"activity"] 
2367
                                  withMessage:[request.userInfo objectForKey:@"stoppedActivityMessage"]];
2368
            });
2369
            [self syncOperationFinishedWithSuccess:NO];
2370
            return;
2371
        }
2372
        NSUInteger retries = [[request.userInfo objectForKey:@"retries"] unsignedIntegerValue];
2373
        if (retries > 0) {
2374
            ASIPithosRequest *newRequest = (ASIPithosRequest *)[PithosUtilities retryWithUpdatedURLRequest:request
2375
                                                                                   andPithosAccountManager:pithosAccount];
2376
            [(NSMutableDictionary *)(newRequest.userInfo)setObject:[NSNumber numberWithUnsignedInteger:(--retries)] forKey:@"retries"];
2377
            [networkQueue addOperation:[PithosUtilities prepareRequest:newRequest priority:[[newRequest.userInfo objectForKey:@"priority"] integerValue]]];
2378
        } else {
2379
            dispatch_async(dispatch_get_main_queue(), ^{
2380
                [activityFacility endActivity:[request.userInfo objectForKey:@"activity"] 
2381
                                  withMessage:[request.userInfo objectForKey:@"failedActivityMessage"]];
2382
            });
2383
            [self syncOperationFinishedWithSuccess:NO];
2384
        }
2385
    }
2386
}
2387

    
2388
@end