diff --git a/package.json b/package.json index 91831d3..ace54a7 100644 --- a/package.json +++ b/package.json @@ -1,29 +1,26 @@ { - "name": "@tauri-apps/plugin-fs", - "version": "2.0.0-beta.5", - "description": "Access the file system.", - "license": "MIT or APACHE-2.0", - "authors": [ - "Tauri Programme within The Commons Conservancy" - ], - "type": "module", - "types": "./dist-js/index.d.ts", - "main": "./dist-js/index.cjs", - "module": "./dist-js/index.js", - "exports": { - "types": "./dist-js/index.d.ts", - "import": "./dist-js/index.js", - "require": "./dist-js/index.cjs" - }, - "scripts": { - "build": "rollup -c" - }, - "files": [ - "dist-js", - "README.md", - "LICENSE" - ], - "dependencies": { - "@tauri-apps/api": "2.0.0-beta.13" - } + "name": "@tauri-apps/plugin-fs", + "description": "Access the file system.", + "exports": { + "import": "./dist-js/index.js", + "require": "./dist-js/index.cjs", + "types": "./dist-js/index.d.ts" + }, + "main": "./dist-js/index.cjs", + "module": "./dist-js/index.js", + "types": "./dist-js/index.d.ts", + "files": [ + "dist-js", + "README.md", + "LICENSE" + ], + "scripts": { + "build": "rollup -c" + }, + "dependencies": { + "@tauri-apps/api": "2.0.0-beta.13" + }, + "authors": [ + "Tauri Programme within The Commons Conservancy" + ] } diff --git a/src/commands.rs b/src/commands.rs deleted file mode 100644 index 9fac4f0..0000000 --- a/src/commands.rs +++ /dev/null @@ -1,999 +0,0 @@ -// Copyright 2019-2023 Tauri Programme within The Commons Conservancy -// Copyright 2018-2023 the Deno authors. -// SPDX-License-Identifier: Apache-2.0 -// SPDX-License-Identifier: MIT - -use serde::{Deserialize, Serialize, Serializer}; -use serde_repr::{Deserialize_repr, Serialize_repr}; -use tauri::{ - ipc::{CommandScope, GlobalScope}, - path::{BaseDirectory, SafePathBuf}, - utils::config::FsScope, - Manager, Resource, ResourceId, Runtime, Webview, -}; - -use std::{ - fs::File, - io::{BufReader, Lines, Read, Write}, - path::{Path, PathBuf}, - sync::Mutex, - time::{SystemTime, UNIX_EPOCH}, -}; - -use crate::{scope::Entry, Error, FsExt}; - -#[derive(Debug, thiserror::Error)] -pub enum CommandError { - #[error(transparent)] - Anyhow(#[from] anyhow::Error), - #[error(transparent)] - Plugin(#[from] Error), - #[error(transparent)] - Tauri(#[from] tauri::Error), - #[error(transparent)] - UrlParseError(#[from] url::ParseError), - #[cfg(feature = "watch")] - #[error(transparent)] - Watcher(#[from] notify::Error), -} - -impl From for CommandError { - fn from(value: String) -> Self { - Self::Anyhow(anyhow::anyhow!(value)) - } -} - -impl From<&str> for CommandError { - fn from(value: &str) -> Self { - Self::Anyhow(anyhow::anyhow!(value.to_string())) - } -} - -impl Serialize for CommandError { - fn serialize(&self, serializer: S) -> std::result::Result - where - S: Serializer, - { - if let Self::Anyhow(err) = self { - serializer.serialize_str(format!("{err:#}").as_ref()) - } else { - serializer.serialize_str(self.to_string().as_ref()) - } - } -} - -pub type CommandResult = std::result::Result; - -#[derive(Debug, Clone, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct BaseOptions { - base_dir: Option, -} - -#[tauri::command] -pub fn create( - webview: Webview, - global_scope: GlobalScope, - command_scope: CommandScope, - path: SafePathBuf, - options: Option, -) -> CommandResult { - let resolved_path = resolve_path( - &webview, - &global_scope, - &command_scope, - path, - options.and_then(|o| o.base_dir), - )?; - let file = File::create(&resolved_path).map_err(|e| { - format!( - "failed to create file at path: {} with error: {e}", - resolved_path.display() - ) - })?; - let rid = webview.resources_table().add(StdFileResource::new(file)); - Ok(rid) -} - -#[derive(Debug, Clone, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct OpenOptions { - #[serde(flatten)] - base: BaseOptions, - #[serde(default = "default_true")] - read: bool, - #[serde(default)] - write: bool, - #[serde(default)] - append: bool, - #[serde(default)] - truncate: bool, - #[serde(default)] - create: bool, - #[serde(default)] - create_new: bool, - #[allow(unused)] - mode: Option, -} - -fn default_true() -> bool { - true -} - -#[tauri::command] -pub fn open( - webview: Webview, - global_scope: GlobalScope, - command_scope: CommandScope, - path: SafePathBuf, - options: Option, -) -> CommandResult { - let resolved_path = resolve_path( - &webview, - &global_scope, - &command_scope, - path, - options.as_ref().and_then(|o| o.base.base_dir), - )?; - - let mut opts = std::fs::OpenOptions::new(); - // default to read-only - opts.read(true); - - if let Some(options) = options { - #[cfg(unix)] - { - use std::os::unix::fs::OpenOptionsExt; - if let Some(mode) = options.mode { - opts.mode(mode); - } - } - - opts.read(options.read) - .create(options.create) - .write(options.write) - .truncate(options.truncate) - .append(options.append) - .create_new(options.create_new); - } - - let file = opts.open(&resolved_path).map_err(|e| { - format!( - "failed to open file at path: {} with error: {e}", - resolved_path.display() - ) - })?; - - let rid = webview.resources_table().add(StdFileResource::new(file)); - - Ok(rid) -} - -#[tauri::command] -pub fn close(webview: Webview, rid: ResourceId) -> CommandResult<()> { - webview.resources_table().close(rid).map_err(Into::into) -} - -#[derive(Debug, Clone, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct CopyFileOptions { - from_path_base_dir: Option, - to_path_base_dir: Option, -} - -#[tauri::command] -pub fn copy_file( - webview: Webview, - global_scope: GlobalScope, - command_scope: CommandScope, - from_path: SafePathBuf, - to_path: SafePathBuf, - options: Option, -) -> CommandResult<()> { - let resolved_from_path = resolve_path( - &webview, - &global_scope, - &command_scope, - from_path, - options.as_ref().and_then(|o| o.from_path_base_dir), - )?; - let resolved_to_path = resolve_path( - &webview, - &global_scope, - &command_scope, - to_path, - options.as_ref().and_then(|o| o.to_path_base_dir), - )?; - std::fs::copy(&resolved_from_path, &resolved_to_path).map_err(|e| { - format!( - "failed to copy file from path: {}, to path: {} with error: {e}", - resolved_from_path.display(), - resolved_to_path.display() - ) - })?; - Ok(()) -} - -#[derive(Debug, Clone, Deserialize)] -pub struct MkdirOptions { - #[serde(flatten)] - base: BaseOptions, - #[allow(unused)] - mode: Option, - recursive: Option, -} - -#[tauri::command] -pub fn mkdir( - webview: Webview, - global_scope: GlobalScope, - command_scope: CommandScope, - path: SafePathBuf, - options: Option, -) -> CommandResult<()> { - let resolved_path = resolve_path( - &webview, - &global_scope, - &command_scope, - path, - options.as_ref().and_then(|o| o.base.base_dir), - )?; - - let mut builder = std::fs::DirBuilder::new(); - builder.recursive(options.as_ref().and_then(|o| o.recursive).unwrap_or(false)); - - #[cfg(unix)] - { - use std::os::unix::fs::DirBuilderExt; - let mode = options.as_ref().and_then(|o| o.mode).unwrap_or(0o777) & 0o777; - builder.mode(mode); - } - - builder - .create(&resolved_path) - .map_err(|e| { - format!( - "failed to create directory at path: {} with error: {e}", - resolved_path.display() - ) - }) - .map_err(Into::into) -} - -#[derive(Debug, Serialize)] -#[serde(rename_all = "camelCase")] -#[non_exhaustive] -pub struct DirEntry { - pub name: Option, - pub is_directory: bool, - pub is_file: bool, - pub is_symlink: bool, -} - -fn read_dir_inner>(path: P) -> crate::Result> { - let mut files_and_dirs: Vec = vec![]; - for entry in std::fs::read_dir(path)? { - let path = entry?.path(); - let file_type = path.metadata()?.file_type(); - files_and_dirs.push(DirEntry { - is_directory: file_type.is_dir(), - is_file: file_type.is_file(), - is_symlink: std::fs::symlink_metadata(&path) - .map(|md| md.file_type().is_symlink()) - .unwrap_or(false), - name: path - .file_name() - .map(|name| name.to_string_lossy()) - .map(|name| name.to_string()), - }); - } - Result::Ok(files_and_dirs) -} - -#[tauri::command] -pub fn read_dir( - webview: Webview, - global_scope: GlobalScope, - command_scope: CommandScope, - path: SafePathBuf, - options: Option, -) -> CommandResult> { - let resolved_path = resolve_path( - &webview, - &global_scope, - &command_scope, - path, - options.as_ref().and_then(|o| o.base_dir), - )?; - - read_dir_inner(&resolved_path) - .map_err(|e| { - format!( - "failed to read directory at path: {} with error: {e}", - resolved_path.display() - ) - }) - .map_err(Into::into) -} - -#[tauri::command] -pub fn read( - webview: Webview, - rid: ResourceId, - len: u32, -) -> CommandResult<(Vec, usize)> { - let mut data = vec![0; len as usize]; - let file = webview.resources_table().get::(rid)?; - let nread = StdFileResource::with_lock(&file, |mut file| file.read(&mut data)) - .map_err(|e| format!("faied to read bytes from file with error: {e}"))?; - Ok((data, nread)) -} - -#[tauri::command] -pub fn read_file( - webview: Webview, - global_scope: GlobalScope, - command_scope: CommandScope, - path: SafePathBuf, - options: Option, -) -> CommandResult { - let resolved_path = resolve_path( - &webview, - &global_scope, - &command_scope, - path, - options.as_ref().and_then(|o| o.base_dir), - )?; - std::fs::read(&resolved_path) - .map(tauri::ipc::Response::new) - .map_err(|e| { - format!( - "failed to read file at path: {} with error: {e}", - resolved_path.display() - ) - }) - .map_err(Into::into) -} - -#[tauri::command] -pub fn read_text_file( - webview: Webview, - global_scope: GlobalScope, - command_scope: CommandScope, - path: SafePathBuf, - options: Option, -) -> CommandResult { - let resolved_path = resolve_path( - &webview, - &global_scope, - &command_scope, - path, - options.as_ref().and_then(|o| o.base_dir), - )?; - std::fs::read_to_string(&resolved_path) - .map_err(|e| { - format!( - "failed to read file as text at path: {} with error: {e}", - resolved_path.display() - ) - }) - .map_err(Into::into) -} - -#[tauri::command] -pub fn read_text_file_lines( - webview: Webview, - global_scope: GlobalScope, - command_scope: CommandScope, - path: SafePathBuf, - options: Option, -) -> CommandResult { - use std::io::BufRead; - - let resolved_path = resolve_path( - &webview, - &global_scope, - &command_scope, - path, - options.as_ref().and_then(|o| o.base_dir), - )?; - - let file = File::open(&resolved_path).map_err(|e| { - format!( - "failed to open file at path: {} with error: {e}", - resolved_path.display() - ) - })?; - - let lines = BufReader::new(file).lines(); - let rid = webview.resources_table().add(StdLinesResource::new(lines)); - - Ok(rid) -} - -#[tauri::command] -pub fn read_text_file_lines_next( - webview: Webview, - rid: ResourceId, -) -> CommandResult<(Option, bool)> { - let mut resource_table = webview.resources_table(); - let lines = resource_table.get::(rid)?; - - let ret = StdLinesResource::with_lock(&lines, |lines| { - lines.next().map(|a| (a.ok(), false)).unwrap_or_else(|| { - let _ = resource_table.close(rid); - (None, true) - }) - }); - - Ok(ret) -} - -#[derive(Debug, Clone, Deserialize)] -pub struct RemoveOptions { - #[serde(flatten)] - base: BaseOptions, - recursive: Option, -} - -#[tauri::command] -pub fn remove( - webview: Webview, - global_scope: GlobalScope, - command_scope: CommandScope, - path: SafePathBuf, - options: Option, -) -> CommandResult<()> { - let resolved_path = resolve_path( - &webview, - &global_scope, - &command_scope, - path, - options.as_ref().and_then(|o| o.base.base_dir), - )?; - - let metadata = std::fs::symlink_metadata(&resolved_path).map_err(|e| { - format!( - "failed to get metadata of path: {} with error: {e}", - resolved_path.display() - ) - })?; - - let file_type = metadata.file_type(); - - // taken from deno source code: https://github.com/denoland/deno/blob/429759fe8b4207240709c240a8344d12a1e39566/runtime/ops/fs.rs#L728 - let res = if file_type.is_file() { - std::fs::remove_file(&resolved_path) - } else if options.as_ref().and_then(|o| o.recursive).unwrap_or(false) { - std::fs::remove_dir_all(&resolved_path) - } else if file_type.is_symlink() { - #[cfg(unix)] - { - std::fs::remove_file(&resolved_path) - } - #[cfg(not(unix))] - { - use std::os::windows::fs::MetadataExt; - const FILE_ATTRIBUTE_DIRECTORY: u32 = 0x00000010; - if metadata.file_attributes() & FILE_ATTRIBUTE_DIRECTORY != 0 { - std::fs::remove_dir(&resolved_path) - } else { - std::fs::remove_file(&resolved_path) - } - } - } else if file_type.is_dir() { - std::fs::remove_dir(&resolved_path) - } else { - // pipes, sockets, etc... - std::fs::remove_file(&resolved_path) - }; - - res.map_err(|e| { - format!( - "failed to remove path: {} with error: {e}", - resolved_path.display() - ) - }) - .map_err(Into::into) -} - -#[derive(Debug, Clone, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct RenameOptions { - new_path_base_dir: Option, - old_path_base_dir: Option, -} - -#[tauri::command] -pub fn rename( - webview: Webview, - global_scope: GlobalScope, - command_scope: CommandScope, - old_path: SafePathBuf, - new_path: SafePathBuf, - options: Option, -) -> CommandResult<()> { - let resolved_old_path = resolve_path( - &webview, - &global_scope, - &command_scope, - old_path, - options.as_ref().and_then(|o| o.old_path_base_dir), - )?; - let resolved_new_path = resolve_path( - &webview, - &global_scope, - &command_scope, - new_path, - options.as_ref().and_then(|o| o.new_path_base_dir), - )?; - std::fs::rename(&resolved_old_path, &resolved_new_path) - .map_err(|e| { - format!( - "failed to rename old path: {} to new path: {} with error: {e}", - resolved_old_path.display(), - resolved_new_path.display() - ) - }) - .map_err(Into::into) -} - -#[derive(Serialize_repr, Deserialize_repr, Clone, Copy, Debug)] -#[repr(u16)] -pub enum SeekMode { - Start = 0, - Current = 1, - End = 2, -} - -#[tauri::command] -pub fn seek( - webview: Webview, - rid: ResourceId, - offset: i64, - whence: SeekMode, -) -> CommandResult { - use std::io::{Seek, SeekFrom}; - let file = webview.resources_table().get::(rid)?; - StdFileResource::with_lock(&file, |mut file| { - file.seek(match whence { - SeekMode::Start => SeekFrom::Start(offset as u64), - SeekMode::Current => SeekFrom::Current(offset), - SeekMode::End => SeekFrom::End(offset), - }) - }) - .map_err(|e| format!("failed to seek file with error: {e}")) - .map_err(Into::into) -} - -#[tauri::command] -pub fn stat( - webview: Webview, - global_scope: GlobalScope, - command_scope: CommandScope, - path: SafePathBuf, - options: Option, -) -> CommandResult { - let resolved_path = resolve_path( - &webview, - &global_scope, - &command_scope, - path, - options.as_ref().and_then(|o| o.base_dir), - )?; - let metadata = std::fs::metadata(&resolved_path).map_err(|e| { - format!( - "failed to get metadata of path: {} with error: {e}", - resolved_path.display() - ) - })?; - Ok(get_stat(metadata)) -} - -#[tauri::command] -pub fn lstat( - webview: Webview, - global_scope: GlobalScope, - command_scope: CommandScope, - path: SafePathBuf, - options: Option, -) -> CommandResult { - let resolved_path = resolve_path( - &webview, - &global_scope, - &command_scope, - path, - options.as_ref().and_then(|o| o.base_dir), - )?; - let metadata = std::fs::symlink_metadata(&resolved_path).map_err(|e| { - format!( - "failed to get metadata of path: {} with error: {e}", - resolved_path.display() - ) - })?; - Ok(get_stat(metadata)) -} - -#[tauri::command] -pub fn fstat(webview: Webview, rid: ResourceId) -> CommandResult { - let file = webview.resources_table().get::(rid)?; - let metadata = StdFileResource::with_lock(&file, |file| file.metadata()) - .map_err(|e| format!("failed to get metadata of file with error: {e}"))?; - Ok(get_stat(metadata)) -} - -#[tauri::command] -pub fn truncate( - webview: Webview, - global_scope: GlobalScope, - command_scope: CommandScope, - path: SafePathBuf, - len: Option, - options: Option, -) -> CommandResult<()> { - let resolved_path = resolve_path( - &webview, - &global_scope, - &command_scope, - path, - options.as_ref().and_then(|o| o.base_dir), - )?; - let f = std::fs::OpenOptions::new() - .write(true) - .open(&resolved_path) - .map_err(|e| { - format!( - "failed to open file at path: {} with error: {e}", - resolved_path.display() - ) - })?; - f.set_len(len.unwrap_or(0)) - .map_err(|e| { - format!( - "failed to truncate file at path: {} with error: {e}", - resolved_path.display() - ) - }) - .map_err(Into::into) -} - -#[tauri::command] -pub fn ftruncate( - webview: Webview, - rid: ResourceId, - len: Option, -) -> CommandResult<()> { - let file = webview.resources_table().get::(rid)?; - StdFileResource::with_lock(&file, |file| file.set_len(len.unwrap_or(0))) - .map_err(|e| format!("failed to truncate file with error: {e}")) - .map_err(Into::into) -} - -#[tauri::command] -pub fn write( - webview: Webview, - rid: ResourceId, - data: Vec, -) -> CommandResult { - let file = webview.resources_table().get::(rid)?; - StdFileResource::with_lock(&file, |mut file| file.write(&data)) - .map_err(|e| format!("failed to write bytes to file with error: {e}")) - .map_err(Into::into) -} - -#[derive(Debug, Clone, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct WriteFileOptions { - #[serde(flatten)] - base: BaseOptions, - #[serde(default)] - append: bool, - #[serde(default = "default_create_value")] - create: bool, - #[serde(default)] - create_new: bool, - #[allow(unused)] - mode: Option, -} - -fn default_create_value() -> bool { - true -} - -fn write_file_inner( - webview: Webview, - global_scope: &GlobalScope, - command_scope: &CommandScope, - path: SafePathBuf, - data: &[u8], - options: Option, -) -> CommandResult<()> { - let resolved_path = resolve_path( - &webview, - global_scope, - command_scope, - path, - options.as_ref().and_then(|o| o.base.base_dir), - )?; - - let mut opts = std::fs::OpenOptions::new(); - // defaults - opts.read(false).write(true).truncate(true).create(true); - - if let Some(options) = options { - #[cfg(unix)] - { - use std::os::unix::fs::OpenOptionsExt; - if let Some(mode) = options.mode { - opts.mode(mode); - } - } - - opts.create(options.create) - .append(options.append) - .truncate(!options.append) - .create_new(options.create_new); - } - - let mut file = opts.open(&resolved_path).map_err(|e| { - format!( - "failed to open file at path: {} with error: {e}", - resolved_path.display() - ) - })?; - - file.write_all(data) - .map_err(|e| { - format!( - "failed to write bytes to file at path: {} with error: {e}", - resolved_path.display() - ) - }) - .map_err(Into::into) -} - -#[tauri::command] -pub fn write_file( - webview: Webview, - global_scope: GlobalScope, - command_scope: CommandScope, - request: tauri::ipc::Request<'_>, -) -> CommandResult<()> { - if let tauri::ipc::InvokeBody::Raw(data) = request.body() { - let path = request - .headers() - .get("path") - .ok_or_else(|| anyhow::anyhow!("missing file path").into()) - .and_then(|p| { - p.to_str() - .map_err(|e| anyhow::anyhow!("invalid path: {e}").into()) - }) - .and_then(|p| SafePathBuf::new(p.into()).map_err(CommandError::from))?; - let options = request - .headers() - .get("options") - .and_then(|p| p.to_str().ok()) - .and_then(|opts| serde_json::from_str(opts).ok()); - write_file_inner(webview, &global_scope, &command_scope, path, data, options) - } else { - Err(anyhow::anyhow!("unexpected invoke body").into()) - } -} - -#[tauri::command] -pub fn write_text_file( - webview: Webview, - global_scope: GlobalScope, - command_scope: CommandScope, - path: SafePathBuf, - data: String, - options: Option, -) -> CommandResult<()> { - write_file_inner( - webview, - &global_scope, - &command_scope, - path, - data.as_bytes(), - options, - ) -} - -#[tauri::command] -pub fn exists( - webview: Webview, - global_scope: GlobalScope, - command_scope: CommandScope, - path: SafePathBuf, - options: Option, -) -> CommandResult { - let resolved_path = resolve_path( - &webview, - &global_scope, - &command_scope, - path, - options.as_ref().and_then(|o| o.base_dir), - )?; - Ok(resolved_path.exists()) -} - -pub fn resolve_path( - app: &Webview, - global_scope: &GlobalScope, - command_scope: &CommandScope, - path: SafePathBuf, - base_dir: Option, -) -> CommandResult { - let path = file_url_to_safe_pathbuf(path)?; - let path = if let Some(base_dir) = base_dir { - app.path().resolve(&path, base_dir)? - } else { - path.as_ref().to_path_buf() - }; - - let scope = tauri::scope::fs::Scope::new( - app, - &FsScope::Scope { - allow: app - .fs_scope() - .allowed - .lock() - .unwrap() - .clone() - .into_iter() - .chain(global_scope.allows().iter().map(|e| e.path.clone())) - .chain(command_scope.allows().iter().map(|e| e.path.clone())) - .collect(), - deny: app - .fs_scope() - .denied - .lock() - .unwrap() - .clone() - .into_iter() - .chain(global_scope.denies().iter().map(|e| e.path.clone())) - .chain(command_scope.denies().iter().map(|e| e.path.clone())) - .collect(), - require_literal_leading_dot: None, - }, - )?; - - if scope.is_allowed(&path) { - Ok(path) - } else { - Err(CommandError::Plugin(Error::PathForbidden(path))) - } -} - -#[inline] -fn file_url_to_safe_pathbuf(path: SafePathBuf) -> CommandResult { - if path.as_ref().starts_with("file:") { - let url = url::Url::parse(&path.display().to_string())? - .to_file_path() - .map_err(|_| "failed to get path from `file:` url")?; - SafePathBuf::new(url).map_err(Into::into) - } else { - Ok(path) - } -} - -struct StdFileResource(Mutex); - -impl StdFileResource { - fn new(file: File) -> Self { - Self(Mutex::new(file)) - } - - fn with_lock R>(&self, mut f: F) -> R { - let file = self.0.lock().unwrap(); - f(&file) - } -} - -impl Resource for StdFileResource {} - -struct StdLinesResource(Mutex>>); - -impl StdLinesResource { - fn new(lines: Lines>) -> Self { - Self(Mutex::new(lines)) - } - - fn with_lock>) -> R>(&self, mut f: F) -> R { - let mut lines = self.0.lock().unwrap(); - f(&mut lines) - } -} - -impl Resource for StdLinesResource {} - -// taken from deno source code: https://github.com/denoland/deno/blob/ffffa2f7c44bd26aec5ae1957e0534487d099f48/runtime/ops/fs.rs#L913 -#[inline] -fn to_msec(maybe_time: std::result::Result) -> Option { - match maybe_time { - Ok(time) => { - let msec = time - .duration_since(UNIX_EPOCH) - .map(|t| t.as_millis() as u64) - .unwrap_or_else(|err| err.duration().as_millis() as u64); - Some(msec) - } - Err(_) => None, - } -} - -// taken from deno source code: https://github.com/denoland/deno/blob/ffffa2f7c44bd26aec5ae1957e0534487d099f48/runtime/ops/fs.rs#L926 -#[derive(Debug, Clone, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct FileInfo { - is_file: bool, - is_directory: bool, - is_symlink: bool, - size: u64, - // In milliseconds, like JavaScript. Available on both Unix or Windows. - mtime: Option, - atime: Option, - birthtime: Option, - readonly: bool, - // Following are only valid under Windows. - file_attribues: Option, - // Following are only valid under Unix. - dev: Option, - ino: Option, - mode: Option, - nlink: Option, - uid: Option, - gid: Option, - rdev: Option, - blksize: Option, - blocks: Option, -} - -// taken from deno source code: https://github.com/denoland/deno/blob/ffffa2f7c44bd26aec5ae1957e0534487d099f48/runtime/ops/fs.rs#L950 -#[inline(always)] -fn get_stat(metadata: std::fs::Metadata) -> FileInfo { - // Unix stat member (number types only). 0 if not on unix. - macro_rules! usm { - ($member:ident) => {{ - #[cfg(unix)] - { - Some(metadata.$member()) - } - #[cfg(not(unix))] - { - None - } - }}; - } - - #[cfg(unix)] - use std::os::unix::fs::MetadataExt; - #[cfg(windows)] - use std::os::windows::fs::MetadataExt; - FileInfo { - is_file: metadata.is_file(), - is_directory: metadata.is_dir(), - is_symlink: metadata.file_type().is_symlink(), - size: metadata.len(), - // In milliseconds, like JavaScript. Available on both Unix or Windows. - mtime: to_msec(metadata.modified()), - atime: to_msec(metadata.accessed()), - birthtime: to_msec(metadata.created()), - readonly: metadata.permissions().readonly(), - // Following are only valid under Windows. - #[cfg(windows)] - file_attribues: Some(metadata.file_attributes()), - #[cfg(not(windows))] - file_attribues: None, - // Following are only valid under Unix. - dev: usm!(dev), - ino: usm!(ino), - mode: usm!(mode), - nlink: usm!(nlink), - uid: usm!(uid), - gid: usm!(gid), - rdev: usm!(rdev), - blksize: usm!(blksize), - blocks: usm!(blocks), - } -} diff --git a/src/config.rs b/src/config.rs deleted file mode 100644 index d8a1971..0000000 --- a/src/config.rs +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright 2019-2023 Tauri Programme within The Commons Conservancy -// SPDX-License-Identifier: Apache-2.0 -// SPDX-License-Identifier: MIT - -use serde::Deserialize; - -#[derive(Deserialize)] -pub struct Config { - /// Whether or not paths that contain components that start with a `.` - /// will require that `.` appears literally in the pattern; `*`, `?`, `**`, - /// or `[...]` will not match. This is useful because such files are - /// conventionally considered hidden on Unix systems and it might be - /// desirable to skip them when listing files. - /// - /// Defaults to `true` on Unix systems and `false` on Windows - // dotfiles are not supposed to be exposed by default on unix - pub require_literal_leading_dot: Option, -} diff --git a/src/error.rs b/src/error.rs deleted file mode 100644 index 2d12b5e..0000000 --- a/src/error.rs +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright 2019-2023 Tauri Programme within The Commons Conservancy -// SPDX-License-Identifier: Apache-2.0 -// SPDX-License-Identifier: MIT - -use std::path::PathBuf; - -use serde::{Serialize, Serializer}; - -#[derive(Debug, thiserror::Error)] -pub enum Error { - #[error(transparent)] - Json(#[from] serde_json::Error), - #[error(transparent)] - Tauri(#[from] tauri::Error), - #[error(transparent)] - Io(#[from] std::io::Error), - #[error("forbidden path: {0}")] - PathForbidden(PathBuf), - /// Invalid glob pattern. - #[error("invalid glob pattern: {0}")] - GlobPattern(#[from] glob::PatternError), - /// Watcher error. - #[cfg(feature = "watch")] - #[error(transparent)] - Watch(#[from] notify::Error), -} - -impl Serialize for Error { - fn serialize(&self, serializer: S) -> std::result::Result - where - S: Serializer, - { - serializer.serialize_str(self.to_string().as_ref()) - } -} diff --git a/src/lib.rs b/src/lib.rs deleted file mode 100644 index 2041936..0000000 --- a/src/lib.rs +++ /dev/null @@ -1,128 +0,0 @@ -// Copyright 2019-2023 Tauri Programme within The Commons Conservancy -// SPDX-License-Identifier: Apache-2.0 -// SPDX-License-Identifier: MIT - -//! [![](https://github.com/tauri-apps/plugins-workspace/raw/v2/plugins/fs/banner.png)](https://github.com/tauri-apps/plugins-workspace/tree/v2/plugins/fs) -//! -//! Access the file system. - -#![doc( - html_logo_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png", - html_favicon_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png" -)] - -use tauri::{ - ipc::ScopeObject, - plugin::{Builder as PluginBuilder, TauriPlugin}, - utils::acl::Value, - AppHandle, DragDropEvent, Manager, RunEvent, Runtime, WindowEvent, -}; - -mod commands; -mod config; -mod error; -mod scope; -#[cfg(feature = "watch")] -mod watcher; - -pub use error::Error; -pub use scope::{Event as ScopeEvent, Scope}; - -type Result = std::result::Result; - -// implement ScopeObject here instead of in the scope module because it is also used on the build script -// and we don't want to add tauri as a build dependency -impl ScopeObject for scope::Entry { - type Error = Error; - fn deserialize( - app: &AppHandle, - raw: Value, - ) -> std::result::Result { - let entry = serde_json::from_value(raw.into()).map(|raw| { - let path = match raw { - scope::EntryRaw::Value(path) => path, - scope::EntryRaw::Object { path } => path, - }; - Self { path } - })?; - - Ok(Self { - path: app.path().parse(entry.path)?, - }) - } -} - -pub trait FsExt { - fn fs_scope(&self) -> &Scope; - fn try_fs_scope(&self) -> Option<&Scope>; -} - -impl> FsExt for T { - fn fs_scope(&self) -> &Scope { - self.state::().inner() - } - - fn try_fs_scope(&self) -> Option<&Scope> { - self.try_state::().map(|s| s.inner()) - } -} - -pub fn init() -> TauriPlugin> { - PluginBuilder::>::new("fs") - .invoke_handler(tauri::generate_handler![ - commands::create, - commands::open, - commands::copy_file, - commands::close, - commands::mkdir, - commands::read_dir, - commands::read, - commands::read_file, - commands::read_text_file, - commands::read_text_file_lines, - commands::read_text_file_lines_next, - commands::remove, - commands::rename, - commands::seek, - commands::stat, - commands::lstat, - commands::fstat, - commands::truncate, - commands::ftruncate, - commands::write, - commands::write_file, - commands::write_text_file, - commands::exists, - #[cfg(feature = "watch")] - watcher::watch, - #[cfg(feature = "watch")] - watcher::unwatch - ]) - .setup(|app, api| { - let mut scope = Scope::default(); - scope.require_literal_leading_dot = api - .config() - .as_ref() - .and_then(|c| c.require_literal_leading_dot); - app.manage(scope); - Ok(()) - }) - .on_event(|app, event| { - if let RunEvent::WindowEvent { - label: _, - event: WindowEvent::DragDrop(DragDropEvent::Dropped { paths, position: _ }), - .. - } = event - { - let scope = app.fs_scope(); - for path in paths { - if path.is_file() { - scope.allow_file(path); - } else { - scope.allow_directory(path, true); - } - } - } - }) - .build() -} diff --git a/src/scope.rs b/src/scope.rs deleted file mode 100644 index f31c786..0000000 --- a/src/scope.rs +++ /dev/null @@ -1,132 +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, - path::{Path, PathBuf}, - sync::{ - atomic::{AtomicU32, Ordering}, - Mutex, - }, -}; - -use serde::Deserialize; - -#[doc(hidden)] -#[derive(Deserialize)] -#[serde(untagged)] -pub enum EntryRaw { - Value(PathBuf), - Object { path: PathBuf }, -} - -#[derive(Debug)] -pub struct Entry { - pub path: PathBuf, -} - -pub type EventId = u32; -type EventListener = Box; - -/// Scope change event. -#[derive(Debug, Clone)] -pub enum Event { - /// A path has been allowed. - PathAllowed(PathBuf), - /// A path has been forbidden. - PathForbidden(PathBuf), -} - -#[derive(Default)] -pub struct Scope { - pub(crate) allowed: Mutex>, - pub(crate) denied: Mutex>, - event_listeners: Mutex>, - next_event_id: AtomicU32, - pub(crate) require_literal_leading_dot: Option, -} - -impl Scope { - /// 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>(&self, path: P, recursive: bool) { - let path = path.as_ref(); - - { - let mut allowed = self.allowed.lock().unwrap(); - allowed.push(path.to_path_buf()); - allowed.push(path.join(if recursive { "**" } else { "*" })); - } - - self.emit(Event::PathAllowed(path.to_path_buf())); - } - - /// 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) { - let path = path.as_ref(); - - self.allowed.lock().unwrap().push(path.to_path_buf()); - - self.emit(Event::PathAllowed(path.to_path_buf())); - } - - /// 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) { - let path = path.as_ref(); - - { - let mut denied = self.denied.lock().unwrap(); - denied.push(path.to_path_buf()); - denied.push(path.join(if recursive { "**" } else { "*" })); - } - - self.emit(Event::PathForbidden(path.to_path_buf())); - } - - /// 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) { - let path = path.as_ref(); - - self.denied.lock().unwrap().push(path.to_path_buf()); - - self.emit(Event::PathForbidden(path.to_path_buf())); - } - - /// List of allowed paths. - pub fn allowed(&self) -> Vec { - self.allowed.lock().unwrap().clone() - } - - /// List of forbidden paths. - pub fn forbidden(&self) -> Vec { - self.denied.lock().unwrap().clone() - } - - fn next_event_id(&self) -> u32 { - self.next_event_id.fetch_add(1, Ordering::Relaxed) - } - - fn emit(&self, event: Event) { - let listeners = self.event_listeners.lock().unwrap(); - let handlers = listeners.values(); - for listener in handlers { - listener(&event); - } - } - - /// Listen to an event on this scope. - pub fn listen(&self, f: F) -> EventId { - let id = self.next_event_id(); - self.event_listeners.lock().unwrap().insert(id, Box::new(f)); - id - } -} diff --git a/src/watcher.rs b/src/watcher.rs deleted file mode 100644 index 986aebf..0000000 --- a/src/watcher.rs +++ /dev/null @@ -1,158 +0,0 @@ -// Copyright 2019-2023 Tauri Programme within The Commons Conservancy -// SPDX-License-Identifier: Apache-2.0 -// SPDX-License-Identifier: MIT - -use notify::{Config, Event, RecommendedWatcher, RecursiveMode, Watcher}; -use notify_debouncer_full::{new_debouncer, DebounceEventResult, Debouncer, FileIdMap}; -use serde::Deserialize; -use tauri::{ - ipc::{Channel, CommandScope, GlobalScope}, - path::{BaseDirectory, SafePathBuf}, - Manager, Resource, ResourceId, Runtime, Webview, -}; - -use std::{ - path::PathBuf, - sync::{ - mpsc::{channel, Receiver}, - Mutex, - }, - thread::spawn, - time::Duration, -}; - -use crate::{ - commands::{resolve_path, CommandResult}, - scope::Entry, -}; - -struct InnerWatcher { - pub kind: WatcherKind, - paths: Vec, -} - -pub struct WatcherResource(Mutex); -impl WatcherResource { - fn new(kind: WatcherKind, paths: Vec) -> Self { - Self(Mutex::new(InnerWatcher { kind, paths })) - } - - fn with_lock R>(&self, mut f: F) -> R { - let mut watcher = self.0.lock().unwrap(); - f(&mut watcher) - } -} - -impl Resource for WatcherResource {} - -enum WatcherKind { - Debouncer(Debouncer), - Watcher(RecommendedWatcher), -} - -fn watch_raw(on_event: Channel, rx: Receiver>) { - spawn(move || { - while let Ok(event) = rx.recv() { - if let Ok(event) = event { - // TODO: Should errors be emitted too? - let _ = on_event.send(&event); - } - } - }); -} - -fn watch_debounced(on_event: Channel, rx: Receiver) { - spawn(move || { - while let Ok(Ok(events)) = rx.recv() { - for event in events { - // TODO: Should errors be emitted too? - let _ = on_event.send(&event.event); - } - } - }); -} - -#[derive(Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct WatchOptions { - base_dir: Option, - recursive: bool, - delay_ms: Option, -} - -#[tauri::command] -pub async fn watch( - webview: Webview, - paths: Vec, - options: WatchOptions, - on_event: Channel, - global_scope: GlobalScope, - command_scope: CommandScope, -) -> CommandResult { - let mut resolved_paths = Vec::with_capacity(paths.capacity()); - for path in paths { - resolved_paths.push(resolve_path( - &webview, - &global_scope, - &command_scope, - path, - options.base_dir, - )?); - } - - let recursive_mode = if options.recursive { - RecursiveMode::Recursive - } else { - RecursiveMode::NonRecursive - }; - - let kind = if let Some(delay) = options.delay_ms { - let (tx, rx) = channel(); - let mut debouncer = new_debouncer(Duration::from_millis(delay), None, tx)?; - for path in &resolved_paths { - debouncer.watcher().watch(path.as_ref(), recursive_mode)?; - debouncer.cache().add_root(path, recursive_mode); - } - watch_debounced(on_event, rx); - WatcherKind::Debouncer(debouncer) - } else { - let (tx, rx) = channel(); - let mut watcher = RecommendedWatcher::new(tx, Config::default())?; - for path in &resolved_paths { - watcher.watch(path.as_ref(), recursive_mode)?; - } - watch_raw(on_event, rx); - WatcherKind::Watcher(watcher) - }; - - let rid = webview - .resources_table() - .add(WatcherResource::new(kind, resolved_paths)); - - Ok(rid) -} - -#[tauri::command] -pub async fn unwatch(webview: Webview, rid: ResourceId) -> CommandResult<()> { - let watcher = webview.resources_table().take::(rid)?; - WatcherResource::with_lock(&watcher, |watcher| { - match &mut watcher.kind { - WatcherKind::Debouncer(ref mut debouncer) => { - for path in &watcher.paths { - debouncer.watcher().unwatch(path.as_ref()).map_err(|e| { - format!("failed to unwatch path: {} with error: {e}", path.display()) - })?; - } - } - WatcherKind::Watcher(ref mut w) => { - for path in &watcher.paths { - w.unwatch(path.as_ref()).map_err(|e| { - format!("failed to unwatch path: {} with error: {e}", path.display()) - })?; - } - } - } - - Ok(()) - }) -}