diff --git a/plugins/dialog/build.rs b/plugins/dialog/build.rs index 4b3bb871..edda5385 100644 --- a/plugins/dialog/build.rs +++ b/plugins/dialog/build.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -const COMMANDS: &[&str] = &["open", "save", "message", "ask", "confirm"]; +const COMMANDS: &[&str] = &["open", "save", "stop_accessing_path", "message", "ask", "confirm"]; fn main() { let result = tauri_plugin::Builder::new(COMMANDS) diff --git a/plugins/dialog/guest-js/index.ts b/plugins/dialog/guest-js/index.ts index 7a30ca3c..b80de382 100644 --- a/plugins/dialog/guest-js/index.ts +++ b/plugins/dialog/guest-js/index.ts @@ -4,6 +4,31 @@ import { invoke } from '@tauri-apps/api/core' +class Path { + public path: string + constructor(path: string) { + this.path = path + } + + destroy() { + return invoke('plugin:dialog|stop-accessing-path', { path: this.path }) + } + + toPath() { + return this.path + } + + toString() { + return this.toPath() + } + + toJSON() { + return { + path: this.path + } + } +} + /** * Extension filters for the file dialog. * @@ -100,13 +125,7 @@ interface ConfirmDialogOptions { cancelLabel?: string } -type OpenDialogReturn = T['directory'] extends true - ? T['multiple'] extends true - ? string[] | null - : string | null - : T['multiple'] extends true - ? string[] | null - : string | null +type OpenDialogReturn = T['multiple'] extends true ? Path[] | null : Path | null /** * Open a file/directory selection dialog. @@ -171,7 +190,17 @@ async function open( Object.freeze(options) } - return await invoke('plugin:dialog|open', { options }) + const path = await invoke('plugin:dialog|open', { options }) + + if (Array.isArray(path)) { + return path.map((p) => new Path(p)) + } + + if (!path) { + return null + } + + return new Path(path) } /** @@ -202,12 +231,18 @@ async function open( * * @since 2.0.0 */ -async function save(options: SaveDialogOptions = {}): Promise { +async function save(options: SaveDialogOptions = {}): Promise { if (typeof options === 'object') { Object.freeze(options) } - return await invoke('plugin:dialog|save', { options }) + const path = await invoke('plugin:dialog|save', { options }) + + if (!path) { + return null + } + + return new Path(path) } /** diff --git a/plugins/dialog/ios/Sources/DialogPlugin.swift b/plugins/dialog/ios/Sources/DialogPlugin.swift index 3f4b558d..a992b644 100644 --- a/plugins/dialog/ios/Sources/DialogPlugin.swift +++ b/plugins/dialog/ios/Sources/DialogPlugin.swift @@ -38,6 +38,10 @@ struct SaveFileDialogOptions: Decodable { var defaultPath: String? } +struct StopAccessingPathOptions: Decodable { + var path: URL +} + class DialogPlugin: Plugin { var filePickerController: FilePickerController! @@ -74,14 +78,8 @@ class DialogPlugin: Plugin { onFilePickerResult = { (event: FilePickerEvent) -> Void in switch event { case .selected(let 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) - } + urls.forEach { $0.startAccessingSecurityScopedResource() } + invoke.resolve(["files": urls]) case .cancelled: invoke.resolve(["files": nil]) case .error(let error): @@ -179,6 +177,12 @@ class DialogPlugin: Plugin { } } + @objc public func stopAccessingPath(_ invoke: Invoke) throws { + let args = try invoke.parseArgs(StopAccessingPathOptions.self) + args.path.stopAccessingSecurityScopedResource() + invoke.resolve() + } + private func presentViewController(_ viewControllerToPresent: UIViewController) { self.manager.viewController?.present(viewControllerToPresent, animated: true, completion: nil) } @@ -203,27 +207,6 @@ 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) diff --git a/plugins/dialog/permissions/autogenerated/commands/stop_accessing_path.toml b/plugins/dialog/permissions/autogenerated/commands/stop_accessing_path.toml new file mode 100644 index 00000000..5f47a5e6 --- /dev/null +++ b/plugins/dialog/permissions/autogenerated/commands/stop_accessing_path.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-stop-accessing-path" +description = "Enables the stop_accessing_path command without any pre-configured scope." +commands.allow = ["stop_accessing_path"] + +[[permission]] +identifier = "deny-stop-accessing-path" +description = "Denies the stop_accessing_path command without any pre-configured scope." +commands.deny = ["stop_accessing_path"] diff --git a/plugins/dialog/permissions/autogenerated/reference.md b/plugins/dialog/permissions/autogenerated/reference.md index 3bbd265b..e427d1bf 100644 --- a/plugins/dialog/permissions/autogenerated/reference.md +++ b/plugins/dialog/permissions/autogenerated/reference.md @@ -14,6 +14,7 @@ All dialog types are enabled. - `allow-message` - `allow-save` - `allow-open` +- `allow-stop-accessing-path` ## Permission Table @@ -151,6 +152,32 @@ Enables the save command without any pre-configured scope. Denies the save command without any pre-configured scope. + + + + + + +`dialog:allow-stop-accessing-path` + + + + +Enables the stop_accessing_path command without any pre-configured scope. + + + + + + + +`dialog:deny-stop-accessing-path` + + + + +Denies the stop_accessing_path command without any pre-configured scope. + diff --git a/plugins/dialog/permissions/default.toml b/plugins/dialog/permissions/default.toml index cc936d90..a927a54a 100644 --- a/plugins/dialog/permissions/default.toml +++ b/plugins/dialog/permissions/default.toml @@ -17,4 +17,5 @@ permissions = [ "allow-message", "allow-save", "allow-open", + "allow-stop-accessing-path" ] diff --git a/plugins/dialog/permissions/schemas/schema.json b/plugins/dialog/permissions/schemas/schema.json index b47417ec..1026e020 100644 --- a/plugins/dialog/permissions/schemas/schema.json +++ b/plugins/dialog/permissions/schemas/schema.json @@ -355,10 +355,22 @@ "markdownDescription": "Denies the save command without any pre-configured scope." }, { - "description": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n\n#### This default permission set includes:\n\n- `allow-ask`\n- `allow-confirm`\n- `allow-message`\n- `allow-save`\n- `allow-open`", + "description": "Enables the stop_accessing_path command without any pre-configured scope.", + "type": "string", + "const": "allow-stop-accessing-path", + "markdownDescription": "Enables the stop_accessing_path command without any pre-configured scope." + }, + { + "description": "Denies the stop_accessing_path command without any pre-configured scope.", + "type": "string", + "const": "deny-stop-accessing-path", + "markdownDescription": "Denies the stop_accessing_path command without any pre-configured scope." + }, + { + "description": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n\n#### This default permission set includes:\n\n- `allow-ask`\n- `allow-confirm`\n- `allow-message`\n- `allow-save`\n- `allow-open`\n- `allow-stop-accessing-path`", "type": "string", "const": "default", - "markdownDescription": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n\n#### This default permission set includes:\n\n- `allow-ask`\n- `allow-confirm`\n- `allow-message`\n- `allow-save`\n- `allow-open`" + "markdownDescription": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n\n#### This default permission set includes:\n\n- `allow-ask`\n- `allow-confirm`\n- `allow-message`\n- `allow-save`\n- `allow-open`\n- `allow-stop-accessing-path`" } ] } diff --git a/plugins/dialog/src/commands.rs b/plugins/dialog/src/commands.rs index c3caf027..011f3c76 100644 --- a/plugins/dialog/src/commands.rs +++ b/plugins/dialog/src/commands.rs @@ -9,6 +9,7 @@ use tauri::{command, Manager, Runtime, State, Window}; use tauri_plugin_fs::FsExt; use crate::{ + StopAccessingPath, Dialog, FileDialogBuilder, FilePath, MessageDialogButtons, MessageDialogKind, Result, CANCEL, NO, OK, YES, }; @@ -241,6 +242,11 @@ pub(crate) async fn save( Ok(path.map(|p| p.simplified())) } +#[command] +pub fn stop_accessing_path(_p: StopAccessingPath) -> bool { + true +} + fn message_dialog( #[allow(unused_variables)] window: Window, dialog: State<'_, Dialog>, diff --git a/plugins/dialog/src/lib.rs b/plugins/dialog/src/lib.rs index d6c4d19f..dd92476d 100644 --- a/plugins/dialog/src/lib.rs +++ b/plugins/dialog/src/lib.rs @@ -9,7 +9,7 @@ html_favicon_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png" )] -use serde::Serialize; +use serde::{Deserialize, Serialize}; use tauri::{ plugin::{Builder, TauriPlugin}, Manager, Runtime, @@ -180,6 +180,7 @@ pub fn init() -> TauriPlugin { .invoke_handler(tauri::generate_handler![ commands::open, commands::save, + commands::stop_accessing_path, commands::message, commands::ask, commands::confirm @@ -337,6 +338,11 @@ pub(crate) struct FileDialogPayload<'a> { multiple: bool, } +#[derive(Debug, Serialize, Deserialize)] +pub struct StopAccessingPath { + path: FilePath, +} + // raw window handle :( unsafe impl Send for FileDialogBuilder {} @@ -566,6 +572,10 @@ impl FileDialogBuilder { pub fn save_file) + Send + 'static>(self, f: F) { save_file(self, f) } + + pub fn stop_accessing_path(self, p: StopAccessingPath) -> bool { + stop_accessing_path(self, p) + } } /// Blocking APIs. @@ -678,4 +688,8 @@ impl FileDialogBuilder { pub fn blocking_save_file(self) -> Option { blocking_fn!(self, save_file) } + + pub fn blocking_stop_accessing_path(self, p: StopAccessingPath) -> bool { + self.stop_accessing_path(p) + } } diff --git a/plugins/dialog/src/mobile.rs b/plugins/dialog/src/mobile.rs index b73def4f..fb434b27 100644 --- a/plugins/dialog/src/mobile.rs +++ b/plugins/dialog/src/mobile.rs @@ -8,7 +8,7 @@ use tauri::{ AppHandle, Runtime, }; -use crate::{FileDialogBuilder, FilePath, MessageDialogBuilder}; +use crate::{FileDialogBuilder, FilePath, MessageDialogBuilder, StopAccessingPath}; #[cfg(target_os = "android")] const PLUGIN_IDENTIFIER: &str = "app.tauri.dialog"; @@ -105,6 +105,23 @@ pub fn save_file) + Send + 'static>( }); } +#[allow(unused_variables)] +pub fn stop_accessing_path(dialog: FileDialogBuilder, p: StopAccessingPath) -> bool { + #[cfg(target_os = "ios")] + { + let res = dialog + .dialog + .0 + .run_mobile_plugin::<()>("stopAccessingPath", p); + + if let Err(_) = res { + return false + } + } + + true +} + #[derive(Debug, Deserialize)] struct ShowMessageDialogResponse { #[allow(dead_code)]