Skip to content

Commit 58070c1

Browse files
authored
feat(core): filesystem and asset protocol scope events (#3609)
1 parent 3fe0260 commit 58070c1

File tree

9 files changed

+117
-52
lines changed

9 files changed

+117
-52
lines changed

.changes/fs-scope-events.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 listening to events on the filesystem and asset scopes.

core/tauri/src/app.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1167,14 +1167,14 @@ impl<R: Runtime> Builder<R> {
11671167
app.package_info(),
11681168
&env,
11691169
&app.config().tauri.allowlist.fs.scope,
1170-
),
1170+
)?,
11711171
#[cfg(protocol_asset)]
11721172
asset_protocol: FsScope::for_fs_api(
11731173
&app.manager.config(),
11741174
app.package_info(),
11751175
&env,
11761176
&app.config().tauri.allowlist.protocol.asset_scope,
1177-
),
1177+
)?,
11781178
#[cfg(http_request)]
11791179
http: crate::scope::HttpScope::for_http_api(&app.config().tauri.allowlist.http.scope),
11801180
#[cfg(shell_scope)]

core/tauri/src/endpoints/dialog.rs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -106,21 +106,23 @@ impl Cmd {
106106
let res = if options.directory {
107107
let folder = dialog_builder.pick_folder();
108108
if let Some(path) = &folder {
109-
scopes.allow_directory(path, options.recursive);
109+
scopes
110+
.allow_directory(path, options.recursive)
111+
.map_err(crate::error::into_anyhow)?;
110112
}
111113
folder.into()
112114
} else if options.multiple {
113115
let files = dialog_builder.pick_files();
114116
if let Some(files) = &files {
115117
for file in files {
116-
scopes.allow_file(file);
118+
scopes.allow_file(file).map_err(crate::error::into_anyhow)?;
117119
}
118120
}
119121
files.into()
120122
} else {
121123
let file = dialog_builder.pick_file();
122124
if let Some(file) = &file {
123-
scopes.allow_file(file);
125+
scopes.allow_file(file).map_err(crate::error::into_anyhow)?;
124126
}
125127
file.into()
126128
};
@@ -151,7 +153,7 @@ impl Cmd {
151153

152154
let path = dialog_builder.save_file();
153155
if let Some(p) = &path {
154-
scopes.allow_file(p);
156+
scopes.allow_file(p).map_err(crate::error::into_anyhow)?;
155157
}
156158

157159
Ok(path)

core/tauri/src/error.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,9 @@ pub enum Error {
107107
/// An invalid window URL was provided. Includes details about the error.
108108
#[error("invalid window url: {0}")]
109109
InvalidWindowUrl(&'static str),
110+
/// Invalid glob pattern.
111+
#[error("invalid glob pattern: {0}")]
112+
GlobPattern(#[from] glob::PatternError),
110113
}
111114

112115
pub(crate) fn into_anyhow<T: std::fmt::Display>(err: T) -> anyhow::Error {

core/tauri/src/manager.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -818,9 +818,9 @@ impl<R: Runtime> WindowManager<R> {
818818
let scopes = window.state::<Scopes>();
819819
for path in &paths {
820820
if path.is_file() {
821-
scopes.allow_file(path);
821+
let _ = scopes.allow_file(path);
822822
} else {
823-
scopes.allow_directory(path, false);
823+
let _ = scopes.allow_directory(path, false);
824824
}
825825
}
826826
window.emit_and_trigger("tauri://file-drop", paths)

core/tauri/src/scope/fs.rs

Lines changed: 87 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -3,33 +3,47 @@
33
// SPDX-License-Identifier: MIT
44

55
use std::{
6+
collections::{HashMap, HashSet},
67
fmt,
78
path::{Path, PathBuf},
89
sync::{Arc, Mutex},
910
};
1011

11-
use glob::Pattern;
12+
pub use glob::Pattern;
1213
use tauri_utils::{
1314
config::{Config, FsAllowlistScope},
1415
Env, PackageInfo,
1516
};
17+
use uuid::Uuid;
1618

1719
use crate::api::path::parse as parse_path;
1820

21+
/// Scope change event.
22+
#[derive(Debug, Clone)]
23+
pub enum Event {
24+
/// A path has been allowed.
25+
PathAllowed(PathBuf),
26+
/// A path has been forbidden.
27+
PathForbidden(PathBuf),
28+
}
29+
30+
type EventListener = Box<dyn Fn(&Event) + Send>;
31+
1932
/// Scope for filesystem access.
2033
#[derive(Clone)]
2134
pub struct Scope {
22-
allow_patterns: Arc<Mutex<Vec<Pattern>>>,
23-
forbidden_patterns: Arc<Mutex<Vec<Pattern>>>,
35+
alllowed_patterns: Arc<Mutex<HashSet<Pattern>>>,
36+
forbidden_patterns: Arc<Mutex<HashSet<Pattern>>>,
37+
event_listeners: Arc<Mutex<HashMap<Uuid, EventListener>>>,
2438
}
2539

2640
impl fmt::Debug for Scope {
2741
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2842
f.debug_struct("Scope")
2943
.field(
30-
"allow_patterns",
44+
"alllowed_patterns",
3145
&self
32-
.allow_patterns
46+
.alllowed_patterns
3347
.lock()
3448
.unwrap()
3549
.iter()
@@ -50,85 +64,123 @@ impl fmt::Debug for Scope {
5064
}
5165
}
5266

53-
fn push_pattern<P: AsRef<Path>>(list: &mut Vec<Pattern>, pattern: P) {
54-
let pattern: PathBuf = pattern.as_ref().components().collect();
55-
list.push(Pattern::new(&pattern.to_string_lossy()).expect("invalid glob pattern"));
67+
fn push_pattern<P: AsRef<Path>>(list: &mut HashSet<Pattern>, pattern: P) -> crate::Result<()> {
68+
let path: PathBuf = pattern.as_ref().components().collect();
69+
list.insert(Pattern::new(&path.to_string_lossy())?);
5670
#[cfg(windows)]
5771
{
58-
list
59-
.push(Pattern::new(&format!("\\\\?\\{}", pattern.display())).expect("invalid glob pattern"));
72+
list.insert(Pattern::new(&format!("\\\\?\\{}", path.display()))?);
6073
}
74+
Ok(())
6175
}
6276

6377
impl Scope {
6478
/// Creates a new scope from a `FsAllowlistScope` configuration.
65-
pub fn for_fs_api(
79+
pub(crate) fn for_fs_api(
6680
config: &Config,
6781
package_info: &PackageInfo,
6882
env: &Env,
6983
scope: &FsAllowlistScope,
70-
) -> Self {
71-
let mut allow_patterns = Vec::new();
84+
) -> crate::Result<Self> {
85+
let mut alllowed_patterns = HashSet::new();
7286
for path in scope.allowed_paths() {
7387
if let Ok(path) = parse_path(config, package_info, env, path) {
74-
push_pattern(&mut allow_patterns, path);
88+
push_pattern(&mut alllowed_patterns, path)?;
7589
}
7690
}
7791

78-
let mut forbidden_patterns = Vec::new();
92+
let mut forbidden_patterns = HashSet::new();
7993
if let Some(forbidden_paths) = scope.forbidden_paths() {
8094
for path in forbidden_paths {
8195
if let Ok(path) = parse_path(config, package_info, env, path) {
82-
push_pattern(&mut forbidden_patterns, path);
96+
push_pattern(&mut forbidden_patterns, path)?;
8397
}
8498
}
8599
}
86100

87-
Self {
88-
allow_patterns: Arc::new(Mutex::new(allow_patterns)),
101+
Ok(Self {
102+
alllowed_patterns: Arc::new(Mutex::new(alllowed_patterns)),
89103
forbidden_patterns: Arc::new(Mutex::new(forbidden_patterns)),
104+
event_listeners: Default::default(),
105+
})
106+
}
107+
108+
/// The list of allowed patterns.
109+
pub fn allowed_patterns(&self) -> HashSet<Pattern> {
110+
self.alllowed_patterns.lock().unwrap().clone()
111+
}
112+
113+
/// The list of forbidden patterns.
114+
pub fn forbidden_patterns(&self) -> HashSet<Pattern> {
115+
self.forbidden_patterns.lock().unwrap().clone()
116+
}
117+
118+
/// Listen to an event on this scope.
119+
pub fn listen<F: Fn(&Event) + Send + 'static>(&self, f: F) -> Uuid {
120+
let id = Uuid::new_v4();
121+
self.event_listeners.lock().unwrap().insert(id, Box::new(f));
122+
id
123+
}
124+
125+
fn trigger(&self, event: Event) {
126+
for listener in self.event_listeners.lock().unwrap().values() {
127+
listener(&event);
90128
}
91129
}
92130

93131
/// Extend the allowed patterns with the given directory.
94132
///
95133
/// After this function has been called, the frontend will be able to use the Tauri API to read
96134
/// the directory and all of its files and subdirectories.
97-
pub fn allow_directory<P: AsRef<Path>>(&self, path: P, recursive: bool) {
135+
pub fn allow_directory<P: AsRef<Path>>(&self, path: P, recursive: bool) -> crate::Result<()> {
98136
let path = path.as_ref().to_path_buf();
99-
let mut list = self.allow_patterns.lock().unwrap();
137+
{
138+
let mut list = self.alllowed_patterns.lock().unwrap();
100139

101-
// allow the directory to be read
102-
push_pattern(&mut list, &path);
103-
// allow its files and subdirectories to be read
104-
push_pattern(&mut list, path.join(if recursive { "**" } else { "*" }));
140+
// allow the directory to be read
141+
push_pattern(&mut list, &path)?;
142+
// allow its files and subdirectories to be read
143+
push_pattern(&mut list, path.join(if recursive { "**" } else { "*" }))?;
144+
}
145+
self.trigger(Event::PathAllowed(path));
146+
Ok(())
105147
}
106148

107149
/// Extend the allowed patterns with the given file path.
108150
///
109151
/// After this function has been called, the frontend will be able to use the Tauri API to read the contents of this file.
110-
pub fn allow_file<P: AsRef<Path>>(&self, path: P) {
111-
push_pattern(&mut self.allow_patterns.lock().unwrap(), path);
152+
pub fn allow_file<P: AsRef<Path>>(&self, path: P) -> crate::Result<()> {
153+
let path = path.as_ref();
154+
push_pattern(&mut self.alllowed_patterns.lock().unwrap(), &path)?;
155+
self.trigger(Event::PathAllowed(path.to_path_buf()));
156+
Ok(())
112157
}
113158

114159
/// Set the given directory path to be forbidden by this scope.
115160
///
116161
/// **Note:** this takes precedence over allowed paths, so its access gets denied **always**.
117-
pub fn forbid_directory<P: AsRef<Path>>(&self, path: P, recursive: bool) {
162+
pub fn forbid_directory<P: AsRef<Path>>(&self, path: P, recursive: bool) -> crate::Result<()> {
118163
let path = path.as_ref().to_path_buf();
119-
let mut list = self.forbidden_patterns.lock().unwrap();
164+
{
165+
let mut list = self.forbidden_patterns.lock().unwrap();
120166

121-
// allow the directory to be read
122-
push_pattern(&mut list, &path);
123-
// allow its files and subdirectories to be read
124-
push_pattern(&mut list, path.join(if recursive { "**" } else { "*" }));
167+
// allow the directory to be read
168+
push_pattern(&mut list, &path)?;
169+
// allow its files and subdirectories to be read
170+
push_pattern(&mut list, path.join(if recursive { "**" } else { "*" }))?;
171+
}
172+
self.trigger(Event::PathForbidden(path));
173+
Ok(())
125174
}
126175

127176
/// Set the given file path to be forbidden by this scope.
128177
///
129178
/// **Note:** this takes precedence over allowed paths, so its access gets denied **always**.
130-
pub fn forbid_file<P: AsRef<Path>>(&self, path: P) {
131-
push_pattern(&mut self.forbidden_patterns.lock().unwrap(), path);
179+
pub fn forbid_file<P: AsRef<Path>>(&self, path: P) -> crate::Result<()> {
180+
let path = path.as_ref();
181+
push_pattern(&mut self.forbidden_patterns.lock().unwrap(), &path)?;
182+
self.trigger(Event::PathForbidden(path.to_path_buf()));
183+
Ok(())
132184
}
133185

134186
/// Determines if the given path is allowed on this scope.
@@ -154,7 +206,7 @@ impl Scope {
154206
false
155207
} else {
156208
let allowed = self
157-
.allow_patterns
209+
.alllowed_patterns
158210
.lock()
159211
.unwrap()
160212
.iter()

core/tauri/src/scope/http.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ pub struct Scope {
1313

1414
impl Scope {
1515
/// Creates a new scope from the allowlist's `http` scope configuration.
16-
pub fn for_http_api(scope: &HttpAllowlistScope) -> Self {
16+
#[allow(dead_code)]
17+
pub(crate) fn for_http_api(scope: &HttpAllowlistScope) -> Self {
1718
Self {
1819
allowed_urls: scope
1920
.0

core/tauri/src/scope/mod.rs

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ mod http;
88
mod shell;
99

1010
pub use self::http::Scope as HttpScope;
11-
pub use fs::Scope as FsScope;
11+
pub use fs::{Event as FsScopeEvent, Pattern as GlobPattern, Scope as FsScope};
1212
#[cfg(shell_scope)]
1313
pub use shell::{
1414
ExecuteArgs, Scope as ShellScope, ScopeAllowedArg as ShellScopeAllowedArg,
@@ -29,16 +29,18 @@ pub(crate) struct Scopes {
2929

3030
impl Scopes {
3131
#[allow(dead_code)]
32-
pub(crate) fn allow_directory(&self, path: &Path, recursive: bool) {
33-
self.fs.allow_directory(path, recursive);
32+
pub(crate) fn allow_directory(&self, path: &Path, recursive: bool) -> crate::Result<()> {
33+
self.fs.allow_directory(path, recursive)?;
3434
#[cfg(protocol_asset)]
35-
self.asset_protocol.allow_directory(path, recursive);
35+
self.asset_protocol.allow_directory(path, recursive)?;
36+
Ok(())
3637
}
3738

3839
#[allow(dead_code)]
39-
pub(crate) fn allow_file(&self, path: &Path) {
40-
self.fs.allow_file(path);
40+
pub(crate) fn allow_file(&self, path: &Path) -> crate::Result<()> {
41+
self.fs.allow_file(path)?;
4142
#[cfg(protocol_asset)]
42-
self.asset_protocol.allow_file(path);
43+
self.asset_protocol.allow_file(path)?;
44+
Ok(())
4345
}
4446
}

core/tauri/src/scope/shell.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ pub enum ScopeError {
193193

194194
impl Scope {
195195
/// Creates a new shell scope.
196-
pub fn new(scope: ScopeConfig) -> Self {
196+
pub(crate) fn new(scope: ScopeConfig) -> Self {
197197
Self(scope)
198198
}
199199

0 commit comments

Comments
 (0)