From f09ca4182985cad9c54c8290c0dacdee6e5862d8 Mon Sep 17 00:00:00 2001 From: Miltiadis Vasilakis Date: Tue, 26 Feb 2013 01:17:51 +0200 Subject: [PATCH] Use user catalog in sync Create sync directories using the account displayname. Store user catalog per sync, and resolve displayname conflicts or renames. Fix bugs. Update version. --- pithos-macos/PithosPreferencesController.m | 6 +- pithos-macos/PithosSyncDaemon.h | 4 + pithos-macos/PithosSyncDaemon.m | 218 ++++++++++++++++++++++++---- pithos-macos/PithosUtilities.h | 1 + pithos-macos/PithosUtilities.m | 14 +- pithos-macos/pithos-macos-Info.plist | 4 +- 6 files changed, 208 insertions(+), 39 deletions(-) diff --git a/pithos-macos/PithosPreferencesController.m b/pithos-macos/PithosPreferencesController.m index a05f72a..52ffe02 100644 --- a/pithos-macos/PithosPreferencesController.m +++ b/pithos-macos/PithosPreferencesController.m @@ -504,7 +504,7 @@ // mixed if in dictionary with exclusions // on if in dictionary without exclusions PithosAccountNode *accountNode = (PithosAccountNode *)item; - NSMutableDictionary *syncContainersDictionary = [syncAccountsDictionary objectForKey:accountNode.displayName]; + NSMutableDictionary *syncContainersDictionary = [syncAccountsDictionary objectForKey:accountNode.sharingAccount]; if (syncContainersDictionary) { for (PithosContainerNode *node in accountNode.children) { NSMutableSet *containerExcludedDirectories = [syncContainersDictionary objectForKey:node.displayName]; @@ -602,9 +602,9 @@ for (PithosContainerNode *node in accountNode.children) { [syncContainersDictionary setObject:[NSMutableSet set] forKey:node.displayName]; } - [syncAccountsDictionary setObject:syncContainersDictionary forKey:accountNode.displayName]; + [syncAccountsDictionary setObject:syncContainersDictionary forKey:accountNode.sharingAccount]; } else { - [syncAccountsDictionary removeObjectForKey:accountNode.displayName]; + [syncAccountsDictionary removeObjectForKey:accountNode.sharingAccount]; } [outlineView reloadItem:item reloadChildren:YES]; } else if ([item class] == [PithosContainerNode class]) { diff --git a/pithos-macos/PithosSyncDaemon.h b/pithos-macos/PithosSyncDaemon.h index 529250c..abc8efe 100644 --- a/pithos-macos/PithosSyncDaemon.h +++ b/pithos-macos/PithosSyncDaemon.h @@ -48,6 +48,7 @@ BOOL skipHidden; ASIPithos *pithos; + NSMutableDictionary *userCatalog; NSUInteger accountsCount; NSMutableArray *accountsNames; NSUInteger accountsIndex; @@ -60,6 +61,7 @@ NSMutableDictionary *storedLocalObjectStates; NSMutableDictionary *currentLocalObjectStates; + NSString *userCatalogFilePath; NSString *pithosStateFilePath; NSString *tempDownloadsDirPath; NSString *tempTrashDirPath; @@ -83,6 +85,7 @@ @property (nonatomic, assign) BOOL skipHidden; @property (nonatomic, strong) ASIPithos *pithos; +@property (nonatomic, strong) NSMutableDictionary *userCatalog; @property (nonatomic, strong) NSMutableArray *accountsNames; @property (nonatomic, strong) NSMutableDictionary *accountsPithosContainers; @@ -91,6 +94,7 @@ @property (nonatomic, strong) NSMutableDictionary *storedLocalObjectStates; @property (nonatomic, strong) NSMutableDictionary *currentLocalObjectStates; +@property (nonatomic, strong, readonly) NSString *userCatalogFilePath; @property (nonatomic, strong, readonly) NSString *pithosStateFilePath; @property (nonatomic, strong, readonly) NSString *tempDownloadsDirPath; @property (nonatomic, strong, readonly) NSString *tempTrashDirPath; diff --git a/pithos-macos/PithosSyncDaemon.m b/pithos-macos/PithosSyncDaemon.m index 442bf7e..35675d7 100644 --- a/pithos-macos/PithosSyncDaemon.m +++ b/pithos-macos/PithosSyncDaemon.m @@ -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 @@ -108,6 +108,14 @@ 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; @@ -162,31 +170,35 @@ // 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]; } } @@ -261,6 +273,28 @@ #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] @@ -426,20 +460,25 @@ 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 - @@ -512,6 +551,95 @@ [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]]) { @@ -700,7 +828,7 @@ 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) { @@ -1779,20 +1907,46 @@ 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); diff --git a/pithos-macos/PithosUtilities.h b/pithos-macos/PithosUtilities.h index 2bbae93..70e41ac 100644 --- a/pithos-macos/PithosUtilities.h +++ b/pithos-macos/PithosUtilities.h @@ -141,6 +141,7 @@ + (NSUInteger)bytesOfFile:(NSString *)filePath; + (NSString *)contentTypeOfFile:(NSString *)filePath error:(NSError **)error; + (BOOL)safeCreateDirectory:(NSString *)directoryPath error:(NSError **)error; ++ (void)removeContentsAtPath:(NSString *)dirPath andDirectory:(BOOL)removeDirectory; + (void)removeContentsAtPath:(NSString *)dirPath; + (BOOL)isContentTypeDirectory:(NSString *)contentType; + (BOOL)objectExistsAtPithos:(ASIPithos *)pithos containerName:(NSString *)containerName objectName:(NSString *)objectName diff --git a/pithos-macos/PithosUtilities.m b/pithos-macos/PithosUtilities.m index 91de07d..d48928a 100644 --- a/pithos-macos/PithosUtilities.m +++ b/pithos-macos/PithosUtilities.m @@ -946,8 +946,8 @@ return YES; } -// Removes contents of a directory -+ (void)removeContentsAtPath:(NSString *)dirPath { +// Removes contents of a directory and the directory itself if selected ++ (void)removeContentsAtPath:(NSString *)dirPath andDirectory:(BOOL)removeDirectory { NSFileManager *fileManager = [NSFileManager defaultManager]; NSError *error = nil; BOOL isDirectory; @@ -969,6 +969,11 @@ } error = nil; } + if (removeDirectory && (![fileManager removeItemAtPath:dirPath error:&error] || error)) { + [self fileActionFailedAlertWithTitle:@"Remove Directory Error" + message:[NSString stringWithFormat:@"Cannot remove directory at '%@'", dirPath] + error:error]; + } } else if (![fileManager removeItemAtPath:dirPath error:&error] || error) { [self fileActionFailedAlertWithTitle:@"Remove File Error" message:[NSString stringWithFormat:@"Cannot remove file at '%@'", dirPath] @@ -976,6 +981,11 @@ } } +// Removes contents of a directory ++ (void)removeContentsAtPath:(NSString *)dirPath { + [self removeContentsAtPath:dirPath andDirectory:NO]; +} + // Returns if an object is a directory based on its content type + (BOOL)isContentTypeDirectory:(NSString *)contentType { return ([contentType isEqualToString:@"application/directory"] || diff --git a/pithos-macos/pithos-macos-Info.plist b/pithos-macos/pithos-macos-Info.plist index e0de2ac..517a75c 100644 --- a/pithos-macos/pithos-macos-Info.plist +++ b/pithos-macos/pithos-macos-Info.plist @@ -17,7 +17,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.1.1 + 1.2 CFBundleSignature ???? CFBundleURLTypes @@ -32,7 +32,7 @@ CFBundleVersion - 20130115.0 + 20130226.0 LSMinimumSystemVersion ${MACOSX_DEPLOYMENT_TARGET} NSMainNibFile -- 1.7.10.4