Skip to content

Commit b744cd2

Browse files
authored
feat: extend scopes with user selected paths, closes #3591 (#3595)
1 parent 64e0054 commit b744cd2

File tree

9 files changed

+149
-36
lines changed

9 files changed

+149
-36
lines changed

.changes/fs-absolute-paths.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"tauri": patch
3+
---
4+
5+
Allow absolute paths on the filesystem APIs as long as it does not include parent directory components.

.changes/fs-scope-runtime.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"tauri": patch
3+
---
4+
5+
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).

core/tauri/src/endpoints/dialog.rs

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
// SPDX-License-Identifier: MIT
44

55
use super::{InvokeContext, InvokeResponse};
6-
#[cfg(any(dialog_open, dialog_save))]
7-
use crate::api::dialog::blocking::FileDialogBuilder;
86
use crate::Runtime;
7+
#[cfg(any(dialog_open, dialog_save))]
8+
use crate::{api::dialog::blocking::FileDialogBuilder, Manager, Scopes};
99
use serde::Deserialize;
1010
use tauri_macros::{module_command_handler, CommandModule};
1111

@@ -36,6 +36,10 @@ pub struct OpenDialogOptions {
3636
pub directory: bool,
3737
/// The initial path of the dialog.
3838
pub default_path: Option<PathBuf>,
39+
/// If [`Self::directory`] is true, indicates that it will be read recursively later.
40+
/// Defines whether subdirectories will be allowed on the scope or not.
41+
#[serde(default)]
42+
pub recursive: bool,
3943
}
4044

4145
/// The options for the save dialog API.
@@ -97,12 +101,28 @@ impl Cmd {
97101
dialog_builder = dialog_builder.add_filter(filter.name, &extensions);
98102
}
99103

104+
let scopes = context.window.state::<Scopes>();
105+
100106
let res = if options.directory {
101-
dialog_builder.pick_folder().into()
107+
let folder = dialog_builder.pick_folder();
108+
if let Some(path) = &folder {
109+
scopes.allow_directory(path, options.recursive);
110+
}
111+
folder.into()
102112
} else if options.multiple {
103-
dialog_builder.pick_files().into()
113+
let files = dialog_builder.pick_files();
114+
if let Some(files) = &files {
115+
for file in files {
116+
scopes.allow_file(file);
117+
}
118+
}
119+
files.into()
104120
} else {
105-
dialog_builder.pick_file().into()
121+
let file = dialog_builder.pick_file();
122+
if let Some(file) = &file {
123+
scopes.allow_file(file);
124+
}
125+
file.into()
106126
};
107127

108128
Ok(res)
@@ -127,7 +147,14 @@ impl Cmd {
127147
dialog_builder = dialog_builder.add_filter(filter.name, &extensions);
128148
}
129149

130-
Ok(dialog_builder.save_file())
150+
let scopes = context.window.state::<Scopes>();
151+
152+
let path = dialog_builder.save_file();
153+
if let Some(p) = &path {
154+
scopes.allow_file(p);
155+
}
156+
157+
Ok(path)
131158
}
132159

133160
#[module_command_handler(dialog_message, "dialog > message")]
@@ -198,6 +225,7 @@ mod tests {
198225
directory: bool::arbitrary(g),
199226
default_path: Option::arbitrary(g),
200227
title: Option::arbitrary(g),
228+
recursive: bool::arbitrary(g),
201229
}
202230
}
203231
}

