From 65b870e3608b712e02f4582e824ad81d42aa9b3f Mon Sep 17 00:00:00 2001 From: Richard Ross Date: Tue, 18 Aug 2015 15:21:40 -0700 Subject: [PATCH] Added PFFileStagingController to manage staging. Previously, our file staging code was not centralized, wasn't very efficient, and most importantly made testing & migration difficult. Fixes half of #18. --- Parse.xcodeproj/project.pbxproj | 10 ++ .../File/Controller/PFFileController.h | 4 +- .../File/Controller/PFFileController.m | 32 ++++--- .../File/Controller/PFFileStagingController.h | 72 ++++++++++++++ .../File/Controller/PFFileStagingController.m | 96 +++++++++++++++++++ Parse/PFFile.m | 71 +++++++------- Tests/Unit/FileControllerTests.m | 28 ++---- Tests/Unit/FileUnitTests.m | 11 ++- 8 files changed, 252 insertions(+), 72 deletions(-) create mode 100644 Parse/Internal/File/Controller/PFFileStagingController.h create mode 100644 Parse/Internal/File/Controller/PFFileStagingController.m diff --git a/Parse.xcodeproj/project.pbxproj b/Parse.xcodeproj/project.pbxproj index 06b1391e6..25969c126 100644 --- a/Parse.xcodeproj/project.pbxproj +++ b/Parse.xcodeproj/project.pbxproj @@ -801,6 +801,8 @@ F50C66331B33A708001941A6 /* PFPushUtilities.h in Headers */ = {isa = PBXBuildFile; fileRef = F50C66311B33A708001941A6 /* PFPushUtilities.h */; }; F50C66341B33A708001941A6 /* PFPushUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = F50C66321B33A708001941A6 /* PFPushUtilities.m */; }; F50C667C1B34B231001941A6 /* PFPushUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = F50C66321B33A708001941A6 /* PFPushUtilities.m */; }; + F50E486E1B83ED270055094D /* PFFileStagingController.h in Headers */ = {isa = PBXBuildFile; fileRef = F50E486C1B83ED270055094D /* PFFileStagingController.h */; }; + F50E486F1B83ED270055094D /* PFFileStagingController.m in Sources */ = {isa = PBXBuildFile; fileRef = F50E486D1B83ED270055094D /* PFFileStagingController.m */; }; F510509F1B6AA4CE00749060 /* ExtensionDataSharingTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814915E61B66D44500EFD14F /* ExtensionDataSharingTests.m */; }; F51050A01B6AA4D100749060 /* ExtensionDataSharingTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814915E61B66D44500EFD14F /* ExtensionDataSharingTests.m */; }; F51050A11B6AA4D600749060 /* ExtensionDataSharingMobileTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 814915E51B66D44500EFD14F /* ExtensionDataSharingMobileTests.m */; }; @@ -901,6 +903,7 @@ F5C42CDB1B38761B00C720D8 /* PFObjectSubclassInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = F5C42CD81B38761B00C720D8 /* PFObjectSubclassInfo.h */; }; F5C42CDC1B38761B00C720D8 /* PFObjectSubclassInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = F5C42CD91B38761B00C720D8 /* PFObjectSubclassInfo.m */; }; F5C42CDD1B38761B00C720D8 /* PFObjectSubclassInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = F5C42CD91B38761B00C720D8 /* PFObjectSubclassInfo.m */; }; + F5C6B38B1B83F7A100690F3A /* PFFileStagingController.m in Sources */ = {isa = PBXBuildFile; fileRef = F50E486D1B83ED270055094D /* PFFileStagingController.m */; }; F5C8F2C01B1F7E7800CD98E7 /* PFAsyncTaskQueue.m in Sources */ = {isa = PBXBuildFile; fileRef = F5C8F2BF1B1F7E6B00CD98E7 /* PFAsyncTaskQueue.m */; }; F5C8F2C11B1F7E7900CD98E7 /* PFAsyncTaskQueue.m in Sources */ = {isa = PBXBuildFile; fileRef = F5C8F2BF1B1F7E6B00CD98E7 /* PFAsyncTaskQueue.m */; }; F5E381311B68832000A3B9F2 /* URLSessionTests.m in Sources */ = {isa = PBXBuildFile; fileRef = F5556A141B66F36000410837 /* URLSessionTests.m */; }; @@ -1488,6 +1491,8 @@ E9E81E8316EEF93E001D034F /* PFSubclassing.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFSubclassing.h; sourceTree = ""; }; F50C66311B33A708001941A6 /* PFPushUtilities.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFPushUtilities.h; sourceTree = ""; }; F50C66321B33A708001941A6 /* PFPushUtilities.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFPushUtilities.m; sourceTree = ""; }; + F50E486C1B83ED270055094D /* PFFileStagingController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFFileStagingController.h; sourceTree = ""; }; + F50E486D1B83ED270055094D /* PFFileStagingController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFFileStagingController.m; sourceTree = ""; }; F51534F61B571E9100C49F56 /* PFACLPrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFACLPrivate.h; sourceTree = ""; }; F51534F81B571E9100C49F56 /* PFACLState.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PFACLState.h; sourceTree = ""; }; F51534F91B571E9100C49F56 /* PFACLState.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PFACLState.m; sourceTree = ""; }; @@ -2892,6 +2897,8 @@ children = ( 81EB595C1AF46434001EA1FC /* PFFileController.h */, 81EB595D1AF46434001EA1FC /* PFFileController.m */, + F50E486C1B83ED270055094D /* PFFileStagingController.h */, + F50E486D1B83ED270055094D /* PFFileStagingController.m */, ); path = Controller; sourceTree = ""; @@ -3154,6 +3161,7 @@ F5B0B2DF1B449EEF00F3EBC4 /* PFCommandCache_Private.h in Headers */, F5B0B2E01B449EEF00F3EBC4 /* PFCommandResult.h in Headers */, 812B02961B5DE3EE003846EE /* PFURLSession.h in Headers */, + F50E486E1B83ED270055094D /* PFFileStagingController.h in Headers */, 8166FC731B50376D003841A2 /* PFObjectController.h in Headers */, F5B0B2EB1B449EEF00F3EBC4 /* PFAlertView.h in Headers */, 8119C9971A76E28F0085B516 /* PFNetworkCommand.h in Headers */, @@ -4031,6 +4039,7 @@ 814881471B795C63008763BF /* PFKeyValueCache.m in Sources */, 81C3825119CCAD2C0066284A /* PFNetworkActivityIndicatorManager.m in Sources */, 81C3824819CCAD2C0066284A /* PFObject.m in Sources */, + F50E486F1B83ED270055094D /* PFFileStagingController.m in Sources */, F51D06351B792CF10044539E /* PFSQLiteDatabaseController.m in Sources */, 815960A31ABCA3B30069EBCC /* PFFileManager.m in Sources */, 81CD66561B4DA5A70042FC0B /* PFCurrentInstallationController.m in Sources */, @@ -4198,6 +4207,7 @@ 81EBF3461B33E7DE00991947 /* PFPushChannelsController.m in Sources */, 9701108A1630B45800AB761E /* PFRole.m in Sources */, 9701108C1630B45800AB761E /* PFUser.m in Sources */, + F5C6B38B1B83F7A100690F3A /* PFFileStagingController.m in Sources */, 81E7A2281B6042BD006CB680 /* PFObjectFileCodingLogic.m in Sources */, 8166FCEB1B504083003841A2 /* PFPushManager.m in Sources */, 819A4B0B1A67330200D01241 /* PFHash.m in Sources */, diff --git a/Parse/Internal/File/Controller/PFFileController.h b/Parse/Internal/File/Controller/PFFileController.h index 99f8ea174..e3690341e 100644 --- a/Parse/Internal/File/Controller/PFFileController.h +++ b/Parse/Internal/File/Controller/PFFileController.h @@ -16,13 +16,15 @@ @class BFCancellationToken; @class BFTask; @class PFFileState; +@class PFFileStagingController; @interface PFFileController : NSObject @property (nonatomic, weak, readonly) id dataSource; +@property (nonatomic, strong, readonly) PFFileStagingController *fileStagingController; + @property (nonatomic, copy, readonly) NSString *cacheFilesDirectoryPath; -@property (nonatomic, copy, readonly) NSString *stagedFilesDirectoryPath; ///-------------------------------------- /// @name Init diff --git a/Parse/Internal/File/Controller/PFFileController.m b/Parse/Internal/File/Controller/PFFileController.m index 2d9f112f7..4c46407df 100644 --- a/Parse/Internal/File/Controller/PFFileController.m +++ b/Parse/Internal/File/Controller/PFFileController.m @@ -17,24 +17,27 @@ #import "PFCommandResult.h" #import "PFCommandRunning.h" #import "PFFileManager.h" +#import "PFFileStagingController.h" #import "PFFileState.h" #import "PFHash.h" #import "PFMacros.h" #import "PFRESTFileCommand.h" static NSString *const PFFileControllerCacheDirectoryName_ = @"PFFileCache"; -static NSString *const PFFileControllerStagingDirectoryName_ = @"PFFileStaging"; @interface PFFileController () { NSMutableDictionary *_downloadTasks; // { "urlString" : BFTask } NSMutableDictionary *_downloadProgressBlocks; // { "urlString" : [ block1, block2 ] } dispatch_queue_t _downloadDataAccessQueue; + dispatch_queue_t _fileStagingControllerAccessQueue; } @end @implementation PFFileController +@synthesize fileStagingController = _fileStagingController; + ///-------------------------------------- #pragma mark - Init ///-------------------------------------- @@ -52,6 +55,7 @@ - (instancetype)initWithDataSource:(id + +#import + +NS_ASSUME_NONNULL_BEGIN + +@protocol PFFileManagerProvider; + +@interface PFFileStagingController : NSObject + +@property (nonatomic, weak, readonly) id dataSource; + +@property (nonatomic, copy, readonly) NSString *stagedFilesDirectoryPath; + +///-------------------------------------- +/// @name Init +///-------------------------------------- + +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithDataSource:(id)dataSource NS_DESIGNATED_INITIALIZER; + ++ (instancetype)controllerWithDataSource:(id)dataSource; + +///-------------------------------------- +/// @name Staging +///-------------------------------------- + +/*! + Moves a file from the specified path to the staging directory based off of the name and unique ID passed in. + + @param filePath The source path to stage + @param name The name of the file to stage + @param uniqueId A unique ID for this file to be used when differentiating between files with the same name. + + @return A task, which yields the path of the staged file on disk. + */ +- (BFTask *)stageFileAsyncAtPath:(NSString *)filePath name:(NSString *)name uniqueId:(uint64_t)uniqueId; + +/*! + Creates a file from the specified data and places it into the staging directory based off of the name and unique + ID passed in. + + @param fileData The data to stage + @param name The name of the file to stage + @param uniqueId The unique ID for this file to be used when differentiating between files with the same name. + + @return A task, which yields the path of the staged file on disk. + */ +- (BFTask *)stageFileAsyncWithData:(NSData *)fileData name:(NSString *)name uniqueId:(uint64_t)uniqueId; + +/*! + Get the staged directory path for a file with the specified name and unique ID. + + @param name The name of the staged file + @param uniqueId The unique ID of the staged file + + @return The path in the staged directory folder which contains the contents of the requested file. + */ +- (NSString *)stagedFilePathForFileWithName:(NSString *)name uniqueId:(uint64_t)uniqueId; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Parse/Internal/File/Controller/PFFileStagingController.m b/Parse/Internal/File/Controller/PFFileStagingController.m new file mode 100644 index 000000000..fb137494e --- /dev/null +++ b/Parse/Internal/File/Controller/PFFileStagingController.m @@ -0,0 +1,96 @@ +/** + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "PFFileStagingController.h" + +#import "BFTask+Private.h" +#import "PFAssert.h" +#import "PFAsyncTaskQueue.h" +#import "PFDataProvider.h" +#import "PFFileManager.h" +#import "PFLogging.h" + +static NSString *const PFFileStagingControllerDirectoryName_ = @"PFFileStaging"; + +@implementation PFFileStagingController { + PFAsyncTaskQueue *_taskQueue; +} + +///-------------------------------------- +#pragma mark - Init +///-------------------------------------- + +- (instancetype)init { + PFNotDesignatedInitializer(); +} + +- (instancetype)initWithDataSource:(id)dataSource { + self = [super init]; + if (!self) return nil; + + _dataSource = dataSource; + _taskQueue = [PFAsyncTaskQueue taskQueue]; + + [self _clearStagedFilesAsync]; + + return self; +} + ++ (instancetype)controllerWithDataSource:(id)dataSource { + return [[self alloc] initWithDataSource:dataSource]; +} + +///-------------------------------------- +#pragma mark - Properties +///-------------------------------------- + +- (NSString *)stagedFilesDirectoryPath { + NSString *folderPath = [self.dataSource.fileManager parseLocalSandboxDataDirectoryPath]; + return [folderPath stringByAppendingPathComponent:PFFileStagingControllerDirectoryName_]; +} + +///-------------------------------------- +#pragma mark - Staging +///-------------------------------------- + +- (BFTask *)stageFileAsyncAtPath:(NSString *)filePath name:(NSString *)name uniqueId:(uint64_t)uniqueId { + return [_taskQueue enqueue:^id(BFTask *task) { + return [[PFFileManager createDirectoryIfNeededAsyncAtPath:[self stagedFilesDirectoryPath]] continueWithBlock:^id(BFTask *task) { + NSString *destinationPath = [self stagedFilePathForFileWithName:name uniqueId:uniqueId]; + return [[PFFileManager copyItemAsyncAtPath:filePath toPath:destinationPath] continueWithSuccessResult:destinationPath]; + }]; + }]; +} + +- (BFTask *)stageFileAsyncWithData:(NSData *)fileData name:(NSString *)name uniqueId:(uint64_t)uniqueId { + return [_taskQueue enqueue:^id(BFTask *task) { + return [[PFFileManager createDirectoryIfNeededAsyncAtPath:[self stagedFilesDirectoryPath]] continueWithBlock:^id(BFTask *task) { + NSString *destinationPath = [self stagedFilePathForFileWithName:name uniqueId:uniqueId]; + return [[PFFileManager writeDataAsync:fileData toFile:destinationPath] continueWithSuccessResult:destinationPath]; + }]; + }]; +} + +- (NSString *)stagedFilePathForFileWithName:(NSString *)name uniqueId:(uint64_t)uniqueId { + NSString *fileName = [NSString stringWithFormat:@"%llX_%@", uniqueId, name]; + return [[self stagedFilesDirectoryPath] stringByAppendingPathComponent:fileName]; +} + +///-------------------------------------- +#pragma mark - Clearing +///-------------------------------------- + +- (BFTask *)_clearStagedFilesAsync { + return [_taskQueue enqueue:^id(BFTask *task) { + NSString *stagedFilesDirectoryPath = [self stagedFilesDirectoryPath]; + return [PFFileManager removeItemAtPathAsync:stagedFilesDirectoryPath]; + }]; +} + +@end diff --git a/Parse/PFFile.m b/Parse/PFFile.m index 89893dcb5..f062b4cec 100644 --- a/Parse/PFFile.m +++ b/Parse/PFFile.m @@ -19,6 +19,7 @@ #import "PFCoreManager.h" #import "PFFileController.h" #import "PFFileManager.h" +#import "PFFileStagingController.h" #import "PFInternalUtils.h" #import "PFMacros.h" #import "PFMutableFileState.h" @@ -80,16 +81,8 @@ + (instancetype)fileWithName:(NSString *)name contentsAtPath:(NSString *)path er PFParameterAssert(length <= PFFileMaxFileSize, @"PFFile cannot be larger than %lli bytes", PFFileMaxFileSize); PFFile *file = [self fileWithName:name url:nil]; - if (file) { - // Copy the file write away, since we can construct staged file path only from a PFFile. - NSError *copyError = nil; - [fileManager copyItemAtPath:path toPath:file.stagedFilePath error:©Error]; - if (copyError) { - if (error) { - *error = copyError; - } - return nil; - } + if (![file _stageWithPath:path error:error]) { + return nil; } return file; } @@ -111,17 +104,7 @@ + (instancetype)fileWithName:(NSString *)name @"PFFile cannot be larger than %llu bytes", PFFileMaxFileSize); PFFile *file = [[self alloc] initWithName:name urlString:nil mimeType:contentType]; - - // Save the file write away, since we can construct staged file path only from a PFFile. - NSError *writeError = nil; - [[PFFileManager writeDataAsync:data toFile:file.stagedFilePath] - waitForResult:&writeError - withMainThreadWarning:NO]; - - if (writeError) { - if (error) { - *error = writeError; - } + if (![file _stageWithData:data error:error]) { return nil; } return file; @@ -427,6 +410,36 @@ - (NSInputStream *)_cachedDataStream { return [NSInputStream inputStreamWithFileAtPath:filePath]; } +///-------------------------------------- +#pragma mark - Staging +///-------------------------------------- + +- (BOOL)_stageWithData:(NSData *)data error:(NSError **)error { + __block BOOL result = NO; + [self _performDataAccessBlock:^{ + _stagedFilePath = [[[[self class] fileController].fileStagingController stageFileAsyncWithData:data + name:self.state.name + uniqueId:(uintptr_t)self] + waitForResult:error withMainThreadWarning:NO]; + + result = (_stagedFilePath != nil); + }]; + return result; +} + +- (BOOL)_stageWithPath:(NSString *)path error:(NSError **)error { + __block BOOL result = NO; + [self _performDataAccessBlock:^{ + _stagedFilePath = [[[[self class] fileController].fileStagingController stageFileAsyncAtPath:path + name:self.state.name + uniqueId:(uintptr_t)self] + waitForResult:error withMainThreadWarning:NO]; + + result = (_stagedFilePath != nil); + }]; + return result; +} + #pragma mark Data Access - (NSString *)name { @@ -469,22 +482,6 @@ - (PFFileState *)_fileState { return state; } -- (NSString *)stagedFilePath { - // Construct a path in PFFile instead of PFFileController, because we need a pointer to PFFile itself. - __block NSString *path = nil; - @weakify(self); - [self _performDataAccessBlock:^{ - @strongify(self); - if (!_stagedFilePath) { - NSString *filename = [NSString stringWithFormat:@"%p_%@", self, self.state.name]; - NSString *stagedDirectoryPath = [[self class] fileController].stagedFilesDirectoryPath; - _stagedFilePath = [stagedDirectoryPath stringByAppendingPathComponent:filename]; - } - path = _stagedFilePath; - }]; - return path; -} - #pragma mark Progress - (void)_performProgressBlockAsync:(PFProgressBlock)block withProgress:(int)progress { diff --git a/Tests/Unit/FileControllerTests.m b/Tests/Unit/FileControllerTests.m index c85c88fb1..0d0963aae 100644 --- a/Tests/Unit/FileControllerTests.m +++ b/Tests/Unit/FileControllerTests.m @@ -54,7 +54,10 @@ - (id)mockedDataSource { OCMStub([mockedDataSource commandRunner]).andReturn(mockedCommandRunner); id mockedFileManager = PFStrictClassMock([PFFileManager class]); + OCMStub([mockedDataSource fileManager]).andReturn(mockedFileManager); + OCMStub([mockedFileManager parseLocalSandboxDataDirectoryPath]).andReturn([self temporaryDirectory]); + return mockedDataSource; } @@ -65,16 +68,13 @@ - (id)mockedDataSource { - (void)setUp { [super setUp]; - [[NSFileManager defaultManager] createDirectoryAtPath:[self temporaryDirectory] - withIntermediateDirectories:YES - attributes:nil - error:NULL]; + [[PFFileManager createDirectoryIfNeededAsyncAtPath:[self temporaryDirectory]] waitUntilFinished]; } - (void)tearDown { - [[NSFileManager defaultManager] removeItemAtPath:[self temporaryDirectory] error:NULL]; - [super tearDown]; + + [[PFFileManager removeItemAtPathAsync:[self temporaryDirectory]] waitUntilFinished]; } ///-------------------------------------- @@ -490,6 +490,7 @@ - (void)testClearCaches { NSString *downloadsPath = [temporaryPath stringByAppendingPathComponent:@"downloads"]; OCMStub([mockedDataSource fileManager]).andReturn(mockedFileManager); + OCMStub([mockedFileManager parseLocalSandboxDataDirectoryPath]).andReturn(temporaryPath); OCMStub([mockedFileManager parseCacheItemPathForPathComponent:@"PFFileCache"]).andReturn(downloadsPath); PFFileController *fileController = [PFFileController controllerWithDataSource:mockedDataSource]; @@ -504,19 +505,4 @@ - (void)testClearCaches { [self waitForTestExpectations]; } -- (void)testStagedDirectoryPath { - id mockedDataSource = PFStrictProtocolMock(@protocol(PFFileManagerProvider)); - id mockedFileManager = PFStrictClassMock([PFFileManager class]); - - NSString *temporaryPath = [self temporaryDirectory]; - - OCMStub([mockedDataSource fileManager]).andReturn(mockedFileManager); - OCMStub([mockedFileManager parseLocalSandboxDataDirectoryPath]).andReturn(temporaryPath); - - PFFileController *fileController = [PFFileController controllerWithDataSource:mockedDataSource]; - - XCTAssertEqualObjects([temporaryPath stringByAppendingPathComponent:@"PFFileStaging"], - [fileController stagedFilesDirectoryPath]); -} - @end diff --git a/Tests/Unit/FileUnitTests.m b/Tests/Unit/FileUnitTests.m index 53673e5e6..acc32381f 100644 --- a/Tests/Unit/FileUnitTests.m +++ b/Tests/Unit/FileUnitTests.m @@ -15,6 +15,7 @@ #import "PFFile.h" #import "PFFileController.h" #import "PFFileManager.h" +#import "PFFileStagingController.h" #import "PFFileState.h" #import "PFFile_Private.h" #import "PFUnitTestCase.h" @@ -93,11 +94,19 @@ - (void)clearStagingAndTemporaryFiles { - (PFFileController *)mockedFileController { id mockedFileController = PFStrictClassMock([PFFileController class]); + id mockedFileStagingController = PFStrictClassMock([PFFileStagingController class]); NSString *stagedDirectory = [self sampleStagingPath]; + NSString *sampleFile = [stagedDirectory stringByAppendingPathComponent:@"stagedFile.dat"]; [self clearStagingAndTemporaryFiles]; - OCMStub([mockedFileController stagedFilesDirectoryPath]).andReturn(stagedDirectory); + OCMStub([mockedFileController fileStagingController]).andReturn(mockedFileStagingController); + OCMStub([[mockedFileStagingController ignoringNonObjectArgs] stageFileAsyncWithData:OCMOCK_ANY + name:OCMOCK_ANY + uniqueId:0]).andReturn([BFTask taskWithResult:sampleFile]); + OCMStub([[mockedFileStagingController ignoringNonObjectArgs] stageFileAsyncAtPath:OCMOCK_ANY + name:OCMOCK_ANY + uniqueId:0]).andReturn([BFTask taskWithResult:sampleFile]); return mockedFileController; }