mirror of https://github.com/tauri-apps/tauri
parent
64e0054299
commit
b744cd2758
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"tauri": patch
|
||||
---
|
||||
|
||||
Allow absolute paths on the filesystem APIs as long as it does not include parent directory components.
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"tauri": patch
|
||||
---
|
||||
|
||||
Extend the allowed patterns for the filesystem and asset protocol when the user selects a path (dialog open and save commands and file drop on the window).
|
|
@ -3,9 +3,9 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use super::{InvokeContext, InvokeResponse};
|
||||
#[cfg(any(dialog_open, dialog_save))]
|
||||
use crate::api::dialog::blocking::FileDialogBuilder;
|
||||
use crate::Runtime;
|
||||
#[cfg(any(dialog_open, dialog_save))]
|
||||
use crate::{api::dialog::blocking::FileDialogBuilder, Manager, Scopes};
|
||||
use serde::Deserialize;
|
||||
use tauri_macros::{module_command_handler, CommandModule};
|
||||
|
||||
|
@ -36,6 +36,10 @@ pub struct OpenDialogOptions {
|
|||
pub directory: bool,
|
||||
/// The initial path of the dialog.
|
||||
pub default_path: Option<PathBuf>,
|
||||
/// If [`Self::directory`] is true, indicates that it will be read recursively later.
|
||||
/// Defines whether subdirectories will be allowed on the scope or not.
|
||||
#[serde(default)]
|
||||
pub recursive: bool,
|
||||
}
|
||||
|
||||
/// The options for the save dialog API.
|
||||
|
@ -97,12 +101,28 @@ impl Cmd {
|
|||
dialog_builder = dialog_builder.add_filter(filter.name, &extensions);
|
||||
}
|
||||
|
||||
let scopes = context.window.state::<Scopes>();
|
||||
|
||||
let res = if options.directory {
|
||||
dialog_builder.pick_folder().into()
|
||||
let folder = dialog_builder.pick_folder();
|
||||
if let Some(path) = &folder {
|
||||
scopes.allow_directory(path, options.recursive);
|
||||
}
|
||||
folder.into()
|
||||
} else if options.multiple {
|
||||
dialog_builder.pick_files().into()
|
||||
let files = dialog_builder.pick_files();
|
||||
if let Some(files) = &files {
|
||||
for file in files {
|
||||
scopes.allow_file(file);
|
||||
}
|
||||
}
|
||||
files.into()
|
||||
} else {
|
||||
dialog_builder.pick_file().into()
|
||||
let file = dialog_builder.pick_file();
|
||||
if let Some(file) = &file {
|
||||
scopes.allow_file(file);
|
||||
}
|
||||
file.into()
|
||||
};
|
||||
|
||||
Ok(res)
|
||||
|
@ -127,7 +147,14 @@ impl Cmd {
|
|||
dialog_builder = dialog_builder.add_filter(filter.name, &extensions);
|
||||
}
|
||||
|
||||
Ok(dialog_builder.save_file())
|
||||
let scopes = context.window.state::<Scopes>();
|
||||
|
||||
let path = dialog_builder.save_file();
|
||||
if let Some(p) = &path {
|
||||
scopes.allow_file(p);
|
||||
}
|
||||
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
#[module_command_handler(dialog_message, "dialog > message")]
|
||||
|
@ -198,6 +225,7 @@ mod tests {
|
|||
directory: bool::arbitrary(g),
|
||||
default_path: Option::arbitrary(g),
|
||||
title: Option::arbitrary(g),
|
||||
recursive: bool::arbitrary(g),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -41,12 +41,7 @@ impl<'de> Deserialize<'de> for SafePathBuf {
|
|||
D: Deserializer<'de>,
|
||||
{
|
||||
let path = std::path::PathBuf::deserialize(deserializer)?;
|
||||
if path.components().any(|x| {
|
||||
matches!(
|
||||
x,
|
||||
Component::ParentDir | Component::RootDir | Component::Prefix(_)
|
||||
)
|
||||
}) {
|
||||
if path.components().any(|x| matches!(x, Component::ParentDir)) {
|
||||
Err(DeError::custom("cannot traverse directory"))
|
||||
} else {
|
||||
Ok(SafePathBuf(path))
|
||||
|
|
|
@ -47,7 +47,7 @@ use crate::{
|
|||
config::{AppUrl, Config, WindowUrl},
|
||||
PackageInfo,
|
||||
},
|
||||
Context, Invoke, Pattern, StateManager, Window,
|
||||
Context, Invoke, Manager, Pattern, Scopes, StateManager, Window,
|
||||
};
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "windows"))]
|
||||
|
@ -828,7 +828,17 @@ impl<R: Runtime> WindowManager<R> {
|
|||
let window = Window::new(manager.clone(), window, app_handle.clone());
|
||||
let _ = match event {
|
||||
FileDropEvent::Hovered(paths) => window.emit_and_trigger("tauri://file-drop-hover", paths),
|
||||
FileDropEvent::Dropped(paths) => window.emit_and_trigger("tauri://file-drop", paths),
|
||||
FileDropEvent::Dropped(paths) => {
|
||||
let scopes = window.state::<Scopes>();
|
||||
for path in &paths {
|
||||
if path.is_file() {
|
||||
scopes.allow_file(path);
|
||||
} else {
|
||||
scopes.allow_directory(path, false);
|
||||
}
|
||||
}
|
||||
window.emit_and_trigger("tauri://file-drop", paths)
|
||||
}
|
||||
FileDropEvent::Cancelled => window.emit_and_trigger("tauri://file-drop-cancelled", ()),
|
||||
_ => unimplemented!(),
|
||||
};
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
use std::{
|
||||
fmt,
|
||||
path::{Path, PathBuf},
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
use glob::Pattern;
|
||||
|
@ -18,7 +19,7 @@ use crate::api::path::parse as parse_path;
|
|||
/// Scope for filesystem access.
|
||||
#[derive(Clone)]
|
||||
pub struct Scope {
|
||||
allow_patterns: Vec<Pattern>,
|
||||
allow_patterns: Arc<Mutex<Vec<Pattern>>>,
|
||||
}
|
||||
|
||||
impl fmt::Debug for Scope {
|
||||
|
@ -28,6 +29,8 @@ impl fmt::Debug for Scope {
|
|||
"allow_patterns",
|
||||
&self
|
||||
.allow_patterns
|
||||
.lock()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|p| p.as_str())
|
||||
.collect::<Vec<&str>>(),
|
||||
|
@ -36,6 +39,16 @@ impl fmt::Debug for Scope {
|
|||
}
|
||||
}
|
||||
|
||||
fn push_pattern<P: AsRef<Path>>(list: &mut Vec<Pattern>, pattern: P) {
|
||||
let pattern: PathBuf = pattern.as_ref().components().collect();
|
||||
list.push(Pattern::new(&pattern.to_string_lossy()).expect("invalid glob pattern"));
|
||||
#[cfg(windows)]
|
||||
{
|
||||
list
|
||||
.push(Pattern::new(&format!("\\\\?\\{}", pattern.display())).expect("invalid glob pattern"));
|
||||
}
|
||||
}
|
||||
|
||||
impl Scope {
|
||||
/// Creates a new scope from a `FsAllowlistScope` configuration.
|
||||
pub fn for_fs_api(
|
||||
|
@ -47,17 +60,33 @@ impl Scope {
|
|||
let mut allow_patterns = Vec::new();
|
||||
for path in &scope.0 {
|
||||
if let Ok(path) = parse_path(config, package_info, env, path) {
|
||||
let path: PathBuf = path.components().collect();
|
||||
allow_patterns.push(Pattern::new(&path.to_string_lossy()).expect("invalid glob pattern"));
|
||||
#[cfg(windows)]
|
||||
{
|
||||
allow_patterns.push(
|
||||
Pattern::new(&format!("\\\\?\\{}", path.display())).expect("invalid glob pattern"),
|
||||
);
|
||||
}
|
||||
push_pattern(&mut allow_patterns, path);
|
||||
}
|
||||
}
|
||||
Self { allow_patterns }
|
||||
Self {
|
||||
allow_patterns: Arc::new(Mutex::new(allow_patterns)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Extend the allowed patterns with the given directory.
|
||||
///
|
||||
/// After this function has been called, the frontend will be able to use the Tauri API to read
|
||||
/// the directory and all of its files and subdirectories.
|
||||
pub fn allow_directory<P: AsRef<Path>>(&self, path: P, recursive: bool) {
|
||||
let path = path.as_ref().to_path_buf();
|
||||
let mut list = self.allow_patterns.lock().unwrap();
|
||||
|
||||
// allow the directory to be read
|
||||
push_pattern(&mut list, &path);
|
||||
// allow its files and subdirectories to be read
|
||||
push_pattern(&mut list, path.join(if recursive { "**" } else { "*" }));
|
||||
}
|
||||
|
||||
/// Extend the allowed patterns with the given file path.
|
||||
///
|
||||
/// After this function has been called, the frontend will be able to use the Tauri API to read the contents of this file.
|
||||
pub fn allow_file<P: AsRef<Path>>(&self, path: P) {
|
||||
push_pattern(&mut self.allow_patterns.lock().unwrap(), path);
|
||||
}
|
||||
|
||||
/// Determines if the given path is allowed on this scope.
|
||||
|
@ -71,7 +100,12 @@ impl Scope {
|
|||
|
||||
if let Ok(path) = path {
|
||||
let path: PathBuf = path.components().collect();
|
||||
let allowed = self.allow_patterns.iter().any(|p| p.matches_path(&path));
|
||||
let allowed = self
|
||||
.allow_patterns
|
||||
.lock()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|p| p.matches_path(&path));
|
||||
allowed
|
||||
} else {
|
||||
false
|
||||
|
|
|
@ -15,6 +15,7 @@ pub use shell::{
|
|||
ScopeAllowedCommand as ShellScopeAllowedCommand, ScopeConfig as ShellScopeConfig,
|
||||
ScopeError as ShellScopeError,
|
||||
};
|
||||
use std::path::Path;
|
||||
|
||||
pub(crate) struct Scopes {
|
||||
pub fs: FsScope,
|
||||
|
@ -25,3 +26,19 @@ pub(crate) struct Scopes {
|
|||
#[cfg(shell_scope)]
|
||||
pub shell: ShellScope,
|
||||
}
|
||||
|
||||
impl Scopes {
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn allow_directory(&self, path: &Path, recursive: bool) {
|
||||
self.fs.allow_directory(path, recursive);
|
||||
#[cfg(protocol_asset)]
|
||||
self.asset_protocol.allow_directory(path, recursive);
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn allow_file(&self, path: &Path) {
|
||||
self.fs.allow_file(path);
|
||||
#[cfg(protocol_asset)]
|
||||
self.asset_protocol.allow_file(path);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -53,6 +53,11 @@ interface OpenDialogOptions {
|
|||
multiple?: boolean
|
||||
/** Whether the dialog is a directory selection or not. */
|
||||
directory?: boolean
|
||||
/**
|
||||
* If `directory` is true, indicates that it will be read recursively later.
|
||||
* Defines whether subdirectories will be allowed on the scope or not.
|
||||
*/
|
||||
recursive?: boolean
|
||||
}
|
||||
|
||||
/** Options for the save dialog. */
|
||||
|
@ -70,7 +75,14 @@ interface SaveDialogOptions {
|
|||
}
|
||||
|
||||
/**
|
||||
* Open a file/directory selection dialog
|
||||
* Open a file/directory selection dialog.
|
||||
*
|
||||
* The selected paths are added to the filesystem and asset protocol allowlist scopes.
|
||||
* When security is more important than the easy of use of this API,
|
||||
* prefer writing a dedicated command instead.
|
||||
*
|
||||
* Note that the allowlist scope change is not persisted, so the values are cleared when the application is restarted.
|
||||
* You can save it to the filesystem using [tauri-plugin-persisted-scope](https://github.com/tauri-apps/tauri-plugin-persisted-scope).
|
||||
*
|
||||
* @returns A promise resolving to the selected path(s)
|
||||
*/
|
||||
|
@ -93,6 +105,13 @@ async function open(
|
|||
/**
|
||||
* Open a file/directory save dialog.
|
||||
*
|
||||
* The selected path is added to the filesystem and asset protocol allowlist scopes.
|
||||
* When security is more important than the easy of use of this API,
|
||||
* prefer writing a dedicated command instead.
|
||||
*
|
||||
* Note that the allowlist scope change is not persisted, so the values are cleared when the application is restarted.
|
||||
* You can save it to the filesystem using [tauri-plugin-persisted-scope](https://github.com/tauri-apps/tauri-plugin-persisted-scope).
|
||||
*
|
||||
* @returns A promise resolving to the selected path.
|
||||
*/
|
||||
async function save(options: SaveDialogOptions = {}): Promise<string> {
|
||||
|
|
|
@ -879,12 +879,12 @@ class WindowManager extends WebviewWindowHandle {
|
|||
type: 'setMinSize',
|
||||
payload: size
|
||||
? {
|
||||
type: size.type,
|
||||
data: {
|
||||
width: size.width,
|
||||
height: size.height
|
||||
type: size.type,
|
||||
data: {
|
||||
width: size.width,
|
||||
height: size.height
|
||||
}
|
||||
}
|
||||
}
|
||||
: null
|
||||
}
|
||||
}
|
||||
|
@ -921,12 +921,12 @@ class WindowManager extends WebviewWindowHandle {
|
|||
type: 'setMaxSize',
|
||||
payload: size
|
||||
? {
|
||||
type: size.type,
|
||||
data: {
|
||||
width: size.width,
|
||||
height: size.height
|
||||
type: size.type,
|
||||
data: {
|
||||
width: size.width,
|
||||
height: size.height
|
||||
}
|
||||
}
|
||||
}
|
||||
: null
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue