Use user catalog in sync
[pithos-macos] / pithos-macos / PithosSyncDaemon.m
index 442bf7e..35675d7 100644 (file)
@@ -82,9 +82,9 @@
 
 @implementation PithosSyncDaemon
 @synthesize directoryPath, accountsDictionary, skipHidden, pithos;
-@synthesize accountsNames, accountsPithosContainers;
+@synthesize userCatalog, accountsNames, accountsPithosContainers;
 @synthesize lastCompletedSync, remoteObjects, previousRemoteObjects, storedLocalObjectStates, currentLocalObjectStates;
-@synthesize pithosStateFilePath, tempDownloadsDirPath, tempTrashDirPath;
+@synthesize userCatalogFilePath, pithosStateFilePath, tempDownloadsDirPath, tempTrashDirPath;
 
 #pragma mark -
 #pragma Object Lifecycle
         else
             [self resetLocalStateWithAll:NO];
         
+        // Load user catalog.
+        if ([[NSFileManager defaultManager] fileExistsAtPath:self.userCatalogFilePath])
+            self.userCatalog = [NSKeyedUnarchiver unarchiveObjectWithFile:self.userCatalogFilePath];
+        else
+            self.userCatalog = [NSMutableDictionary dictionary];
+        if (!userCatalog)
+            self.userCatalog = [NSMutableDictionary dictionary];
+        
         networkQueue = [[ASINetworkQueue alloc] init];
         networkQueue.showAccurateProgress = YES;
         networkQueue.shouldCancelAllRequestsOnFailure = NO;
             // Remove containers that don't interest us anymore and save
             if (!storedLocalObjectStates)
                 [self loadLocalState];
-            for (NSString *accountName in storedLocalObjectStates) {
+            for (NSString *accountName in [storedLocalObjectStates allKeys]) {
                 NSMutableDictionary *accountStoredLocalObjectStates = [storedLocalObjectStates objectForKey:accountName];
                 NSDictionary *containersDictionary = [accountsDictionary objectForKey:accountName];
                 if (!containersDictionary) {
                     if (self.tempDownloadsDirPath) {
                         if ([accountName isEqualToString:@""]) {
                             for (NSString *containerName in accountStoredLocalObjectStates) {
-                                [PithosUtilities removeContentsAtPath:[self.tempDownloadsDirPath stringByAppendingPathComponent:containerName]];
+                                [PithosUtilities removeContentsAtPath:[self.tempDownloadsDirPath stringByAppendingPathComponent:containerName]
+                                                         andDirectory:YES];
                             }
                         } else {
                             [PithosUtilities removeContentsAtPath:[[self.tempDownloadsDirPath stringByAppendingPathComponent:@"shared with me"]
-                                                                   stringByAppendingPathComponent:accountName]];
+                                                                   stringByAppendingPathComponent:accountName]
+                                                     andDirectory:YES];
                         }
                     }
                     [storedLocalObjectStates removeObjectForKey:accountName];
                 } else {
                     // Check the account's containers
-                    for (NSString *containerName in accountStoredLocalObjectStates) {
+                    for (NSString *containerName in [accountStoredLocalObjectStates allKeys]) {
                         if (![containersDictionary objectForKey:containerName]) {
                             if ([accountName isEqualToString:@""])
-                                [PithosUtilities removeContentsAtPath:[self.tempDownloadsDirPath stringByAppendingPathComponent:containerName]];
+                                [PithosUtilities removeContentsAtPath:[self.tempDownloadsDirPath stringByAppendingPathComponent:containerName]
+                                                         andDirectory:YES];
                             else
                                 [PithosUtilities removeContentsAtPath:[[[self.tempDownloadsDirPath stringByAppendingPathComponent:@"shared with me"]
                                                                         stringByAppendingPathComponent:accountName] 
-                                                                       stringByAppendingPathComponent:containerName]];
+                                                                       stringByAppendingPathComponent:containerName]
+                                                         andDirectory:YES];
                             [accountStoredLocalObjectStates removeObjectForKey:containerName];
                         }
                     }
 #pragma mark -
 #pragma mark Properties
 
