mirror of
https://github.com/tauri-apps/tauri.git
synced 2026-04-03 10:11:15 +02:00
feat(core): filesystem and asset protocol scope events (#3609)
This commit is contained in:
committed by
GitHub
parent
3fe0260f4c
commit
58070c1eb4
5
.changes/fs-scope-events.md
Normal file
5
.changes/fs-scope-events.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"tauri": patch
|
||||
---
|
||||
|
||||
Allow listening to events on the filesystem and asset scopes.
|
||||
@@ -1167,14 +1167,14 @@ impl<R: Runtime> Builder<R> {
|
||||
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)]
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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<T: std::fmt::Display>(err: T) -> anyhow::Error {
|
||||
|
||||
@@ -818,9 +818,9 @@ impl<R: Runtime> WindowManager<R> {
|
||||
let scopes = window.state::<Scopes>();
|
||||
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)
|
||||
|
||||
@@ -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<dyn Fn(&Event) + Send>;
|
||||
|
||||
/// Scope for filesystem access.
|
||||
#[derive(Clone)]
|
||||
pub struct Scope {
|
||||
allow_patterns: Arc<Mutex<Vec<Pattern>>>,
|
||||
forbidden_patterns: Arc<Mutex<Vec<Pattern>>>,
|
||||
alllowed_patterns: Arc<Mutex<HashSet<Pattern>>>,
|
||||
forbidden_patterns: Arc<Mutex<HashSet<Pattern>>>,
|
||||
event_listeners: Arc<Mutex<HashMap<Uuid, EventListener>>>,
|
||||
}
|
||||
|
||||
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<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"));
|
||||
fn push_pattern<P: AsRef<Path>>(list: &mut HashSet<Pattern>, 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<Self> {
|
||||
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<Pattern> {
|
||||
self.alllowed_patterns.lock().unwrap().clone()
|
||||
}
|
||||
|
||||
/// The list of forbidden patterns.
|
||||
pub fn forbidden_patterns(&self) -> HashSet<Pattern> {
|
||||
self.forbidden_patterns.lock().unwrap().clone()
|
||||
}
|
||||
|
||||
/// Listen to an event on this scope.
|
||||
pub fn listen<F: Fn(&Event) + Send + 'static>(&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<P: AsRef<Path>>(&self, path: P, recursive: bool) {
|
||||
pub fn allow_directory<P: AsRef<Path>>(&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<P: AsRef<Path>>(&self, path: P) {
|
||||
push_pattern(&mut self.allow_patterns.lock().unwrap(), path);
|
||||
pub fn allow_file<P: AsRef<Path>>(&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<P: AsRef<Path>>(&self, path: P, recursive: bool) {
|
||||
pub fn forbid_directory<P: AsRef<Path>>(&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<P: AsRef<Path>>(&self, path: P) {
|
||||
push_pattern(&mut self.forbidden_patterns.lock().unwrap(), path);
|
||||
pub fn forbid_file<P: AsRef<Path>>(&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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user