Skip to content

Commit 0ead2a5

Browse files
iOS : add mode option (#345)
* iOS : add mode option * iOS : fix add mode option * readme: fix releaseSecureAccess signature * fix: do not mutate collection while enumerating Co-authored-by: Vojtech Novak <vonovak@gmail.com>
1 parent 49984af commit 0ead2a5

File tree

4 files changed

+123
-62
lines changed

4 files changed

+123
-62
lines changed

README.md

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@ The type or types of documents to allow selection of. May be an array of types a
4747
- If `type` is omitted it will be treated as `*/*` or `public.item`.
4848
- 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.
4949

50+
##### [iOS only] `mode`:`"import" | "open"`:
51+
52+
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.
53+
5054
##### [iOS only] `copyTo`:`"cachesDirectory" | "documentDirectory"`:
5155

5256
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.
@@ -76,7 +80,7 @@ The object a `pick` Promise resolves to or the objects in the array a `pickMulti
7680

7781
##### `uri`:
7882

79-
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._
83+
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._
8084

8185
##### `fileCopyUri`:
8286

@@ -116,10 +120,15 @@ The base64 encoded content of the picked file if the option `readContent` was se
116120
- `DocumentPicker.types.xls`: xls files
117121
- `DocumentPicker.types.xlsx`: xlsx files
118122

119-
### `DocumentPicker.isCancel(err)`
123+
#### `DocumentPicker.isCancel(err)`
120124

121125
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.
122126

127+
#### [iOS only] `DocumentPicker.releaseSecureAccess(uris: Array<string>)`
128+
129+
If `mode` is set to `open` iOS is giving you a secure access to a file located outside from your sandbox.
130+
In that case Apple is asking you to release the access as soon as you finish using the resource.
131+
123132
## Example
124133

125134
```javascript

index.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ declare module 'react-native-document-picker' {
6666
};
6767
interface DocumentPickerOptions<OS extends keyof PlatformTypes> {
6868
type: Array<PlatformTypes[OS][keyof PlatformTypes[OS]]> | DocumentType[OS];
69+
mode?: 'import' | 'open';
6970
copyTo?: 'cachesDirectory' | 'documentDirectory';
7071
}
7172
interface DocumentPickerResponse {
@@ -86,5 +87,6 @@ declare module 'react-native-document-picker' {
8687
options: DocumentPickerOptions<OS>
8788
): Promise<DocumentPickerResponse[]>;
8889
static isCancel<IError extends { code?: string }>(err?: IError): boolean;
90+
static releaseSecureAccess(uris: Array<string>): void;
8991
}
9092
}

index.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,13 +62,35 @@ function pick(opts) {
6262
);
6363
}
6464

65+
if ('mode' in opts && !['import', 'open'].includes(opts.mode)) {
66+
throw new TypeError('Invalid mode option: ' + opts.mode);
67+
}
68+
6569
if ('copyTo' in opts && !['cachesDirectory', 'documentDirectory'].includes(opts.copyTo)) {
6670
throw new TypeError('Invalid copyTo option: ' + opts.copyTo);
6771
}
6872

6973
return RNDocumentPicker.pick(opts);
7074
}
7175

