Skip to content

Commit

Permalink
feat: fileCopyUri to be null if copyTo not provided (#481)
Browse files Browse the repository at this point in the history
BREAKING CHANGE - fileCopyUri can be null
  • Loading branch information
vonovak authored Oct 6, 2021
1 parent 104b9a9 commit 6ff5fd3
Show file tree
Hide file tree
Showing 5 changed files with 85 additions and 74 deletions.
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,10 +112,12 @@ Defaults to `import`. If `mode` is set to `import` the document picker imports t

##### [iOS and Android only] `copyTo`:`"cachesDirectory" | "documentDirectory"`

If specified, the picked file is copied to `NSCachesDirectory` / `NSDocumentDirectory` (iOS) or `getCacheDir` / `getFilesDir` (Android). 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.
If specified, the picked file is copied to `NSCachesDirectory` / `NSDocumentDirectory` (iOS) or `getCacheDir` / `getFilesDir` (Android). The uri of the copy will be available in result's `fileCopyUri`. If copying the file fails (eg. due to lack of free space), `fileCopyUri` will be `null`, and more details about the error will be available in `copyError` field in the result.

This should help if you need to work with the file(s) later on, because by default, [the picked documents are temporary files. They remain available only until your application terminates](https://developer.apple.com/documentation/uikit/uidocumentpickerdelegate/2902364-documentpicker). This may impact performance for large files, so keep this in mind if you expect users to pick particularly large files and your app does not need immediate read access.

On Android, this can be used to obtain local, on-device copy of the file (eg. if user picks a document from google drive, this will download it locally to the phone).

##### [Windows only] `readContent`:`boolean`

Defaults to `false`. If `readContent` is set to true the content of the picked file/files will be read and supplied in the result object.
Expand Down Expand Up @@ -143,7 +145,7 @@ The URI representing the document picked by the user. _On iOS this will be a `fi

##### `fileCopyUri`

Same as `uri`, but has special meaning if `copyTo` option is specified.
If `copyTo` option is specified, this will point to a local copy of picked file. Otherwise, this is `null`.

##### `type`

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -300,15 +300,15 @@ private void prepareFileUri(Context context, WritableMap map, Uri uri) {
fileName = String.valueOf(System.currentTimeMillis());
}
File destFile = new File(dir, fileName);
String path = copyFile(context, uri, destFile);
map.putString(FIELD_FILE_COPY_URI, path);
String copyPath = copyFile(context, uri, destFile);
map.putString(FIELD_FILE_COPY_URI, copyPath);
} catch (Exception e) {
e.printStackTrace();
map.putString(FIELD_FILE_COPY_URI, uri.toString());
map.putString(FIELD_COPY_ERROR, e.getMessage());
map.putNull(FIELD_FILE_COPY_URI);
map.putString(FIELD_COPY_ERROR, e.getLocalizedMessage());
}
} else {
map.putString(FIELD_FILE_COPY_URI, uri.toString());
map.putNull(FIELD_FILE_COPY_URI);
}
}

Expand Down
1 change: 1 addition & 0 deletions example/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export default function App() {
try {
const pickerResult = await DocumentPicker.pickSingle({
presentationStyle: 'fullScreen',
copyTo: 'cachesDirectory',
})
setResult([pickerResult])
} catch (e) {
Expand Down
134 changes: 71 additions & 63 deletions ios/RNDocumentPicker.m
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
@implementation RCTConvert (ModalPresentationStyle)


// how to de-duplicate from https://github.com/facebook/react-native/blob/v0.66.0/React/Views/RCTModalHostViewManager.m?
// TODO how to de-duplicate from https://github.com/facebook/react-native/blob/v0.66.0/React/Views/RCTModalHostViewManager.m?
RCT_ENUM_CONVERTER(
UIModalPresentationStyle,
(@{
Expand All @@ -44,7 +44,7 @@ @implementation RNDocumentPicker {
UIDocumentPickerMode mode;
NSString *copyDestination;
RNCPromiseWrapper* promiseWrapper;
NSMutableArray *urls;
NSMutableArray *urlsInOpenMode;
}

@synthesize bridge = _bridge;
Expand All @@ -53,14 +53,14 @@ - (instancetype)init
{
if ((self = [super init])) {
promiseWrapper = [RNCPromiseWrapper new];
urls = [NSMutableArray new];
urlsInOpenMode = [NSMutableArray new];
}
return self;
}

- (void)dealloc
{
for (NSURL *url in urls) {
for (NSURL *url in urlsInOpenMode) {
[url stopAccessingSecurityScopedResource];
}
}
Expand All @@ -82,7 +82,7 @@ - (dispatch_queue_t)methodQueue
rejecter:(RCTPromiseRejectBlock)reject)
{
mode = options[@"mode"] && [options[@"mode"] isEqualToString:@"open"] ? UIDocumentPickerModeOpen : UIDocumentPickerModeImport;
copyDestination = options[@"copyTo"] ? options[@"copyTo"] : nil;
copyDestination = options[@"copyTo"];
UIModalPresentationStyle presentationStyle = [RCTConvert UIModalPresentationStyle:options[@"presentationStyle"]];
[promiseWrapper setPromiseWithInProgressCheck:resolve rejecter:reject fromCallSite:@"pick"];

Expand All @@ -93,63 +93,88 @@ - (dispatch_queue_t)methodQueue
documentPicker.delegate = self;
documentPicker.presentationController.delegate = self;

if (@available(iOS 11.0, *)) {
documentPicker.allowsMultipleSelection = [RCTConvert BOOL:options[OPTION_MULTIPLE]];
}
documentPicker.allowsMultipleSelection = [RCTConvert BOOL:options[OPTION_MULTIPLE]];

UIViewController *rootViewController = RCTPresentedViewController();

[rootViewController presentViewController:documentPicker animated:YES completion:nil];
}


- (void)documentPicker:(UIDocumentPickerViewController *)controller didPickDocumentsAtURLs:(NSArray<NSURL *> *)urls
{
NSMutableArray *results = [NSMutableArray array];
for (id url in urls) {
NSError *error;
NSMutableDictionary *result = [self getMetadataForUrl:url error:&error];
if (result) {
[results addObject:result];
} else {
[promiseWrapper reject:E_INVALID_DATA_RETURNED withError:error];
return;
}
}

[promiseWrapper resolve:results];
}

- (NSMutableDictionary *)getMetadataForUrl:(NSURL *)url error:(NSError **)error
{
__block NSMutableDictionary *result = [NSMutableDictionary dictionary];

if (mode == UIDocumentPickerModeOpen)
[urls addObject:url];
if (mode == UIDocumentPickerModeOpen) {
[urlsInOpenMode addObject:url];
}

[url startAccessingSecurityScopedResource];

NSFileCoordinator *coordinator = [NSFileCoordinator new];
NSError *fileError;

// TODO double check this implemenation, see eg. https://developer.apple.com/documentation/foundation/nsfilecoordinator/1412420-prepareforreadingitemsaturls
[coordinator coordinateReadingItemAtURL:url options:NSFileCoordinatorReadingResolvesSymbolicLink error:&fileError byAccessor:^(NSURL *newURL) {
// If the coordinated operation fails, then the accessor block never runs
result[FIELD_URI] = ((mode == UIDocumentPickerModeOpen) ? url : newURL).absoluteString;

NSError *copyError;
NSString *maybeFileCopyPath = copyDestination ? [RNDocumentPicker copyToUniqueDestinationFrom:newURL usingDestinationPreset:copyDestination error:copyError].absoluteString : nil;

if (!copyError) {
result[FIELD_FILE_COPY_URI] = RCTNullIfNil(maybeFileCopyPath);
} else {
result[FIELD_COPY_ERR] = copyError.localizedDescription;
result[FIELD_FILE_COPY_URI] = [NSNull null];
}

if (!fileError) {
[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];
if (copyError) {
[result setValue:copyError.description forKey:FIELD_COPY_ERR];
}
result[FIELD_NAME] = newURL.lastPathComponent;

[result setValue:[newURL lastPathComponent] forKey:FIELD_NAME];
NSError *attributesError = nil;
NSDictionary *fileAttributes = [NSFileManager.defaultManager attributesOfItemAtPath:newURL.path error:&attributesError];
if(!attributesError) {
result[FIELD_SIZE] = fileAttributes[NSFileSize];
} else {
result[FIELD_SIZE] = [NSNull null];
NSLog(@"RNDocumentPicker: %@", attributesError);
}

NSError *attributesError = nil;
NSDictionary *fileAttributes = [[NSFileManager defaultManager] attributesOfItemAtPath:newURL.path error:&attributesError];
if(!attributesError) {
[result setValue:[fileAttributes objectForKey:NSFileSize] forKey:FIELD_SIZE];
} else {
NSLog(@"%@", attributesError);
if (newURL.pathExtension != nil) {
CFStringRef extension = (__bridge CFStringRef)[newURL pathExtension];
CFStringRef uti = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, extension, NULL);
CFStringRef mimeType = UTTypeCopyPreferredTagWithClass(uti, kUTTagClassMIMEType);
if (uti) {
CFRelease(uti);
}

if ( newURL.pathExtension != nil ) {
CFStringRef extension = (__bridge CFStringRef)[newURL pathExtension];
CFStringRef uti = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, extension, NULL);
CFStringRef mimeType = UTTypeCopyPreferredTagWithClass(uti, kUTTagClassMIMEType);
if (uti) {
CFRelease(uti);
}

NSString *mimeTypeString = (__bridge_transfer NSString *)mimeType;
[result setValue:mimeTypeString forKey:FIELD_TYPE];
}
NSString *mimeTypeString = (__bridge_transfer NSString *)mimeType;
result[FIELD_TYPE] = mimeTypeString;
} else {
result[FIELD_TYPE] = [NSNull null];
}
}];

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

if (fileError) {
*error = fileError;
Expand All @@ -165,29 +190,18 @@ - (NSMutableDictionary *)getMetadataForUrl:(NSURL *)url error:(NSError **)error
{
NSMutableArray *discardedItems = [NSMutableArray array];
for (NSString *uri in uris) {
for (NSURL *url in urls) {
for (NSURL *url in urlsInOpenMode) {
if ([url.absoluteString isEqual:uri]) {
[url stopAccessingSecurityScopedResource];
[discardedItems addObject:url];
break;
}
}
}
[urls removeObjectsInArray:discardedItems];
[urlsInOpenMode removeObjectsInArray:discardedItems];
resolve(nil);
}

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

+ (NSURL *)copyToUniqueDestinationFrom:(NSURL *)url usingDestinationPreset:(NSString *)copyToDirectory error:(NSError *)error
{
NSURL *destinationRootDir = [self getDirectoryForFileCopy:copyToDirectory];
Expand All @@ -208,21 +222,15 @@ + (NSURL *)copyToUniqueDestinationFrom:(NSURL *)url usingDestinationPreset:(NSSt
}
}

- (void)documentPicker:(UIDocumentPickerViewController *)controller didPickDocumentsAtURLs:(NSArray<NSURL *> *)urls
+ (NSURL *)getDirectoryForFileCopy:(NSString *)copyToDirectory
{
NSMutableArray *results = [NSMutableArray array];
for (id url in urls) {
NSError *error;
NSMutableDictionary *result = [self getMetadataForUrl:url error:&error];
if (result) {
[results addObject:result];
} else {
[promiseWrapper reject:E_INVALID_DATA_RETURNED withError:error];
return;
}
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;
}

[promiseWrapper resolve:results];
// this should not happen as the value is checked in JS, but we fall back to NSTemporaryDirectory()
return [NSURL fileURLWithPath:NSTemporaryDirectory() isDirectory:YES];
}

- (void)documentPickerWasCancelled:(UIDocumentPickerViewController *)controller
Expand Down
8 changes: 4 additions & 4 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ import { perPlatformTypes } from './fileTypes'

export type DocumentPickerResponse = {
uri: string
fileCopyUri: string
copyError?: string
type: string
name: string
size: number
copyError?: string
fileCopyUri: string | null
type: string | null
size: number | null
}

export const types = perPlatformTypes[Platform.OS]
Expand Down

0 comments on commit 6ff5fd3

Please sign in to comment.