pull/2548/merge
nashaofu 1 day ago committed by GitHub
commit 633518c259
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -2,7 +2,14 @@
// 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)

@ -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 extends OpenDialogOptions> = T['directory'] extends true
? T['multiple'] extends true
? string[] | null
: string | null
: T['multiple'] extends true
? string[] | null
: string | null
type OpenDialogReturn<T extends OpenDialogOptions> = T['multiple'] extends true ? Path[] | null : Path | null
/**
* Open a file/directory selection dialog.
@ -156,6 +175,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
@ -167,7 +190,17 @@ async function open<T extends OpenDialogOptions>(
Object.freeze(options)
}
return await invoke('plugin:dialog|open', { options })
const path = await invoke<string[] | string | null>('plugin:dialog|open', { options })
if (Array.isArray(path)) {
return path.map((p) => new Path(p))
}
if (!path) {
return null
}
return new Path(path)
}
/**
@ -190,16 +223,26 @@ 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
*/
async function save(options: SaveDialogOptions = {}): Promise<string | null> {
async function save(options: SaveDialogOptions = {}): Promise<Path | null> {
if (typeof options === 'object') {
Object.freeze(options)
}
return await invoke('plugin:dialog|save', { options })
const path = await invoke<string | null>('plugin:dialog|save', { options })
if (!path) {
return null
}
return new Path(path)
}
/**

@ -38,6 +38,10 @@ struct SaveFileDialogOptions: Decodable {
var defaultPath: String?
}
struct StopAccessingPathOptions: Decodable {
var path: URL
}
class DialogPlugin: Plugin {
var filePickerController: FilePickerController!
@ -74,10 +78,12 @@ class DialogPlugin: Plugin {
onFilePickerResult = { (event: FilePickerEvent) -> Void in
switch event {
case .selected(let urls):
urls.forEach { $0.startAccessingSecurityScopedResource() }
invoke.resolve(["files": urls])
case .cancelled:
invoke.resolve(["files": nil])
case .error(let error):
Logger.error("failed to pick file: \(error)")
invoke.reject(error)
}
}
@ -149,10 +155,13 @@ class DialogPlugin: Plugin {
onFilePickerResult = { (event: FilePickerEvent) -> Void in
switch event {
case .selected(let urls):
Logger.info("picked file to save: \(urls.first!)")
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)
}
}
@ -168,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)
}
@ -206,8 +221,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 +234,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))
}
}
}

@ -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"]

@ -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.
</td>
</tr>
<tr>
<td>
`dialog:allow-stop-accessing-path`
</td>
<td>
Enables the stop_accessing_path command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`dialog:deny-stop-accessing-path`
</td>
<td>
Denies the stop_accessing_path command without any pre-configured scope.
</td>
</tr>
</table>

@ -17,4 +17,5 @@ permissions = [
"allow-message",
"allow-save",
"allow-open",
"allow-stop-accessing-path"
]

@ -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`"
}
]
}

@ -9,8 +9,8 @@ use tauri::{command, Manager, Runtime, State, Window};
use tauri_plugin_fs::FsExt;
use crate::{
Dialog, FileDialogBuilder, FilePath, MessageDialogButtons, MessageDialogKind, Result, CANCEL,
NO, OK, YES,
Dialog, FileDialogBuilder, FilePath, MessageDialogButtons, MessageDialogKind, Result,
StopAccessingPath, CANCEL, NO, OK, YES,
};
#[derive(Serialize)]
@ -241,6 +241,11 @@ pub(crate) async fn save<R: Runtime>(
Ok(path.map(|p| p.simplified()))
}
#[command]
pub fn stop_accessing_path(_p: StopAccessingPath) -> bool {
true
}
fn message_dialog<R: Runtime>(
#[allow(unused_variables)] window: Window<R>,
dialog: State<'_, Dialog<R>>,

@ -13,7 +13,7 @@ use rfd::{AsyncFileDialog, AsyncMessageDialog};
use serde::de::DeserializeOwned;
use tauri::{plugin::PluginApi, AppHandle, Runtime};
use crate::{models::*, FileDialogBuilder, FilePath, MessageDialogBuilder, OK};
use crate::{models::*, FileDialogBuilder, FilePath, MessageDialogBuilder, StopAccessingPath, OK};
pub fn init<R: Runtime, C: DeserializeOwned>(
app: &AppHandle<R>,
@ -207,6 +207,13 @@ pub fn save_file<R: Runtime, F: FnOnce(Option<FilePath>) + Send + 'static>(
});
}
pub fn stop_accessing_path<R: Runtime>(
_dialog: FileDialogBuilder<R>,
_p: StopAccessingPath,
) -> bool {
true
}
/// Shows a message dialog
pub fn show_message_dialog<R: Runtime, F: FnOnce(bool) + Send + 'static>(
dialog: MessageDialogBuilder<R>,

@ -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<R: Runtime>() -> TauriPlugin<R> {
.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<R: Runtime> Send for FileDialogBuilder<R> {}
@ -440,6 +446,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,9 +563,19 @@ 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)
}
pub fn stop_accessing_path(self, p: StopAccessingPath) -> bool {
stop_accessing_path(self, p)
}
}
/// Blocking APIs.
@ -573,6 +595,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,7 +679,17 @@ 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)
}
pub fn blocking_stop_accessing_path(self, p: StopAccessingPath) -> bool {
self.stop_accessing_path(p)
}
}

@ -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<R: Runtime, F: FnOnce(Option<FilePath>) + Send + 'static>(
});
}
#[allow(unused_variables)]
pub fn stop_accessing_path<R: Runtime>(dialog: FileDialogBuilder<R>, p: StopAccessingPath) -> bool {
#[cfg(target_os = "ios")]
{
let res = dialog
.dialog
.0
.run_mobile_plugin::<()>("stopAccessingPath", p);
if res.is_err() {
return false;
}
}
true
}
#[derive(Debug, Deserialize)]
struct ShowMessageDialogResponse {
#[allow(dead_code)]

Loading…
Cancel
Save