mirror of
https://github.com/tauri-apps/plugins-workspace.git
synced 2026-04-21 11:26:15 +02:00
refactor(fs): use scope from tauri core (#825)
* fix(fs): scope checks on Android On Android, when we call canonicalize() on "/data/user/0/appid" (which is the data dir), the result is a "/data/data/appid" path, so we need to adjust our scope for that. * use scope from core * update persisted-scope * fix build * dev branch
This commit is contained in:
committed by
GitHub
parent
1eaf640255
commit
10b80391fc
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"fs": patch
|
||||
"persisted-scope": patch
|
||||
---
|
||||
|
||||
Use `tauri::scope::fs::Scope` instead of local copy of its implementation.
|
||||
+7
-2
@@ -1,5 +1,10 @@
|
||||
[workspace]
|
||||
members = ["plugins/*", "plugins/*/tests/*", "plugins/*/examples/*/src-tauri", "examples/*/src-tauri"]
|
||||
members = [
|
||||
"plugins/*",
|
||||
"plugins/*/tests/*",
|
||||
"plugins/*/examples/*/src-tauri",
|
||||
"examples/*/src-tauri",
|
||||
]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.dependencies]
|
||||
@@ -13,7 +18,7 @@ url = "2"
|
||||
|
||||
[workspace.package]
|
||||
edition = "2021"
|
||||
authors = [ "Tauri Programme within The Commons Conservancy" ]
|
||||
authors = ["Tauri Programme within The Commons Conservancy"]
|
||||
license = "Apache-2.0 OR MIT"
|
||||
rust-version = "1.70"
|
||||
|
||||
|
||||
@@ -2,60 +2,10 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use serde::Deserialize;
|
||||
use tauri::utils::config::FsScope;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct Config {
|
||||
pub scope: FsScope,
|
||||
}
|
||||
|
||||
/// Protocol scope definition.
|
||||
/// It is a list of glob patterns that restrict the API access from the webview.
|
||||
///
|
||||
/// Each pattern can start with a variable that resolves to a system base directory.
|
||||
/// The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`,
|
||||
/// `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`,
|
||||
/// `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`,
|
||||
/// `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum FsScope {
|
||||
/// A list of paths that are allowed by this scope.
|
||||
AllowedPaths(Vec<PathBuf>),
|
||||
/// A complete scope configuration.
|
||||
Scope {
|
||||
/// A list of paths that are allowed by this scope.
|
||||
#[serde(default)]
|
||||
allow: Vec<PathBuf>,
|
||||
/// A list of paths that are not allowed by this scope.
|
||||
/// This gets precedence over the [`Self::Scope::allow`] list.
|
||||
#[serde(default)]
|
||||
deny: Vec<PathBuf>,
|
||||
},
|
||||
}
|
||||
|
||||
impl Default for FsScope {
|
||||
fn default() -> Self {
|
||||
Self::AllowedPaths(Vec::new())
|
||||
}
|
||||
}
|
||||
|
||||
impl FsScope {
|
||||
/// The list of allowed paths.
|
||||
pub fn allowed_paths(&self) -> &Vec<PathBuf> {
|
||||
match self {
|
||||
Self::AllowedPaths(p) => p,
|
||||
Self::Scope { allow, .. } => allow,
|
||||
}
|
||||
}
|
||||
|
||||
/// The list of forbidden paths.
|
||||
pub fn forbidden_paths(&self) -> Option<&Vec<PathBuf>> {
|
||||
match self {
|
||||
Self::AllowedPaths(_) => None,
|
||||
Self::Scope { deny, .. } => Some(deny),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,22 +11,21 @@
|
||||
html_favicon_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png"
|
||||
)]
|
||||
|
||||
use config::FsScope;
|
||||
use tauri::{
|
||||
plugin::{Builder as PluginBuilder, TauriPlugin},
|
||||
scope::fs::Scope,
|
||||
utils::config::FsScope,
|
||||
FileDropEvent, Manager, RunEvent, Runtime, WindowEvent,
|
||||
};
|
||||
|
||||
mod commands;
|
||||
mod config;
|
||||
mod error;
|
||||
mod scope;
|
||||
#[cfg(feature = "watch")]
|
||||
mod watcher;
|
||||
|
||||
pub use config::Config;
|
||||
pub use error::Error;
|
||||
pub use scope::{Event as ScopeEvent, Scope};
|
||||
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
|
||||
@@ -1,368 +0,0 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
fmt,
|
||||
path::{Path, PathBuf, MAIN_SEPARATOR},
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
use crate::config::FsScope;
|
||||
pub use glob::Pattern;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{Manager, Runtime};
|
||||
|
||||
/// 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 {
|
||||
allowed_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(
|
||||
"allowed_patterns",
|
||||
&self
|
||||
.allowed_patterns
|
||||
.lock()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|p| p.as_str())
|
||||
.collect::<Vec<&str>>(),
|
||||
)
|
||||
.field(
|
||||
"forbidden_patterns",
|
||||
&self
|
||||
.forbidden_patterns
|
||||
.lock()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|p| p.as_str())
|
||||
.collect::<Vec<&str>>(),
|
||||
)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
fn push_pattern<P: AsRef<Path>, F: Fn(&str) -> Result<Pattern, glob::PatternError>>(
|
||||
list: &mut HashSet<Pattern>,
|
||||
pattern: P,
|
||||
f: F,
|
||||
) -> crate::Result<()> {
|
||||
let path: PathBuf = pattern.as_ref().components().collect();
|
||||
list.insert(f(&path.to_string_lossy())?);
|
||||
#[cfg(windows)]
|
||||
{
|
||||
if let Ok(p) = std::fs::canonicalize(&path) {
|
||||
list.insert(f(&p.to_string_lossy())?);
|
||||
} else {
|
||||
list.insert(f(&format!("\\\\?\\{}", path.display()))?);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl Scope {
|
||||
/// Creates a new scope from a `FsScope` configuration.
|
||||
pub(crate) fn new<R: Runtime, M: Manager<R>>(
|
||||
manager: &M,
|
||||
scope: &FsScope,
|
||||
) -> crate::Result<Self> {
|
||||
let mut allowed_patterns = HashSet::new();
|
||||
for path in scope.allowed_paths() {
|
||||
if let Ok(path) = manager.path().parse(path) {
|
||||
push_pattern(&mut allowed_patterns, path, Pattern::new)?;
|
||||
}
|
||||
}
|
||||
|
||||
let mut forbidden_patterns = HashSet::new();
|
||||
if let Some(forbidden_paths) = scope.forbidden_paths() {
|
||||
for path in forbidden_paths {
|
||||
if let Ok(path) = manager.path().parse(path) {
|
||||
push_pattern(&mut forbidden_patterns, path, Pattern::new)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
allowed_patterns: Arc::new(Mutex::new(allowed_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.allowed_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) {
|
||||
let listeners = self.event_listeners.lock().unwrap();
|
||||
let handlers = listeners.values();
|
||||
for listener in handlers {
|
||||
listener(&event);
|
||||
}
|
||||
}
|
||||
|
||||
/// Extend the allowed patterns with the given directory.
|
||||
///
|
||||
/// 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. If `recursive` is `true`, subdirectories will be accessible too.
|
||||
pub fn allow_directory<P: AsRef<Path>>(&self, path: P, recursive: bool) -> crate::Result<()> {
|
||||
let path = path.as_ref();
|
||||
{
|
||||
let mut list = self.allowed_patterns.lock().unwrap();
|
||||
|
||||
// allow the directory to be read
|
||||
push_pattern(&mut list, path, escaped_pattern)?;
|
||||
// allow its files and subdirectories to be read
|
||||
push_pattern(&mut list, path, |p| {
|
||||
escaped_pattern_with(p, if recursive { "**" } else { "*" })
|
||||
})?;
|
||||
}
|
||||
self.trigger(Event::PathAllowed(path.to_path_buf()));
|
||||
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) -> crate::Result<()> {
|
||||
let path = path.as_ref();
|
||||
push_pattern(
|
||||
&mut self.allowed_patterns.lock().unwrap(),
|
||||
path,
|
||||
escaped_pattern,
|
||||
)?;
|
||||
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) -> crate::Result<()> {
|
||||
let path = path.as_ref();
|
||||
{
|
||||
let mut list = self.forbidden_patterns.lock().unwrap();
|
||||
|
||||
// allow the directory to be read
|
||||
push_pattern(&mut list, path, escaped_pattern)?;
|
||||
// allow its files and subdirectories to be read
|
||||
push_pattern(&mut list, path, |p| {
|
||||
escaped_pattern_with(p, if recursive { "**" } else { "*" })
|
||||
})?;
|
||||
}
|
||||
self.trigger(Event::PathForbidden(path.to_path_buf()));
|
||||
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) -> crate::Result<()> {
|
||||
let path = path.as_ref();
|
||||
push_pattern(
|
||||
&mut self.forbidden_patterns.lock().unwrap(),
|
||||
path,
|
||||
escaped_pattern,
|
||||
)?;
|
||||
self.trigger(Event::PathForbidden(path.to_path_buf()));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Determines if the given path is allowed on this scope.
|
||||
pub fn is_allowed<P: AsRef<Path>>(&self, path: P) -> bool {
|
||||
let path = path.as_ref();
|
||||
let path = if !path.exists() {
|
||||
crate::Result::Ok(path.to_path_buf())
|
||||
} else {
|
||||
std::fs::canonicalize(path).map_err(Into::into)
|
||||
};
|
||||
|
||||
if let Ok(path) = path {
|
||||
let path: PathBuf = path.components().collect();
|
||||
let options = glob::MatchOptions {
|
||||
// this is needed so `/dir/*` doesn't match files within subdirectories such as `/dir/subdir/file.txt`
|
||||
// see: https://github.com/tauri-apps/tauri/security/advisories/GHSA-6mv3-wm7j-h4w5
|
||||
require_literal_separator: true,
|
||||
// dotfiles are not supposed to be exposed by default
|
||||
#[cfg(unix)]
|
||||
require_literal_leading_dot: true,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let forbidden = self
|
||||
.forbidden_patterns
|
||||
.lock()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|p| p.matches_path_with(&path, options));
|
||||
|
||||
if forbidden {
|
||||
false
|
||||
} else {
|
||||
let allowed = self
|
||||
.allowed_patterns
|
||||
.lock()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|p| p.matches_path_with(&path, options));
|
||||
allowed
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn escaped_pattern(p: &str) -> Result<Pattern, glob::PatternError> {
|
||||
Pattern::new(&glob::Pattern::escape(p))
|
||||
}
|
||||
|
||||
fn escaped_pattern_with(p: &str, append: &str) -> Result<Pattern, glob::PatternError> {
|
||||
Pattern::new(&format!(
|
||||
"{}{}{append}",
|
||||
glob::Pattern::escape(p),
|
||||
MAIN_SEPARATOR
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::Scope;
|
||||
|
||||
fn new_scope() -> Scope {
|
||||
Scope {
|
||||
allowed_patterns: Default::default(),
|
||||
forbidden_patterns: Default::default(),
|
||||
event_listeners: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn path_is_escaped() {
|
||||
let scope = new_scope();
|
||||
#[cfg(unix)]
|
||||
{
|
||||
scope.allow_directory("/home/tauri/**", false).unwrap();
|
||||
assert!(scope.is_allowed("/home/tauri/**"));
|
||||
assert!(scope.is_allowed("/home/tauri/**/file"));
|
||||
assert!(!scope.is_allowed("/home/tauri/anyfile"));
|
||||
}
|
||||
#[cfg(windows)]
|
||||
{
|
||||
scope.allow_directory("C:\\home\\tauri\\**", false).unwrap();
|
||||
assert!(scope.is_allowed("C:\\home\\tauri\\**"));
|
||||
assert!(scope.is_allowed("C:\\home\\tauri\\**\\file"));
|
||||
assert!(!scope.is_allowed("C:\\home\\tauri\\anyfile"));
|
||||
}
|
||||
|
||||
let scope = new_scope();
|
||||
#[cfg(unix)]
|
||||
{
|
||||
scope.allow_file("/home/tauri/**").unwrap();
|
||||
assert!(scope.is_allowed("/home/tauri/**"));
|
||||
assert!(!scope.is_allowed("/home/tauri/**/file"));
|
||||
assert!(!scope.is_allowed("/home/tauri/anyfile"));
|
||||
}
|
||||
#[cfg(windows)]
|
||||
{
|
||||
scope.allow_file("C:\\home\\tauri\\**").unwrap();
|
||||
assert!(scope.is_allowed("C:\\home\\tauri\\**"));
|
||||
assert!(!scope.is_allowed("C:\\home\\tauri\\**\\file"));
|
||||
assert!(!scope.is_allowed("C:\\home\\tauri\\anyfile"));
|
||||
}
|
||||
|
||||
let scope = new_scope();
|
||||
#[cfg(unix)]
|
||||
{
|
||||
scope.allow_directory("/home/tauri", true).unwrap();
|
||||
scope.forbid_directory("/home/tauri/**", false).unwrap();
|
||||
assert!(!scope.is_allowed("/home/tauri/**"));
|
||||
assert!(!scope.is_allowed("/home/tauri/**/file"));
|
||||
assert!(scope.is_allowed("/home/tauri/**/inner/file"));
|
||||
assert!(scope.is_allowed("/home/tauri/inner/folder/anyfile"));
|
||||
assert!(scope.is_allowed("/home/tauri/anyfile"));
|
||||
}
|
||||
#[cfg(windows)]
|
||||
{
|
||||
scope.allow_directory("C:\\home\\tauri", true).unwrap();
|
||||
scope
|
||||
.forbid_directory("C:\\home\\tauri\\**", false)
|
||||
.unwrap();
|
||||
assert!(!scope.is_allowed("C:\\home\\tauri\\**"));
|
||||
assert!(!scope.is_allowed("C:\\home\\tauri\\**\\file"));
|
||||
assert!(scope.is_allowed("C:\\home\\tauri\\**\\inner\\file"));
|
||||
assert!(scope.is_allowed("C:\\home\\tauri\\inner\\folder\\anyfile"));
|
||||
assert!(scope.is_allowed("C:\\home\\tauri\\anyfile"));
|
||||
}
|
||||
|
||||
let scope = new_scope();
|
||||
#[cfg(unix)]
|
||||
{
|
||||
scope.allow_directory("/home/tauri", true).unwrap();
|
||||
scope.forbid_file("/home/tauri/**").unwrap();
|
||||
assert!(!scope.is_allowed("/home/tauri/**"));
|
||||
assert!(scope.is_allowed("/home/tauri/**/file"));
|
||||
assert!(scope.is_allowed("/home/tauri/**/inner/file"));
|
||||
assert!(scope.is_allowed("/home/tauri/anyfile"));
|
||||
}
|
||||
#[cfg(windows)]
|
||||
{
|
||||
scope.allow_directory("C:\\home\\tauri", true).unwrap();
|
||||
scope.forbid_file("C:\\home\\tauri\\**").unwrap();
|
||||
assert!(!scope.is_allowed("C:\\home\\tauri\\**"));
|
||||
assert!(scope.is_allowed("C:\\home\\tauri\\**\\file"));
|
||||
assert!(scope.is_allowed("C:\\home\\tauri\\**\\inner\\file"));
|
||||
assert!(scope.is_allowed("C:\\home\\tauri\\anyfile"));
|
||||
}
|
||||
|
||||
let scope = new_scope();
|
||||
#[cfg(unix)]
|
||||
{
|
||||
scope.allow_directory("/home/tauri", false).unwrap();
|
||||
assert!(scope.is_allowed("/home/tauri/**"));
|
||||
assert!(!scope.is_allowed("/home/tauri/**/file"));
|
||||
assert!(!scope.is_allowed("/home/tauri/**/inner/file"));
|
||||
assert!(scope.is_allowed("/home/tauri/anyfile"));
|
||||
}
|
||||
#[cfg(windows)]
|
||||
{
|
||||
scope.allow_directory("C:\\home\\tauri", false).unwrap();
|
||||
assert!(scope.is_allowed("C:\\home\\tauri\\**"));
|
||||
assert!(!scope.is_allowed("C:\\home\\tauri\\**\\file"));
|
||||
assert!(!scope.is_allowed("C:\\home\\tauri\\**\\inner\\file"));
|
||||
assert!(scope.is_allowed("C:\\home\\tauri\\anyfile"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,14 +13,14 @@
|
||||
|
||||
use aho_corasick::AhoCorasick;
|
||||
use serde::{Deserialize, Serialize};
|
||||
#[cfg(feature = "protocol-asset")]
|
||||
|
||||
use tauri::scope::fs::{Event as FsScopeEvent, Scope as FsScope};
|
||||
use tauri::{
|
||||
plugin::{Builder, TauriPlugin},
|
||||
scope::fs::Pattern as GlobPattern,
|
||||
Manager, Runtime,
|
||||
};
|
||||
use tauri_plugin_fs::{FsExt, Scope as FsPluginScope, ScopeEvent as FsPluginScopeEvent};
|
||||
use tauri_plugin_fs::FsExt;
|
||||
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
@@ -58,33 +58,6 @@ trait ScopeExt {
|
||||
fn forbidden_patterns(&self) -> HashSet<GlobPattern>;
|
||||
}
|
||||
|
||||
impl ScopeExt for FsPluginScope {
|
||||
fn allow_file(&self, path: &Path) {
|
||||
let _ = FsPluginScope::allow_file(self, path);
|
||||
}
|
||||
|
||||
fn allow_directory(&self, path: &Path, recursive: bool) {
|
||||
let _ = FsPluginScope::allow_directory(self, path, recursive);
|
||||
}
|
||||
|
||||
fn forbid_file(&self, path: &Path) {
|
||||
let _ = FsPluginScope::forbid_file(self, path);
|
||||
}
|
||||
|
||||
fn forbid_directory(&self, path: &Path, recursive: bool) {
|
||||
let _ = FsPluginScope::forbid_directory(self, path, recursive);
|
||||
}
|
||||
|
||||
fn allowed_patterns(&self) -> HashSet<GlobPattern> {
|
||||
FsPluginScope::allowed_patterns(self)
|
||||
}
|
||||
|
||||
fn forbidden_patterns(&self) -> HashSet<GlobPattern> {
|
||||
FsPluginScope::forbidden_patterns(self)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "protocol-asset")]
|
||||
impl ScopeExt for FsScope {
|
||||
fn allow_file(&self, path: &Path) {
|
||||
let _ = FsScope::allow_file(self, path);
|
||||
@@ -299,7 +272,7 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
||||
if let Some(fs_scope) = fs_scope {
|
||||
let fs_scope_ = fs_scope.clone();
|
||||
fs_scope.listen(move |event| {
|
||||
if let FsPluginScopeEvent::PathAllowed(_) = event {
|
||||
if let FsScopeEvent::PathAllowed(_) = event {
|
||||
save_scopes(&fs_scope_, &app_dir, &fs_scope_state_path);
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user