@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 to me"]
- stringByAppendingPathComponent:accountName]];
+ [PithosUtilities removeContentsAtPath:[[self.tempDownloadsDirPath stringByAppendingPathComponent:@"shared with me"]
+ 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 to me"]
+ [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]
pithos.storageURLPrefix = [aPithos.storageURLPrefix copy];
pithos.authURL = [aPithos.authURL copy];
pithos.publicURLPrefix = [aPithos.publicURLPrefix copy];
+ pithos.userCatalogURL = [aPithos.userCatalogURL copy];
}
if (aPithos &&
(![aPithos.authUser isEqualToString:pithos.authUser] ||
pithos.storageURLPrefix = [aPithos.storageURLPrefix copy];
pithos.authURL = [aPithos.authURL copy];
pithos.publicURLPrefix = [aPithos.publicURLPrefix copy];
- }
+ pithos.userCatalogURL = [aPithos.userCatalogURL copy];
+ }
}
#pragma mark -
return YES;
}
-- (NSString *)dirPathForAccount:(NSString *)accountName container:(NSString *)containerName {
- if ([accountName isEqualToString:@""])
- return [directoryPath stringByAppendingPathComponent:containerName];
- else
- return [[[directoryPath stringByAppendingPathComponent:@"shared to me"]
- stringByAppendingPathComponent:accountName]
- stringByAppendingPathComponent:containerName];
-}
-
- (NSString *)relativeDirPathForAccount:(NSString *)accountName container:(NSString *)containerName {
- if ([accountName isEqualToString:@""])
+ if ([accountName isEqualToString:@""]) {
return containerName;
- else
- return [[@"shared to 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]]) {
BOOL isDirectory;
BOOL fileExists = [fileManager fileExistsAtPath:filePath isDirectory:&isDirectory];
NSError *error = nil;
- NSString *containerDirectoryPath = [self dirPathForAccount:accountName container:pithosContainer.name];
- NSString *newFilePath = [filePath stringByReplacingOccurrencesOfString:containerDirectoryPath withString:self.tempTrashDirPath];
+ NSString *newFilePath = [filePath stringByReplacingOccurrencesOfString:directoryPath withString:self.tempTrashDirPath];
NSString *newDirPath = [newFilePath stringByDeletingLastPathComponent];
if (fileExists && isDirectory) {
NSArray *subPaths = [fileManager subpathsOfDirectoryAtPath:filePath error:&error];
}
for (NSString *subPath in subPaths) {
NSString *subFilePath = [filePath stringByAppendingPathComponent:subPath];
- NSString *newSubFilePath = [subFilePath stringByReplacingOccurrencesOfString:containerDirectoryPath
+ NSString *newSubFilePath = [subFilePath stringByReplacingOccurrencesOfString:directoryPath
withString:self.tempTrashDirPath];
currentState = [currentLocalObjectStates objectForKey:subFilePath];
if (currentState) {
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) {
if (!object || !object.objectHash) {
// Delete local object
if (![accountName isEqualToString:@""])
- // If "shared to me" skip
+ // If "shared with me" skip
return;
if (remoteObjectExists) {
// Remote object created in the meantime, just mark the sync cycle as incomplete, but do delete the local object
}
}
if (![object.allowedTo isEqualToString:@"write"]) {
- // If read-only "shared to me" skip
+ // If read-only "shared with me" skip
[self syncOperationFinishedWithSuccess:YES];
return;
}
} else if (![currentState exists]) {
// Delete remote object
if (![accountName isEqualToString:@""]) {
- // If "shared to me" skip
+ // If "shared with me" skip
[self syncOperationFinishedWithSuccess:YES];
return;
}
}
}
if (![object.allowedTo isEqualToString:@"write"]) {
- // If read-only "shared to me" skip
+ // If read-only "shared with me" skip
[self syncOperationFinishedWithSuccess:YES];
return;
}
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);
if (newSyncRequested || syncLate || operation.isCancelled) {
[self requestFailed:objectRequest];
} else {
- __unsafe_unretained ASIPithosObjectRequest *newObjectRequest = [PithosUtilities objectBlockDataRequestWithPithos:pithos
+ __block ASIPithosObjectRequest *newObjectRequest = [PithosUtilities objectBlockDataRequestWithPithos:pithos
containerName:pithosContainer.name
object:object
blockIndex:missingBlockIndex
currentBytes:(activity.totalBytes - [missingBlocks count]*pithosContainer.blockSize)];
});
- __unsafe_unretained ASIPithosObjectRequest *newObjectRequest = [PithosUtilities objectBlockDataRequestWithPithos:pithos
+ __block ASIPithosObjectRequest *newObjectRequest = [PithosUtilities objectBlockDataRequestWithPithos:pithos
containerName:pithosContainer.name
object:object
blockIndex:missingBlockIndex
currentBytes:currentBytes];
});
NSUInteger missingBlockIndex = [missingBlocks firstIndex];
- __unsafe_unretained ASIPithosContainerRequest *newContainerRequest = [PithosUtilities updateContainerDataRequestWithPithos:pithos
+ __block ASIPithosContainerRequest *newContainerRequest = [PithosUtilities updateContainerDataRequestWithPithos:pithos
containerName:pithosContainer.name
blockSize:pithosContainer.blockSize
forFile:[objectRequest.userInfo objectForKey:@"filePath"]
if (newSyncRequested || syncLate || operation.isCancelled) {
[self requestFailed:containerRequest];
} else {
- __unsafe_unretained ASIPithosContainerRequest *newContainerRequest = [PithosUtilities updateContainerDataRequestWithPithos:pithos
+ __block ASIPithosContainerRequest *newContainerRequest = [PithosUtilities updateContainerDataRequestWithPithos:pithos
containerName:pithosContainer.name
blockSize:pithosContainer.blockSize
forFile:[containerRequest.userInfo objectForKey:@"filePath"]