fix(dialog): do not create file copy for save file picker on iOS

On iOS the file picker returns a security scoped resource file path on the save() file picker: https://developer.apple.com/documentation/uikit/uidocumentpickerviewcontroller?language=objc#Work-with-external-documents

this means we can't directly access it without calling [startAccessingSecurityScopedResource](https://developer.apple.com/documentation/foundation/nsurl/1417051-startaccessingsecurityscopedreso?language=objc)

this PR changes the plugin to not access the save file early, leaving that to the user and returning its actual path
pull/2548/head
Lucas Nogueira 4 months ago
parent 38deef43dc
commit 84209f3564
No known key found for this signature in database
GPG Key ID: 7C32FCA95C8C95D7

@ -387,7 +387,7 @@
CODE_SIGN_ENTITLEMENTS = api_iOS/api_iOS.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = Q93MBH6S2F;
DEVELOPMENT_TEAM = "Q93MBH6S2F";
ENABLE_BITCODE = NO;
"EXCLUDED_ARCHS[sdk=iphoneos*]" = "arm64-sim x86_64";
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
@ -442,7 +442,7 @@
CODE_SIGN_ENTITLEMENTS = api_iOS/api_iOS.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = Q93MBH6S2F;
DEVELOPMENT_TEAM = "Q93MBH6S2F";
ENABLE_BITCODE = NO;
"EXCLUDED_ARCHS[sdk=iphoneos*]" = "arm64-sim x86_64";
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;

