diff --git a/.changes/fs-scope-events.md b/.changes/fs-scope-events.md new file mode 100644 index 000000000..71d263079 --- /dev/null +++ b/.changes/fs-scope-events.md @@ -0,0 +1,5 @@ +--- +"tauri": patch +--- + +Allow listening to events on the filesystem and asset scopes. diff --git a/core/tauri/src/app.rs b/core/tauri/src/app.rs index 7e4223ad4..7f90516b8 100644 --- a/core/tauri/src/app.rs +++ b/core/tauri/src/app.rs @@ -1167,14 +1167,14 @@ impl Builder { app.package_info(), &env, &app.config().tauri.allowlist.fs.scope, - ), + )?, #[cfg(protocol_asset)] asset_protocol: FsScope::for_fs_api( &app.manager.config(), app.package_info(), &env, &app.config().tauri.allowlist.protocol.asset_scope, - ), + )?, #[cfg(http_request)] http: crate::scope::HttpScope::for_http_api(&app.config().tauri.allowlist.http.scope), #[cfg(shell_scope)] diff --git a/core/tauri/src/endpoints/dialog.rs b/core/tauri/src/endpoints/dialog.rs index e88127d94..29708beb4 100644 --- a/core/tauri/src/endpoints/dialog.rs +++ b/core/tauri/src/endpoints/dialog.rs @@ -106,21 +106,23 @@ impl Cmd { let res = if options.directory { let folder = dialog_builder.pick_folder(); if let Some(path) = &folder { - scopes.allow_directory(path, options.recursive); + scopes + .allow_directory(path, options.recursive) + .map_err(crate::error::into_anyhow)?; } folder.into() } else if options.multiple { let files = dialog_builder.pick_files(); if let Some(files) = &files { for file in files { - scopes.allow_file(file); + scopes.allow_file(file).map_err(crate::error::into_anyhow)?; } } files.into() } else { let file = dialog_builder.pick_file(); if let Some(file) = &file { - scopes.allow_file(file); + scopes.allow_file(file).map_err(crate::error::into_anyhow)?; } file.into() }; @@ -151,7 +153,7 @@ impl Cmd { let path = dialog_builder.save_file(); if let Some(p) = &path { - scopes.allow_file(p); + scopes.allow_file(p).map_err(crate::error::into_anyhow)?; } Ok(path) diff --git a/core/tauri/src/error.rs b/core/tauri/src/error.rs index ca518701d..db99e452b 100644 --- a/core/tauri/src/error.rs +++ b/core/tauri/src/error.rs @@ -107,6 +107,9 @@ pub enum Error { /// An invalid window URL was provided. Includes details about the error. #[error("invalid window url: {0}")] InvalidWindowUrl(&'static str), + /// Invalid glob pattern. + #[error("invalid glob pattern: {0}")] + GlobPattern(#[from] glob::PatternError), } pub(crate) fn into_anyhow(err: T) -> anyhow::Error { diff --git a/core/tauri/src/manager.rs b/core/tauri/src/manager.rs index 4295a6668..e167470a1 100644 --- a/core/tauri/src/manager.rs +++ b/core/tauri/src/manager.rs @@ -818,9 +818,9 @@ impl WindowManager { let scopes = window.state::(); for path in &paths { if path.is_file() { - scopes.allow_file(path); + let _ = scopes.allow_file(path); } else { - scopes.allow_directory(path, false); + let _ = scopes.allow_directory(path, false); } } window.emit_and_trigger("tauri://file-drop", paths) diff --git a/core/tauri/src/scope/fs.rs b/core/tauri/src/scope/fs.rs index 15d95e36a..4ec32b318 100644 --- a/core/tauri/src/scope/fs.rs +++ b/core/tauri/src/scope/fs.rs @@ -3,33 +3,47 @@ // SPDX-License-Identifier: MIT use std::{ + collections::{HashMap, HashSet}, fmt, path::{Path, PathBuf}, sync::{Arc, Mutex}, }; -use glob::Pattern; +pub use glob::Pattern; use tauri_utils::{ config::{Config, FsAllowlistScope}, Env, PackageInfo, }; +use uuid::Uuid; use crate::api::path::parse as parse_path; +/// Scope change event. +#[derive(Debug, Clone)] +pub enum Event { + /// A path has been allowed. + PathAllowed(PathBuf), + /// A path has been forbidden. + PathForbidden(PathBuf), +} + +type EventListener = Box; + /// Scope for filesystem access. #[derive(Clone)] pub struct Scope { - allow_patterns: Arc>>, - forbidden_patterns: Arc>>, + alllowed_patterns: Arc>>, + forbidden_patterns: Arc>>, + event_listeners: Arc>>, } impl fmt::Debug for Scope { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("Scope") .field( - "allow_patterns", + "alllowed_patterns", &self - .allow_patterns + .alllowed_patterns .lock() .unwrap() .iter() @@ -50,43 +64,67 @@ impl fmt::Debug for Scope { } } -fn push_pattern>(list: &mut Vec, pattern: P) { - let pattern: PathBuf = pattern.as_ref().components().collect(); - list.push(Pattern::new(&pattern.to_string_lossy()).expect("invalid glob pattern")); +fn push_pattern>(list: &mut HashSet, pattern: P) -> crate::Result<()> { + let path: PathBuf = pattern.as_ref().components().collect(); + list.insert(Pattern::new(&path.to_string_lossy())?); #[cfg(windows)] { - list - .push(Pattern::new(&format!("\\\\?\\{}", pattern.display())).expect("invalid glob pattern")); + list.insert(Pattern::new(&format!("\\\\?\\{}", path.display()))?); } + Ok(()) } impl Scope { /// Creates a new scope from a `FsAllowlistScope` configuration. - pub fn for_fs_api( + pub(crate) fn for_fs_api( config: &Config, package_info: &PackageInfo, env: &Env, scope: &FsAllowlistScope, - ) -> Self { - let mut allow_patterns = Vec::new(); + ) -> crate::Result { + let mut alllowed_patterns = HashSet::new(); for path in scope.allowed_paths() { if let Ok(path) = parse_path(config, package_info, env, path) { - push_pattern(&mut allow_patterns, path); + push_pattern(&mut alllowed_patterns, path)?; } } - let mut forbidden_patterns = Vec::new(); + let mut forbidden_patterns = HashSet::new(); if let Some(forbidden_paths) = scope.forbidden_paths() { for path in forbidden_paths { if let Ok(path) = parse_path(config, package_info, env, path) { - push_pattern(&mut forbidden_patterns, path); + push_pattern(&mut forbidden_patterns, path)?; } } } - Self { - allow_patterns: Arc::new(Mutex::new(allow_patterns)), + Ok(Self { + alllowed_patterns: Arc::new(Mutex::new(alllowed_patterns)), forbidden_patterns: Arc::new(Mutex::new(forbidden_patterns)), + event_listeners: Default::default(), + }) + } + + /// The list of allowed patterns. + pub fn allowed_patterns(&self) -> HashSet { + self.alllowed_patterns.lock().unwrap().clone() + } + + /// The list of forbidden patterns. + pub fn forbidden_patterns(&self) -> HashSet { + self.forbidden_patterns.lock().unwrap().clone() + } + + /// Listen to an event on this scope. + pub fn listen(&self, f: F) -> Uuid { + let id = Uuid::new_v4(); + self.event_listeners.lock().unwrap().insert(id, Box::new(f)); + id + } + + fn trigger(&self, event: Event) { + for listener in self.event_listeners.lock().unwrap().values() { + listener(&event); } } @@ -94,41 +132,55 @@ impl Scope { /// /// 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>(&self, path: P, recursive: bool) { + pub fn allow_directory>(&self, path: P, recursive: bool) -> crate::Result<()> { let path = path.as_ref().to_path_buf(); - let mut list = self.allow_patterns.lock().unwrap(); + { + let mut list = self.alllowed_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 { "*" })); + // 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 { "*" }))?; + } + self.trigger(Event::PathAllowed(path)); + Ok(()) } /// 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>(&self, path: P) { - push_pattern(&mut self.allow_patterns.lock().unwrap(), path); + pub fn allow_file>(&self, path: P) -> crate::Result<()> { + let path = path.as_ref(); + push_pattern(&mut self.alllowed_patterns.lock().unwrap(), &path)?; + self.trigger(Event::PathAllowed(path.to_path_buf())); + Ok(()) } /// Set the given directory path to be forbidden by this scope. /// /// **Note:** this takes precedence over allowed paths, so its access gets denied **always**. - pub fn forbid_directory>(&self, path: P, recursive: bool) { + pub fn forbid_directory>(&self, path: P, recursive: bool) -> crate::Result<()> { let path = path.as_ref().to_path_buf(); - let mut list = self.forbidden_patterns.lock().unwrap(); + { + let mut list = self.forbidden_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 { "*" })); + // 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 { "*" }))?; + } + self.trigger(Event::PathForbidden(path)); + Ok(()) } /// Set the given file path to be forbidden by this scope. /// /// **Note:** this takes precedence over allowed paths, so its access gets denied **always**. - pub fn forbid_file>(&self, path: P) { - push_pattern(&mut self.forbidden_patterns.lock().unwrap(), path); + pub fn forbid_file>(&self, path: P) -> crate::Result<()> { + let path = path.as_ref(); + push_pattern(&mut self.forbidden_patterns.lock().unwrap(), &path)?; + self.trigger(Event::PathForbidden(path.to_path_buf())); + Ok(()) } /// Determines if the given path is allowed on this scope. @@ -154,7 +206,7 @@ impl Scope { false } else { let allowed = self - .allow_patterns + .alllowed_patterns .lock() .unwrap() .iter() diff --git a/core/tauri/src/scope/http.rs b/core/tauri/src/scope/http.rs index bef22dd01..cb49ad581 100644 --- a/core/tauri/src/scope/http.rs +++ b/core/tauri/src/scope/http.rs @@ -13,7 +13,8 @@ pub struct Scope { impl Scope { /// Creates a new scope from the allowlist's `http` scope configuration. - pub fn for_http_api(scope: &HttpAllowlistScope) -> Self { + #[allow(dead_code)] + pub(crate) fn for_http_api(scope: &HttpAllowlistScope) -> Self { Self { allowed_urls: scope .0 diff --git a/core/tauri/src/scope/mod.rs b/core/tauri/src/scope/mod.rs index 527c17ebb..c4626dd3a 100644 --- a/core/tauri/src/scope/mod.rs +++ b/core/tauri/src/scope/mod.rs @@ -8,7 +8,7 @@ mod http; mod shell; pub use self::http::Scope as HttpScope; -pub use fs::Scope as FsScope; +pub use fs::{Event as FsScopeEvent, Pattern as GlobPattern, Scope as FsScope}; #[cfg(shell_scope)] pub use shell::{ ExecuteArgs, Scope as ShellScope, ScopeAllowedArg as ShellScopeAllowedArg, @@ -29,16 +29,18 @@ pub(crate) struct Scopes { impl Scopes { #[allow(dead_code)] - pub(crate) fn allow_directory(&self, path: &Path, recursive: bool) { - self.fs.allow_directory(path, recursive); + pub(crate) fn allow_directory(&self, path: &Path, recursive: bool) -> crate::Result<()> { + self.fs.allow_directory(path, recursive)?; #[cfg(protocol_asset)] - self.asset_protocol.allow_directory(path, recursive); + self.asset_protocol.allow_directory(path, recursive)?; + Ok(()) } #[allow(dead_code)] - pub(crate) fn allow_file(&self, path: &Path) { - self.fs.allow_file(path); + pub(crate) fn allow_file(&self, path: &Path) -> crate::Result<()> { + self.fs.allow_file(path)?; #[cfg(protocol_asset)] - self.asset_protocol.allow_file(path); + self.asset_protocol.allow_file(path)?; + Ok(()) } } diff --git a/core/tauri/src/scope/shell.rs b/core/tauri/src/scope/shell.rs index 5b9dbdb81..fdfab0243 100644 --- a/core/tauri/src/scope/shell.rs +++ b/core/tauri/src/scope/shell.rs @@ -193,7 +193,7 @@ pub enum ScopeError { impl Scope { /// Creates a new shell scope. - pub fn new(scope: ScopeConfig) -> Self { + pub(crate) fn new(scope: ScopeConfig) -> Self { Self(scope) }