Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: v2
Choose a base branch
from
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
8 changes: 8 additions & 0 deletions plugins/dialog/guest-js/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
37 changes: 32 additions & 5 deletions plugins/dialog/ios/Sources/DialogPlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Expand Down Expand Up @@ -149,10 +157,12 @@ class DialogPlugin: Plugin {
onFilePickerResult = { (event: FilePickerEvent) -> Void in
switch event {
case .selected(let urls):
urls.first?.startAccessingSecurityScopedResource()
invoke.resolve(["file": urls.first!])
case .cancelled:
invoke.resolve(["file": nil])
case .error(let error):
Logger.error("failed to pick file to save: \(error)")
invoke.reject(error)
}
}
Expand Down Expand Up @@ -192,6 +202,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)
Expand All @@ -206,8 +237,6 @@ class DialogPlugin: Plugin {
UIAlertAction(
title: cancelButtonLabel, style: UIAlertAction.Style.default,
handler: { (_) -> Void in
Logger.error("cancel")

invoke.resolve([
"value": false,
"cancelled": false,
Expand All @@ -221,8 +250,6 @@ class DialogPlugin: Plugin {
UIAlertAction(
title: okButtonLabel, style: UIAlertAction.Style.default,
handler: { (_) -> Void in
Logger.error("ok")

invoke.resolve([
"value": true,
"cancelled": false,
Expand Down
53 changes: 7 additions & 46 deletions plugins/dialog/ios/Sources/FilePickerController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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)
}
Expand All @@ -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 {
Expand All @@ -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()
Expand All @@ -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"
Expand All @@ -227,7 +188,7 @@ extension FilePickerController: PHPickerViewControllerDelegate {
self.plugin.onFilePickerEvent(.error(errorMessage))
return
}
self.plugin.onFilePickerEvent(.selected(temporaryUrls))
self.plugin.onFilePickerEvent(.selected(urls))
}
}
}
}
24 changes: 24 additions & 0 deletions plugins/dialog/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down Expand Up @@ -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)
}
Expand All @@ -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)
}
Expand Down Expand Up @@ -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)
}
Expand Down
Loading