From cf12ec819240f56884da8c3d6a3d7af6cac57fbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20Fe=CC=81vrier?= Date: Thu, 3 Sep 2020 14:59:36 +0200 Subject: [PATCH 1/4] iOS : add mode option --- README.md | 13 ++- index.d.ts | 2 + index.js | 26 +++++ ios/RNDocumentPicker/RNDocumentPicker.m | 137 +++++++++++++----------- 4 files changed, 116 insertions(+), 62 deletions(-) diff --git a/README.md b/README.md index 218b1fd4..fda6b3b1 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,10 @@ The type or types of documents to allow selection of. May be an array of types a - If `type` is omitted it will be treated as `*/*` or `public.content`. - Multiple type strings are not supported on Android before KitKat (API level 19), Jellybean will fall back to `*/*` if you provide an array with more than one value. +##### [iOS only] `mode`:`"import" | "open"`: + +Defaults to `import`. If `mode` is set to `import` the document picker imports the file from outside to inside the sandbox, otherwise if `mode` is set to `open` the document picker opens the file right in place. + ##### [iOS only] `copyTo`:`"cachesDirectory" | "documentDirectory"`: If specified, the picked file is copied to `NSCachesDirectory` / `NSDocumentDirectory` directory. The uri of the copy will be available in result's `fileCopyUri`. If copying the file fails (eg. due to lack of space), `fileCopyUri` will be the same as `uri`, and more details about the error will be available in `copyError` field in the result. @@ -75,7 +79,7 @@ The object a `pick` Promise resolves to or the objects in the array a `pickMulti ##### `uri`: -The URI representing the document picked by the user. _On iOS this will be a `file://` URI for a temporary file in your app's container. On Android this will be a `content://` URI for a document provided by a DocumentProvider that must be accessed with a ContentResolver._ +The URI representing the document picked by the user. _On iOS this will be a `file://` URI for a temporary file in your app's container if `mode` is not specified or set at `import` otherwise it will be the original `file://` URI. On Android this will be a `content://` URI for a document provided by a DocumentProvider that must be accessed with a ContentResolver._ ##### `fileCopyUri`: @@ -109,10 +113,15 @@ The base64 encoded content of the picked file if the option `readContent` was se - `DocumentPicker.types.zip`: Zip files (`application/zip` or `public.zip-archive`) - `DocumentPicker.types.csv`: Csv files (`text/csv` or `public.comma-separated-values-text`) -### `DocumentPicker.isCancel(err)` +#### `DocumentPicker.isCancel(err)` If the user cancels the document picker without choosing a file (by pressing the system back button on Android or the Cancel button on iOS) the Promise will be rejected with a cancellation error. You can check for this error using `DocumentPicker.isCancel(err)` allowing you to ignore it and cleanup any parts of your interface that may not be needed anymore. +#### [iOS only] `DocumentPicker.releaseSecureAccess(uri)` + +If `mode` is set to `open` iOS is giving you a secure access to a file located outside from your sandbox. +In that case Apple is asking you to release the access as soon as you finish using the resource. + ## Example ```javascript diff --git a/index.d.ts b/index.d.ts index 42ef9e59..796aa243 100644 --- a/index.d.ts +++ b/index.d.ts @@ -48,6 +48,7 @@ declare module 'react-native-document-picker' { }; interface DocumentPickerOptions { type: Array | DocumentType[OS]; + mode?: 'import' | 'open'; copyTo?: 'cachesDirectory' | 'documentDirectory'; } interface DocumentPickerResponse { @@ -68,5 +69,6 @@ declare module 'react-native-document-picker' { options: DocumentPickerOptions ): Promise; static isCancel(err?: IError): boolean; + static releaseSecureAccess(uris: Array): void; } } diff --git a/index.js b/index.js index 7e49badc..3ddd1d68 100644 --- a/index.js +++ b/index.js @@ -62,6 +62,10 @@ function pick(opts) { ); } + if ('mode' in opts && !['import', 'open'].includes(opts.mode)) { + throw new TypeError('Invalid mode option: ' + opts.mode); + } + if ('copyTo' in opts && !['cachesDirectory', 'documentDirectory'].includes(opts.copyTo)) { throw new TypeError('Invalid copyTo option: ' + opts.copyTo); } @@ -69,6 +73,24 @@ function pick(opts) { return RNDocumentPicker.pick(opts); } +function releaseSecureAccess(uris) { + if (Platform.OS !== 'ios') { + return; + } + + if (!Array.isArray(uris)) { + throw new TypeError('`uris` should be an array of strings'); + } + + uris.forEach((uri) => { + if (typeof uri !== 'string') { + throw new TypeError('Invalid uri parameter, expected a string not: ' + uri); + } + }); + + RNDocumentPicker.releaseSecureAccess(uris); +} + const Types = { mimeTypes: { allFiles: '*/*', @@ -138,4 +160,8 @@ export default class DocumentPicker { static isCancel(err) { return err && err.code === E_DOCUMENT_PICKER_CANCELED; } + + static releaseSecureAccess(uris) { + releaseSecureAccess(uris); + } } diff --git a/ios/RNDocumentPicker/RNDocumentPicker.m b/ios/RNDocumentPicker/RNDocumentPicker.m index 2b44a039..4429b447 100644 --- a/ios/RNDocumentPicker/RNDocumentPicker.m +++ b/ios/RNDocumentPicker/RNDocumentPicker.m @@ -11,7 +11,7 @@ static NSString *const E_INVALID_DATA_RETURNED = @"INVALID_DATA_RETURNED"; static NSString *const OPTION_TYPE = @"type"; -static NSString *const OPTION_MULIPLE = @"multiple"; +static NSString *const OPTION_MULTIPLE = @"multiple"; static NSString *const FIELD_URI = @"uri"; static NSString *const FIELD_FILE_COPY_URI = @"fileCopyUri"; @@ -24,9 +24,11 @@ @interface RNDocumentPicker () @end @implementation RNDocumentPicker { + UIDocumentPickerMode mode; + NSString *copyDestination; NSMutableArray *composeResolvers; NSMutableArray *composeRejecters; - NSString* copyDestination; + NSMutableArray *urls; } @synthesize bridge = _bridge; @@ -34,13 +36,22 @@ @implementation RNDocumentPicker { - (instancetype)init { if ((self = [super init])) { - composeResolvers = [[NSMutableArray alloc] init]; - composeRejecters = [[NSMutableArray alloc] init]; + composeResolvers = [NSMutableArray new]; + composeRejecters = [NSMutableArray new]; + urls = [NSMutableArray new]; } return self; } -+ (BOOL)requiresMainQueueSetup { +- (void)dealloc +{ + for (NSURL *url in urls) { + [url stopAccessingSecurityScopedResource]; + } +} + ++ (BOOL)requiresMainQueueSetup +{ return NO; } @@ -55,20 +66,19 @@ - (dispatch_queue_t)methodQueue resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { - NSArray *allowedUTIs = [RCTConvert NSArray:options[OPTION_TYPE]]; - UIDocumentPickerViewController *documentPicker = [[UIDocumentPickerViewController alloc] initWithDocumentTypes:(NSArray *)allowedUTIs inMode:UIDocumentPickerModeImport]; - + mode = options[@"mode"] && [options[@"mode"] isEqualToString:@"open"] ? UIDocumentPickerModeOpen : UIDocumentPickerModeImport; + copyDestination = options[@"copyTo"] ? options[@"copyTo"] : nil; [composeResolvers addObject:resolve]; [composeRejecters addObject:reject]; - copyDestination = options[@"copyTo"] ? options[@"copyTo"] : nil; - + NSArray *allowedUTIs = [RCTConvert NSArray:options[OPTION_TYPE]]; + UIDocumentPickerViewController *documentPicker = [[UIDocumentPickerViewController alloc] initWithDocumentTypes:(NSArray *)allowedUTIs inMode:mode]; documentPicker.delegate = self; documentPicker.modalPresentationStyle = UIModalPresentationFormSheet; #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000 if (@available(iOS 11, *)) { - documentPicker.allowsMultipleSelection = [RCTConvert BOOL:options[OPTION_MULIPLE]]; + documentPicker.allowsMultipleSelection = [RCTConvert BOOL:options[OPTION_MULTIPLE]]; } #endif @@ -79,19 +89,22 @@ - (dispatch_queue_t)methodQueue - (NSMutableDictionary *)getMetadataForUrl:(NSURL *)url error:(NSError **)error { - __block NSMutableDictionary* result = [NSMutableDictionary dictionary]; + __block NSMutableDictionary *result = [NSMutableDictionary dictionary]; + if (mode == UIDocumentPickerModeOpen) + [urls addObject:url]; [url startAccessingSecurityScopedResource]; - NSFileCoordinator *coordinator = [[NSFileCoordinator alloc] init]; + NSFileCoordinator *coordinator = [NSFileCoordinator new]; NSError *fileError; [coordinator coordinateReadingItemAtURL:url options:NSFileCoordinatorReadingResolvesSymbolicLink error:&fileError byAccessor:^(NSURL *newURL) { + if (!fileError) { - [result setValue:newURL.absoluteString forKey:FIELD_URI]; + [result setValue:((mode == UIDocumentPickerModeOpen) ? url : newURL).absoluteString forKey:FIELD_URI]; NSError *copyError; - NSURL* maybeFileCopyPath = copyDestination ? [RNDocumentPicker copyToUniqueDestinationFrom:newURL usingDestinationPreset:copyDestination error:copyError] : newURL; - [result setValue: maybeFileCopyPath.absoluteString forKey:FIELD_FILE_COPY_URI]; + NSURL *maybeFileCopyPath = copyDestination ? [RNDocumentPicker copyToUniqueDestinationFrom:newURL usingDestinationPreset:copyDestination error:copyError] : newURL; + [result setValue:maybeFileCopyPath.absoluteString forKey:FIELD_FILE_COPY_URI]; if (copyError) { [result setValue:copyError.description forKey:FIELD_COPY_ERR]; } @@ -118,7 +131,8 @@ - (NSMutableDictionary *)getMetadataForUrl:(NSURL *)url error:(NSError **)error } }]; - [url stopAccessingSecurityScopedResource]; + if (mode != UIDocumentPickerModeOpen) + [url stopAccessingSecurityScopedResource]; if (fileError) { *error = fileError; @@ -128,19 +142,28 @@ - (NSMutableDictionary *)getMetadataForUrl:(NSURL *)url error:(NSError **)error } } -+ (NSURL*)getDirectoryForFileCopy:(NSString*) copyToDirectory { +RCT_EXPORT_METHOD(releaseSecureAccess:(NSArray *)uris) +{ + for (NSString *uri in uris) { + NSURL *url = [NSURL URLWithString:uri]; + [url stopAccessingSecurityScopedResource]; + } +} + ++ (NSURL *)getDirectoryForFileCopy:(NSString *)copyToDirectory +{ if ([@"cachesDirectory" isEqualToString:copyToDirectory]) { return [NSFileManager.defaultManager URLsForDirectory:NSCachesDirectory inDomains:NSUserDomainMask].firstObject; } else if ([@"documentDirectory" isEqualToString:copyToDirectory]) { return [NSFileManager.defaultManager URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask].firstObject; } // this should not happen as the value is checked in JS, but we fall back to NSTemporaryDirectory() - return [NSURL fileURLWithPath: NSTemporaryDirectory() isDirectory: YES]; + return [NSURL fileURLWithPath:NSTemporaryDirectory() isDirectory:YES]; } -+ (NSURL *)copyToUniqueDestinationFrom:(NSURL *) url usingDestinationPreset: (NSString*) copyToDirectory error:(NSError *)error ++ (NSURL *)copyToUniqueDestinationFrom:(NSURL *)url usingDestinationPreset:(NSString *)copyToDirectory error:(NSError *)error { - NSURL* destinationRootDir = [self getDirectoryForFileCopy:copyToDirectory]; + NSURL *destinationRootDir = [self getDirectoryForFileCopy:copyToDirectory]; // we don't want to rename the file so we put it into a unique location NSString *uniqueSubDirName = [[NSUUID UUID] UUIDString]; NSURL *destinationDir = [destinationRootDir URLByAppendingPathComponent:[NSString stringWithFormat:@"%@/", uniqueSubDirName]]; @@ -160,56 +183,50 @@ + (NSURL *)copyToUniqueDestinationFrom:(NSURL *) url usingDestinationPreset: (NS - (void)documentPicker:(UIDocumentPickerViewController *)controller didPickDocumentAtURL:(NSURL *)url { - if (controller.documentPickerMode == UIDocumentPickerModeImport) { - RCTPromiseResolveBlock resolve = [composeResolvers lastObject]; - RCTPromiseRejectBlock reject = [composeRejecters lastObject]; - [composeResolvers removeLastObject]; - [composeRejecters removeLastObject]; - - NSError *error; - NSMutableDictionary* result = [self getMetadataForUrl:url error:&error]; - if (result) { - NSArray *results = @[result]; - resolve(results); - } else { - reject(E_INVALID_DATA_RETURNED, error.localizedDescription, error); - } + RCTPromiseResolveBlock resolve = [composeResolvers lastObject]; + RCTPromiseRejectBlock reject = [composeRejecters lastObject]; + [composeResolvers removeLastObject]; + [composeRejecters removeLastObject]; + + NSError *error; + NSMutableDictionary *result = [self getMetadataForUrl:url error:&error]; + if (result) { + NSArray *results = @[result]; + resolve(results); + } else { + reject(E_INVALID_DATA_RETURNED, error.localizedDescription, error); } } - (void)documentPicker:(UIDocumentPickerViewController *)controller didPickDocumentsAtURLs:(NSArray *)urls { - if (controller.documentPickerMode == UIDocumentPickerModeImport) { - RCTPromiseResolveBlock resolve = [composeResolvers lastObject]; - RCTPromiseRejectBlock reject = [composeRejecters lastObject]; - [composeResolvers removeLastObject]; - [composeRejecters removeLastObject]; - - NSMutableArray *results = [NSMutableArray array]; - for (id url in urls) { - NSError *error; - NSMutableDictionary* result = [self getMetadataForUrl:url error:&error]; - if (result) { - [results addObject:result]; - } else { - reject(E_INVALID_DATA_RETURNED, error.localizedDescription, error); - return; - } + RCTPromiseResolveBlock resolve = [composeResolvers lastObject]; + RCTPromiseRejectBlock reject = [composeRejecters lastObject]; + [composeResolvers removeLastObject]; + [composeRejecters removeLastObject]; + + NSMutableArray *results = [NSMutableArray array]; + for (id url in urls) { + NSError *error; + NSMutableDictionary *result = [self getMetadataForUrl:url error:&error]; + if (result) { + [results addObject:result]; + } else { + reject(E_INVALID_DATA_RETURNED, error.localizedDescription, error); + return; } - - resolve(results); } + + resolve(results); } - (void)documentPickerWasCancelled:(UIDocumentPickerViewController *)controller { - if (controller.documentPickerMode == UIDocumentPickerModeImport) { - RCTPromiseRejectBlock reject = [composeRejecters lastObject]; - [composeResolvers removeLastObject]; - [composeRejecters removeLastObject]; - - reject(E_DOCUMENT_PICKER_CANCELED, @"User canceled document picker", nil); - } + RCTPromiseRejectBlock reject = [composeRejecters lastObject]; + [composeResolvers removeLastObject]; + [composeRejecters removeLastObject]; + + reject(E_DOCUMENT_PICKER_CANCELED, @"User canceled document picker", nil); } @end From 4b4fb06a06310489de64d1fcdb17b5184b855661 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20Fe=CC=81vrier?= Date: Sat, 10 Oct 2020 14:00:11 +0200 Subject: [PATCH 2/4] iOS : fix add mode option --- ios/RNDocumentPicker/RNDocumentPicker.m | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/ios/RNDocumentPicker/RNDocumentPicker.m b/ios/RNDocumentPicker/RNDocumentPicker.m index 4429b447..b1868045 100644 --- a/ios/RNDocumentPicker/RNDocumentPicker.m +++ b/ios/RNDocumentPicker/RNDocumentPicker.m @@ -145,8 +145,13 @@ - (NSMutableDictionary *)getMetadataForUrl:(NSURL *)url error:(NSError **)error RCT_EXPORT_METHOD(releaseSecureAccess:(NSArray *)uris) { for (NSString *uri in uris) { - NSURL *url = [NSURL URLWithString:uri]; - [url stopAccessingSecurityScopedResource]; + for (NSURL *url in urls) { + if ([url.absoluteString isEqual:uri]) { + [url stopAccessingSecurityScopedResource]; + [urls removeObject:url]; + break; + } + } } } From d328ad750182b4dfb5ad4db5a6e5a447b7335229 Mon Sep 17 00:00:00 2001 From: Vojtech Novak Date: Tue, 15 Dec 2020 01:47:54 +0100 Subject: [PATCH 3/4] readme: fix releaseSecureAccess signature --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index fda6b3b1..f579291f 100644 --- a/README.md +++ b/README.md @@ -117,7 +117,7 @@ The base64 encoded content of the picked file if the option `readContent` was se If the user cancels the document picker without choosing a file (by pressing the system back button on Android or the Cancel button on iOS) the Promise will be rejected with a cancellation error. You can check for this error using `DocumentPicker.isCancel(err)` allowing you to ignore it and cleanup any parts of your interface that may not be needed anymore. -#### [iOS only] `DocumentPicker.releaseSecureAccess(uri)` +#### [iOS only] `DocumentPicker.releaseSecureAccess(uris: Array)` If `mode` is set to `open` iOS is giving you a secure access to a file located outside from your sandbox. In that case Apple is asking you to release the access as soon as you finish using the resource. From 9e8dd38fa4058009735a31e1776899d27baaadd4 Mon Sep 17 00:00:00 2001 From: Vojtech Novak Date: Fri, 25 Dec 2020 19:12:35 +0100 Subject: [PATCH 4/4] fix: do not mutate collection while enumerating --- ios/RNDocumentPicker/RNDocumentPicker.m | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ios/RNDocumentPicker/RNDocumentPicker.m b/ios/RNDocumentPicker/RNDocumentPicker.m index b1868045..de08e900 100644 --- a/ios/RNDocumentPicker/RNDocumentPicker.m +++ b/ios/RNDocumentPicker/RNDocumentPicker.m @@ -144,15 +144,17 @@ - (NSMutableDictionary *)getMetadataForUrl:(NSURL *)url error:(NSError **)error RCT_EXPORT_METHOD(releaseSecureAccess:(NSArray *)uris) { + NSMutableArray *discardedItems = [NSMutableArray array]; for (NSString *uri in uris) { for (NSURL *url in urls) { if ([url.absoluteString isEqual:uri]) { [url stopAccessingSecurityScopedResource]; - [urls removeObject:url]; + [discardedItems addObject:url]; break; } } } + [urls removeObjectsInArray:discardedItems]; } + (NSURL *)getDirectoryForFileCopy:(NSString *)copyToDirectory