feat(core): filesystem and asset protocol scope events (#3609)

This commit is contained in:
Lucas Fernandes Nogueira
2022-03-04 21:18:50 -03:00
committed by GitHub
parent 3fe0260f4c
commit 58070c1eb4
9 changed files with 117 additions and 52 deletions

View File

@@ -0,0 +1,5 @@
---
"tauri": patch
---
Allow listening to events on the filesystem and asset scopes.

View File

@@ -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)]

View File

@@ -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)

View File

@@ -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 {

View File

@@ -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)

View File

@@ -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()

View File

@@ -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

View File

@@ -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(())
}
}

View File

@@ -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)
}