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
5 changes: 5 additions & 0 deletions .changeset/spotty-queens-smash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@react-native-documents/picker": major
---

feat(ios): support swift 6 concurrency
60 changes: 19 additions & 41 deletions packages/document-picker/ios/RNDocumentPicker.mm
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,23 @@ @interface RNDocumentPicker ()
@end

@implementation RNDocumentPicker {
DocPicker *docPicker;
DocSaver *docSaver;
DocPicker *_docPicker;
DocSaver *_docSaver;
}

- (instancetype)init {
if ((self = [super init])) {
docPicker = [DocPicker new];
docSaver = [DocSaver new];
// initialization happens on serial queue so there are no races
- (DocPicker *)docPicker {
if (!_docPicker) {
_docPicker = [DocPicker new];
}
return self;
return _docPicker;
}

+ (BOOL)requiresMainQueueSetup {
return NO;
- (DocSaver *)docSaver {
if (!_docSaver) {
_docSaver = [DocSaver new];
}
return _docSaver;
}

RCT_EXPORT_MODULE()
Expand All @@ -43,26 +46,9 @@ + (BOOL)requiresMainQueueSetup {
reject:
(RCTPromiseRejectBlock) reject)
{
// https://stackoverflow.com/questions/5270519/what-is-difference-between-uimodaltransitionstyle-and-uimodalpresentationstyle
UIModalPresentationStyle presentationStyle = [RCTConvert UIModalPresentationStyle:options[@"presentationStyle"]];
UIModalTransitionStyle transitionStyle = [RCTConvert UIModalTransitionStyle:options[@"transitionStyle"]];
NSArray *allowedUTIs = [RCTConvert NSArray:options[@"type"]];
BOOL allowMultiple = [RCTConvert BOOL:options[@"allowMultiSelection"]];
BOOL showExtensions = [RCTConvert BOOL:options[@"showFileExtensions"]];
NSString *mode = options[@"mode"];
NSString *initialDir = options[@"initialDirectoryUrl"];
BOOL requestLongTermAccess = [RCTConvert BOOL:options[@"requestLongTermAccess"]];

PickerOptions *pickerOptions = [[PickerOptions alloc] initWithTypes:allowedUTIs
mode:mode
initialDirectoryUrl:initialDir
allowMultiSelection:allowMultiple
shouldShowFileExtensions:showExtensions
transitionStyle:transitionStyle
presentationStyle:presentationStyle
requestLongTermAccess:requestLongTermAccess];

[docPicker presentWithOptions:pickerOptions resolve:resolve reject:reject];
dispatch_async(dispatch_get_main_queue(), ^{
[self.docPicker presentWithOptionsDict:options resolve:resolve reject:reject];
});
}

RCT_EXPORT_METHOD(pickDirectory:(NSDictionary *)options resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) {
Expand Down Expand Up @@ -92,21 +78,13 @@ + (BOOL)requiresMainQueueSetup {
}

RCT_EXPORT_METHOD(writeDocuments:(NSDictionary *)options resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) {
UIModalPresentationStyle presentationStyle = [RCTConvert UIModalPresentationStyle:options[@"presentationStyle"]];
UIModalTransitionStyle transitionStyle = [RCTConvert UIModalTransitionStyle:options[@"transitionStyle"]];
BOOL showExtensions = [RCTConvert BOOL:options[@"showFileExtensions"]];
BOOL asCopy = [RCTConvert BOOL:options[@"copy"]];

NSString *initialDir = options[@"initialDirectoryUri"];
NSArray<NSString*> *documentUrl = options[@"sourceUris"];

SaverOptions* saverOptions = [[SaverOptions alloc] initWithSourceUrlStrings:documentUrl asCopy:asCopy initialDirectoryUrl:initialDir shouldShowFileExtensions:showExtensions transitionStyle:transitionStyle presentationStyle:presentationStyle];

[docSaver presentWithOptions:saverOptions resolve:resolve reject:reject];
dispatch_async(dispatch_get_main_queue(), ^{
[self.docSaver presentWithOptionsDict:options resolve:resolve reject:reject];
});
}

RCT_EXPORT_METHOD(releaseSecureAccess:(NSArray *)uris resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) {
[docPicker stopAccessingOpenedUrls:uris];
[self.docPicker stopAccessingOpenedUrls:uris];
resolve([NSNull null]);
}

Expand Down
74 changes: 46 additions & 28 deletions packages/document-picker/ios/swift/DocPicker.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,40 +2,48 @@

import Foundation
import UniformTypeIdentifiers
import MobileCoreServices

@objc public class DocPicker: PickerWithMetadataImpl {
@objc public class DocPicker: PickerBase {
var pickerOptions: PickerOptions?
var openedUrls: Set<URL> = []

var currentOptions: PickerOptions? = nil
@MainActor
override func createDocumentPicker(from dictionary: NSDictionary) -> UIDocumentPickerViewController {
let options = PickerOptions(dictionary: dictionary)
self.pickerOptions = options
return options.createDocumentPicker()
}

@objc public func present(options: PickerOptions, resolve: @escaping RNDPPromiseResolveBlock, reject: @escaping RNDPPromiseRejectBlock) {
// TODO fix callsite param
if (!promiseWrapper.trySetPromiseRejectingIncoming(resolve, rejecter: reject, fromCallSite: "pick")) {
return;
}
currentOptions = options;
DispatchQueue.main.async {
let documentPicker = UIDocumentPickerViewController(forOpeningContentTypes: options.allowedTypes, asCopy: options.modeAsCopy())
public override func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
guard let promise = promiseWrapper.takeCallbacks() else { return }
let options = self.pickerOptions

documentPicker.modalPresentationStyle = options.presentationStyle
documentPicker.allowsMultipleSelection = options.allowMultiSelection
documentPicker.modalTransitionStyle = options.transitionStyle
// documentPicker.directoryURL = options.initialDirectoryUrl
// documentPicker.shouldShowFileExtensions = options.shouldShowFileExtensions
Task.detached(priority: .userInitiated) {
let documentsInfo = self.createDocumentMetadataWithOptions(for: urls, options: options)
.compactMap { $0.build() }
promise.resolve(documentsInfo)
}
}

self.presentInternal(documentPicker: documentPicker)
nonisolated private func createDocumentMetadataWithOptions(for urls: [URL], options: PickerOptions?) -> [DocumentMetadataBuilder] {
return urls.compactMap { url in
do {
return try self.getMetadataForWithOptions(url: url, options: options)
} catch {
return DocumentMetadataBuilder(forUri: url, error: error)
}
}
}

public func getMetadataFor(url: URL) throws -> DocumentMetadataBuilder {
return if (currentOptions?.isOpenMode() == true) {
try self.getOpenedDocumentInfo(url: url, requestLongTermAccess: currentOptions?.requestLongTermAccess ?? false)
nonisolated private func getMetadataForWithOptions(url: URL, options: PickerOptions?) throws -> DocumentMetadataBuilder {
return if options?.isOpenMode() == true {
try self.getOpenedDocumentInfo(url: url, requestLongTermAccess: options?.requestLongTermAccess ?? false)
} else {
try self.getAnyModeMetadata(url: url)
}
}

private func getAnyModeMetadata(url: URL) throws -> DocumentMetadataBuilder {
nonisolated private func getAnyModeMetadata(url: URL) throws -> DocumentMetadataBuilder {
let resourceValues = try url.resourceValues(forKeys: [.fileSizeKey, .nameKey, .isDirectoryKey, .contentTypeKey])

return DocumentMetadataBuilder(forUri: url, resourceValues: resourceValues)
Expand All @@ -45,18 +53,20 @@ import MobileCoreServices
case sourceAccessError
}

func getOpenedDocumentInfo(url: URL, requestLongTermAccess: Bool) throws -> DocumentMetadataBuilder {
nonisolated private func getOpenedDocumentInfo(url: URL, requestLongTermAccess: Bool) throws -> DocumentMetadataBuilder {
guard url.startAccessingSecurityScopedResource() else {
throw KeepLocalCopyError.sourceAccessError
}

// url.stopAccessingSecurityScopedResource() must be called later
openedUrls.append(url)
// url.stopAccessingSecurityScopedResource() must be called later by user
DispatchQueue.main.async { [weak self] in
self?.openedUrls.insert(url)
}

// Use file coordination for reading and writing any of the URLs content.
// Use file coordination for reading and writing any of the URL's content.
var error: NSError? = nil
var success = false
var metadataBuilder: DocumentMetadataBuilder = DocumentMetadataBuilder(forUri: url)
var metadataBuilder = DocumentMetadataBuilder(forUri: url)

NSFileCoordinator().coordinate(readingItemAt: url, error: &error) { (url) in
do {
Expand All @@ -66,7 +76,7 @@ import MobileCoreServices
metadataBuilder.setMetadataReadingError(error)
}

if (requestLongTermAccess == true) {
if requestLongTermAccess {
do {
let bookmarkData = try url.bookmarkData(options: .minimalBookmark, includingResourceValuesForKeys: nil, relativeTo: nil)
metadataBuilder.setBookmark(bookmarkData)
Expand All @@ -75,10 +85,18 @@ import MobileCoreServices
}
}
}
if let err = error, success == false {
if let err = error, !success {
throw err
}
return metadataBuilder
}

@objc public func stopAccessingOpenedUrls(_ urlStrings: [String]) {
let incomingUrls = Set(urlStrings.compactMap { URL(string: $0) })
for url in openedUrls.intersection(incomingUrls) {
url.stopAccessingSecurityScopedResource()
openedUrls.remove(url)
}
}

}
41 changes: 18 additions & 23 deletions packages/document-picker/ios/swift/DocSaver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,33 +9,28 @@

import Foundation
import UniformTypeIdentifiers
import MobileCoreServices

@objc public class DocSaver: PickerWithMetadataImpl {
@objc public class DocSaver: PickerBase {

@objc public func present(options: SaverOptions, resolve: @escaping (Any?) -> Void, reject: @escaping (String?, String?, Error?) -> Void) {
if (!promiseWrapper.trySetPromiseRejectingIncoming(resolve, rejecter: reject, fromCallSite: "saveDocuments")) {
return;
}
DispatchQueue.main.async {
let documentPicker = UIDocumentPickerViewController(forExporting: options.sourceUrls, asCopy: options.asCopy)

documentPicker.modalPresentationStyle = options.presentationStyle
documentPicker.modalTransitionStyle = options.transitionStyle
// documentPicker.directoryURL = options.initialDirectoryUrl
// documentPicker.shouldShowFileExtensions = options.shouldShowFileExtensions

self.presentInternal(documentPicker: documentPicker)
}
@MainActor
override func createDocumentPicker(from dictionary: NSDictionary) -> UIDocumentPickerViewController {
let options = SaverOptions(dictionary: dictionary)
return options.createDocumentPicker()
}

public func getMetadataFor(url: URL) throws -> DocumentMetadataBuilder {
let name = url.lastPathComponent.removingPercentEncoding

var resourceValues = URLResourceValues()
resourceValues.name = name

return DocumentMetadataBuilder(forUri: url, resourceValues: resourceValues)
public override func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
guard let promise = promiseWrapper.takeCallbacks() else { return }

Task.detached(priority: .userInitiated) {
// runs off main thread - preserves I/O performance
let documentsInfo = urls.compactMap { url -> [String: Any?]? in
let name = url.lastPathComponent.removingPercentEncoding
var resourceValues = URLResourceValues()
resourceValues.name = name
return DocumentMetadataBuilder(forUri: url, resourceValues: resourceValues).build()
}
promise.resolve(documentsInfo)
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public class DocumentMetadataBuilder {

func build() -> [String: Any?] {
var dictionary: [String: Any?] = [:]
if (resourceValues?.isDirectory ?? false == false) {
if resourceValues?.isDirectory != true {
let utTypeFromFile: UTType? = resourceValues?.contentType
let utType: UTType? = utTypeFromFile ?? UTType(filenameExtension: uri.pathExtension)

Expand Down
40 changes: 21 additions & 19 deletions packages/document-picker/ios/swift/FileOperations.swift
Original file line number Diff line number Diff line change
@@ -1,35 +1,37 @@
// LICENSE: see License.md in the package root

import Foundation
import React

@objc public class FileOperations: NSObject {

@objc public static func keepLocalCopyAtUniqueDestination(from: [[String: String]], destinationPreset: String, resolve: @escaping RNDPPromiseResolveBlock) {
DispatchQueue.global(qos: .utility).async {
let results = moveFiles(from: from, destinationPreset: destinationPreset)
resolve(results)

@objc public static func keepLocalCopyAtUniqueDestination(from: [[String: String]], destinationPreset: String, resolve: @escaping RCTPromiseResolveBlock) {
let callback = ResolveCallback(resolve: resolve)
Task.detached(priority: .userInitiated) {
let results = Self.moveFiles(from: from, destinationPreset: destinationPreset)
callback.resolve(results)
}
}
static func moveFiles(from: [[String: String]], destinationPreset: String) -> [[String: String?]] {

private static func moveFiles(from: [[String: String]], destinationPreset: String) -> [[String: String?]] {
let destinationRootDir = getDirectoryForFileDestination(destinationPreset)
let uniqueSubDirName = UUID().uuidString
let destinationDir = destinationRootDir.appendingPathComponent(uniqueSubDirName, isDirectory: true)

do {
try FileManager.default.createDirectory(at: destinationDir, withIntermediateDirectories: true, attributes: nil)
} catch {
return from.map { dictionary in
LocalCopyResponse.error(sourceUri: dictionary["uri"], copyError: "Failed to create destination directory: \(error.localizedDescription)").dictionaryRepresentation
}
}

// move files
return from.map { dictionary in
moveSingleFile(dictionary: dictionary, destinationDir: destinationDir).dictionaryRepresentation
Self.moveSingleFile(dictionary: dictionary, destinationDir: destinationDir).dictionaryRepresentation
}
}

private static func moveSingleFile(dictionary: [String: String], destinationDir: URL) -> LocalCopyResponse {
guard let uriString = dictionary["uri"],
let uri = URL(string: uriString),
Expand All @@ -39,32 +41,32 @@ import Foundation
copyError: "Invalid URI or fileName"
)
}

do {
let destinationUrl = try moveToDestination(from: uri, usingFilename: fileName, destinationDir: destinationDir)
return LocalCopyResponse.success(sourceUri: uri.absoluteString, localUri: destinationUrl.absoluteString)
} catch {
return LocalCopyResponse.error(sourceUri: uriString, copyError: error.localizedDescription)
}
}
static func moveToDestination(from: URL, usingFilename fileName: String, destinationDir: URL) throws -> URL {

private static func moveToDestination(from: URL, usingFilename fileName: String, destinationDir: URL) throws -> URL {
let destinationFile = destinationDir.appendingPathComponent(fileName).standardized

guard destinationFile.path.hasPrefix(destinationDir.standardized.path) else {
throw NSError(
domain: "PathTraversalPrevention",
code: 400,
userInfo: [NSLocalizedDescriptionKey: "The copied file is attempting to write outside of the target directory."]
)
}

try FileManager.default.moveItem(at: from, to: destinationFile)

return destinationFile
}
static func getDirectoryForFileDestination(_ copyToDirectory: String) -> URL {

private static func getDirectoryForFileDestination(_ copyToDirectory: String) -> URL {
let searchPath: FileManager.SearchPathDirectory = copyToDirectory == "documentDirectory" ? .documentDirectory : .cachesDirectory
return FileManager.default.urls(for: searchPath, in: .userDomainMask).first!
}
Expand Down
Loading
Loading