+- (NSString *)userCatalogFilePath {
+    if (!userCatalogFilePath) {
+        userCatalogFilePath = [[[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) objectAtIndex:0]
+                                stringByAppendingPathComponent:[[NSBundle mainBundle] bundleIdentifier]]
+                               stringByAppendingPathComponent:[NSString stringWithFormat:@"%@-SyncUserCatalog.archive",
+                                                               pithosAccount.uniqueName]];
+        NSString *userCatalogFileDirPath = [userCatalogFilePath stringByDeletingLastPathComponent];
+        NSFileManager *fileManager = [NSFileManager defaultManager];
+        BOOL isDirectory;
+        BOOL fileExists = [fileManager fileExistsAtPath:userCatalogFileDirPath isDirectory:&isDirectory];
+        NSError *error = nil;
+        if (fileExists && !isDirectory)
+            [fileManager removeItemAtPath:userCatalogFileDirPath error:&error];
+        if (!error && !fileExists)
+            [fileManager createDirectoryAtPath:userCatalogFileDirPath withIntermediateDirectories:YES attributes:nil error:&error];
+        //if (error)
+        //  userCatalogFilePath = nil;
+        // XXX create a dir using mktmps?
+    }
+    return [userCatalogFilePath copy];
+}
+
 - (NSString *)pithosStateFilePath {
     if (!pithosStateFilePath) {
         pithosStateFilePath = [[[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) objectAtIndex:0] 
     return YES;
 }
 
-- (NSString *)dirPathForAccount:(NSString *)accountName container:(NSString *)containerName {
-    if ([accountName isEqualToString:@""])
-        return [directoryPath stringByAppendingPathComponent:containerName];
-    else
-        return [[[directoryPath stringByAppendingPathComponent:@"shared with me"]
-                 stringByAppendingPathComponent:accountName] 
-                stringByAppendingPathComponent:containerName];
-}
-
 - (NSString *)relativeDirPathForAccount:(NSString *)accountName container:(NSString *)containerName {
-    if ([accountName isEqualToString:@""])
+    if ([accountName isEqualToString:@""]) {
         return containerName;
-    else
-        return [[@"shared with me" stringByAppendingPathComponent:accountName] stringByAppendingPathComponent:containerName];
+    } else {
+        NSMutableDictionary *displaynameDictionary = [userCatalog objectForKey:accountName];
+        if ([[displaynameDictionary objectForKey:@"suffix"] unsignedIntegerValue] == 0) {
+            return [[@"shared with me" stringByAppendingPathComponent:[displaynameDictionary objectForKey:@"displayname"]]
+                    stringByAppendingPathComponent:containerName];
+        } else {
+            return [[@"shared with me" stringByAppendingPathComponent:[NSString stringWithFormat:@"%@ (%@)",
+                                                                       [displaynameDictionary objectForKey:@"displayname"],
+                                                                       [displaynameDictionary objectForKey:@"suffix"]]]
+                    stringByAppendingPathComponent:containerName];
+        }
+    }
+}
+
+- (NSString *)dirPathForAccount:(NSString *)accountName container:(NSString *)containerName {
+    return [directoryPath stringByAppendingPathComponent:[self relativeDirPathForAccount:accountName container:containerName]];
 }
 
 #pragma mark -
         [self syncOperationFinishedWithSuccess:NO];
         return;
     }
+    // Update user catalog for accountsNames.
+    ASIPithosRequest *userCatalogRequest = [pithosAccount updateUserCatalogForForDisplaynames:nil UUIDs:accountsNames];
+    if (userCatalogRequest.error || ((userCatalogRequest.responseStatusCode != 200) && (userCatalogRequest.responseStatusCode != 404))) {
+        // Update failed try sync again later.
+        [self syncOperationFinishedWithSuccess:NO];
+        return;
+    }
+    for (NSString *accountName in accountsNames) {
+        if (![accountName isEqualToString:@""]) {
+            // If 404, displayname will be same as accountName.
+            NSString *displayname = [pithosAccount displaynameForUUID:accountName safe:YES];
+            NSMutableDictionary *displaynameDictionary = [userCatalog objectForKey:accountName];
+            if (!displaynameDictionary) {
+                // New entry in the user catalog. Determine suffix discriminator (0 is for no suffix).
+                NSNumber *suffix = [NSNumber numberWithInteger:0];
+                for (NSMutableDictionary *otherDisplaynameDictionary in [userCatalog objectEnumerator]) {
+                    if ([[otherDisplaynameDictionary objectForKey:@"displayname"] isEqualToString:displayname] &&
+                        [[otherDisplaynameDictionary objectForKey:@"suffix"] isGreaterThanOrEqualTo:suffix]) {
+                        suffix = [NSNumber numberWithUnsignedInteger:([[otherDisplaynameDictionary objectForKey:@"suffix"] unsignedIntegerValue] + 1)];
+                    }
+                }
+                [userCatalog setObject:[NSMutableDictionary dictionaryWithObjectsAndKeys:
+                                        displayname, @"displayname",
+                                        suffix, @"suffix",
+                                        nil]
+                                forKey:accountName];
+                // Save user catalog.
+                [NSKeyedArchiver archiveRootObject:userCatalog toFile:self.userCatalogFilePath];
+            } else if (![[displaynameDictionary objectForKey:@"displayname"] isEqualToString:displayname]) {
+                // First determine new suffix.
+                NSNumber *suffix = [NSNumber numberWithInteger:0];
+                for (NSMutableDictionary *otherDisplaynameDictionary in [userCatalog objectEnumerator]) {
+                    if ([[otherDisplaynameDictionary objectForKey:@"displayname"] isEqualToString:displayname] &&
+                        [[otherDisplaynameDictionary objectForKey:@"suffix"] isGreaterThanOrEqualTo:suffix]) {
+                        suffix = [NSNumber numberWithUnsignedInteger:([[otherDisplaynameDictionary objectForKey:@"suffix"] unsignedIntegerValue] + 1)];
+                    }
+                }
+                // Compute old and new sync account dirPaths.
+                NSString *accountDirPath = (([[displaynameDictionary objectForKey:@"suffix"] unsignedIntegerValue] == 0) ?
+                                            [[directoryPath stringByAppendingPathComponent:@"shared with me"]
+                                             stringByAppendingPathComponent:[displaynameDictionary objectForKey:@"displayname"]] :
+                                            [[directoryPath stringByAppendingPathComponent:@"shared with me"]
+                                             stringByAppendingPathComponent:[NSString stringWithFormat:@"%@ (%@)",
+                                                                             [displaynameDictionary objectForKey:@"displayname"],
+                                                                             [displaynameDictionary objectForKey:@"suffix"]]]);
+                NSString *newAccountDirPath = (([suffix unsignedIntegerValue] == 0) ?
+                                               [[directoryPath stringByAppendingPathComponent:@"shared with me"]
+                                                stringByAppendingPathComponent:displayname] :
+                                               [[directoryPath stringByAppendingPathComponent:@"shared with me"]
+                                                stringByAppendingPathComponent:[NSString stringWithFormat:@"%@ (%@)",
+                                                                                displayname, suffix]]);
+                // Check if the old account sync directory exists.
+                NSFileManager *fileManager = [NSFileManager defaultManager];
+                BOOL isDirectory;
+                NSError *error = nil;
+                if ([fileManager fileExistsAtPath:accountDirPath isDirectory:&isDirectory] && isDirectory &&
+                    (![fileManager moveItemAtPath:accountDirPath toPath:newAccountDirPath error:&error] || error)) {
+                    // If so try to move it. We know that the containing directory "shared with me" exists.
+                    [PithosUtilities fileActionFailedAlertWithTitle:@"Move Directory Error"
+                                                            message:[NSString stringWithFormat:@"Cannot move directory at '%@' to '%@'",
+                                                                     accountDirPath, newAccountDirPath]
+                                                              error:error];
+                    [self syncOperationFinishedWithSuccess:NO];
+                    return;
+                }
+                // Else the new account sync directory will be created afterwards.
+                // Fix filePaths of the stored local object states of the account.
+                for (NSMutableDictionary *containerStoredLocalObjectStates in [[storedLocalObjectStates objectForKey:accountName] objectEnumerator]) {
+                    for (PithosLocalObjectState *storedLocalObjectState in [containerStoredLocalObjectStates objectEnumerator]) {
+                        if ([storedLocalObjectState.filePath hasPrefix:accountDirPath]) {
+                            storedLocalObjectState.filePath = [newAccountDirPath stringByAppendingString:
+                                                               [storedLocalObjectState.filePath substringFromIndex:accountDirPath.length]];
+                        }
+                    }
+                }
+                [self saveLocalState];
+                // Finally update user catalog.
+                [userCatalog setObject:[NSMutableDictionary dictionaryWithObjectsAndKeys:
+                                        displayname, @"displayname",
+                                        suffix, @"suffix",
+                                        nil]
+                                forKey:accountName];
+                // Save user catalog.
+                [NSKeyedArchiver archiveRootObject:userCatalog toFile:self.userCatalogFilePath];
+            }
+            // Else no change is required.
+        }
+    }
+
     for (NSString *accountName in accountsNames) {
         for (ASIPithosContainer *pithosContainer in [accountsPithosContainers objectForKey:accountName]) {
             if (![self createSyncDirectory:[self dirPathForAccount:accountName container:pithosContainer.name]]) {
         NSFileManager *fileManager = [NSFileManager defaultManager];
         BOOL isDirectory;
         NSError *error = nil;
-        for (NSString *localFilePath in currentLocalObjectStates) {
+        for (NSString *localFilePath in [currentLocalObjectStates allKeys]) {
             localState = [currentLocalObjectStates objectForKey:localFilePath];
             if (!localState.isDirectory && [hash isEqualToString:localState.hash] && 
                 [fileManager fileExistsAtPath:localFilePath isDirectory:&isDirectory] && !isDirectory) {
             NSError *error;
             PithosActivity *activity = [objectRequest.userInfo objectForKey:@"activity"];
             
-            NSString *downloadsDirPath = self.tempDownloadsDirPath;
-            if (!downloadsDirPath) {
-                dispatch_async(dispatch_get_main_queue(), ^{
-                    [activityFacility endActivity:activity 
-                                      withMessage:[objectRequest.userInfo objectForKey:@"failedActivityMessage"]];
-                });
-                [self syncOperationFinishedWithSuccess:NO];
-                return;
-            }
-            
-            PithosLocalObjectState *storedState = [[[storedLocalObjectStates objectForKey:accountName] 
+            PithosLocalObjectState *storedState = [[[storedLocalObjectStates objectForKey:accountName]
                                                     objectForKey:pithosContainer.name] 
                                                    objectForKey:object.name];
             if ((storedState.tmpFilePath == nil) || ![fileManager fileExistsAtPath:storedState.tmpFilePath]) {
+                NSString *downloadsDirPath = self.tempDownloadsDirPath;
+                if (!downloadsDirPath) {
+                    dispatch_async(dispatch_get_main_queue(), ^{
+                        [activityFacility endActivity:activity
+                                          withMessage:[objectRequest.userInfo objectForKey:@"failedActivityMessage"]];
+                    });
+                    [self syncOperationFinishedWithSuccess:NO];
+                    return;
+                }
+                if ([accountName isEqualToString:@""]) {
+                    downloadsDirPath = [downloadsDirPath stringByAppendingPathComponent:pithosContainer.name];
+                } else {
+                    downloadsDirPath = [[[downloadsDirPath stringByAppendingPathComponent:@"shared with me"]
+                                         stringByAppendingPathComponent:accountName]
+                                        stringByAppendingPathComponent:pithosContainer.name];
+                }
+                BOOL isDirectory;
+                error = nil;
+                if (![fileManager fileExistsAtPath:downloadsDirPath isDirectory:&isDirectory]) {
+                    if (![fileManager createDirectoryAtPath:downloadsDirPath withIntermediateDirectories:YES attributes:nil error:&error] || error) {
+                        dispatch_async(dispatch_get_main_queue(), ^{
+                            [activityFacility endActivity:activity
+                                              withMessage:[objectRequest.userInfo objectForKey:@"failedActivityMessage"]];
+                        });
+                        [self syncOperationFinishedWithSuccess:NO];
+                        return;
+                    }
+                } else if (!isDirectory) {
+                    dispatch_async(dispatch_get_main_queue(), ^{
+                        [activityFacility endActivity:activity
+                                          withMessage:[objectRequest.userInfo objectForKey:@"failedActivityMessage"]];
+                    });
+                    [self syncOperationFinishedWithSuccess:NO];
+                    return;
+                }
+                
                 NSString *tempFileTemplate = [downloadsDirPath stringByAppendingPathComponent:@"download.XXXXXX"];
                 const char *tempFileTemplateCString = [tempFileTemplate fileSystemRepresentation];
                 char *tempFileNameCString = (char *)malloc(strlen(tempFileTemplateCString) + 1);