core/tauri/src/endpoints/file_system.rs

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,7 @@ impl<'de> Deserialize<'de> for SafePathBuf {
4141
D: Deserializer<'de>,
4242
{
4343
let path = std::path::PathBuf::deserialize(deserializer)?;
44-
if path.components().any(|x| {
45-
matches!(
46-
x,
47-
Component::ParentDir | Component::RootDir | Component::Prefix(_)
48-
)
49-
}) {
44+
if path.components().any(|x| matches!(x, Component::ParentDir)) {
5045
Err(DeError::custom("cannot traverse directory"))
5146
} else {
5247
Ok(SafePathBuf(path))

core/tauri/src/manager.rs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ use crate::{
4747
config::{AppUrl, Config, WindowUrl},
4848
PackageInfo,
4949
},
50-
Context, Invoke, Pattern, StateManager, Window,
50+
Context, Invoke, Manager, Pattern, Scopes, StateManager, Window,
5151
};
5252

5353
#[cfg(any(target_os = "linux", target_os = "windows"))]
@@ -828,7 +828,17 @@ impl<R: Runtime> WindowManager<R> {
828828
let window = Window::new(manager.clone(), window, app_handle.clone());
829829
let _ = match event {
830830
FileDropEvent::Hovered(paths) => window.emit_and_trigger("tauri://file-drop-hover", paths),
831-
FileDropEvent::Dropped(paths) => window.emit_and_trigger("tauri://file-drop", paths),
831+
FileDropEvent::Dropped(paths) => {
832+
let scopes = window.state::<Scopes>();
833+
for path in &paths {
834+
if path.is_file() {
835+
scopes.allow_file(path);
836+
} else {
837+
scopes.allow_directory(path, false);
838+
}
839+
}
840+
window.emit_and_trigger("tauri://file-drop", paths)
841+
}
832842
FileDropEvent::Cancelled => window.emit_and_trigger("tauri://file-drop-cancelled", ()),
833843
_ => unimplemented!(),
834844
};

core/tauri/src/scope/fs.rs

Lines changed: 45 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use std::{
66
fmt,
77
path::{Path, PathBuf},
8+
sync::{Arc, Mutex},
89
};
910

1011
use glob::Pattern;
@@ -18,7 +19,7 @@ use crate::api::path::parse as parse_path;
1819
/// Scope for filesystem access.
1920
#[derive(Clone)]
2021
pub struct Scope {
21-
allow_patterns: Vec<Pattern>,
22+
allow_patterns: Arc<Mutex<Vec<Pattern>>>,
2223
}
2324

2425
impl fmt::Debug for Scope {
@@ -28,6 +29,8 @@ impl fmt::Debug for Scope {
2829
"allow_patterns",
2930
&self
3031
.allow_patterns
32+
.lock()
33+
.unwrap()
3134
.iter()
3235
.map(|p| p.as_str())
3336
.collect::<Vec<&str>>(),
@@ -36,6 +39,16 @@ impl fmt::Debug for Scope {
3639
}
3740
}
3841

42+
fn push_pattern<P: AsRef<Path>>(list: &mut Vec<Pattern>, pattern: P) {
43+
let pattern: PathBuf = pattern.as_ref().components().collect();
44+
list.push(Pattern::new(&pattern.to_string_lossy()).expect("invalid glob pattern"));
45+
#[cfg(windows)]
46+
{
47+
list
48+
.push(Pattern::new(&format!("\\\\?\\{}", pattern.display())).expect("invalid glob pattern"));
49+
}
50+
}
51+
3952
impl Scope {
4053
/// Creates a new scope from a `FsAllowlistScope` configuration.
4154
pub fn for_fs_api(
@@ -47,17 +60,33 @@ impl Scope {
4760
let mut allow_patterns = Vec::new();
4861
for path in &scope.0 {
4962
if let Ok(path) = parse_path(config, package_info, env, path) {
50-
let path: PathBuf = path.components().collect();
51-
allow_patterns.push(Pattern::new(&path.to_string_lossy()).expect("invalid glob pattern"));
52-
#[cfg(windows)]
53-
{
54-
allow_patterns.push(
55-
Pattern::new(&format!("\\\\?\\{}", path.display())).expect("invalid glob pattern"),
56-
);
57-
}
63+
push_pattern(&mut allow_patterns, path);
5864
}
5965
}
60-
Self { allow_patterns }
66+
Self {
67+
allow_patterns: Arc::new(Mutex::new(allow_patterns)),
68+
}
69+
}
70+
71+
/// Extend the allowed patterns with the given directory.
72+
///
73+
/// After this function has been called, the frontend will be able to use the Tauri API to read
74+
/// the directory and all of its files and subdirectories.
75+
pub fn allow_directory<P: AsRef<Path>>(&self, path: P, recursive: bool) {
76+
let path = path.as_ref().to_path_buf();
77+
let mut list = self.allow_patterns.lock().unwrap();
78+
79+
// allow the directory to be read
80+
push_pattern(&mut list, &path);
81+
// allow its files and subdirectories to be read
82+
push_pattern(&mut list, path.join(if recursive { "**" } else { "*" }));
83+
}
84+
85+
/// Extend the allowed patterns with the given file path.
86+
///
87+
/// After this function has been called, the frontend will be able to use the Tauri API to read the contents of this file.
88+
pub fn allow_file<P: AsRef<Path>>(&self, path: P) {
89+
push_pattern(&mut self.allow_patterns.lock().unwrap(), path);
6190
}
6291

6392
/// Determines if the given path is allowed on this scope.
@@ -71,7 +100,12 @@ impl Scope {
71100

72101
if let Ok(path) = path {
73102
let path: PathBuf = path.components().collect();
74-
let allowed = self.allow_patterns.iter().any(|p| p.matches_path(&path));
103+
let allowed = self
104+
.allow_patterns
105+
.lock()
106+
.unwrap()
107+
.iter()
108+
.any(|p| p.matches_path(&path));
75109
allowed
76110
} else {
77111
false

core/tauri/src/scope/mod.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ pub use shell::{
1515
ScopeAllowedCommand as ShellScopeAllowedCommand, ScopeConfig as ShellScopeConfig,
1616
ScopeError as ShellScopeError,
1717
};
18+
use std::path::Path;
1819

1920
pub(crate) struct Scopes {
2021
pub fs: FsScope,
@@ -25,3 +26,19 @@ pub(crate) struct Scopes {
2526
#[cfg(shell_scope)]
2627
pub shell: ShellScope,
2728
}
29+
30+
impl Scopes {
31+
#[allow(dead_code)]
32+
pub(crate) fn allow_directory(&self, path: &Path, recursive: bool) {
33+
self.fs.allow_directory(path, recursive);
34+
#[cfg(protocol_asset)]
35+
self.asset_protocol.allow_directory(path, recursive);
36+
}
37+
38+
#[allow(dead_code)]
39+
pub(crate) fn allow_file(&self, path: &Path) {
40+
self.fs.allow_file(path);
41+
#[cfg(protocol_asset)]
42+
self.asset_protocol.allow_file(path);
43+
}
44+
}

tooling/api/src/dialog.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,11 @@ interface OpenDialogOptions {
5353
multiple?: boolean
5454
/** Whether the dialog is a directory selection or not. */
5555
directory?: boolean
56+
/**
57+
* If `directory` is true, indicates that it will be read recursively later.
58+
* Defines whether subdirectories will be allowed on the scope or not.
59+
*/
60+
recursive?: boolean
5661
}
5762

5863
/** Options for the save dialog. */
@@ -70,7 +75,14 @@ interface SaveDialogOptions {
7075
}
7176

7277
/**
73-
* Open a file/directory selection dialog
78+
* Open a file/directory selection dialog.
79+
*
80+
* The selected paths are added to the filesystem and asset protocol allowlist scopes.
81+
* When security is more important than the easy of use of this API,
82+
* prefer writing a dedicated command instead.
83+
*
84+
* Note that the allowlist scope change is not persisted, so the values are cleared when the application is restarted.
85+
* You can save it to the filesystem using [tauri-plugin-persisted-scope](https://github.com/tauri-apps/tauri-plugin-persisted-scope).
7486
*
7587
* @returns A promise resolving to the selected path(s)
7688
*/
@@ -93,6 +105,13 @@ async function open(
93105
/**
94106
* Open a file/directory save dialog.
95107
*
108+
* The selected path is added to the filesystem and asset protocol allowlist scopes.
109+
* When security is more important than the easy of use of this API,
110+
* prefer writing a dedicated command instead.
111+
*
112+
* Note that the allowlist scope change is not persisted, so the values are cleared when the application is restarted.
113+
* You can save it to the filesystem using [tauri-plugin-persisted-scope](https://github.com/tauri-apps/tauri-plugin-persisted-scope).
114+
*
96115
* @returns A promise resolving to the selected path.
97116
*/
98117
async function save(options: SaveDialogOptions = {}): Promise<string> {

tooling/api/src/window.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -879,12 +879,12 @@ class WindowManager extends WebviewWindowHandle {
879879
type: 'setMinSize',
880880
payload: size
881881
? {
882-
type: size.type,
883-
data: {
884-
width: size.width,
885-
height: size.height
882+
type: size.type,
883+
data: {
884+
width: size.width,
885+
height: size.height
886+
}
886887
}
887-
}
888888
: null
889889
}
890890
}
@@ -921,12 +921,12 @@ class WindowManager extends WebviewWindowHandle {
921921
type: 'setMaxSize',
922922
payload: size
923923
? {
924-
type: size.type,
925-
data: {
926-
width: size.width,
927-
height: size.height
924+
type: size.type,
925+
data: {
926+
width: size.width,
927+
height: size.height
928+
}
928929
}
929-
}
930930
: null
931931
}
932932
}

0 commit comments

Comments
 (0)