Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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`:

Expand Down Expand Up @@ -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(uris: Array<string>)`

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
Expand Down
2 changes: 2 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ declare module 'react-native-document-picker' {
};
interface DocumentPickerOptions<OS extends keyof PlatformTypes> {
type: Array<PlatformTypes[OS][keyof PlatformTypes[OS]]> | DocumentType[OS];
mode?: 'import' | 'open';
copyTo?: 'cachesDirectory' | 'documentDirectory';
}
interface DocumentPickerResponse {
Expand All @@ -68,5 +69,6 @@ declare module 'react-native-document-picker' {
options: DocumentPickerOptions<OS>
): Promise<DocumentPickerResponse[]>;
static isCancel<IError extends { code?: string }>(err?: IError): boolean;
static releaseSecureAccess(uris: Array<string>): void;
}
}
26 changes: 26 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,13 +62,35 @@ 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);
}

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: '*/*',
Expand Down Expand Up @@ -138,4 +160,8 @@ export default class DocumentPicker {
static isCancel(err) {
return err && err.code === E_DOCUMENT_PICKER_CANCELED;
}

static releaseSecureAccess(uris) {
releaseSecureAccess(uris);
}
}
144 changes: 84 additions & 60 deletions ios/RNDocumentPicker/RNDocumentPicker.m
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -24,23 +24,34 @@ @interface RNDocumentPicker () <UIDocumentPickerDelegate>
@end

@implementation RNDocumentPicker {
UIDocumentPickerMode mode;
NSString *copyDestination;
NSMutableArray *composeResolvers;
NSMutableArray *composeRejecters;
NSString* copyDestination;
NSMutableArray *urls;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: maybe I'm missing something; where is this array allocated?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My bad, it's fixed now.

}

@synthesize bridge = _bridge;

- (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;
}

Expand All @@ -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

Expand All @@ -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];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just wondering, why is this needed? thanks!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because sometimes those two urls where a little bit different during my tests and I wasn't able to save documents with the new one.

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];
}
Expand All @@ -118,7 +131,8 @@ - (NSMutableDictionary *)getMetadataForUrl:(NSURL *)url error:(NSError **)error
}
}];

[url stopAccessingSecurityScopedResource];
if (mode != UIDocumentPickerModeOpen)
[url stopAccessingSecurityScopedResource];

if (fileError) {
*error = fileError;
Expand All @@ -128,19 +142,35 @@ - (NSMutableDictionary *)getMetadataForUrl:(NSURL *)url error:(NSError **)error
}
}

+ (NSURL*)getDirectoryForFileCopy:(NSString*) copyToDirectory {
RCT_EXPORT_METHOD(releaseSecureAccess:(NSArray<NSString *> *)uris)
{
NSMutableArray *discardedItems = [NSMutableArray array];
for (NSString *uri in uris) {
for (NSURL *url in urls) {
if ([url.absoluteString isEqual:uri]) {
[url stopAccessingSecurityScopedResource];
[discardedItems addObject:url];
break;
}
}
}
[urls removeObjectsInArray:discardedItems];
}

+ (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]];
Expand All @@ -160,56 +190,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<NSURL *> *)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