76+
function releaseSecureAccess(uris) {
77+
if (Platform.OS !== 'ios') {
78+
return;
79+
}
80+
81+
if (!Array.isArray(uris)) {
82+
throw new TypeError('`uris` should be an array of strings');
83+
}
84+
85+
uris.forEach((uri) => {
86+
if (typeof uri !== 'string') {
87+
throw new TypeError('Invalid uri parameter, expected a string not: ' + uri);
88+
}
89+
});
90+
91+
RNDocumentPicker.releaseSecureAccess(uris);
92+
}
93+
7294
const Types = {
7395
mimeTypes: {
7496
allFiles: '*/*',
@@ -156,4 +178,8 @@ export default class DocumentPicker {
156178
static isCancel(err) {
157179
return err && err.code === E_DOCUMENT_PICKER_CANCELED;
158180
}
181+
182+
static releaseSecureAccess(uris) {
183+
releaseSecureAccess(uris);
184+
}
159185
}

ios/RNDocumentPicker/RNDocumentPicker.m

Lines changed: 84 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
static NSString *const E_INVALID_DATA_RETURNED = @"INVALID_DATA_RETURNED";
1212

1313
static NSString *const OPTION_TYPE = @"type";
14-
static NSString *const OPTION_MULIPLE = @"multiple";
14+
static NSString *const OPTION_MULTIPLE = @"multiple";
1515

1616
static NSString *const FIELD_URI = @"uri";
1717
static NSString *const FIELD_FILE_COPY_URI = @"fileCopyUri";
@@ -24,23 +24,34 @@ @interface RNDocumentPicker () <UIDocumentPickerDelegate>
2424
@end
2525

2626
@implementation RNDocumentPicker {
27+
UIDocumentPickerMode mode;
28+
NSString *copyDestination;
2729
NSMutableArray *composeResolvers;
2830
NSMutableArray *composeRejecters;
29-
NSString* copyDestination;
31+
NSMutableArray *urls;
3032
}
3133

3234
@synthesize bridge = _bridge;
3335

3436
- (instancetype)init
3537
{
3638
if ((self = [super init])) {
37-
composeResolvers = [[NSMutableArray alloc] init];
38-
composeRejecters = [[NSMutableArray alloc] init];
39+
composeResolvers = [NSMutableArray new];
40+
composeRejecters = [NSMutableArray new];
41+
urls = [NSMutableArray new];
3942
}
4043
return self;
4144
}
4245

43-
+ (BOOL)requiresMainQueueSetup {
46+
- (void)dealloc
47+
{
48+
for (NSURL *url in urls) {
49+
[url stopAccessingSecurityScopedResource];
50+
}
51+
}
52+
53+
+ (BOOL)requiresMainQueueSetup
54+
{
4455
return NO;
4556
}
4657

@@ -55,20 +66,19 @@ - (dispatch_queue_t)methodQueue
5566
resolver:(RCTPromiseResolveBlock)resolve
5667
rejecter:(RCTPromiseRejectBlock)reject)
5768
{
58-
NSArray *allowedUTIs = [RCTConvert NSArray:options[OPTION_TYPE]];
59-
UIDocumentPickerViewController *documentPicker = [[UIDocumentPickerViewController alloc] initWithDocumentTypes:(NSArray *)allowedUTIs inMode:UIDocumentPickerModeImport];
60-
69+
mode = options[@"mode"] && [options[@"mode"] isEqualToString:@"open"] ? UIDocumentPickerModeOpen : UIDocumentPickerModeImport;
70+
copyDestination = options[@"copyTo"] ? options[@"copyTo"] : nil;
6171
[composeResolvers addObject:resolve];
6272
[composeRejecters addObject:reject];
63-
copyDestination = options[@"copyTo"] ? options[@"copyTo"] : nil;
6473

65-
74+
NSArray *allowedUTIs = [RCTConvert NSArray:options[OPTION_TYPE]];
75+
UIDocumentPickerViewController *documentPicker = [[UIDocumentPickerViewController alloc] initWithDocumentTypes:(NSArray *)allowedUTIs inMode:mode];
6676
documentPicker.delegate = self;
6777
documentPicker.modalPresentationStyle = UIModalPresentationFormSheet;
6878

6979
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000
7080
if (@available(iOS 11, *)) {
71-
documentPicker.allowsMultipleSelection = [RCTConvert BOOL:options[OPTION_MULIPLE]];
81+
documentPicker.allowsMultipleSelection = [RCTConvert BOOL:options[OPTION_MULTIPLE]];
7282
}
7383
#endif
7484

@@ -79,19 +89,22 @@ - (dispatch_queue_t)methodQueue
7989

8090
- (NSMutableDictionary *)getMetadataForUrl:(NSURL *)url error:(NSError **)error
8191
{
82-
__block NSMutableDictionary* result = [NSMutableDictionary dictionary];
92+
__block NSMutableDictionary *result = [NSMutableDictionary dictionary];
8393

94+
if (mode == UIDocumentPickerModeOpen)
95+
[urls addObject:url];
8496
[url startAccessingSecurityScopedResource];
8597

86-
NSFileCoordinator *coordinator = [[NSFileCoordinator alloc] init];
98+
NSFileCoordinator *coordinator = [NSFileCoordinator new];
8799
NSError *fileError;
88100

89101
[coordinator coordinateReadingItemAtURL:url options:NSFileCoordinatorReadingResolvesSymbolicLink error:&fileError byAccessor:^(NSURL *newURL) {
102+
90103
if (!fileError) {
91-
[result setValue:newURL.absoluteString forKey:FIELD_URI];
104+
[result setValue:((mode == UIDocumentPickerModeOpen) ? url : newURL).absoluteString forKey:FIELD_URI];
92105
NSError *copyError;
93-
NSURL* maybeFileCopyPath = copyDestination ? [RNDocumentPicker copyToUniqueDestinationFrom:newURL usingDestinationPreset:copyDestination error:copyError] : newURL;
94-
[result setValue: maybeFileCopyPath.absoluteString forKey:FIELD_FILE_COPY_URI];
106+
NSURL *maybeFileCopyPath = copyDestination ? [RNDocumentPicker copyToUniqueDestinationFrom:newURL usingDestinationPreset:copyDestination error:copyError] : newURL;
107+
[result setValue:maybeFileCopyPath.absoluteString forKey:FIELD_FILE_COPY_URI];
95108
if (copyError) {
96109
[result setValue:copyError.description forKey:FIELD_COPY_ERR];
97110
}
@@ -118,7 +131,8 @@ - (NSMutableDictionary *)getMetadataForUrl:(NSURL *)url error:(NSError **)error
118131
}
119132
}];
120133

121-
[url stopAccessingSecurityScopedResource];
134+
if (mode != UIDocumentPickerModeOpen)
135+
[url stopAccessingSecurityScopedResource];
122136

123137
if (fileError) {
124138
*error = fileError;
@@ -128,19 +142,35 @@ - (NSMutableDictionary *)getMetadataForUrl:(NSURL *)url error:(NSError **)error
128142
}
129143
}
130144

131-
+ (NSURL*)getDirectoryForFileCopy:(NSString*) copyToDirectory {
145+
RCT_EXPORT_METHOD(releaseSecureAccess:(NSArray<NSString *> *)uris)
146+
{
147+
NSMutableArray *discardedItems = [NSMutableArray array];
148+
for (NSString *uri in uris) {
149+
for (NSURL *url in urls) {
150+
if ([url.absoluteString isEqual:uri]) {
151+
[url stopAccessingSecurityScopedResource];
152+
[discardedItems addObject:url];
153+
break;
154+
}
155+
}
156+
}
157+
[urls removeObjectsInArray:discardedItems];
158+
}
159+
160+
+ (NSURL *)getDirectoryForFileCopy:(NSString *)copyToDirectory
161+
{
132162
if ([@"cachesDirectory" isEqualToString:copyToDirectory]) {
133163
return [NSFileManager.defaultManager URLsForDirectory:NSCachesDirectory inDomains:NSUserDomainMask].firstObject;
134164
} else if ([@"documentDirectory" isEqualToString:copyToDirectory]) {
135165
return [NSFileManager.defaultManager URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask].firstObject;
136166
}
137167
// this should not happen as the value is checked in JS, but we fall back to NSTemporaryDirectory()
138-
return [NSURL fileURLWithPath: NSTemporaryDirectory() isDirectory: YES];
168+
return [NSURL fileURLWithPath:NSTemporaryDirectory() isDirectory:YES];
139169
}
140170

141-
+ (NSURL *)copyToUniqueDestinationFrom:(NSURL *) url usingDestinationPreset: (NSString*) copyToDirectory error:(NSError *)error
171+
+ (NSURL *)copyToUniqueDestinationFrom:(NSURL *)url usingDestinationPreset:(NSString *)copyToDirectory error:(NSError *)error
142172
{
143-
NSURL* destinationRootDir = [self getDirectoryForFileCopy:copyToDirectory];
173+
NSURL *destinationRootDir = [self getDirectoryForFileCopy:copyToDirectory];
144174
// we don't want to rename the file so we put it into a unique location
145175
NSString *uniqueSubDirName = [[NSUUID UUID] UUIDString];
146176
NSURL *destinationDir = [destinationRootDir URLByAppendingPathComponent:[NSString stringWithFormat:@"%@/", uniqueSubDirName]];
@@ -160,56 +190,50 @@ + (NSURL *)copyToUniqueDestinationFrom:(NSURL *) url usingDestinationPreset: (NS
160190

161191
- (void)documentPicker:(UIDocumentPickerViewController *)controller didPickDocumentAtURL:(NSURL *)url
162192
{
163-
if (controller.documentPickerMode == UIDocumentPickerModeImport) {
164-
RCTPromiseResolveBlock resolve = [composeResolvers lastObject];
165-
RCTPromiseRejectBlock reject = [composeRejecters lastObject];
166-
[composeResolvers removeLastObject];
167-
[composeRejecters removeLastObject];
168-
169-
NSError *error;
170-
NSMutableDictionary* result = [self getMetadataForUrl:url error:&error];
171-
if (result) {
172-
NSArray *results = @[result];
173-
resolve(results);
174-
} else {
175-
reject(E_INVALID_DATA_RETURNED, error.localizedDescription, error);
176-
}
193+
RCTPromiseResolveBlock resolve = [composeResolvers lastObject];
194+
RCTPromiseRejectBlock reject = [composeRejecters lastObject];
195+
[composeResolvers removeLastObject];
196+
[composeRejecters removeLastObject];
197+
198+
NSError *error;
199+
NSMutableDictionary *result = [self getMetadataForUrl:url error:&error];
200+
if (result) {
201+
NSArray *results = @[result];
202+
resolve(results);
203+
} else {
204+
reject(E_INVALID_DATA_RETURNED, error.localizedDescription, error);
177205
}
178206
}
179207

180208
- (void)documentPicker:(UIDocumentPickerViewController *)controller didPickDocumentsAtURLs:(NSArray<NSURL *> *)urls
181209
{
182-
if (controller.documentPickerMode == UIDocumentPickerModeImport) {
183-
RCTPromiseResolveBlock resolve = [composeResolvers lastObject];
184-
RCTPromiseRejectBlock reject = [composeRejecters lastObject];
185-
[composeResolvers removeLastObject];
186-
[composeRejecters removeLastObject];
187-
188-
NSMutableArray *results = [NSMutableArray array];
189-
for (id url in urls) {
190-
NSError *error;
191-
NSMutableDictionary* result = [self getMetadataForUrl:url error:&error];
192-
if (result) {
193-
[results addObject:result];
194-
} else {
195-
reject(E_INVALID_DATA_RETURNED, error.localizedDescription, error);
196-
return;
197-
}
210+
RCTPromiseResolveBlock resolve = [composeResolvers lastObject];
211+
RCTPromiseRejectBlock reject = [composeRejecters lastObject];
212+
[composeResolvers removeLastObject];
213+
[composeRejecters removeLastObject];
214+
215+
NSMutableArray *results = [NSMutableArray array];
216+
for (id url in urls) {
217+
NSError *error;
218+
NSMutableDictionary *result = [self getMetadataForUrl:url error:&error];
219+
if (result) {
220+
[results addObject:result];
221+
} else {
222+
reject(E_INVALID_DATA_RETURNED, error.localizedDescription, error);
223+
return;
198224
}
199-
200-
resolve(results);
201225
}
226+
227+
resolve(results);
202228
}
203229

204230
- (void)documentPickerWasCancelled:(UIDocumentPickerViewController *)controller
205231
{
206-
if (controller.documentPickerMode == UIDocumentPickerModeImport) {
207-
RCTPromiseRejectBlock reject = [composeRejecters lastObject];
208-
[composeResolvers removeLastObject];
209-
[composeRejecters removeLastObject];
210-
211-
reject(E_DOCUMENT_PICKER_CANCELED, @"User canceled document picker", nil);
212-
}
232+
RCTPromiseRejectBlock reject = [composeRejecters lastObject];
233+
[composeResolvers removeLastObject];
234+
[composeRejecters removeLastObject];
235+
236+
reject(E_DOCUMENT_PICKER_CANCELED, @"User canceled document picker", nil);
213237
}
214238

215239
@end

0 commit comments

Comments
 (0)