@ -156,6 +156,10 @@ type OpenDialogReturn<T extends OpenDialogOptions> = T['directory'] extends true
* }
* ```
*
* ## Platform-specific
*
* - **iOS**: Returns a copy of the file to bypass [security scoped resource](https://developer.apple.com/documentation/foundation/nsurl/1417051-startaccessingsecurityscopedreso?language=objc).
*
* @returns A promise resolving to the selected path(s)
*
* @since 2.0.0
@ -190,6 +194,10 @@ async function open<T extends OpenDialogOptions>(
* });
* ```
*
* #### Platform-specific
*
* - **iOS**: Returns a copy of the file to bypass [security scoped resource](https://developer.apple.com/documentation/foundation/nsurl/1417051-startaccessingsecurityscopedreso?language=objc).
*
* @returns A promise resolving to the selected path.
*
* @since 2.0.0

@ -74,10 +74,18 @@ class DialogPlugin: Plugin {
onFilePickerResult = { (event: FilePickerEvent) -> Void in
switch event {
case .selected(let urls):
invoke.resolve(["files": urls])
do {
let temporaryUrls = try urls.map { try self.saveTemporaryFile($0) }
invoke.resolve(["files": temporaryUrls])
} catch {
let message = "Failed to create a temporary copy of the file: \(error)"
Logger.error("\(message)")
invoke.reject(message)
}
case .cancelled:
invoke.resolve(["files": nil])
case .error(let error):
Logger.error("failed to pick file: \(error)")
invoke.reject(error)
}
}
@ -153,6 +161,7 @@ class DialogPlugin: Plugin {
case .cancelled:
invoke.resolve(["file": nil])
case .error(let error):
Logger.error("failed to pick file to save: \(error)")
invoke.reject(error)
}
}
@ -192,6 +201,27 @@ class DialogPlugin: Plugin {
self.onFilePickerResult?(event)
}
private func saveTemporaryFile(_ sourceUrl: URL) throws -> URL {
var directory = URL(fileURLWithPath: NSTemporaryDirectory())
if let cachesDirectory = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)
.first
{
directory = cachesDirectory
}
let targetUrl = directory.appendingPathComponent(sourceUrl.lastPathComponent)
do {
try deleteFile(targetUrl)
}
try FileManager.default.copyItem(at: sourceUrl, to: targetUrl)
return targetUrl
}
private func deleteFile(_ url: URL) throws {
if FileManager.default.fileExists(atPath: url.path) {
try FileManager.default.removeItem(atPath: url.path)
}
}
@objc public func showMessageDialog(_ invoke: Invoke) throws {
let manager = self.manager
let args = try invoke.parseArgs(MessageDialogOptions.self)
@ -206,8 +236,6 @@ class DialogPlugin: Plugin {
UIAlertAction(
title: cancelButtonLabel, style: UIAlertAction.Style.default,
handler: { (_) -> Void in
Logger.error("cancel")
invoke.resolve([
"value": false,
"cancelled": false,
@ -221,8 +249,6 @@ class DialogPlugin: Plugin {
UIAlertAction(
title: okButtonLabel, style: UIAlertAction.Style.default,
handler: { (_) -> Void in
Logger.error("ok")
invoke.resolve([
"value": true,
"cancelled": false,

@ -95,35 +95,11 @@ public class FilePickerController: NSObject {
return nil
}
}
private func saveTemporaryFile(_ sourceUrl: URL) throws -> URL {
var directory = URL(fileURLWithPath: NSTemporaryDirectory())
if let cachesDirectory = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first {
directory = cachesDirectory
}
let targetUrl = directory.appendingPathComponent(sourceUrl.lastPathComponent)
do {
try deleteFile(targetUrl)
}
try FileManager.default.copyItem(at: sourceUrl, to: targetUrl)
return targetUrl
}
private func deleteFile(_ url: URL) throws {
if FileManager.default.fileExists(atPath: url.path) {
try FileManager.default.removeItem(atPath: url.path)
}
}
}
extension FilePickerController: UIDocumentPickerDelegate {
public func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
do {
let temporaryUrls = try urls.map { try saveTemporaryFile($0) }
self.plugin.onFilePickerEvent(.selected(temporaryUrls))
} catch {
self.plugin.onFilePickerEvent(.error("Failed to create a temporary copy of the file"))
}
self.plugin.onFilePickerEvent(.selected(urls))
}
public func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) {
@ -148,12 +124,7 @@ extension FilePickerController: UIImagePickerControllerDelegate, UINavigationCon
public func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {
dismissViewController(picker) {
if let url = info[.mediaURL] as? URL {
do {
let temporaryUrl = try self.saveTemporaryFile(url)
self.plugin.onFilePickerEvent(.selected([temporaryUrl]))
} catch {
self.plugin.onFilePickerEvent(.error("Failed to create a temporary copy of the file"))
}
self.plugin.onFilePickerEvent(.selected([url]))
} else {
self.plugin.onFilePickerEvent(.cancelled)
}
@ -169,7 +140,7 @@ extension FilePickerController: PHPickerViewControllerDelegate {
self.plugin.onFilePickerEvent(.cancelled)
return
}
var temporaryUrls: [URL] = []
var urls: [URL] = []
var errorMessage: String?
let dispatchGroup = DispatchGroup()
for result in results {
@ -190,12 +161,7 @@ extension FilePickerController: PHPickerViewControllerDelegate {
errorMessage = "Unknown error"
return
}
do {
let temporaryUrl = try self.saveTemporaryFile(url)
temporaryUrls.append(temporaryUrl)
} catch {
errorMessage = "Failed to create a temporary copy of the file"
}
urls.append(url)
})
} else if result.itemProvider.hasItemConformingToTypeIdentifier(UTType.image.identifier) {
dispatchGroup.enter()
@ -211,12 +177,7 @@ extension FilePickerController: PHPickerViewControllerDelegate {
errorMessage = "Unknown error"
return
}
do {
let temporaryUrl = try self.saveTemporaryFile(url)
temporaryUrls.append(temporaryUrl)
} catch {
errorMessage = "Failed to create a temporary copy of the file"
}
urls.append(url)
})
} else {
errorMessage = "Unsupported file type identifier"
@ -227,7 +188,7 @@ extension FilePickerController: PHPickerViewControllerDelegate {
self.plugin.onFilePickerEvent(.error(errorMessage))
return
}
self.plugin.onFilePickerEvent(.selected(temporaryUrls))
self.plugin.onFilePickerEvent(.selected(urls))
}
}
}
}

@ -440,6 +440,12 @@ impl<R: Runtime> FileDialogBuilder<R> {
/// Ok(())
/// });
/// ```
///
/// ## Platform-specific
///
/// - **iOS**: Returns a copy of the file to bypass [security scoped resource].
///
/// [security scoped resource]: https://developer.apple.com/documentation/foundation/nsurl/1417051-startaccessingsecurityscopedreso?language=objc
pub fn pick_file<F: FnOnce(Option<FilePath>) + Send + 'static>(self, f: F) {
pick_file(self, f)
}
@ -551,6 +557,12 @@ impl<R: Runtime> FileDialogBuilder<R> {
/// Ok(())
/// });
/// ```
///
/// ## Platform-specific
///
/// - **iOS**: Returns a [security scoped resource] so you must request access before reading or writing to the file.
///
/// [security scoped resource]: https://developer.apple.com/documentation/foundation/nsurl/1417051-startaccessingsecurityscopedreso?language=objc
pub fn save_file<F: FnOnce(Option<FilePath>) + Send + 'static>(self, f: F) {
save_file(self, f)
}
@ -573,6 +585,12 @@ impl<R: Runtime> FileDialogBuilder<R> {
/// // the file path is `None` if the user closed the dialog
/// }
/// ```
///
/// ## Platform-specific
///
/// - **iOS**: Returns a copy of the file to bypass [security scoped resource].
///
/// [security scoped resource]: https://developer.apple.com/documentation/foundation/nsurl/1417051-startaccessingsecurityscopedreso?language=objc
pub fn blocking_pick_file(self) -> Option<FilePath> {
blocking_fn!(self, pick_file)
}
@ -651,6 +669,12 @@ impl<R: Runtime> FileDialogBuilder<R> {
/// // the file path is `None` if the user closed the dialog
/// }
/// ```
///
/// ## Platform-specific
///
/// - **iOS**: Returns a [security scoped resource] so you must request access before reading or writing to the file.
///
/// [security scoped resource]: https://developer.apple.com/documentation/foundation/nsurl/1417051-startaccessingsecurityscopedreso?language=objc
pub fn blocking_save_file(self) -> Option<FilePath> {
blocking_fn!(self, save_file)
}

Loading…
Cancel
Save