diff --git a/Source/commands.rs b/Source/commands.rs index b567a08..cb40c3e 100644 --- a/Source/commands.rs +++ b/Source/commands.rs @@ -6,66 +6,66 @@ use serde::{Deserialize, Serialize, Serializer}; use serde_repr::{Deserialize_repr, Serialize_repr}; use tauri::{ - ipc::{CommandScope, GlobalScope}, - path::BaseDirectory, - utils::config::FsScope, - AppHandle, Manager, Resource, ResourceId, Runtime, Webview, + ipc::{CommandScope, GlobalScope}, + path::BaseDirectory, + utils::config::FsScope, + AppHandle, Manager, Resource, ResourceId, Runtime, Webview, }; use std::{ - borrow::Cow, - fs::File, - io::{BufReader, Lines, Read, Write}, - path::{Path, PathBuf}, - str::FromStr, - sync::Mutex, - time::{SystemTime, UNIX_EPOCH}, + borrow::Cow, + fs::File, + io::{BufReader, Lines, Read, Write}, + path::{Path, PathBuf}, + str::FromStr, + sync::Mutex, + time::{SystemTime, UNIX_EPOCH}, }; use crate::{scope::Entry, Error, FsExt, SafeFilePath}; #[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)] - Json(#[from] serde_json::Error), - #[error(transparent)] - Io(#[from] std::io::Error), - #[error(transparent)] - UrlParseError(#[from] url::ParseError), - #[cfg(feature = "watch")] - #[error(transparent)] - Watcher(#[from] notify::Error), + #[error(transparent)] + Anyhow(#[from] anyhow::Error), + #[error(transparent)] + Plugin(#[from] Error), + #[error(transparent)] + Tauri(#[from] tauri::Error), + #[error(transparent)] + Json(#[from] serde_json::Error), + #[error(transparent)] + Io(#[from] std::io::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)) - } + 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())) - } + 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()) - } - } + 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; @@ -73,871 +73,961 @@ pub type CommandResult = std::result::Result; #[derive(Debug, Default, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct BaseOptions { - base_dir: Option, + base_dir: Option, } #[tauri::command] pub fn create( - webview: Webview, - global_scope: GlobalScope, - command_scope: CommandScope, - path: SafeFilePath, - options: Option, + webview: Webview, + global_scope: GlobalScope, + command_scope: CommandScope, + path: SafeFilePath, + 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) + 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, Default, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct OpenOptions { - #[serde(flatten)] - base: BaseOptions, - #[serde(flatten)] - options: crate::OpenOptions, + #[serde(flatten)] + base: BaseOptions, + #[serde(flatten)] + options: crate::OpenOptions, } #[tauri::command] pub fn open( - webview: Webview, - global_scope: GlobalScope, - command_scope: CommandScope, - path: SafeFilePath, - options: Option, + webview: Webview, + global_scope: GlobalScope, + command_scope: CommandScope, + path: SafeFilePath, + options: Option, ) -> CommandResult { - let (file, _path) = resolve_file( - &webview, - &global_scope, - &command_scope, - path, - if let Some(opts) = options { - OpenOptions { base: opts.base, options: opts.options } - } else { - OpenOptions { - base: BaseOptions { base_dir: None }, - options: crate::OpenOptions { - read: true, - write: false, - truncate: false, - create: false, - create_new: false, - append: false, - mode: None, - custom_flags: None, - }, - } - }, - )?; - - let rid = webview.resources_table().add(StdFileResource::new(file)); - - Ok(rid) + let (file, _path) = resolve_file( + &webview, + &global_scope, + &command_scope, + path, + if let Some(opts) = options { + OpenOptions { + base: opts.base, + options: opts.options, + } + } else { + OpenOptions { + base: BaseOptions { base_dir: None }, + options: crate::OpenOptions { + read: true, + write: false, + truncate: false, + create: false, + create_new: false, + append: false, + mode: None, + custom_flags: None, + }, + } + }, + )?; + + 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) + 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, + from_path_base_dir: Option, + to_path_base_dir: Option, } #[tauri::command] pub async fn copy_file( - webview: Webview, - global_scope: GlobalScope, - command_scope: CommandScope, - from_path: SafeFilePath, - to_path: SafeFilePath, - options: Option, + webview: Webview, + global_scope: GlobalScope, + command_scope: CommandScope, + from_path: SafeFilePath, + to_path: SafeFilePath, + 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(()) + 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, + #[serde(flatten)] + base: BaseOptions, + #[allow(unused)] + mode: Option, + recursive: Option, } #[tauri::command] pub fn mkdir( - webview: Webview, - global_scope: GlobalScope, - command_scope: CommandScope, - path: SafeFilePath, - options: Option, + webview: Webview, + global_scope: GlobalScope, + command_scope: CommandScope, + path: SafeFilePath, + 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) + 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, + 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) + 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 async fn read_dir( - webview: Webview, - global_scope: GlobalScope, - command_scope: CommandScope, - path: SafeFilePath, - options: Option, + webview: Webview, + global_scope: GlobalScope, + command_scope: CommandScope, + path: SafeFilePath, + 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) + 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 async fn read( - webview: Webview, - rid: ResourceId, - len: u32, + 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)) + 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 async fn read_file( - webview: Webview, - global_scope: GlobalScope, - command_scope: CommandScope, - path: SafeFilePath, - options: Option, + webview: Webview, + global_scope: GlobalScope, + command_scope: CommandScope, + path: SafeFilePath, + options: Option, ) -> CommandResult { - let (mut file, path) = resolve_file( - &webview, - &global_scope, - &command_scope, - path, - OpenOptions { - base: BaseOptions { base_dir: options.as_ref().and_then(|o| o.base_dir) }, - options: crate::OpenOptions { read: true, ..Default::default() }, - }, - )?; - - let mut contents = Vec::new(); - - file.read_to_end(&mut contents).map_err(|e| { - format!("failed to read file as text at path: {} with error: {e}", path.display()) - })?; - - Ok(tauri::ipc::Response::new(contents)) + let (mut file, path) = resolve_file( + &webview, + &global_scope, + &command_scope, + path, + OpenOptions { + base: BaseOptions { + base_dir: options.as_ref().and_then(|o| o.base_dir), + }, + options: crate::OpenOptions { + read: true, + ..Default::default() + }, + }, + )?; + + let mut contents = Vec::new(); + + file.read_to_end(&mut contents).map_err(|e| { + format!( + "failed to read file as text at path: {} with error: {e}", + path.display() + ) + })?; + + Ok(tauri::ipc::Response::new(contents)) } #[tauri::command] pub async fn read_text_file( - webview: Webview, - global_scope: GlobalScope, - command_scope: CommandScope, - path: SafeFilePath, - options: Option, + webview: Webview, + global_scope: GlobalScope, + command_scope: CommandScope, + path: SafeFilePath, + options: Option, ) -> CommandResult { - let (mut file, path) = resolve_file( - &webview, - &global_scope, - &command_scope, - path, - OpenOptions { - base: BaseOptions { base_dir: options.as_ref().and_then(|o| o.base_dir) }, - options: crate::OpenOptions { read: true, ..Default::default() }, - }, - )?; - - let mut contents = String::new(); - - file.read_to_string(&mut contents).map_err(|e| { - format!("failed to read file as text at path: {} with error: {e}", path.display()) - })?; - - Ok(contents) + let (mut file, path) = resolve_file( + &webview, + &global_scope, + &command_scope, + path, + OpenOptions { + base: BaseOptions { + base_dir: options.as_ref().and_then(|o| o.base_dir), + }, + options: crate::OpenOptions { + read: true, + ..Default::default() + }, + }, + )?; + + let mut contents = String::new(); + + file.read_to_string(&mut contents).map_err(|e| { + format!( + "failed to read file as text at path: {} with error: {e}", + path.display() + ) + })?; + + Ok(contents) } #[tauri::command] pub fn read_text_file_lines( - webview: Webview, - global_scope: GlobalScope, - command_scope: CommandScope, - path: SafeFilePath, - options: Option, + webview: Webview, + global_scope: GlobalScope, + command_scope: CommandScope, + path: SafeFilePath, + 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) + 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 async fn read_text_file_lines_next( - webview: Webview, - rid: ResourceId, + webview: Webview, + rid: ResourceId, ) -> CommandResult<(Option, bool)> { - let mut resource_table = webview.resources_table(); - let lines = resource_table.get::(rid)?; + 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) - }) - }); + 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) + Ok(ret) } #[derive(Debug, Clone, Deserialize)] pub struct RemoveOptions { - #[serde(flatten)] - base: BaseOptions, - recursive: Option, + #[serde(flatten)] + base: BaseOptions, + recursive: Option, } #[tauri::command] pub fn remove( - webview: Webview, - global_scope: GlobalScope, - command_scope: CommandScope, - path: SafeFilePath, - options: Option, + webview: Webview, + global_scope: GlobalScope, + command_scope: CommandScope, + path: SafeFilePath, + 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) + 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, + 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: SafeFilePath, - new_path: SafeFilePath, - options: Option, + webview: Webview, + global_scope: GlobalScope, + command_scope: CommandScope, + old_path: SafeFilePath, + new_path: SafeFilePath, + 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) + 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, + Start = 0, + Current = 1, + End = 2, } #[tauri::command] pub async fn seek( - webview: Webview, - rid: ResourceId, - offset: i64, - whence: SeekMode, + 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) + 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) } #[cfg(target_os = "android")] fn get_metadata std::io::Result>( - metadata_fn: F, - webview: &Webview, - global_scope: &GlobalScope, - command_scope: &CommandScope, - path: SafeFilePath, - options: Option, + metadata_fn: F, + webview: &Webview, + global_scope: &GlobalScope, + command_scope: &CommandScope, + path: SafeFilePath, + options: Option, ) -> CommandResult { - match path { - SafeFilePath::Url(url) => { - let (file, path) = resolve_file( - webview, - global_scope, - command_scope, - SafeFilePath::Url(url), - OpenOptions { - base: BaseOptions { base_dir: None }, - options: crate::OpenOptions { read: true, ..Default::default() }, - }, - )?; - file.metadata().map_err(|e| { - format!("failed to get metadata of path: {} with error: {e}", path.display()).into() - }) - } - SafeFilePath::Path(p) => get_fs_metadata( - metadata_fn, - webview, - global_scope, - command_scope, - SafeFilePath::Path(p), - options, - ), - } + match path { + SafeFilePath::Url(url) => { + let (file, path) = resolve_file( + webview, + global_scope, + command_scope, + SafeFilePath::Url(url), + OpenOptions { + base: BaseOptions { base_dir: None }, + options: crate::OpenOptions { + read: true, + ..Default::default() + }, + }, + )?; + file.metadata().map_err(|e| { + format!( + "failed to get metadata of path: {} with error: {e}", + path.display() + ) + .into() + }) + } + SafeFilePath::Path(p) => get_fs_metadata( + metadata_fn, + webview, + global_scope, + command_scope, + SafeFilePath::Path(p), + options, + ), + } } #[cfg(not(target_os = "android"))] fn get_metadata std::io::Result>( - metadata_fn: F, - webview: &Webview, - global_scope: &GlobalScope, - command_scope: &CommandScope, - path: SafeFilePath, - options: Option, + metadata_fn: F, + webview: &Webview, + global_scope: &GlobalScope, + command_scope: &CommandScope, + path: SafeFilePath, + options: Option, ) -> CommandResult { - get_fs_metadata(metadata_fn, webview, global_scope, command_scope, path, options) + get_fs_metadata( + metadata_fn, + webview, + global_scope, + command_scope, + path, + options, + ) } fn get_fs_metadata std::io::Result>( - metadata_fn: F, - webview: &Webview, - global_scope: &GlobalScope, - command_scope: &CommandScope, - path: SafeFilePath, - options: Option, + metadata_fn: F, + webview: &Webview, + global_scope: &GlobalScope, + command_scope: &CommandScope, + path: SafeFilePath, + 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 = metadata_fn(&resolved_path).map_err(|e| { - format!("failed to get metadata of path: {} with error: {e}", resolved_path.display()) - })?; - Ok(metadata) + let resolved_path = resolve_path( + webview, + global_scope, + command_scope, + path, + options.as_ref().and_then(|o| o.base_dir), + )?; + let metadata = metadata_fn(&resolved_path).map_err(|e| { + format!( + "failed to get metadata of path: {} with error: {e}", + resolved_path.display() + ) + })?; + Ok(metadata) } #[tauri::command] pub fn stat( - webview: Webview, - global_scope: GlobalScope, - command_scope: CommandScope, - path: SafeFilePath, - options: Option, + webview: Webview, + global_scope: GlobalScope, + command_scope: CommandScope, + path: SafeFilePath, + options: Option, ) -> CommandResult { - let metadata = get_metadata( - |p| std::fs::metadata(p), - &webview, - &global_scope, - &command_scope, - path, - options, - )?; - - Ok(get_stat(metadata)) + let metadata = get_metadata( + |p| std::fs::metadata(p), + &webview, + &global_scope, + &command_scope, + path, + options, + )?; + + Ok(get_stat(metadata)) } #[tauri::command] pub fn lstat( - webview: Webview, - global_scope: GlobalScope, - command_scope: CommandScope, - path: SafeFilePath, - options: Option, + webview: Webview, + global_scope: GlobalScope, + command_scope: CommandScope, + path: SafeFilePath, + options: Option, ) -> CommandResult { - let metadata = get_metadata( - |p| std::fs::symlink_metadata(p), - &webview, - &global_scope, - &command_scope, - path, - options, - )?; - Ok(get_stat(metadata)) + let metadata = get_metadata( + |p| std::fs::symlink_metadata(p), + &webview, + &global_scope, + &command_scope, + path, + options, + )?; + 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)) + 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 async fn truncate( - webview: Webview, - global_scope: GlobalScope, - command_scope: CommandScope, - path: SafeFilePath, - len: Option, - options: Option, + webview: Webview, + global_scope: GlobalScope, + command_scope: CommandScope, + path: SafeFilePath, + 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) + 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 async fn ftruncate( - webview: Webview, - rid: ResourceId, - len: Option, + 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) + 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 async fn write( - webview: Webview, - rid: ResourceId, - data: Vec, + 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) + 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, + #[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 + true } fn write_file_inner( - webview: Webview, - global_scope: &GlobalScope, - command_scope: &CommandScope, - path: SafeFilePath, - data: &[u8], - options: Option, + webview: Webview, + global_scope: &GlobalScope, + command_scope: &CommandScope, + path: SafeFilePath, + data: &[u8], + options: Option, ) -> CommandResult<()> { - let (mut file, path) = resolve_file( - &webview, - global_scope, - command_scope, - path, - if let Some(opts) = options { - OpenOptions { - base: opts.base, - options: crate::OpenOptions { - read: false, - write: true, - create: opts.create, - truncate: !opts.append, - append: opts.append, - create_new: opts.create_new, - mode: opts.mode, - custom_flags: None, - }, - } - } else { - OpenOptions { - base: BaseOptions { base_dir: None }, - options: crate::OpenOptions { - read: false, - write: true, - truncate: true, - create: true, - create_new: false, - append: false, - mode: None, - custom_flags: None, - }, - } - }, - )?; - - file.write_all(data) - .map_err(|e| { - format!("failed to write bytes to file at path: {} with error: {e}", path.display()) - }) - .map_err(Into::into) + let (mut file, path) = resolve_file( + &webview, + global_scope, + command_scope, + path, + if let Some(opts) = options { + OpenOptions { + base: opts.base, + options: crate::OpenOptions { + read: false, + write: true, + create: opts.create, + truncate: !opts.append, + append: opts.append, + create_new: opts.create_new, + mode: opts.mode, + custom_flags: None, + }, + } + } else { + OpenOptions { + base: BaseOptions { base_dir: None }, + options: crate::OpenOptions { + read: false, + write: true, + truncate: true, + create: true, + create_new: false, + append: false, + mode: None, + custom_flags: None, + }, + } + }, + )?; + + file.write_all(data) + .map_err(|e| { + format!( + "failed to write bytes to file at path: {} with error: {e}", + path.display() + ) + }) + .map_err(Into::into) } #[tauri::command] pub async fn write_file( - webview: Webview, - global_scope: GlobalScope, - command_scope: CommandScope, - request: tauri::ipc::Request<'_>, + webview: Webview, + global_scope: GlobalScope, + command_scope: CommandScope, + request: tauri::ipc::Request<'_>, ) -> CommandResult<()> { - let data = match request.body() { - tauri::ipc::InvokeBody::Raw(data) => Cow::Borrowed(data), - tauri::ipc::InvokeBody::Json(serde_json::Value::Array(data)) => Cow::Owned( - data.iter() - .flat_map(|v| v.as_number().and_then(|v| v.as_u64().map(|v| v as u8))) - .collect(), - ), - _ => return Err(anyhow::anyhow!("unexpected invoke body").into()), - }; - - let path = request - .headers() - .get("path") - .ok_or_else(|| anyhow::anyhow!("missing file path").into()) - .and_then(|p| { - percent_encoding::percent_decode(p.as_ref()) - .decode_utf8() - .map_err(|_| anyhow::anyhow!("path is not a valid UTF-8").into()) - }) - .and_then(|p| SafeFilePath::from_str(&p).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) + let data = match request.body() { + tauri::ipc::InvokeBody::Raw(data) => Cow::Borrowed(data), + tauri::ipc::InvokeBody::Json(serde_json::Value::Array(data)) => Cow::Owned( + data.iter() + .flat_map(|v| v.as_number().and_then(|v| v.as_u64().map(|v| v as u8))) + .collect(), + ), + _ => return Err(anyhow::anyhow!("unexpected invoke body").into()), + }; + + let path = request + .headers() + .get("path") + .ok_or_else(|| anyhow::anyhow!("missing file path").into()) + .and_then(|p| { + percent_encoding::percent_decode(p.as_ref()) + .decode_utf8() + .map_err(|_| anyhow::anyhow!("path is not a valid UTF-8").into()) + }) + .and_then(|p| SafeFilePath::from_str(&p).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) } #[tauri::command] pub async fn write_text_file( - #[allow(unused)] app: AppHandle, - #[allow(unused)] webview: Webview, - #[allow(unused)] global_scope: GlobalScope, - #[allow(unused)] command_scope: CommandScope, - path: SafeFilePath, - data: String, - #[allow(unused)] options: Option, + #[allow(unused)] app: AppHandle, + #[allow(unused)] webview: Webview, + #[allow(unused)] global_scope: GlobalScope, + #[allow(unused)] command_scope: CommandScope, + path: SafeFilePath, + data: String, + #[allow(unused)] options: Option, ) -> CommandResult<()> { - write_file_inner(webview, &global_scope, &command_scope, path, data.as_bytes(), options) + 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: SafeFilePath, - options: Option, + webview: Webview, + global_scope: GlobalScope, + command_scope: CommandScope, + path: SafeFilePath, + 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()) + let resolved_path = resolve_path( + &webview, + &global_scope, + &command_scope, + path, + options.as_ref().and_then(|o| o.base_dir), + )?; + Ok(resolved_path.exists()) } #[cfg(not(target_os = "android"))] pub fn resolve_file( - webview: &Webview, - global_scope: &GlobalScope, - command_scope: &CommandScope, - path: SafeFilePath, - open_options: OpenOptions, + webview: &Webview, + global_scope: &GlobalScope, + command_scope: &CommandScope, + path: SafeFilePath, + open_options: OpenOptions, ) -> CommandResult<(File, PathBuf)> { - resolve_file_in_fs(webview, global_scope, command_scope, path, open_options) + resolve_file_in_fs(webview, global_scope, command_scope, path, open_options) } fn resolve_file_in_fs( - webview: &Webview, - global_scope: &GlobalScope, - command_scope: &CommandScope, - path: SafeFilePath, - open_options: OpenOptions, + webview: &Webview, + global_scope: &GlobalScope, + command_scope: &CommandScope, + path: SafeFilePath, + open_options: OpenOptions, ) -> CommandResult<(File, PathBuf)> { - let path = - resolve_path(webview, global_scope, command_scope, path, open_options.base.base_dir)?; - - let file = std::fs::OpenOptions::from(open_options.options) - .open(&path) - .map_err(|e| format!("failed to open file at path: {} with error: {e}", path.display()))?; - Ok((file, path)) + let path = resolve_path( + webview, + global_scope, + command_scope, + path, + open_options.base.base_dir, + )?; + + let file = std::fs::OpenOptions::from(open_options.options) + .open(&path) + .map_err(|e| { + format!( + "failed to open file at path: {} with error: {e}", + path.display() + ) + })?; + Ok((file, path)) } #[cfg(target_os = "android")] pub fn resolve_file( - webview: &Webview, - global_scope: &GlobalScope, - command_scope: &CommandScope, - path: SafeFilePath, - open_options: OpenOptions, + webview: &Webview, + global_scope: &GlobalScope, + command_scope: &CommandScope, + path: SafeFilePath, + open_options: OpenOptions, ) -> CommandResult<(File, PathBuf)> { - match path { - SafeFilePath::Url(url) => { - let path = url.as_str().into(); - let file = webview.fs().open(SafeFilePath::Url(url), open_options.options)?; - Ok((file, path)) - } - SafeFilePath::Path(path) => resolve_file_in_fs( - webview, - global_scope, - command_scope, - SafeFilePath::Path(path), - open_options, - ), - } + match path { + SafeFilePath::Url(url) => { + let path = url.as_str().into(); + let file = webview + .fs() + .open(SafeFilePath::Url(url), open_options.options)?; + Ok((file, path)) + } + SafeFilePath::Path(path) => resolve_file_in_fs( + webview, + global_scope, + command_scope, + SafeFilePath::Path(path), + open_options, + ), + } } pub fn resolve_path( - webview: &Webview, - global_scope: &GlobalScope, - command_scope: &CommandScope, - path: SafeFilePath, - base_dir: Option, + webview: &Webview, + global_scope: &GlobalScope, + command_scope: &CommandScope, + path: SafeFilePath, + base_dir: Option, ) -> CommandResult { - let path = path.into_path()?; - let path = - if let Some(base_dir) = base_dir { webview.path().resolve(&path, base_dir)? } else { path }; - - let scope = tauri::scope::fs::Scope::new( - webview, - &FsScope::Scope { - allow: webview - .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: webview - .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: webview.fs_scope().require_literal_leading_dot, - }, - )?; - - if scope.is_allowed(&path) { - Ok(path) - } else { - Err(CommandError::Plugin(Error::PathForbidden(path))) - } + let path = path.into_path()?; + let path = if let Some(base_dir) = base_dir { + webview.path().resolve(&path, base_dir)? + } else { + path + }; + + let scope = tauri::scope::fs::Scope::new( + webview, + &FsScope::Scope { + allow: webview + .fs_scope() + .allowed + .lock() + .unwrap() + .clone() + .into_iter() + .chain(global_scope.allows().iter().filter_map(|e| e.path.clone())) + .chain(command_scope.allows().iter().filter_map(|e| e.path.clone())) + .collect(), + deny: webview + .fs_scope() + .denied + .lock() + .unwrap() + .clone() + .into_iter() + .chain(global_scope.denies().iter().filter_map(|e| e.path.clone())) + .chain(command_scope.denies().iter().filter_map(|e| e.path.clone())) + .collect(), + require_literal_leading_dot: webview.fs_scope().require_literal_leading_dot, + }, + )?; + + if scope.is_allowed(&path) { + Ok(path) + } else { + Err(CommandError::Plugin(Error::PathForbidden(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) - } + 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 {} @@ -945,14 +1035,14 @@ 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) - } + 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 {} @@ -960,106 +1050,106 @@ 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, - } + 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, + 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), - } + // 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), + } } mod test { - #[test] - fn safe_file_path_parse() { - use super::SafeFilePath; - - assert!(matches!( - serde_json::from_str::("\"C:/Users\""), - Ok(SafeFilePath::Path(_)) - )); - assert!(matches!( - serde_json::from_str::("\"file:///C:/Users\""), - Ok(SafeFilePath::Url(_)) - )); - } + #[test] + fn safe_file_path_parse() { + use super::SafeFilePath; + + assert!(matches!( + serde_json::from_str::("\"C:/Users\""), + Ok(SafeFilePath::Path(_)) + )); + assert!(matches!( + serde_json::from_str::("\"file:///C:/Users\""), + Ok(SafeFilePath::Url(_)) + )); + } } diff --git a/Source/config.rs b/Source/config.rs index 672ce42..db3bae4 100644 --- a/Source/config.rs +++ b/Source/config.rs @@ -7,13 +7,13 @@ use serde::Deserialize; #[derive(Deserialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] 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, + /// 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/Source/desktop.rs b/Source/desktop.rs index f221369..477c053 100644 --- a/Source/desktop.rs +++ b/Source/desktop.rs @@ -11,25 +11,25 @@ use crate::{FilePath, OpenOptions}; pub struct Fs(pub(crate) AppHandle); fn path_or_err>(p: P) -> std::io::Result { - match p.into() { - FilePath::Path(p) => Ok(p), - FilePath::Url(u) if u.scheme() == "file" => u - .to_file_path() - .map_err(|_| std::io::Error::new(std::io::ErrorKind::InvalidInput, "invalid file URL")), - FilePath::Url(_) => Err(std::io::Error::new( - std::io::ErrorKind::InvalidInput, - "cannot use a URL to load files on desktop and iOS", - )), - } + match p.into() { + FilePath::Path(p) => Ok(p), + FilePath::Url(u) if u.scheme() == "file" => u + .to_file_path() + .map_err(|_| std::io::Error::new(std::io::ErrorKind::InvalidInput, "invalid file URL")), + FilePath::Url(_) => Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "cannot use a URL to load files on desktop and iOS", + )), + } } impl Fs { - pub fn open>( - &self, - path: P, - opts: OpenOptions, - ) -> std::io::Result { - let path = path_or_err(path)?; - std::fs::OpenOptions::from(opts).open(path) - } + pub fn open>( + &self, + path: P, + opts: OpenOptions, + ) -> std::io::Result { + let path = path_or_err(path)?; + std::fs::OpenOptions::from(opts).open(path) + } } diff --git a/Source/error.rs b/Source/error.rs index 38480ab..0c98e83 100644 --- a/Source/error.rs +++ b/Source/error.rs @@ -9,35 +9,35 @@ use serde::{Serialize, Serializer}; #[derive(Debug, thiserror::Error)] #[non_exhaustive] 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), - #[cfg(target_os = "android")] - #[error(transparent)] - PluginInvoke(#[from] tauri::plugin::mobile::PluginInvokeError), - #[error("URL is not a valid path")] - InvalidPathUrl, - #[error("Unsafe PathBuf: {0}")] - UnsafePathBuf(&'static str), + #[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), + #[cfg(target_os = "android")] + #[error(transparent)] + PluginInvoke(#[from] tauri::plugin::mobile::PluginInvokeError), + #[error("URL is not a valid path")] + InvalidPathUrl, + #[error("Unsafe PathBuf: {0}")] + UnsafePathBuf(&'static str), } impl Serialize for Error { - fn serialize(&self, serializer: S) -> std::result::Result - where - S: Serializer, - { - serializer.serialize_str(self.to_string().as_ref()) - } + fn serialize(&self, serializer: S) -> std::result::Result + where + S: Serializer, + { + serializer.serialize_str(self.to_string().as_ref()) + } } diff --git a/Source/file_path.rs b/Source/file_path.rs index 69e6345..9ff7a94 100644 --- a/Source/file_path.rs +++ b/Source/file_path.rs @@ -3,9 +3,9 @@ // SPDX-License-Identifier: MIT use std::{ - convert::Infallible, - path::{Path, PathBuf}, - str::FromStr, + convert::Infallible, + path::{Path, PathBuf}, + str::FromStr, }; use serde::Serialize; @@ -18,287 +18,297 @@ use crate::{Error, Result}; #[derive(Debug, Serialize, Clone)] #[serde(untagged)] pub enum FilePath { - /// `file://` URIs or Android `content://` URIs. - Url(url::Url), - /// Regular [`PathBuf`] - Path(PathBuf), + /// `file://` URIs or Android `content://` URIs. + Url(url::Url), + /// Regular [`PathBuf`] + Path(PathBuf), } /// Represents either a safe filesystem path or a URI pointing to a file /// such as `file://` URIs or Android `content://` URIs. #[derive(Debug, Clone, Serialize)] pub enum SafeFilePath { - /// `file://` URIs or Android `content://` URIs. - Url(url::Url), - /// Safe [`PathBuf`], see [`SafePathBuf``]. - Path(SafePathBuf), + /// `file://` URIs or Android `content://` URIs. + Url(url::Url), + /// Safe [`PathBuf`], see [`SafePathBuf``]. + Path(SafePathBuf), } impl FilePath { - /// Get a reference to the contained [`Path`] if the variant is [`FilePath::Path`]. - /// - /// Use [`FilePath::into_path`] to try to convert the [`FilePath::Url`] variant as well. - #[inline] - pub fn as_path(&self) -> Option<&Path> { - match self { - Self::Url(_) => None, - Self::Path(p) => Some(p), - } - } - - /// Try to convert into [`PathBuf`] if possible. - /// - /// This calls [`Url::to_file_path`](url::Url::to_file_path) if the variant is [`FilePath::Url`], - /// otherwise returns the contained [PathBuf] as is. - #[inline] - pub fn into_path(self) -> Result { - match self { - Self::Url(url) => { - url.to_file_path().map(PathBuf::from).map_err(|_| Error::InvalidPathUrl) - } - Self::Path(p) => Ok(p), - } - } - - /// Takes the contained [`PathBuf`] if the variant is [`FilePath::Path`], - /// and when possible, converts Windows UNC paths to regular paths. - #[inline] - pub fn simplified(self) -> Self { - match self { - Self::Url(url) => Self::Url(url), - Self::Path(p) => Self::Path(dunce::simplified(&p).to_path_buf()), - } - } + /// Get a reference to the contained [`Path`] if the variant is [`FilePath::Path`]. + /// + /// Use [`FilePath::into_path`] to try to convert the [`FilePath::Url`] variant as well. + #[inline] + pub fn as_path(&self) -> Option<&Path> { + match self { + Self::Url(_) => None, + Self::Path(p) => Some(p), + } + } + + /// Try to convert into [`PathBuf`] if possible. + /// + /// This calls [`Url::to_file_path`](url::Url::to_file_path) if the variant is [`FilePath::Url`], + /// otherwise returns the contained [PathBuf] as is. + #[inline] + pub fn into_path(self) -> Result { + match self { + Self::Url(url) => url + .to_file_path() + .map(PathBuf::from) + .map_err(|_| Error::InvalidPathUrl), + Self::Path(p) => Ok(p), + } + } + + /// Takes the contained [`PathBuf`] if the variant is [`FilePath::Path`], + /// and when possible, converts Windows UNC paths to regular paths. + #[inline] + pub fn simplified(self) -> Self { + match self { + Self::Url(url) => Self::Url(url), + Self::Path(p) => Self::Path(dunce::simplified(&p).to_path_buf()), + } + } } impl SafeFilePath { - /// Get a reference to the contained [`Path`] if the variant is [`SafeFilePath::Path`]. - /// - /// Use [`SafeFilePath::into_path`] to try to convert the [`SafeFilePath::Url`] variant as well. - #[inline] - pub fn as_path(&self) -> Option<&Path> { - match self { - Self::Url(_) => None, - Self::Path(p) => Some(p.as_ref()), - } - } - - /// Try to convert into [`PathBuf`] if possible. - /// - /// This calls [`Url::to_file_path`](url::Url::to_file_path) if the variant is [`SafeFilePath::Url`], - /// otherwise returns the contained [PathBuf] as is. - #[inline] - pub fn into_path(self) -> Result { - match self { - Self::Url(url) => { - url.to_file_path().map(PathBuf::from).map_err(|_| Error::InvalidPathUrl) - } - Self::Path(p) => Ok(p.as_ref().to_owned()), - } - } - - /// Takes the contained [`PathBuf`] if the variant is [`SafeFilePath::Path`], - /// and when possible, converts Windows UNC paths to regular paths. - #[inline] - pub fn simplified(self) -> Self { - match self { - Self::Url(url) => Self::Url(url), - Self::Path(p) => { - // Safe to unwrap since it was a safe file path already - Self::Path(SafePathBuf::new(dunce::simplified(p.as_ref()).to_path_buf()).unwrap()) - } - } - } + /// Get a reference to the contained [`Path`] if the variant is [`SafeFilePath::Path`]. + /// + /// Use [`SafeFilePath::into_path`] to try to convert the [`SafeFilePath::Url`] variant as well. + #[inline] + pub fn as_path(&self) -> Option<&Path> { + match self { + Self::Url(_) => None, + Self::Path(p) => Some(p.as_ref()), + } + } + + /// Try to convert into [`PathBuf`] if possible. + /// + /// This calls [`Url::to_file_path`](url::Url::to_file_path) if the variant is [`SafeFilePath::Url`], + /// otherwise returns the contained [PathBuf] as is. + #[inline] + pub fn into_path(self) -> Result { + match self { + Self::Url(url) => url + .to_file_path() + .map(PathBuf::from) + .map_err(|_| Error::InvalidPathUrl), + Self::Path(p) => Ok(p.as_ref().to_owned()), + } + } + + /// Takes the contained [`PathBuf`] if the variant is [`SafeFilePath::Path`], + /// and when possible, converts Windows UNC paths to regular paths. + #[inline] + pub fn simplified(self) -> Self { + match self { + Self::Url(url) => Self::Url(url), + Self::Path(p) => { + // Safe to unwrap since it was a safe file path already + Self::Path(SafePathBuf::new(dunce::simplified(p.as_ref()).to_path_buf()).unwrap()) + } + } + } } impl std::fmt::Display for FilePath { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Url(u) => u.fmt(f), - Self::Path(p) => p.display().fmt(f), - } - } + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Url(u) => u.fmt(f), + Self::Path(p) => p.display().fmt(f), + } + } } impl std::fmt::Display for SafeFilePath { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Url(u) => u.fmt(f), - Self::Path(p) => p.display().fmt(f), - } - } + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Url(u) => u.fmt(f), + Self::Path(p) => p.display().fmt(f), + } + } } impl<'de> serde::Deserialize<'de> for FilePath { - fn deserialize(deserializer: D) -> std::result::Result - where - D: serde::Deserializer<'de>, - { - struct FilePathVisitor; - - impl<'de> serde::de::Visitor<'de> for FilePathVisitor { - type Value = FilePath; - - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - formatter.write_str("a string representing an file URL or a path") - } - - fn visit_str(self, s: &str) -> std::result::Result - where - E: serde::de::Error, - { - FilePath::from_str(s).map_err(|e| { - serde::de::Error::invalid_value( - serde::de::Unexpected::Str(s), - &e.to_string().as_str(), - ) - }) - } - } - - deserializer.deserialize_str(FilePathVisitor) - } + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct FilePathVisitor; + + impl<'de> serde::de::Visitor<'de> for FilePathVisitor { + type Value = FilePath; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a string representing an file URL or a path") + } + + fn visit_str(self, s: &str) -> std::result::Result + where + E: serde::de::Error, + { + FilePath::from_str(s).map_err(|e| { + serde::de::Error::invalid_value( + serde::de::Unexpected::Str(s), + &e.to_string().as_str(), + ) + }) + } + } + + deserializer.deserialize_str(FilePathVisitor) + } } impl<'de> serde::Deserialize<'de> for SafeFilePath { - fn deserialize(deserializer: D) -> std::result::Result - where - D: serde::Deserializer<'de>, - { - struct SafeFilePathVisitor; - - impl<'de> serde::de::Visitor<'de> for SafeFilePathVisitor { - type Value = SafeFilePath; - - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - formatter.write_str("a string representing an file URL or a path") - } - - fn visit_str(self, s: &str) -> std::result::Result - where - E: serde::de::Error, - { - SafeFilePath::from_str(s).map_err(|e| { - serde::de::Error::invalid_value( - serde::de::Unexpected::Str(s), - &e.to_string().as_str(), - ) - }) - } - } - - deserializer.deserialize_str(SafeFilePathVisitor) - } + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct SafeFilePathVisitor; + + impl<'de> serde::de::Visitor<'de> for SafeFilePathVisitor { + type Value = SafeFilePath; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a string representing an file URL or a path") + } + + fn visit_str(self, s: &str) -> std::result::Result + where + E: serde::de::Error, + { + SafeFilePath::from_str(s).map_err(|e| { + serde::de::Error::invalid_value( + serde::de::Unexpected::Str(s), + &e.to_string().as_str(), + ) + }) + } + } + + deserializer.deserialize_str(SafeFilePathVisitor) + } } impl FromStr for FilePath { - type Err = Infallible; - fn from_str(s: &str) -> std::result::Result { - if let Ok(url) = url::Url::from_str(s) { - if url.scheme().len() != 1 { - return Ok(Self::Url(url)); - } - } - Ok(Self::Path(PathBuf::from(s))) - } + type Err = Infallible; + fn from_str(s: &str) -> std::result::Result { + if let Ok(url) = url::Url::from_str(s) { + if url.scheme().len() != 1 { + return Ok(Self::Url(url)); + } + } + Ok(Self::Path(PathBuf::from(s))) + } } impl FromStr for SafeFilePath { - type Err = Error; - fn from_str(s: &str) -> Result { - if let Ok(url) = url::Url::from_str(s) { - if url.scheme().len() != 1 { - return Ok(Self::Url(url)); - } - } - - SafePathBuf::new(s.into()).map(SafeFilePath::Path).map_err(Error::UnsafePathBuf) - } + type Err = Error; + fn from_str(s: &str) -> Result { + if let Ok(url) = url::Url::from_str(s) { + if url.scheme().len() != 1 { + return Ok(Self::Url(url)); + } + } + + SafePathBuf::new(s.into()) + .map(SafeFilePath::Path) + .map_err(Error::UnsafePathBuf) + } } impl From for FilePath { - fn from(value: PathBuf) -> Self { - Self::Path(value) - } + fn from(value: PathBuf) -> Self { + Self::Path(value) + } } impl TryFrom for SafeFilePath { - type Error = Error; - fn try_from(value: PathBuf) -> Result { - SafePathBuf::new(value).map(SafeFilePath::Path).map_err(Error::UnsafePathBuf) - } + type Error = Error; + fn try_from(value: PathBuf) -> Result { + SafePathBuf::new(value) + .map(SafeFilePath::Path) + .map_err(Error::UnsafePathBuf) + } } impl From<&Path> for FilePath { - fn from(value: &Path) -> Self { - Self::Path(value.to_owned()) - } + fn from(value: &Path) -> Self { + Self::Path(value.to_owned()) + } } impl TryFrom<&Path> for SafeFilePath { - type Error = Error; - fn try_from(value: &Path) -> Result { - SafePathBuf::new(value.to_path_buf()).map(SafeFilePath::Path).map_err(Error::UnsafePathBuf) - } + type Error = Error; + fn try_from(value: &Path) -> Result { + SafePathBuf::new(value.to_path_buf()) + .map(SafeFilePath::Path) + .map_err(Error::UnsafePathBuf) + } } impl From<&PathBuf> for FilePath { - fn from(value: &PathBuf) -> Self { - Self::Path(value.to_owned()) - } + fn from(value: &PathBuf) -> Self { + Self::Path(value.to_owned()) + } } impl TryFrom<&PathBuf> for SafeFilePath { - type Error = Error; - fn try_from(value: &PathBuf) -> Result { - SafePathBuf::new(value.to_owned()).map(SafeFilePath::Path).map_err(Error::UnsafePathBuf) - } + type Error = Error; + fn try_from(value: &PathBuf) -> Result { + SafePathBuf::new(value.to_owned()) + .map(SafeFilePath::Path) + .map_err(Error::UnsafePathBuf) + } } impl From for FilePath { - fn from(value: url::Url) -> Self { - Self::Url(value) - } + fn from(value: url::Url) -> Self { + Self::Url(value) + } } impl From for SafeFilePath { - fn from(value: url::Url) -> Self { - Self::Url(value) - } + fn from(value: url::Url) -> Self { + Self::Url(value) + } } impl TryFrom for PathBuf { - type Error = Error; - fn try_from(value: FilePath) -> Result { - value.into_path() - } + type Error = Error; + fn try_from(value: FilePath) -> Result { + value.into_path() + } } impl TryFrom for PathBuf { - type Error = Error; - fn try_from(value: SafeFilePath) -> Result { - value.into_path() - } + type Error = Error; + fn try_from(value: SafeFilePath) -> Result { + value.into_path() + } } impl From for FilePath { - fn from(value: SafeFilePath) -> Self { - match value { - SafeFilePath::Url(url) => FilePath::Url(url), - SafeFilePath::Path(p) => FilePath::Path(p.as_ref().to_owned()), - } - } + fn from(value: SafeFilePath) -> Self { + match value { + SafeFilePath::Url(url) => FilePath::Url(url), + SafeFilePath::Path(p) => FilePath::Path(p.as_ref().to_owned()), + } + } } impl TryFrom for SafeFilePath { - type Error = Error; - - fn try_from(value: FilePath) -> Result { - match value { - FilePath::Url(url) => Ok(SafeFilePath::Url(url)), - FilePath::Path(p) => { - SafePathBuf::new(p).map(SafeFilePath::Path).map_err(Error::UnsafePathBuf) - } - } - } + type Error = Error; + + fn try_from(value: FilePath) -> Result { + match value { + FilePath::Url(url) => Ok(SafeFilePath::Url(url)), + FilePath::Path(p) => SafePathBuf::new(p) + .map(SafeFilePath::Path) + .map_err(Error::UnsafePathBuf), + } + } } diff --git a/Source/lib.rs b/Source/lib.rs index e13a05d..a1cf276 100644 --- a/Source/lib.rs +++ b/Source/lib.rs @@ -7,18 +7,18 @@ //! 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" + 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 std::io::Read; use serde::Deserialize; use tauri::{ - ipc::ScopeObject, - plugin::{Builder as PluginBuilder, TauriPlugin}, - utils::acl::Value, - AppHandle, DragDropEvent, Manager, RunEvent, Runtime, WindowEvent, + ipc::ScopeObject, + plugin::{Builder as PluginBuilder, TauriPlugin}, + utils::acl::Value, + AppHandle, DragDropEvent, Manager, RunEvent, Runtime, WindowEvent, }; mod commands; @@ -43,399 +43,417 @@ pub use mobile::Fs; pub use error::Error; pub use scope::{Event as ScopeEvent, Scope}; -pub use file_path::{FilePath, SafeFilePath}; +pub use file_path::FilePath; +pub use file_path::SafeFilePath; type Result = std::result::Result; #[derive(Debug, Default, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct OpenOptions { - #[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, - #[serde(default)] - #[allow(unused)] - mode: Option, - #[serde(default)] - #[allow(unused)] - custom_flags: Option, + #[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, + #[serde(default)] + #[allow(unused)] + mode: Option, + #[serde(default)] + #[allow(unused)] + custom_flags: Option, } fn default_true() -> bool { - true + true } impl From for std::fs::OpenOptions { - fn from(open_options: OpenOptions) -> Self { - let mut opts = std::fs::OpenOptions::new(); - - #[cfg(unix)] - { - use std::os::unix::fs::OpenOptionsExt; - if let Some(mode) = open_options.mode { - opts.mode(mode); - } - if let Some(flags) = open_options.custom_flags { - opts.custom_flags(flags); - } - } - - opts.read(open_options.read) - .write(open_options.write) - .create(open_options.create) - .append(open_options.append) - .truncate(open_options.truncate) - .create_new(open_options.create_new); - - opts - } + fn from(open_options: OpenOptions) -> Self { + let mut opts = std::fs::OpenOptions::new(); + + #[cfg(unix)] + { + use std::os::unix::fs::OpenOptionsExt; + if let Some(mode) = open_options.mode { + opts.mode(mode); + } + if let Some(flags) = open_options.custom_flags { + opts.custom_flags(flags); + } + } + + opts.read(open_options.read) + .write(open_options.write) + .create(open_options.create) + .append(open_options.append) + .truncate(open_options.truncate) + .create_new(open_options.create_new); + + opts + } } impl OpenOptions { - /// Creates a blank new set of options ready for configuration. - /// - /// All options are initially set to `false`. - /// - /// # Examples - /// - /// ```no_run - /// use tauri_plugin_fs::OpenOptions; - /// - /// let mut options = OpenOptions::new(); - /// let file = options.read(true).open("foo.txt"); - /// ``` - #[must_use] - pub fn new() -> Self { - Self::default() - } - - /// Sets the option for read access. - /// - /// This option, when true, will indicate that the file should be - /// `read`-able if opened. - /// - /// # Examples - /// - /// ```no_run - /// use tauri_plugin_fs::OpenOptions; - /// - /// let file = OpenOptions::new().read(true).open("foo.txt"); - /// ``` - pub fn read(&mut self, read: bool) -> &mut Self { - self.read = read; - self - } - - /// Sets the option for write access. - /// - /// This option, when true, will indicate that the file should be - /// `write`-able if opened. - /// - /// If the file already exists, any write calls on it will overwrite its - /// contents, without truncating it. - /// - /// # Examples - /// - /// ```no_run - /// use tauri_plugin_fs::OpenOptions; - /// - /// let file = OpenOptions::new().write(true).open("foo.txt"); - /// ``` - pub fn write(&mut self, write: bool) -> &mut Self { - self.write = write; - self - } - - /// Sets the option for the append mode. - /// - /// This option, when true, means that writes will append to a file instead - /// of overwriting previous contents. - /// Note that setting `.write(true).append(true)` has the same effect as - /// setting only `.append(true)`. - /// - /// Append mode guarantees that writes will be positioned at the current end of file, - /// even when there are other processes or threads appending to the same file. This is - /// unlike [seek]\([SeekFrom]::[End]\(0)) followed by `write()`, which - /// has a race between seeking and writing during which another writer can write, with - /// our `write()` overwriting their data. - /// - /// Keep in mind that this does not necessarily guarantee that data appended by - /// different processes or threads does not interleave. The amount of data accepted a - /// single `write()` call depends on the operating system and file system. A - /// successful `write()` is allowed to write only part of the given data, so even if - /// you're careful to provide the whole message in a single call to `write()`, there - /// is no guarantee that it will be written out in full. If you rely on the filesystem - /// accepting the message in a single write, make sure that all data that belongs - /// together is written in one operation. This can be done by concatenating strings - /// before passing them to [`write()`]. - /// - /// If a file is opened with both read and append access, beware that after - /// opening, and after every write, the position for reading may be set at the - /// end of the file. So, before writing, save the current position (using - /// [Seek]::[stream_position]), and restore it before the next read. - /// - /// ## Note - /// - /// This function doesn't create the file if it doesn't exist. Use the - /// [`OpenOptions::create`] method to do so. - /// - /// [`write()`]: Write::write "io::Write::write" - /// [`flush()`]: Write::flush "io::Write::flush" - /// [stream_position]: Seek::stream_position "io::Seek::stream_position" - /// [seek]: Seek::seek "io::Seek::seek" - /// [Current]: SeekFrom::Current "io::SeekFrom::Current" - /// [End]: SeekFrom::End "io::SeekFrom::End" - /// - /// # Examples - /// - /// ```no_run - /// use tauri_plugin_fs::OpenOptions; - /// - /// let file = OpenOptions::new().append(true).open("foo.txt"); - /// ``` - pub fn append(&mut self, append: bool) -> &mut Self { - self.append = append; - self - } - - /// Sets the option for truncating a previous file. - /// - /// If a file is successfully opened with this option set it will truncate - /// the file to 0 length if it already exists. - /// - /// The file must be opened with write access for truncate to work. - /// - /// # Examples - /// - /// ```no_run - /// use tauri_plugin_fs::OpenOptions; - /// - /// let file = OpenOptions::new().write(true).truncate(true).open("foo.txt"); - /// ``` - pub fn truncate(&mut self, truncate: bool) -> &mut Self { - self.truncate = truncate; - self - } - - /// Sets the option to create a new file, or open it if it already exists. - /// - /// In order for the file to be created, [`OpenOptions::write`] or - /// [`OpenOptions::append`] access must be used. - /// - /// - /// # Examples - /// - /// ```no_run - /// use tauri_plugin_fs::OpenOptions; - /// - /// let file = OpenOptions::new().write(true).create(true).open("foo.txt"); - /// ``` - pub fn create(&mut self, create: bool) -> &mut Self { - self.create = create; - self - } - - /// Sets the option to create a new file, failing if it already exists. - /// - /// No file is allowed to exist at the target location, also no (dangling) symlink. In this - /// way, if the call succeeds, the file returned is guaranteed to be new. - /// If a file exists at the target location, creating a new file will fail with [`AlreadyExists`] - /// or another error based on the situation. See [`OpenOptions::open`] for a - /// non-exhaustive list of likely errors. - /// - /// This option is useful because it is atomic. Otherwise between checking - /// whether a file exists and creating a new one, the file may have been - /// created by another process (a TOCTOU race condition / attack). - /// - /// If `.create_new(true)` is set, [`.create()`] and [`.truncate()`] are - /// ignored. - /// - /// The file must be opened with write or append access in order to create - /// a new file. - /// - /// [`.create()`]: OpenOptions::create - /// [`.truncate()`]: OpenOptions::truncate - /// [`AlreadyExists`]: io::ErrorKind::AlreadyExists - /// - /// # Examples - /// - /// ```no_run - /// use tauri_plugin_fs::OpenOptions; - /// - /// let file = OpenOptions::new().write(true) - /// .create_new(true) - /// .open("foo.txt"); - /// ``` - pub fn create_new(&mut self, create_new: bool) -> &mut Self { - self.create_new = create_new; - self - } + /// Creates a blank new set of options ready for configuration. + /// + /// All options are initially set to `false`. + /// + /// # Examples + /// + /// ```no_run + /// use tauri_plugin_fs::OpenOptions; + /// + /// let mut options = OpenOptions::new(); + /// let file = options.read(true).open("foo.txt"); + /// ``` + #[must_use] + pub fn new() -> Self { + Self::default() + } + + /// Sets the option for read access. + /// + /// This option, when true, will indicate that the file should be + /// `read`-able if opened. + /// + /// # Examples + /// + /// ```no_run + /// use tauri_plugin_fs::OpenOptions; + /// + /// let file = OpenOptions::new().read(true).open("foo.txt"); + /// ``` + pub fn read(&mut self, read: bool) -> &mut Self { + self.read = read; + self + } + + /// Sets the option for write access. + /// + /// This option, when true, will indicate that the file should be + /// `write`-able if opened. + /// + /// If the file already exists, any write calls on it will overwrite its + /// contents, without truncating it. + /// + /// # Examples + /// + /// ```no_run + /// use tauri_plugin_fs::OpenOptions; + /// + /// let file = OpenOptions::new().write(true).open("foo.txt"); + /// ``` + pub fn write(&mut self, write: bool) -> &mut Self { + self.write = write; + self + } + + /// Sets the option for the append mode. + /// + /// This option, when true, means that writes will append to a file instead + /// of overwriting previous contents. + /// Note that setting `.write(true).append(true)` has the same effect as + /// setting only `.append(true)`. + /// + /// Append mode guarantees that writes will be positioned at the current end of file, + /// even when there are other processes or threads appending to the same file. This is + /// unlike [seek]\([SeekFrom]::[End]\(0)) followed by `write()`, which + /// has a race between seeking and writing during which another writer can write, with + /// our `write()` overwriting their data. + /// + /// Keep in mind that this does not necessarily guarantee that data appended by + /// different processes or threads does not interleave. The amount of data accepted a + /// single `write()` call depends on the operating system and file system. A + /// successful `write()` is allowed to write only part of the given data, so even if + /// you're careful to provide the whole message in a single call to `write()`, there + /// is no guarantee that it will be written out in full. If you rely on the filesystem + /// accepting the message in a single write, make sure that all data that belongs + /// together is written in one operation. This can be done by concatenating strings + /// before passing them to [`write()`]. + /// + /// If a file is opened with both read and append access, beware that after + /// opening, and after every write, the position for reading may be set at the + /// end of the file. So, before writing, save the current position (using + /// [Seek]::[stream_position]), and restore it before the next read. + /// + /// ## Note + /// + /// This function doesn't create the file if it doesn't exist. Use the + /// [`OpenOptions::create`] method to do so. + /// + /// [`write()`]: Write::write "io::Write::write" + /// [`flush()`]: Write::flush "io::Write::flush" + /// [stream_position]: Seek::stream_position "io::Seek::stream_position" + /// [seek]: Seek::seek "io::Seek::seek" + /// [Current]: SeekFrom::Current "io::SeekFrom::Current" + /// [End]: SeekFrom::End "io::SeekFrom::End" + /// + /// # Examples + /// + /// ```no_run + /// use tauri_plugin_fs::OpenOptions; + /// + /// let file = OpenOptions::new().append(true).open("foo.txt"); + /// ``` + pub fn append(&mut self, append: bool) -> &mut Self { + self.append = append; + self + } + + /// Sets the option for truncating a previous file. + /// + /// If a file is successfully opened with this option set it will truncate + /// the file to 0 length if it already exists. + /// + /// The file must be opened with write access for truncate to work. + /// + /// # Examples + /// + /// ```no_run + /// use tauri_plugin_fs::OpenOptions; + /// + /// let file = OpenOptions::new().write(true).truncate(true).open("foo.txt"); + /// ``` + pub fn truncate(&mut self, truncate: bool) -> &mut Self { + self.truncate = truncate; + self + } + + /// Sets the option to create a new file, or open it if it already exists. + /// + /// In order for the file to be created, [`OpenOptions::write`] or + /// [`OpenOptions::append`] access must be used. + /// + /// + /// # Examples + /// + /// ```no_run + /// use tauri_plugin_fs::OpenOptions; + /// + /// let file = OpenOptions::new().write(true).create(true).open("foo.txt"); + /// ``` + pub fn create(&mut self, create: bool) -> &mut Self { + self.create = create; + self + } + + /// Sets the option to create a new file, failing if it already exists. + /// + /// No file is allowed to exist at the target location, also no (dangling) symlink. In this + /// way, if the call succeeds, the file returned is guaranteed to be new. + /// If a file exists at the target location, creating a new file will fail with [`AlreadyExists`] + /// or another error based on the situation. See [`OpenOptions::open`] for a + /// non-exhaustive list of likely errors. + /// + /// This option is useful because it is atomic. Otherwise between checking + /// whether a file exists and creating a new one, the file may have been + /// created by another process (a TOCTOU race condition / attack). + /// + /// If `.create_new(true)` is set, [`.create()`] and [`.truncate()`] are + /// ignored. + /// + /// The file must be opened with write or append access in order to create + /// a new file. + /// + /// [`.create()`]: OpenOptions::create + /// [`.truncate()`]: OpenOptions::truncate + /// [`AlreadyExists`]: io::ErrorKind::AlreadyExists + /// + /// # Examples + /// + /// ```no_run + /// use tauri_plugin_fs::OpenOptions; + /// + /// let file = OpenOptions::new().write(true) + /// .create_new(true) + /// .open("foo.txt"); + /// ``` + pub fn create_new(&mut self, create_new: bool) -> &mut Self { + self.create_new = create_new; + self + } } #[cfg(unix)] impl std::os::unix::fs::OpenOptionsExt for OpenOptions { - fn custom_flags(&mut self, flags: i32) -> &mut Self { - self.custom_flags.replace(flags); - self - } - - fn mode(&mut self, mode: u32) -> &mut Self { - self.mode.replace(mode); - self - } + fn custom_flags(&mut self, flags: i32) -> &mut Self { + self.custom_flags.replace(flags); + self + } + + fn mode(&mut self, mode: u32) -> &mut Self { + self.mode.replace(mode); + self + } } impl OpenOptions { - #[cfg(target_os = "android")] - fn android_mode(&self) -> String { - let mut mode = String::new(); - - if self.read { - mode.push('r'); - } - if self.write { - mode.push('w'); - } - if self.truncate { - mode.push('t'); - } - if self.append { - mode.push('a'); - } - - mode - } + #[cfg(target_os = "android")] + fn android_mode(&self) -> String { + let mut mode = String::new(); + + if self.read { + mode.push('r'); + } + if self.write { + mode.push('w'); + } + if self.truncate { + mode.push('t'); + } + if self.append { + mode.push('a'); + } + + mode + } } impl Fs { - pub fn read_to_string>(&self, path: P) -> std::io::Result { - let mut s = String::new(); - self.open(path, OpenOptions { read: true, ..Default::default() })? - .read_to_string(&mut s)?; - Ok(s) - } - - pub fn read>(&self, path: P) -> std::io::Result> { - let mut buf = Vec::new(); - self.open(path, OpenOptions { read: true, ..Default::default() })?.read_to_end(&mut buf)?; - Ok(buf) - } + pub fn read_to_string>(&self, path: P) -> std::io::Result { + let mut s = String::new(); + self.open( + path, + OpenOptions { + read: true, + ..Default::default() + }, + )? + .read_to_string(&mut s)?; + Ok(s) + } + + pub fn read>(&self, path: P) -> std::io::Result> { + let mut buf = Vec::new(); + self.open( + path, + OpenOptions { + read: true, + ..Default::default() + }, + )? + .read_to_end(&mut buf)?; + Ok(buf) + } } // 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)? }) - } + type Error = Error; + fn deserialize( + app: &AppHandle, + raw: Value, + ) -> std::result::Result { + let path = serde_json::from_value(raw.into()).map(|raw| match raw { + scope::EntryRaw::Value(path) => path, + scope::EntryRaw::Object { path } => path, + })?; + + match app.path().parse(path) { + Ok(path) => Ok(Self { path: Some(path) }), + #[cfg(not(target_os = "android"))] + Err(tauri::Error::UnknownPath) => Ok(Self { path: None }), + Err(err) => Err(err.into()), + } + } } pub trait FsExt { - fn fs_scope(&self) -> &Scope; - fn try_fs_scope(&self) -> Option<&Scope>; + fn fs_scope(&self) -> &Scope; + fn try_fs_scope(&self) -> Option<&Scope>; - /// Cross platform file system APIs that also support manipulating Android files. - fn fs(&self) -> &Fs; + /// Cross platform file system APIs that also support manipulating Android files. + fn fs(&self) -> &Fs; } impl> FsExt for T { - fn fs_scope(&self) -> &Scope { - self.state::().inner() - } + fn fs_scope(&self) -> &Scope { + self.state::().inner() + } - fn try_fs_scope(&self) -> Option<&Scope> { - self.try_state::().map(|s| s.inner()) - } + fn try_fs_scope(&self) -> Option<&Scope> { + self.try_state::().map(|s| s.inner()) + } - fn fs(&self) -> &Fs { - self.state::>().inner() - } + fn fs(&self) -> &Fs { + self.state::>().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); - - #[cfg(target_os = "android")] - { - let fs = mobile::init(app, api)?; - app.manage(fs); - } - #[cfg(not(target_os = "android"))] - app.manage(Fs(app.clone())); - - app.manage(scope); - Ok(()) - }) - .on_event(|app, event| { - if let RunEvent::WindowEvent { - label: _, - event: WindowEvent::DragDrop(DragDropEvent::Drop { 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() + 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); + + #[cfg(target_os = "android")] + { + let fs = mobile::init(app, api)?; + app.manage(fs); + } + #[cfg(not(target_os = "android"))] + app.manage(Fs(app.clone())); + + app.manage(scope); + Ok(()) + }) + .on_event(|app, event| { + if let RunEvent::WindowEvent { + label: _, + event: WindowEvent::DragDrop(DragDropEvent::Drop { 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/Source/mobile.rs b/Source/mobile.rs index 364d21a..06422be 100644 --- a/Source/mobile.rs +++ b/Source/mobile.rs @@ -4,8 +4,8 @@ use serde::de::DeserializeOwned; use tauri::{ - plugin::{PluginApi, PluginHandle}, - AppHandle, Runtime, + plugin::{PluginApi, PluginHandle}, + AppHandle, Runtime, }; use crate::{models::*, FilePath, OpenOptions}; @@ -18,74 +18,79 @@ tauri::ios_plugin_binding!(init_plugin_fs); // initializes the Kotlin or Swift plugin classes pub fn init( - _app: &AppHandle, - api: PluginApi, + _app: &AppHandle, + api: PluginApi, ) -> crate::Result> { - #[cfg(target_os = "android")] - let handle = api.register_android_plugin(PLUGIN_IDENTIFIER, "FsPlugin").unwrap(); - #[cfg(target_os = "ios")] - let handle = api.register_ios_plugin(init_plugin_android - intent - send)?; - Ok(Fs(handle)) + #[cfg(target_os = "android")] + let handle = api + .register_android_plugin(PLUGIN_IDENTIFIER, "FsPlugin") + .unwrap(); + #[cfg(target_os = "ios")] + let handle = api.register_ios_plugin(init_plugin_android - intent - send)?; + Ok(Fs(handle)) } /// Access to the android-intent-send APIs. pub struct Fs(PluginHandle); impl Fs { - pub fn open>( - &self, - path: P, - opts: OpenOptions, - ) -> std::io::Result { - match path.into() { - FilePath::Url(u) => { - self.resolve_content_uri(u.to_string(), opts.android_mode()).map_err(|e| { - std::io::Error::new( - std::io::ErrorKind::Other, - format!("failed to open file: {e}"), - ) - }) - } - FilePath::Path(p) => { - // tauri::utils::platform::resources_dir() returns a PathBuf with the Android asset URI prefix - // we must resolve that file with the Android API - if p.strip_prefix(tauri::utils::platform::ANDROID_ASSET_PROTOCOL_URI_PREFIX).is_ok() - { - self.resolve_content_uri(p.to_string_lossy(), opts.android_mode()).map_err( - |e| { - std::io::Error::new( - std::io::ErrorKind::Other, - format!("failed to open file: {e}"), - ) - }, - ) - } else { - std::fs::OpenOptions::from(opts).open(p) - } - } - } - } + pub fn open>( + &self, + path: P, + opts: OpenOptions, + ) -> std::io::Result { + match path.into() { + FilePath::Url(u) => self + .resolve_content_uri(u.to_string(), opts.android_mode()) + .map_err(|e| { + std::io::Error::new( + std::io::ErrorKind::Other, + format!("failed to open file: {e}"), + ) + }), + FilePath::Path(p) => { + // tauri::utils::platform::resources_dir() returns a PathBuf with the Android asset URI prefix + // we must resolve that file with the Android API + if p.strip_prefix(tauri::utils::platform::ANDROID_ASSET_PROTOCOL_URI_PREFIX) + .is_ok() + { + self.resolve_content_uri(p.to_string_lossy(), opts.android_mode()) + .map_err(|e| { + std::io::Error::new( + std::io::ErrorKind::Other, + format!("failed to open file: {e}"), + ) + }) + } else { + std::fs::OpenOptions::from(opts).open(p) + } + } + } + } - #[cfg(target_os = "android")] - fn resolve_content_uri( - &self, - uri: impl Into, - mode: impl Into, - ) -> crate::Result { - #[cfg(target_os = "android")] - { - let result = self.0.run_mobile_plugin::( - "getFileDescriptor", - GetFileDescriptorPayload { uri: uri.into(), mode: mode.into() }, - )?; - if let Some(fd) = result.fd { - Ok(unsafe { - use std::os::fd::FromRawFd; - std::fs::File::from_raw_fd(fd) - }) - } else { - todo!() - } - } - } + #[cfg(target_os = "android")] + fn resolve_content_uri( + &self, + uri: impl Into, + mode: impl Into, + ) -> crate::Result { + #[cfg(target_os = "android")] + { + let result = self.0.run_mobile_plugin::( + "getFileDescriptor", + GetFileDescriptorPayload { + uri: uri.into(), + mode: mode.into(), + }, + )?; + if let Some(fd) = result.fd { + Ok(unsafe { + use std::os::fd::FromRawFd; + std::fs::File::from_raw_fd(fd) + }) + } else { + todo!() + } + } + } } diff --git a/Source/models.rs b/Source/models.rs index 91bac20..b9edc2c 100644 --- a/Source/models.rs +++ b/Source/models.rs @@ -7,12 +7,12 @@ use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Default, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct GetFileDescriptorPayload { - pub uri: String, - pub mode: String, + pub uri: String, + pub mode: String, } #[derive(Debug, Clone, Default, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct GetFileDescriptorResponse { - pub fd: Option, + pub fd: Option, } diff --git a/Source/scope.rs b/Source/scope.rs index e43c350..fd94b0e 100644 --- a/Source/scope.rs +++ b/Source/scope.rs @@ -3,12 +3,12 @@ // SPDX-License-Identifier: MIT use std::{ - collections::HashMap, - path::{Path, PathBuf}, - sync::{ - atomic::{AtomicU32, Ordering}, - Mutex, - }, + collections::HashMap, + path::{Path, PathBuf}, + sync::{ + atomic::{AtomicU32, Ordering}, + Mutex, + }, }; use serde::Deserialize; @@ -17,13 +17,13 @@ use serde::Deserialize; #[derive(Deserialize)] #[serde(untagged)] pub enum EntryRaw { - Value(PathBuf), - Object { path: PathBuf }, + Value(PathBuf), + Object { path: PathBuf }, } #[derive(Debug)] pub struct Entry { - pub path: PathBuf, + pub path: Option, } pub type EventId = u32; @@ -32,101 +32,101 @@ 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), + /// 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, + 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 - } + /// 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/Source/watcher.rs b/Source/watcher.rs index 786ad0c..cf2af50 100644 --- a/Source/watcher.rs +++ b/Source/watcher.rs @@ -6,149 +6,154 @@ 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, - Manager, Resource, ResourceId, Runtime, Webview, + ipc::{Channel, CommandScope, GlobalScope}, + path::BaseDirectory, + Manager, Resource, ResourceId, Runtime, Webview, }; use std::{ - path::PathBuf, - sync::{ - mpsc::{channel, Receiver}, - Mutex, - }, - thread::spawn, - time::Duration, + path::PathBuf, + sync::{ + mpsc::{channel, Receiver}, + Mutex, + }, + thread::spawn, + time::Duration, }; use crate::{ - commands::{resolve_path, CommandResult}, - scope::Entry, - SafeFilePath, + commands::{resolve_path, CommandResult}, + scope::Entry, + SafeFilePath, }; struct InnerWatcher { - pub kind: WatcherKind, - paths: Vec, + 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) - } + 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), + 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); - } - } - }); + 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); - } - } - }); + 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(Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct WatchOptions { - base_dir: Option, - recursive: bool, - delay_ms: Option, + 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, + 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) + 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(()) - }) + 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(()) + }) } diff --git a/android/src/androidTest/java/ExampleInstrumentedTest.kt b/android/src/androidTest/java/ExampleInstrumentedTest.kt deleted file mode 100644 index c3b473f..0000000 --- a/android/src/androidTest/java/ExampleInstrumentedTest.kt +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright 2019-2023 Tauri Programme within The Commons Conservancy -// SPDX-License-Identifier: Apache-2.0 -// SPDX-License-Identifier: MIT - -package com.plugin.fs - -import androidx.test.platform.app.InstrumentationRegistry -import androidx.test.ext.junit.runners.AndroidJUnit4 - -import org.junit.Test -import org.junit.runner.RunWith - -import org.junit.Assert.* - -/** - * Instrumented test, which will execute on an Android device. - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -@RunWith(AndroidJUnit4::class) -class ExampleInstrumentedTest { - @Test - fun useAppContext() { - // Context of the app under test. - val appContext = InstrumentationRegistry.getInstrumentation().targetContext - assertEquals("com.plugin.fs", appContext.packageName) - } -} diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml deleted file mode 100644 index 9a40236..0000000 --- a/android/src/main/AndroidManifest.xml +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/android/src/main/java/FsPlugin.kt b/android/src/main/java/FsPlugin.kt deleted file mode 100644 index 877fbf4..0000000 --- a/android/src/main/java/FsPlugin.kt +++ /dev/null @@ -1,93 +0,0 @@ -// Copyright 2019-2023 Tauri Programme within The Commons Conservancy -// SPDX-License-Identifier: Apache-2.0 -// SPDX-License-Identifier: MIT - -package com.plugin.fs - -import android.annotation.SuppressLint -import android.app.Activity -import android.content.res.AssetManager.ACCESS_BUFFER -import android.net.Uri -import android.os.ParcelFileDescriptor -import app.tauri.annotation.Command -import app.tauri.annotation.InvokeArg -import app.tauri.annotation.TauriPlugin -import app.tauri.plugin.Invoke -import app.tauri.plugin.JSObject -import app.tauri.plugin.Plugin -import java.io.File -import java.io.FileOutputStream -import java.io.IOException -import java.io.InputStream -import java.io.OutputStream - -@InvokeArg -class WriteTextFileArgs { - val uri: String = "" - val content: String = "" -} - -@InvokeArg -class GetFileDescriptorArgs { - lateinit var uri: String - lateinit var mode: String -} - -@TauriPlugin -class FsPlugin(private val activity: Activity): Plugin(activity) { - @SuppressLint("Recycle") - @Command - fun getFileDescriptor(invoke: Invoke) { - val args = invoke.parseArgs(GetFileDescriptorArgs::class.java) - - val res = JSObject() - - if (args.uri.startsWith(app.tauri.TAURI_ASSETS_DIRECTORY_URI)) { - val path = args.uri.substring(app.tauri.TAURI_ASSETS_DIRECTORY_URI.length) - try { - val fd = activity.assets.openFd(path).parcelFileDescriptor?.detachFd() - res.put("fd", fd) - } catch (e: IOException) { - // if the asset is compressed, we cannot open a file descriptor directly - // so we copy it to the cache and get a fd from there - // this is a lot faster than serializing the file and sending it as invoke response - // because on the Rust side we can leverage the custom protocol IPC and read the file directly - val cacheFile = File(activity.cacheDir, "_assets/$path") - cacheFile.parentFile?.mkdirs() - copyAsset(path, cacheFile) - - val fd = ParcelFileDescriptor.open(cacheFile, ParcelFileDescriptor.parseMode(args.mode)).detachFd() - res.put("fd", fd) - } - } else { - val fd = activity.contentResolver.openAssetFileDescriptor( - Uri.parse(args.uri), - args.mode - )?.parcelFileDescriptor?.detachFd() - res.put("fd", fd) - } - - invoke.resolve(res) - } - - @Throws(IOException::class) - private fun copy(input: InputStream, output: OutputStream) { - val buf = ByteArray(1024) - var len: Int - while ((input.read(buf).also { len = it }) > 0) { - output.write(buf, 0, len) - } - } - - @Throws(IOException::class) - private fun copyAsset(assetPath: String, cacheFile: File) { - val input = activity.assets.open(assetPath, ACCESS_BUFFER) - input.use { i -> - val output = FileOutputStream(cacheFile, false) - output.use { o -> - copy(i, o) - } - } - } -} - diff --git a/android/src/test/java/ExampleUnitTest.kt b/android/src/test/java/ExampleUnitTest.kt deleted file mode 100644 index 340839a..0000000 --- a/android/src/test/java/ExampleUnitTest.kt +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright 2019-2023 Tauri Programme within The Commons Conservancy -// SPDX-License-Identifier: Apache-2.0 -// SPDX-License-Identifier: MIT - -package com.plugin.fs - -import org.junit.Test - -import org.junit.Assert.* - -/** - * Example local unit test, which will execute on the development machine (host). - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -class ExampleUnitTest { - @Test - fun addition_isCorrect() { - assertEquals(4, 2 + 2) - } -} diff --git a/package.json b/package.json index 7fd93a0..a334321 100644 --- a/package.json +++ b/package.json @@ -1,30 +1,26 @@ { - "name": "@tauri-apps/plugin-fs", - "version": "2.0.0-rc.2", - "description": "Access the file system.", - "license": "MIT or APACHE-2.0", - "authors": [ - "Tauri Programme within The Commons Conservancy" - ], - "repository": "https://github.com/tauri-apps/plugins-workspace", - "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-rc.4" - } + "authors": [ + "Tauri Programme within The Commons Conservancy" + ], + "dependencies": { + "@tauri-apps/api": "^2.0.0-rc.4" + }, + "description": "Access the file system.", + "exports": { + "import": "./dist-js/index.js", + "require": "./dist-js/index.cjs", + "types": "./dist-js/index.d.ts" + }, + "files": [ + "dist-js", + "README.md", + "LICENSE" + ], + "main": "./dist-js/index.cjs", + "module": "./dist-js/index.js", + "name": "@tauri-apps/plugin-fs", + "scripts": { + "build": "rollup -c" + }, + "types": "./dist-js/index.d.ts" } diff --git a/src/commands.rs b/src/commands.rs deleted file mode 100644 index cb40c3e..0000000 --- a/src/commands.rs +++ /dev/null @@ -1,1155 +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, - utils::config::FsScope, - AppHandle, Manager, Resource, ResourceId, Runtime, Webview, -}; - -use std::{ - borrow::Cow, - fs::File, - io::{BufReader, Lines, Read, Write}, - path::{Path, PathBuf}, - str::FromStr, - sync::Mutex, - time::{SystemTime, UNIX_EPOCH}, -}; - -use crate::{scope::Entry, Error, FsExt, SafeFilePath}; - -#[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)] - Json(#[from] serde_json::Error), - #[error(transparent)] - Io(#[from] std::io::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, Default, 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: SafeFilePath, - 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, Default, Clone, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct OpenOptions { - #[serde(flatten)] - base: BaseOptions, - #[serde(flatten)] - options: crate::OpenOptions, -} - -#[tauri::command] -pub fn open( - webview: Webview, - global_scope: GlobalScope, - command_scope: CommandScope, - path: SafeFilePath, - options: Option, -) -> CommandResult { - let (file, _path) = resolve_file( - &webview, - &global_scope, - &command_scope, - path, - if let Some(opts) = options { - OpenOptions { - base: opts.base, - options: opts.options, - } - } else { - OpenOptions { - base: BaseOptions { base_dir: None }, - options: crate::OpenOptions { - read: true, - write: false, - truncate: false, - create: false, - create_new: false, - append: false, - mode: None, - custom_flags: None, - }, - } - }, - )?; - - 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 async fn copy_file( - webview: Webview, - global_scope: GlobalScope, - command_scope: CommandScope, - from_path: SafeFilePath, - to_path: SafeFilePath, - 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: SafeFilePath, - 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 async fn read_dir( - webview: Webview, - global_scope: GlobalScope, - command_scope: CommandScope, - path: SafeFilePath, - 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 async 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 async fn read_file( - webview: Webview, - global_scope: GlobalScope, - command_scope: CommandScope, - path: SafeFilePath, - options: Option, -) -> CommandResult { - let (mut file, path) = resolve_file( - &webview, - &global_scope, - &command_scope, - path, - OpenOptions { - base: BaseOptions { - base_dir: options.as_ref().and_then(|o| o.base_dir), - }, - options: crate::OpenOptions { - read: true, - ..Default::default() - }, - }, - )?; - - let mut contents = Vec::new(); - - file.read_to_end(&mut contents).map_err(|e| { - format!( - "failed to read file as text at path: {} with error: {e}", - path.display() - ) - })?; - - Ok(tauri::ipc::Response::new(contents)) -} - -#[tauri::command] -pub async fn read_text_file( - webview: Webview, - global_scope: GlobalScope, - command_scope: CommandScope, - path: SafeFilePath, - options: Option, -) -> CommandResult { - let (mut file, path) = resolve_file( - &webview, - &global_scope, - &command_scope, - path, - OpenOptions { - base: BaseOptions { - base_dir: options.as_ref().and_then(|o| o.base_dir), - }, - options: crate::OpenOptions { - read: true, - ..Default::default() - }, - }, - )?; - - let mut contents = String::new(); - - file.read_to_string(&mut contents).map_err(|e| { - format!( - "failed to read file as text at path: {} with error: {e}", - path.display() - ) - })?; - - Ok(contents) -} - -#[tauri::command] -pub fn read_text_file_lines( - webview: Webview, - global_scope: GlobalScope, - command_scope: CommandScope, - path: SafeFilePath, - 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 async 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: SafeFilePath, - 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: SafeFilePath, - new_path: SafeFilePath, - 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 async 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) -} - -#[cfg(target_os = "android")] -fn get_metadata std::io::Result>( - metadata_fn: F, - webview: &Webview, - global_scope: &GlobalScope, - command_scope: &CommandScope, - path: SafeFilePath, - options: Option, -) -> CommandResult { - match path { - SafeFilePath::Url(url) => { - let (file, path) = resolve_file( - webview, - global_scope, - command_scope, - SafeFilePath::Url(url), - OpenOptions { - base: BaseOptions { base_dir: None }, - options: crate::OpenOptions { - read: true, - ..Default::default() - }, - }, - )?; - file.metadata().map_err(|e| { - format!( - "failed to get metadata of path: {} with error: {e}", - path.display() - ) - .into() - }) - } - SafeFilePath::Path(p) => get_fs_metadata( - metadata_fn, - webview, - global_scope, - command_scope, - SafeFilePath::Path(p), - options, - ), - } -} - -#[cfg(not(target_os = "android"))] -fn get_metadata std::io::Result>( - metadata_fn: F, - webview: &Webview, - global_scope: &GlobalScope, - command_scope: &CommandScope, - path: SafeFilePath, - options: Option, -) -> CommandResult { - get_fs_metadata( - metadata_fn, - webview, - global_scope, - command_scope, - path, - options, - ) -} - -fn get_fs_metadata std::io::Result>( - metadata_fn: F, - webview: &Webview, - global_scope: &GlobalScope, - command_scope: &CommandScope, - path: SafeFilePath, - 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 = metadata_fn(&resolved_path).map_err(|e| { - format!( - "failed to get metadata of path: {} with error: {e}", - resolved_path.display() - ) - })?; - Ok(metadata) -} - -#[tauri::command] -pub fn stat( - webview: Webview, - global_scope: GlobalScope, - command_scope: CommandScope, - path: SafeFilePath, - options: Option, -) -> CommandResult { - let metadata = get_metadata( - |p| std::fs::metadata(p), - &webview, - &global_scope, - &command_scope, - path, - options, - )?; - - Ok(get_stat(metadata)) -} - -#[tauri::command] -pub fn lstat( - webview: Webview, - global_scope: GlobalScope, - command_scope: CommandScope, - path: SafeFilePath, - options: Option, -) -> CommandResult { - let metadata = get_metadata( - |p| std::fs::symlink_metadata(p), - &webview, - &global_scope, - &command_scope, - path, - options, - )?; - 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 async fn truncate( - webview: Webview, - global_scope: GlobalScope, - command_scope: CommandScope, - path: SafeFilePath, - 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 async 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 async 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: SafeFilePath, - data: &[u8], - options: Option, -) -> CommandResult<()> { - let (mut file, path) = resolve_file( - &webview, - global_scope, - command_scope, - path, - if let Some(opts) = options { - OpenOptions { - base: opts.base, - options: crate::OpenOptions { - read: false, - write: true, - create: opts.create, - truncate: !opts.append, - append: opts.append, - create_new: opts.create_new, - mode: opts.mode, - custom_flags: None, - }, - } - } else { - OpenOptions { - base: BaseOptions { base_dir: None }, - options: crate::OpenOptions { - read: false, - write: true, - truncate: true, - create: true, - create_new: false, - append: false, - mode: None, - custom_flags: None, - }, - } - }, - )?; - - file.write_all(data) - .map_err(|e| { - format!( - "failed to write bytes to file at path: {} with error: {e}", - path.display() - ) - }) - .map_err(Into::into) -} - -#[tauri::command] -pub async fn write_file( - webview: Webview, - global_scope: GlobalScope, - command_scope: CommandScope, - request: tauri::ipc::Request<'_>, -) -> CommandResult<()> { - let data = match request.body() { - tauri::ipc::InvokeBody::Raw(data) => Cow::Borrowed(data), - tauri::ipc::InvokeBody::Json(serde_json::Value::Array(data)) => Cow::Owned( - data.iter() - .flat_map(|v| v.as_number().and_then(|v| v.as_u64().map(|v| v as u8))) - .collect(), - ), - _ => return Err(anyhow::anyhow!("unexpected invoke body").into()), - }; - - let path = request - .headers() - .get("path") - .ok_or_else(|| anyhow::anyhow!("missing file path").into()) - .and_then(|p| { - percent_encoding::percent_decode(p.as_ref()) - .decode_utf8() - .map_err(|_| anyhow::anyhow!("path is not a valid UTF-8").into()) - }) - .and_then(|p| SafeFilePath::from_str(&p).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) -} - -#[tauri::command] -pub async fn write_text_file( - #[allow(unused)] app: AppHandle, - #[allow(unused)] webview: Webview, - #[allow(unused)] global_scope: GlobalScope, - #[allow(unused)] command_scope: CommandScope, - path: SafeFilePath, - data: String, - #[allow(unused)] 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: SafeFilePath, - 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()) -} - -#[cfg(not(target_os = "android"))] -pub fn resolve_file( - webview: &Webview, - global_scope: &GlobalScope, - command_scope: &CommandScope, - path: SafeFilePath, - open_options: OpenOptions, -) -> CommandResult<(File, PathBuf)> { - resolve_file_in_fs(webview, global_scope, command_scope, path, open_options) -} - -fn resolve_file_in_fs( - webview: &Webview, - global_scope: &GlobalScope, - command_scope: &CommandScope, - path: SafeFilePath, - open_options: OpenOptions, -) -> CommandResult<(File, PathBuf)> { - let path = resolve_path( - webview, - global_scope, - command_scope, - path, - open_options.base.base_dir, - )?; - - let file = std::fs::OpenOptions::from(open_options.options) - .open(&path) - .map_err(|e| { - format!( - "failed to open file at path: {} with error: {e}", - path.display() - ) - })?; - Ok((file, path)) -} - -#[cfg(target_os = "android")] -pub fn resolve_file( - webview: &Webview, - global_scope: &GlobalScope, - command_scope: &CommandScope, - path: SafeFilePath, - open_options: OpenOptions, -) -> CommandResult<(File, PathBuf)> { - match path { - SafeFilePath::Url(url) => { - let path = url.as_str().into(); - let file = webview - .fs() - .open(SafeFilePath::Url(url), open_options.options)?; - Ok((file, path)) - } - SafeFilePath::Path(path) => resolve_file_in_fs( - webview, - global_scope, - command_scope, - SafeFilePath::Path(path), - open_options, - ), - } -} - -pub fn resolve_path( - webview: &Webview, - global_scope: &GlobalScope, - command_scope: &CommandScope, - path: SafeFilePath, - base_dir: Option, -) -> CommandResult { - let path = path.into_path()?; - let path = if let Some(base_dir) = base_dir { - webview.path().resolve(&path, base_dir)? - } else { - path - }; - - let scope = tauri::scope::fs::Scope::new( - webview, - &FsScope::Scope { - allow: webview - .fs_scope() - .allowed - .lock() - .unwrap() - .clone() - .into_iter() - .chain(global_scope.allows().iter().filter_map(|e| e.path.clone())) - .chain(command_scope.allows().iter().filter_map(|e| e.path.clone())) - .collect(), - deny: webview - .fs_scope() - .denied - .lock() - .unwrap() - .clone() - .into_iter() - .chain(global_scope.denies().iter().filter_map(|e| e.path.clone())) - .chain(command_scope.denies().iter().filter_map(|e| e.path.clone())) - .collect(), - require_literal_leading_dot: webview.fs_scope().require_literal_leading_dot, - }, - )?; - - if scope.is_allowed(&path) { - Ok(path) - } else { - Err(CommandError::Plugin(Error::PathForbidden(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), - } -} - -mod test { - #[test] - fn safe_file_path_parse() { - use super::SafeFilePath; - - assert!(matches!( - serde_json::from_str::("\"C:/Users\""), - Ok(SafeFilePath::Path(_)) - )); - assert!(matches!( - serde_json::from_str::("\"file:///C:/Users\""), - Ok(SafeFilePath::Url(_)) - )); - } -} diff --git a/src/config.rs b/src/config.rs deleted file mode 100644 index db3bae4..0000000 --- a/src/config.rs +++ /dev/null @@ -1,19 +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)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -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/desktop.rs b/src/desktop.rs deleted file mode 100644 index 477c053..0000000 --- a/src/desktop.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 tauri::{AppHandle, Runtime}; - -use crate::{FilePath, OpenOptions}; - -pub struct Fs(pub(crate) AppHandle); - -fn path_or_err>(p: P) -> std::io::Result { - match p.into() { - FilePath::Path(p) => Ok(p), - FilePath::Url(u) if u.scheme() == "file" => u - .to_file_path() - .map_err(|_| std::io::Error::new(std::io::ErrorKind::InvalidInput, "invalid file URL")), - FilePath::Url(_) => Err(std::io::Error::new( - std::io::ErrorKind::InvalidInput, - "cannot use a URL to load files on desktop and iOS", - )), - } -} - -impl Fs { - pub fn open>( - &self, - path: P, - opts: OpenOptions, - ) -> std::io::Result { - let path = path_or_err(path)?; - std::fs::OpenOptions::from(opts).open(path) - } -} diff --git a/src/error.rs b/src/error.rs deleted file mode 100644 index 0c98e83..0000000 --- a/src/error.rs +++ /dev/null @@ -1,43 +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)] -#[non_exhaustive] -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), - #[cfg(target_os = "android")] - #[error(transparent)] - PluginInvoke(#[from] tauri::plugin::mobile::PluginInvokeError), - #[error("URL is not a valid path")] - InvalidPathUrl, - #[error("Unsafe PathBuf: {0}")] - UnsafePathBuf(&'static str), -} - -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/file_path.rs b/src/file_path.rs deleted file mode 100644 index 9ff7a94..0000000 --- a/src/file_path.rs +++ /dev/null @@ -1,314 +0,0 @@ -// Copyright 2019-2023 Tauri Programme within The Commons Conservancy -// SPDX-License-Identifier: Apache-2.0 -// SPDX-License-Identifier: MIT - -use std::{ - convert::Infallible, - path::{Path, PathBuf}, - str::FromStr, -}; - -use serde::Serialize; -use tauri::path::SafePathBuf; - -use crate::{Error, Result}; - -/// Represents either a filesystem path or a URI pointing to a file -/// such as `file://` URIs or Android `content://` URIs. -#[derive(Debug, Serialize, Clone)] -#[serde(untagged)] -pub enum FilePath { - /// `file://` URIs or Android `content://` URIs. - Url(url::Url), - /// Regular [`PathBuf`] - Path(PathBuf), -} - -/// Represents either a safe filesystem path or a URI pointing to a file -/// such as `file://` URIs or Android `content://` URIs. -#[derive(Debug, Clone, Serialize)] -pub enum SafeFilePath { - /// `file://` URIs or Android `content://` URIs. - Url(url::Url), - /// Safe [`PathBuf`], see [`SafePathBuf``]. - Path(SafePathBuf), -} - -impl FilePath { - /// Get a reference to the contained [`Path`] if the variant is [`FilePath::Path`]. - /// - /// Use [`FilePath::into_path`] to try to convert the [`FilePath::Url`] variant as well. - #[inline] - pub fn as_path(&self) -> Option<&Path> { - match self { - Self::Url(_) => None, - Self::Path(p) => Some(p), - } - } - - /// Try to convert into [`PathBuf`] if possible. - /// - /// This calls [`Url::to_file_path`](url::Url::to_file_path) if the variant is [`FilePath::Url`], - /// otherwise returns the contained [PathBuf] as is. - #[inline] - pub fn into_path(self) -> Result { - match self { - Self::Url(url) => url - .to_file_path() - .map(PathBuf::from) - .map_err(|_| Error::InvalidPathUrl), - Self::Path(p) => Ok(p), - } - } - - /// Takes the contained [`PathBuf`] if the variant is [`FilePath::Path`], - /// and when possible, converts Windows UNC paths to regular paths. - #[inline] - pub fn simplified(self) -> Self { - match self { - Self::Url(url) => Self::Url(url), - Self::Path(p) => Self::Path(dunce::simplified(&p).to_path_buf()), - } - } -} - -impl SafeFilePath { - /// Get a reference to the contained [`Path`] if the variant is [`SafeFilePath::Path`]. - /// - /// Use [`SafeFilePath::into_path`] to try to convert the [`SafeFilePath::Url`] variant as well. - #[inline] - pub fn as_path(&self) -> Option<&Path> { - match self { - Self::Url(_) => None, - Self::Path(p) => Some(p.as_ref()), - } - } - - /// Try to convert into [`PathBuf`] if possible. - /// - /// This calls [`Url::to_file_path`](url::Url::to_file_path) if the variant is [`SafeFilePath::Url`], - /// otherwise returns the contained [PathBuf] as is. - #[inline] - pub fn into_path(self) -> Result { - match self { - Self::Url(url) => url - .to_file_path() - .map(PathBuf::from) - .map_err(|_| Error::InvalidPathUrl), - Self::Path(p) => Ok(p.as_ref().to_owned()), - } - } - - /// Takes the contained [`PathBuf`] if the variant is [`SafeFilePath::Path`], - /// and when possible, converts Windows UNC paths to regular paths. - #[inline] - pub fn simplified(self) -> Self { - match self { - Self::Url(url) => Self::Url(url), - Self::Path(p) => { - // Safe to unwrap since it was a safe file path already - Self::Path(SafePathBuf::new(dunce::simplified(p.as_ref()).to_path_buf()).unwrap()) - } - } - } -} - -impl std::fmt::Display for FilePath { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Url(u) => u.fmt(f), - Self::Path(p) => p.display().fmt(f), - } - } -} - -impl std::fmt::Display for SafeFilePath { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Url(u) => u.fmt(f), - Self::Path(p) => p.display().fmt(f), - } - } -} - -impl<'de> serde::Deserialize<'de> for FilePath { - fn deserialize(deserializer: D) -> std::result::Result - where - D: serde::Deserializer<'de>, - { - struct FilePathVisitor; - - impl<'de> serde::de::Visitor<'de> for FilePathVisitor { - type Value = FilePath; - - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - formatter.write_str("a string representing an file URL or a path") - } - - fn visit_str(self, s: &str) -> std::result::Result - where - E: serde::de::Error, - { - FilePath::from_str(s).map_err(|e| { - serde::de::Error::invalid_value( - serde::de::Unexpected::Str(s), - &e.to_string().as_str(), - ) - }) - } - } - - deserializer.deserialize_str(FilePathVisitor) - } -} - -impl<'de> serde::Deserialize<'de> for SafeFilePath { - fn deserialize(deserializer: D) -> std::result::Result - where - D: serde::Deserializer<'de>, - { - struct SafeFilePathVisitor; - - impl<'de> serde::de::Visitor<'de> for SafeFilePathVisitor { - type Value = SafeFilePath; - - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - formatter.write_str("a string representing an file URL or a path") - } - - fn visit_str(self, s: &str) -> std::result::Result - where - E: serde::de::Error, - { - SafeFilePath::from_str(s).map_err(|e| { - serde::de::Error::invalid_value( - serde::de::Unexpected::Str(s), - &e.to_string().as_str(), - ) - }) - } - } - - deserializer.deserialize_str(SafeFilePathVisitor) - } -} - -impl FromStr for FilePath { - type Err = Infallible; - fn from_str(s: &str) -> std::result::Result { - if let Ok(url) = url::Url::from_str(s) { - if url.scheme().len() != 1 { - return Ok(Self::Url(url)); - } - } - Ok(Self::Path(PathBuf::from(s))) - } -} - -impl FromStr for SafeFilePath { - type Err = Error; - fn from_str(s: &str) -> Result { - if let Ok(url) = url::Url::from_str(s) { - if url.scheme().len() != 1 { - return Ok(Self::Url(url)); - } - } - - SafePathBuf::new(s.into()) - .map(SafeFilePath::Path) - .map_err(Error::UnsafePathBuf) - } -} - -impl From for FilePath { - fn from(value: PathBuf) -> Self { - Self::Path(value) - } -} - -impl TryFrom for SafeFilePath { - type Error = Error; - fn try_from(value: PathBuf) -> Result { - SafePathBuf::new(value) - .map(SafeFilePath::Path) - .map_err(Error::UnsafePathBuf) - } -} - -impl From<&Path> for FilePath { - fn from(value: &Path) -> Self { - Self::Path(value.to_owned()) - } -} - -impl TryFrom<&Path> for SafeFilePath { - type Error = Error; - fn try_from(value: &Path) -> Result { - SafePathBuf::new(value.to_path_buf()) - .map(SafeFilePath::Path) - .map_err(Error::UnsafePathBuf) - } -} - -impl From<&PathBuf> for FilePath { - fn from(value: &PathBuf) -> Self { - Self::Path(value.to_owned()) - } -} - -impl TryFrom<&PathBuf> for SafeFilePath { - type Error = Error; - fn try_from(value: &PathBuf) -> Result { - SafePathBuf::new(value.to_owned()) - .map(SafeFilePath::Path) - .map_err(Error::UnsafePathBuf) - } -} - -impl From for FilePath { - fn from(value: url::Url) -> Self { - Self::Url(value) - } -} - -impl From for SafeFilePath { - fn from(value: url::Url) -> Self { - Self::Url(value) - } -} - -impl TryFrom for PathBuf { - type Error = Error; - fn try_from(value: FilePath) -> Result { - value.into_path() - } -} - -impl TryFrom for PathBuf { - type Error = Error; - fn try_from(value: SafeFilePath) -> Result { - value.into_path() - } -} - -impl From for FilePath { - fn from(value: SafeFilePath) -> Self { - match value { - SafeFilePath::Url(url) => FilePath::Url(url), - SafeFilePath::Path(p) => FilePath::Path(p.as_ref().to_owned()), - } - } -} - -impl TryFrom for SafeFilePath { - type Error = Error; - - fn try_from(value: FilePath) -> Result { - match value { - FilePath::Url(url) => Ok(SafeFilePath::Url(url)), - FilePath::Path(p) => SafePathBuf::new(p) - .map(SafeFilePath::Path) - .map_err(Error::UnsafePathBuf), - } - } -} diff --git a/src/lib.rs b/src/lib.rs deleted file mode 100644 index a1cf276..0000000 --- a/src/lib.rs +++ /dev/null @@ -1,459 +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 std::io::Read; - -use serde::Deserialize; -use tauri::{ - ipc::ScopeObject, - plugin::{Builder as PluginBuilder, TauriPlugin}, - utils::acl::Value, - AppHandle, DragDropEvent, Manager, RunEvent, Runtime, WindowEvent, -}; - -mod commands; -mod config; -#[cfg(not(target_os = "android"))] -mod desktop; -mod error; -mod file_path; -#[cfg(target_os = "android")] -mod mobile; -#[cfg(target_os = "android")] -mod models; -mod scope; -#[cfg(feature = "watch")] -mod watcher; - -#[cfg(not(target_os = "android"))] -pub use desktop::Fs; -#[cfg(target_os = "android")] -pub use mobile::Fs; - -pub use error::Error; -pub use scope::{Event as ScopeEvent, Scope}; - -pub use file_path::FilePath; -pub use file_path::SafeFilePath; - -type Result = std::result::Result; - -#[derive(Debug, Default, Clone, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct OpenOptions { - #[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, - #[serde(default)] - #[allow(unused)] - mode: Option, - #[serde(default)] - #[allow(unused)] - custom_flags: Option, -} - -fn default_true() -> bool { - true -} - -impl From for std::fs::OpenOptions { - fn from(open_options: OpenOptions) -> Self { - let mut opts = std::fs::OpenOptions::new(); - - #[cfg(unix)] - { - use std::os::unix::fs::OpenOptionsExt; - if let Some(mode) = open_options.mode { - opts.mode(mode); - } - if let Some(flags) = open_options.custom_flags { - opts.custom_flags(flags); - } - } - - opts.read(open_options.read) - .write(open_options.write) - .create(open_options.create) - .append(open_options.append) - .truncate(open_options.truncate) - .create_new(open_options.create_new); - - opts - } -} - -impl OpenOptions { - /// Creates a blank new set of options ready for configuration. - /// - /// All options are initially set to `false`. - /// - /// # Examples - /// - /// ```no_run - /// use tauri_plugin_fs::OpenOptions; - /// - /// let mut options = OpenOptions::new(); - /// let file = options.read(true).open("foo.txt"); - /// ``` - #[must_use] - pub fn new() -> Self { - Self::default() - } - - /// Sets the option for read access. - /// - /// This option, when true, will indicate that the file should be - /// `read`-able if opened. - /// - /// # Examples - /// - /// ```no_run - /// use tauri_plugin_fs::OpenOptions; - /// - /// let file = OpenOptions::new().read(true).open("foo.txt"); - /// ``` - pub fn read(&mut self, read: bool) -> &mut Self { - self.read = read; - self - } - - /// Sets the option for write access. - /// - /// This option, when true, will indicate that the file should be - /// `write`-able if opened. - /// - /// If the file already exists, any write calls on it will overwrite its - /// contents, without truncating it. - /// - /// # Examples - /// - /// ```no_run - /// use tauri_plugin_fs::OpenOptions; - /// - /// let file = OpenOptions::new().write(true).open("foo.txt"); - /// ``` - pub fn write(&mut self, write: bool) -> &mut Self { - self.write = write; - self - } - - /// Sets the option for the append mode. - /// - /// This option, when true, means that writes will append to a file instead - /// of overwriting previous contents. - /// Note that setting `.write(true).append(true)` has the same effect as - /// setting only `.append(true)`. - /// - /// Append mode guarantees that writes will be positioned at the current end of file, - /// even when there are other processes or threads appending to the same file. This is - /// unlike [seek]\([SeekFrom]::[End]\(0)) followed by `write()`, which - /// has a race between seeking and writing during which another writer can write, with - /// our `write()` overwriting their data. - /// - /// Keep in mind that this does not necessarily guarantee that data appended by - /// different processes or threads does not interleave. The amount of data accepted a - /// single `write()` call depends on the operating system and file system. A - /// successful `write()` is allowed to write only part of the given data, so even if - /// you're careful to provide the whole message in a single call to `write()`, there - /// is no guarantee that it will be written out in full. If you rely on the filesystem - /// accepting the message in a single write, make sure that all data that belongs - /// together is written in one operation. This can be done by concatenating strings - /// before passing them to [`write()`]. - /// - /// If a file is opened with both read and append access, beware that after - /// opening, and after every write, the position for reading may be set at the - /// end of the file. So, before writing, save the current position (using - /// [Seek]::[stream_position]), and restore it before the next read. - /// - /// ## Note - /// - /// This function doesn't create the file if it doesn't exist. Use the - /// [`OpenOptions::create`] method to do so. - /// - /// [`write()`]: Write::write "io::Write::write" - /// [`flush()`]: Write::flush "io::Write::flush" - /// [stream_position]: Seek::stream_position "io::Seek::stream_position" - /// [seek]: Seek::seek "io::Seek::seek" - /// [Current]: SeekFrom::Current "io::SeekFrom::Current" - /// [End]: SeekFrom::End "io::SeekFrom::End" - /// - /// # Examples - /// - /// ```no_run - /// use tauri_plugin_fs::OpenOptions; - /// - /// let file = OpenOptions::new().append(true).open("foo.txt"); - /// ``` - pub fn append(&mut self, append: bool) -> &mut Self { - self.append = append; - self - } - - /// Sets the option for truncating a previous file. - /// - /// If a file is successfully opened with this option set it will truncate - /// the file to 0 length if it already exists. - /// - /// The file must be opened with write access for truncate to work. - /// - /// # Examples - /// - /// ```no_run - /// use tauri_plugin_fs::OpenOptions; - /// - /// let file = OpenOptions::new().write(true).truncate(true).open("foo.txt"); - /// ``` - pub fn truncate(&mut self, truncate: bool) -> &mut Self { - self.truncate = truncate; - self - } - - /// Sets the option to create a new file, or open it if it already exists. - /// - /// In order for the file to be created, [`OpenOptions::write`] or - /// [`OpenOptions::append`] access must be used. - /// - /// - /// # Examples - /// - /// ```no_run - /// use tauri_plugin_fs::OpenOptions; - /// - /// let file = OpenOptions::new().write(true).create(true).open("foo.txt"); - /// ``` - pub fn create(&mut self, create: bool) -> &mut Self { - self.create = create; - self - } - - /// Sets the option to create a new file, failing if it already exists. - /// - /// No file is allowed to exist at the target location, also no (dangling) symlink. In this - /// way, if the call succeeds, the file returned is guaranteed to be new. - /// If a file exists at the target location, creating a new file will fail with [`AlreadyExists`] - /// or another error based on the situation. See [`OpenOptions::open`] for a - /// non-exhaustive list of likely errors. - /// - /// This option is useful because it is atomic. Otherwise between checking - /// whether a file exists and creating a new one, the file may have been - /// created by another process (a TOCTOU race condition / attack). - /// - /// If `.create_new(true)` is set, [`.create()`] and [`.truncate()`] are - /// ignored. - /// - /// The file must be opened with write or append access in order to create - /// a new file. - /// - /// [`.create()`]: OpenOptions::create - /// [`.truncate()`]: OpenOptions::truncate - /// [`AlreadyExists`]: io::ErrorKind::AlreadyExists - /// - /// # Examples - /// - /// ```no_run - /// use tauri_plugin_fs::OpenOptions; - /// - /// let file = OpenOptions::new().write(true) - /// .create_new(true) - /// .open("foo.txt"); - /// ``` - pub fn create_new(&mut self, create_new: bool) -> &mut Self { - self.create_new = create_new; - self - } -} - -#[cfg(unix)] -impl std::os::unix::fs::OpenOptionsExt for OpenOptions { - fn custom_flags(&mut self, flags: i32) -> &mut Self { - self.custom_flags.replace(flags); - self - } - - fn mode(&mut self, mode: u32) -> &mut Self { - self.mode.replace(mode); - self - } -} - -impl OpenOptions { - #[cfg(target_os = "android")] - fn android_mode(&self) -> String { - let mut mode = String::new(); - - if self.read { - mode.push('r'); - } - if self.write { - mode.push('w'); - } - if self.truncate { - mode.push('t'); - } - if self.append { - mode.push('a'); - } - - mode - } -} - -impl Fs { - pub fn read_to_string>(&self, path: P) -> std::io::Result { - let mut s = String::new(); - self.open( - path, - OpenOptions { - read: true, - ..Default::default() - }, - )? - .read_to_string(&mut s)?; - Ok(s) - } - - pub fn read>(&self, path: P) -> std::io::Result> { - let mut buf = Vec::new(); - self.open( - path, - OpenOptions { - read: true, - ..Default::default() - }, - )? - .read_to_end(&mut buf)?; - Ok(buf) - } -} - -// 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 path = serde_json::from_value(raw.into()).map(|raw| match raw { - scope::EntryRaw::Value(path) => path, - scope::EntryRaw::Object { path } => path, - })?; - - match app.path().parse(path) { - Ok(path) => Ok(Self { path: Some(path) }), - #[cfg(not(target_os = "android"))] - Err(tauri::Error::UnknownPath) => Ok(Self { path: None }), - Err(err) => Err(err.into()), - } - } -} - -pub trait FsExt { - fn fs_scope(&self) -> &Scope; - fn try_fs_scope(&self) -> Option<&Scope>; - - /// Cross platform file system APIs that also support manipulating Android files. - fn fs(&self) -> &Fs; -} - -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()) - } - - fn fs(&self) -> &Fs { - self.state::>().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); - - #[cfg(target_os = "android")] - { - let fs = mobile::init(app, api)?; - app.manage(fs); - } - #[cfg(not(target_os = "android"))] - app.manage(Fs(app.clone())); - - app.manage(scope); - Ok(()) - }) - .on_event(|app, event| { - if let RunEvent::WindowEvent { - label: _, - event: WindowEvent::DragDrop(DragDropEvent::Drop { 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/mobile.rs b/src/mobile.rs deleted file mode 100644 index 06422be..0000000 --- a/src/mobile.rs +++ /dev/null @@ -1,96 +0,0 @@ -// Copyright 2019-2023 Tauri Programme within The Commons Conservancy -// SPDX-License-Identifier: Apache-2.0 -// SPDX-License-Identifier: MIT - -use serde::de::DeserializeOwned; -use tauri::{ - plugin::{PluginApi, PluginHandle}, - AppHandle, Runtime, -}; - -use crate::{models::*, FilePath, OpenOptions}; - -#[cfg(target_os = "android")] -const PLUGIN_IDENTIFIER: &str = "com.plugin.fs"; - -#[cfg(target_os = "ios")] -tauri::ios_plugin_binding!(init_plugin_fs); - -// initializes the Kotlin or Swift plugin classes -pub fn init( - _app: &AppHandle, - api: PluginApi, -) -> crate::Result> { - #[cfg(target_os = "android")] - let handle = api - .register_android_plugin(PLUGIN_IDENTIFIER, "FsPlugin") - .unwrap(); - #[cfg(target_os = "ios")] - let handle = api.register_ios_plugin(init_plugin_android - intent - send)?; - Ok(Fs(handle)) -} - -/// Access to the android-intent-send APIs. -pub struct Fs(PluginHandle); - -impl Fs { - pub fn open>( - &self, - path: P, - opts: OpenOptions, - ) -> std::io::Result { - match path.into() { - FilePath::Url(u) => self - .resolve_content_uri(u.to_string(), opts.android_mode()) - .map_err(|e| { - std::io::Error::new( - std::io::ErrorKind::Other, - format!("failed to open file: {e}"), - ) - }), - FilePath::Path(p) => { - // tauri::utils::platform::resources_dir() returns a PathBuf with the Android asset URI prefix - // we must resolve that file with the Android API - if p.strip_prefix(tauri::utils::platform::ANDROID_ASSET_PROTOCOL_URI_PREFIX) - .is_ok() - { - self.resolve_content_uri(p.to_string_lossy(), opts.android_mode()) - .map_err(|e| { - std::io::Error::new( - std::io::ErrorKind::Other, - format!("failed to open file: {e}"), - ) - }) - } else { - std::fs::OpenOptions::from(opts).open(p) - } - } - } - } - - #[cfg(target_os = "android")] - fn resolve_content_uri( - &self, - uri: impl Into, - mode: impl Into, - ) -> crate::Result { - #[cfg(target_os = "android")] - { - let result = self.0.run_mobile_plugin::( - "getFileDescriptor", - GetFileDescriptorPayload { - uri: uri.into(), - mode: mode.into(), - }, - )?; - if let Some(fd) = result.fd { - Ok(unsafe { - use std::os::fd::FromRawFd; - std::fs::File::from_raw_fd(fd) - }) - } else { - todo!() - } - } - } -} diff --git a/src/models.rs b/src/models.rs deleted file mode 100644 index b9edc2c..0000000 --- a/src/models.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, Serialize}; - -#[derive(Debug, Clone, Default, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct GetFileDescriptorPayload { - pub uri: String, - pub mode: String, -} - -#[derive(Debug, Clone, Default, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct GetFileDescriptorResponse { - pub fd: Option, -} diff --git a/src/scope.rs b/src/scope.rs deleted file mode 100644 index fd94b0e..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: Option, -} - -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 cf2af50..0000000 --- a/src/watcher.rs +++ /dev/null @@ -1,159 +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, - 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, - SafeFilePath, -}; - -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(Clone, 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(()) - }) -}