Files
2014-02-25 07:09:41 -05:00

591 lines
24 KiB
Objective-C

//
// CDEICloudFileSystem.m
// Ensembles
//
// Created by Drew McCormack on 20/09/13.
// Copyright (c) 2013 Drew McCormack. All rights reserved.
//
#import "CDEICloudFileSystem.h"
#import "CDECloudDirectory.h"
#import "CDECloudFile.h"
#import "CDEAvailabilityMacros.h"
NSString * const CDEICloudFileSystemDidDownloadFilesNotification = @"CDEICloudFileSystemDidUpdateFilesNotification";
@implementation CDEICloudFileSystem {
NSFileManager *fileManager;
NSURL *rootDirectoryURL;
NSMetadataQuery *metadataQuery;
NSOperationQueue *operationQueue;
NSString *ubiquityContainerIdentifier;
dispatch_queue_t timeOutQueue;
dispatch_queue_t initiatingDownloadsQueue;
id ubiquityIdentityObserver;
NSUInteger numberOfUnfinishedDownloads;
NSOperationQueue *downloadTrackingQueue;
}
- (instancetype)initWithUbiquityContainerIdentifier:(NSString *)newIdentifier
{
self = [super init];
if (self) {
fileManager = [[NSFileManager alloc] init];
operationQueue = [[NSOperationQueue alloc] init];
operationQueue.maxConcurrentOperationCount = 1;
downloadTrackingQueue = [[NSOperationQueue alloc] init];
downloadTrackingQueue.maxConcurrentOperationCount = 1;
timeOutQueue = dispatch_queue_create("com.mentalfaculty.ensembles.queue.icloudtimeout", DISPATCH_QUEUE_SERIAL);
initiatingDownloadsQueue = dispatch_queue_create("com.mentalfaculty.ensembles.queue.initiatedownloads", DISPATCH_QUEUE_SERIAL);
rootDirectoryURL = nil;
metadataQuery = nil;
ubiquityContainerIdentifier = [newIdentifier copy];
ubiquityIdentityObserver = nil;
[self performInitialPreparation:NULL];
}
return self;
}
- (instancetype)init
{
@throw [NSException exceptionWithName:CDEException reason:@"iCloud initializer requires container identifier" userInfo:nil];
return nil;
}
- (void)dealloc
{
[self removeUbiquityContainerNotificationObservers];
[self stopMonitoring];
[operationQueue cancelAllOperations];
}
#pragma mark - User Identity
- (id <NSObject, NSCoding, NSCopying>)identityToken
{
return [fileManager ubiquityIdentityToken];
}
#pragma mark - Initial Preparation
- (void)performInitialPreparation:(CDECompletionBlock)completion
{
if (fileManager.ubiquityIdentityToken) {
[self setupRootDirectory:^(NSError *error) {
[self startMonitoringMetadata];
[self addUbiquityContainerNotificationObservers];
dispatch_async(dispatch_get_main_queue(), ^{
if (completion) completion(error);
});
}];
}
else {
[self addUbiquityContainerNotificationObservers];
dispatch_async(dispatch_get_main_queue(), ^{
if (completion) completion(nil);
});
}
}
#pragma mark - Root Directory
- (void)setupRootDirectory:(CDECompletionBlock)completion
{
[operationQueue addOperationWithBlock:^{
NSURL *newURL = [fileManager URLForUbiquityContainerIdentifier:ubiquityContainerIdentifier];
newURL = [newURL URLByAppendingPathComponent:@"com.mentalfaculty.ensembles.clouddata"];
rootDirectoryURL = newURL;
if (!rootDirectoryURL) {
NSError *error = [NSError errorWithDomain:CDEErrorDomain code:CDEErrorCodeServerError userInfo:@{NSLocalizedDescriptionKey : @"Could not retrieve URLForUbiquityContainerIdentifier. Check container id for iCloud."}];
CDELog(CDELoggingLevelError, @"Failed to get the URL of the iCloud ubiquity container: %@", error);
[self dispatchCompletion:completion withError:error];
return;
}
NSError *error = nil;
__block BOOL fileExistsAtPath = NO;
__block BOOL existingFileIsDirectory = NO;
NSFileCoordinator *coordinator = [[NSFileCoordinator alloc] initWithFilePresenter:nil];
[coordinator coordinateReadingItemAtURL:rootDirectoryURL options:NSFileCoordinatorReadingWithoutChanges error:&error byAccessor:^(NSURL *newURL) {
fileExistsAtPath = [fileManager fileExistsAtPath:newURL.path isDirectory:&existingFileIsDirectory];
}];
if (error) {
CDELog(CDELoggingLevelWarning, @"File coordinator error: %@", error);
[self dispatchCompletion:completion withError:error];
return;
}
if (!fileExistsAtPath) {
[coordinator coordinateWritingItemAtURL:rootDirectoryURL options:0 error:&error byAccessor:^(NSURL *newURL) {
[fileManager createDirectoryAtURL:newURL withIntermediateDirectories:YES attributes:nil error:NULL];
}];
}
else if (fileExistsAtPath && !existingFileIsDirectory) {
[coordinator coordinateWritingItemAtURL:rootDirectoryURL options:NSFileCoordinatorWritingForReplacing error:&error byAccessor:^(NSURL *newURL) {
[fileManager removeItemAtURL:newURL error:NULL];
[fileManager createDirectoryAtURL:newURL withIntermediateDirectories:YES attributes:nil error:NULL];
}];
}
if (error) CDELog(CDELoggingLevelWarning, @"File coordinator error: %@", error);
[self dispatchCompletion:completion withError:error];
}];
}
- (void)dispatchCompletion:(CDECompletionBlock)completion withError:(NSError *)error
{
dispatch_sync(dispatch_get_main_queue(), ^{
if (completion) completion(error);
});
}
- (NSString *)fullPathForPath:(NSString *)path
{
return [rootDirectoryURL.path stringByAppendingPathComponent:path];
}
#pragma mark - Notifications
- (void)removeUbiquityContainerNotificationObservers
{
[[NSNotificationCenter defaultCenter] removeObserver:ubiquityIdentityObserver];
ubiquityIdentityObserver = nil;
}
- (void)addUbiquityContainerNotificationObservers
{
[self removeUbiquityContainerNotificationObservers];
__weak typeof(self) weakSelf = self;
ubiquityIdentityObserver = [[NSNotificationCenter defaultCenter] addObserverForName:NSUbiquityIdentityDidChangeNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) {
__strong typeof(weakSelf) strongSelf = weakSelf;
[strongSelf stopMonitoring];
[strongSelf willChangeValueForKey:@"identityToken"];
[strongSelf didChangeValueForKey:@"identityToken"];
}];
}
#pragma mark - Connection
- (BOOL)isConnected
{
return fileManager.ubiquityIdentityToken != nil;
}
- (void)connect:(CDECompletionBlock)completion
{
dispatch_async(dispatch_get_main_queue(), ^{
BOOL loggedIn = fileManager.ubiquityIdentityToken != nil;
NSError *error = loggedIn ? nil : [NSError errorWithDomain:CDEErrorDomain code:CDEErrorCodeAuthenticationFailure userInfo:@{NSLocalizedDescriptionKey : NSLocalizedString(@"User is not logged into iCloud.", @"")} ];
if (completion) completion(error);
});
}
#pragma mark - Metadata Query to download new files
- (void)startMonitoringMetadata
{
[self stopMonitoring];
if (!rootDirectoryURL) return;
// Determine downloading key and set the appropriate predicate. This is OS dependent.
NSPredicate *metadataPredicate = nil;
#if (__IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_7_0) && (__MAC_OS_X_VERSION_MIN_REQUIRED < __MAC_10_9)
metadataPredicate = [NSPredicate predicateWithFormat:@"%K = FALSE AND %K = FALSE AND %K BEGINSWITH %@",
NSMetadataUbiquitousItemIsDownloadedKey, NSMetadataUbiquitousItemIsDownloadingKey, NSMetadataItemPathKey, rootDirectoryURL.path];
#else
metadataPredicate = [NSPredicate predicateWithFormat:@"%K != %@ AND %K = FALSE AND %K BEGINSWITH %@",
NSMetadataUbiquitousItemDownloadingStatusKey, NSMetadataUbiquitousItemDownloadingStatusCurrent, NSMetadataUbiquitousItemIsDownloadingKey, NSMetadataItemPathKey, rootDirectoryURL.path];
#endif
metadataQuery = [[NSMetadataQuery alloc] init];
metadataQuery.searchScopes = [NSArray arrayWithObject:NSMetadataQueryUbiquitousDataScope];
metadataQuery.predicate = metadataPredicate;
NSNotificationCenter *notifationCenter = [NSNotificationCenter defaultCenter];
[notifationCenter addObserver:self selector:@selector(metadataQueryDidFinishGathering:) name:NSMetadataQueryDidFinishGatheringNotification object:metadataQuery];
[notifationCenter addObserver:self selector:@selector(metadataQueryDidUpdate:) name:NSMetadataQueryDidUpdateNotification object:metadataQuery];
[metadataQuery startQuery];
}
- (void)stopMonitoring
{
if (!metadataQuery) return;
[metadataQuery disableUpdates];
[metadataQuery stopQuery];
[[NSNotificationCenter defaultCenter] removeObserver:self name:NSMetadataQueryDidFinishGatheringNotification object:metadataQuery];
[[NSNotificationCenter defaultCenter] removeObserver:self name:NSMetadataQueryDidUpdateNotification object:metadataQuery];
metadataQuery = nil;
}
- (void)metadataQueryDidFinishGathering:(NSNotification *)notif
{
[metadataQuery disableUpdates];
[self initiateDownloads];
if (metadataQuery.resultCount > 0) {
// Restart query if there are non-zero results, because updates never
// seem to remove results, even if they no longer pass the predicate
[metadataQuery stopQuery];
[metadataQuery startQuery];
}
[metadataQuery enableUpdates];
}
- (void)metadataQueryDidUpdate:(NSNotification *)notif
{
// Need to stop and restart the query, because for some reason, NSMetadataQuery
// does not remove results that no longer meet the criteria
[metadataQuery stopQuery];
[self initiateDownloads];
[metadataQuery startQuery];
}
- (void)initiateDownloads
{
NSUInteger count = [metadataQuery resultCount];
for ( NSUInteger i = 0; i < count; i++ ) {
@autoreleasepool {
NSURL *url = [metadataQuery valueOfAttribute:NSMetadataItemURLKey forResultAtIndex:i];
dispatch_async(initiatingDownloadsQueue, ^{
NSError *error;
BOOL startedDownload = [fileManager startDownloadingUbiquitousItemAtURL:url error:&error];
if ( startedDownload ) {
@synchronized(self) {
numberOfUnfinishedDownloads++;
}
[downloadTrackingQueue addOperationWithBlock:^{
NSFileCoordinator *coordinator = [[NSFileCoordinator alloc] initWithFilePresenter:nil];
NSError *error = nil;
[coordinator coordinateReadingItemAtURL:url options:0 error:&error byAccessor:^(NSURL *newURL) {
BOOL complete = NO;
@synchronized(self) {
numberOfUnfinishedDownloads--;
if (numberOfUnfinishedDownloads == 0) {
complete = YES;
}
}
// If there were downloads, and aren't anymore, fire notification
if (complete) {
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:CDEICloudFileSystemDidDownloadFilesNotification object:self];
});
}
}];
}];
} else {
CDELog(CDELoggingLevelWarning, @"Error starting download: %@", error);
}
});
}
}
}
#pragma mark - File Operations
static const NSTimeInterval CDEFileCoordinatorTimeOut = 10.0;
- (NSError *)specializedErrorForCocoaError:(NSError *)cocoaError
{
NSError *error = cocoaError;
if ([cocoaError.domain isEqualToString:NSCocoaErrorDomain] && cocoaError.code == NSUserCancelledError) {
error = [NSError errorWithDomain:CDEErrorDomain code:CDEErrorCodeFileCoordinatorTimedOut userInfo:nil];
}
return error;
}
- (NSError *)notConnectedError
{
NSError *error = [NSError errorWithDomain:CDEErrorDomain code:CDEErrorCodeConnectionError userInfo:@{NSLocalizedDescriptionKey : @"Attempted to access iCloud when not connected."}];
return error;
}
- (void)fileExistsAtPath:(NSString *)path completion:(void(^)(BOOL exists, BOOL isDirectory, NSError *error))block
{
[operationQueue addOperationWithBlock:^{
if (!self.isConnected) {
dispatch_async(dispatch_get_main_queue(), ^{
if (block) block(NO, NO, [self notConnectedError]);
});
return;
}
NSError *fileCoordinatorError = nil;
__block NSError *timeoutError = nil;
__block BOOL coordinatorExecuted = NO;
__block BOOL isDirectory = NO;
__block BOOL exists = NO;
NSFileCoordinator *coordinator = [[NSFileCoordinator alloc] initWithFilePresenter:nil];
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, CDEFileCoordinatorTimeOut * NSEC_PER_SEC);
dispatch_after(popTime, timeOutQueue, ^{
if (!coordinatorExecuted) {
[coordinator cancel];
timeoutError = [NSError errorWithDomain:CDEErrorDomain code:CDEErrorCodeFileCoordinatorTimedOut userInfo:nil];
}
});
NSURL *url = [NSURL fileURLWithPath:[self fullPathForPath:path]];
[coordinator coordinateReadingItemAtURL:url options:0 error:&fileCoordinatorError byAccessor:^(NSURL *newURL) {
dispatch_sync(timeOutQueue, ^{ coordinatorExecuted = YES; });
if (timeoutError) return;
exists = [fileManager fileExistsAtPath:newURL.path isDirectory:&isDirectory];
}];
NSError *error = fileCoordinatorError ? : timeoutError ? : nil;
error = [self specializedErrorForCocoaError:error];
dispatch_async(dispatch_get_main_queue(), ^{
if (block) block(exists, isDirectory, error);
});
}];
}
- (void)contentsOfDirectoryAtPath:(NSString *)path completion:(void(^)(NSArray *contents, NSError *error))block
{
[operationQueue addOperationWithBlock:^{
if (!self.isConnected) {
dispatch_async(dispatch_get_main_queue(), ^{
if (block) block(nil, [self notConnectedError]);
});
return;
}
NSError *fileCoordinatorError = nil;
__block NSError *timeoutError = nil;
__block NSError *fileManagerError = nil;
__block BOOL coordinatorExecuted = NO;
NSFileCoordinator *coordinator = [[NSFileCoordinator alloc] initWithFilePresenter:nil];
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, CDEFileCoordinatorTimeOut * NSEC_PER_SEC);
dispatch_after(popTime, timeOutQueue, ^{
if (!coordinatorExecuted) {
[coordinator cancel];
timeoutError = [NSError errorWithDomain:CDEErrorDomain code:CDEErrorCodeFileCoordinatorTimedOut userInfo:nil];
}
});
__block NSArray *contents = nil;
NSURL *url = [NSURL fileURLWithPath:[self fullPathForPath:path]];
[coordinator coordinateReadingItemAtURL:url options:0 error:&fileCoordinatorError byAccessor:^(NSURL *newURL) {
dispatch_sync(timeOutQueue, ^{ coordinatorExecuted = YES; });
if (timeoutError) return;
NSDirectoryEnumerator *dirEnum = [fileManager enumeratorAtPath:[self fullPathForPath:path]];
NSDictionary *info = @{NSLocalizedDescriptionKey : [NSString stringWithFormat:@"Couldn't create directory enumerator for path: %@", path]};
if (!dirEnum) fileManagerError = [NSError errorWithDomain:CDEErrorDomain code:CDEErrorCodeFileAccessFailed userInfo:info];
NSString *filename;
NSMutableArray *mutableContents = [[NSMutableArray alloc] init];
while ((filename = [dirEnum nextObject])) {
if ([filename hasPrefix:@"."]) continue; // Skip .DS_Store and other system files
NSString *filePath = [path stringByAppendingPathComponent:filename];
if ([dirEnum.fileAttributes.fileType isEqualToString:NSFileTypeDirectory]) {
[dirEnum skipDescendants];
CDECloudDirectory *dir = [[CDECloudDirectory alloc] init];
dir.name = filename;
dir.path = filePath;
[mutableContents addObject:dir];
}
else {
CDECloudFile *file = [CDECloudFile new];
file.name = filename;
file.path = filePath;
file.size = dirEnum.fileAttributes.fileSize;
[mutableContents addObject:file];
}
}
if (!fileManagerError) contents = mutableContents;
}];
NSError *error = fileCoordinatorError ? : timeoutError ? : fileManagerError ? : nil;
error = [self specializedErrorForCocoaError:error];
dispatch_async(dispatch_get_main_queue(), ^{
if (block) block(contents, error);
});
}];
}
- (void)createDirectoryAtPath:(NSString *)path completion:(CDECompletionBlock)block
{
[operationQueue addOperationWithBlock:^{
if (!self.isConnected) {
dispatch_async(dispatch_get_main_queue(), ^{
if (block) block([self notConnectedError]);
});
return;
}
NSError *fileCoordinatorError = nil;
__block NSError *timeoutError = nil;
__block NSError *fileManagerError = nil;
__block BOOL coordinatorExecuted = NO;
NSFileCoordinator *coordinator = [[NSFileCoordinator alloc] initWithFilePresenter:nil];
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, CDEFileCoordinatorTimeOut * NSEC_PER_SEC);
dispatch_after(popTime, timeOutQueue, ^{
if (!coordinatorExecuted) {
[coordinator cancel];
timeoutError = [NSError errorWithDomain:CDEErrorDomain code:CDEErrorCodeFileCoordinatorTimedOut userInfo:nil];
}
});
NSURL *url = [NSURL fileURLWithPath:[self fullPathForPath:path]];
[coordinator coordinateWritingItemAtURL:url options:0 error:&fileCoordinatorError byAccessor:^(NSURL *newURL) {
dispatch_sync(timeOutQueue, ^{ coordinatorExecuted = YES; });
if (timeoutError) return;
[fileManager createDirectoryAtPath:newURL.path withIntermediateDirectories:NO attributes:nil error:&fileManagerError];
}];
NSError *error = fileCoordinatorError ? : timeoutError ? : fileManagerError ? : nil;
error = [self specializedErrorForCocoaError:error];
dispatch_async(dispatch_get_main_queue(), ^{
if (block) block(error);
});
}];
}
- (void)removeItemAtPath:(NSString *)path completion:(CDECompletionBlock)block
{
[operationQueue addOperationWithBlock:^{
if (!self.isConnected) {
dispatch_async(dispatch_get_main_queue(), ^{
if (block) block([self notConnectedError]);
});
return;
}
NSError *fileCoordinatorError = nil;
__block NSError *timeoutError = nil;
__block NSError *fileManagerError = nil;
__block BOOL coordinatorExecuted = NO;
NSFileCoordinator *coordinator = [[NSFileCoordinator alloc] initWithFilePresenter:nil];
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, CDEFileCoordinatorTimeOut * NSEC_PER_SEC);
dispatch_after(popTime, timeOutQueue, ^{
if (!coordinatorExecuted) {
[coordinator cancel];
timeoutError = [NSError errorWithDomain:CDEErrorDomain code:CDEErrorCodeFileCoordinatorTimedOut userInfo:nil];
}
});
NSURL *url = [NSURL fileURLWithPath:[self fullPathForPath:path]];
[coordinator coordinateWritingItemAtURL:url options:NSFileCoordinatorWritingForDeleting error:&fileCoordinatorError byAccessor:^(NSURL *newURL) {
dispatch_sync(timeOutQueue, ^{ coordinatorExecuted = YES; });
if (timeoutError) return;
[fileManager removeItemAtPath:newURL.path error:&fileManagerError];
}];
NSError *error = fileCoordinatorError ? : timeoutError ? : fileManagerError ? : nil;
error = [self specializedErrorForCocoaError:error];
dispatch_async(dispatch_get_main_queue(), ^{
if (block) block(error);
});
}];
}
- (void)uploadLocalFile:(NSString *)fromPath toPath:(NSString *)toPath completion:(CDECompletionBlock)block
{
[operationQueue addOperationWithBlock:^{
if (!self.isConnected) {
dispatch_async(dispatch_get_main_queue(), ^{
if (block) block([self notConnectedError]);
});
return;
}
NSError *fileCoordinatorError = nil;
__block NSError *timeoutError = nil;
__block NSError *fileManagerError = nil;
__block BOOL coordinatorExecuted = NO;
NSFileCoordinator *coordinator = [[NSFileCoordinator alloc] initWithFilePresenter:nil];
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, CDEFileCoordinatorTimeOut * NSEC_PER_SEC);
dispatch_after(popTime, timeOutQueue, ^{
if (!coordinatorExecuted) {
[coordinator cancel];
timeoutError = [NSError errorWithDomain:CDEErrorDomain code:CDEErrorCodeFileCoordinatorTimedOut userInfo:nil];
}
});
NSURL *fromURL = [NSURL fileURLWithPath:fromPath];
NSURL *toURL = [NSURL fileURLWithPath:[self fullPathForPath:toPath]];
[coordinator coordinateReadingItemAtURL:fromURL options:0 writingItemAtURL:toURL options:NSFileCoordinatorWritingForReplacing error:&fileCoordinatorError byAccessor:^(NSURL *newReadingURL, NSURL *newWritingURL) {
dispatch_sync(timeOutQueue, ^{ coordinatorExecuted = YES; });
if (timeoutError) return;
[fileManager removeItemAtPath:newWritingURL.path error:NULL];
[fileManager copyItemAtPath:newReadingURL.path toPath:newWritingURL.path error:&fileManagerError];
}];
NSError *error = fileCoordinatorError ? : timeoutError ? : fileManagerError ? : nil;
error = [self specializedErrorForCocoaError:error];
dispatch_async(dispatch_get_main_queue(), ^{
if (block) block(error);
});
}];
}
- (void)downloadFromPath:(NSString *)fromPath toLocalFile:(NSString *)toPath completion:(CDECompletionBlock)block
{
[operationQueue addOperationWithBlock:^{
if (!self.isConnected) {
dispatch_async(dispatch_get_main_queue(), ^{
if (block) block([self notConnectedError]);
});
return;
}
NSError *fileCoordinatorError = nil;
__block NSError *timeoutError = nil;
__block NSError *fileManagerError = nil;
__block BOOL coordinatorExecuted = NO;
NSFileCoordinator *coordinator = [[NSFileCoordinator alloc] initWithFilePresenter:nil];
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, CDEFileCoordinatorTimeOut * NSEC_PER_SEC);
dispatch_after(popTime, timeOutQueue, ^{
if (!coordinatorExecuted) {
[coordinator cancel];
timeoutError = [NSError errorWithDomain:CDEErrorDomain code:CDEErrorCodeFileCoordinatorTimedOut userInfo:nil];
}
});
NSURL *fromURL = [NSURL fileURLWithPath:[self fullPathForPath:fromPath]];
NSURL *toURL = [NSURL fileURLWithPath:toPath];
[coordinator coordinateReadingItemAtURL:fromURL options:0 writingItemAtURL:toURL options:NSFileCoordinatorWritingForReplacing error:&fileCoordinatorError byAccessor:^(NSURL *newReadingURL, NSURL *newWritingURL) {
dispatch_sync(timeOutQueue, ^{ coordinatorExecuted = YES; });
if (timeoutError) return;
[fileManager removeItemAtPath:newWritingURL.path error:NULL];
[fileManager copyItemAtPath:newReadingURL.path toPath:newWritingURL.path error:&fileManagerError];
}];
NSError *error = fileCoordinatorError ? : timeoutError ? : fileManagerError ? : nil;
error = [self specializedErrorForCocoaError:error];
dispatch_async(dispatch_get_main_queue(), ^{
if (block) block(error);
});
}];
}
@end