From 55fe1d144f30aa5aebeeb8ef937335f094720b4b Mon Sep 17 00:00:00 2001 From: bicarlsen Date: Sun, 10 Sep 2023 09:24:27 +0200 Subject: [PATCH] Implemented `fs` module (#19) --- Cargo.lock | 5 +- Cargo.toml | 4 +- README.md | 3 +- src/dialog.rs | 2 +- src/error.rs | 9 +- src/fs.rs | 513 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 12 +- 7 files changed, 535 insertions(+), 13 deletions(-) create mode 100644 src/fs.rs diff --git a/Cargo.lock b/Cargo.lock index e896a45..c906db9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2468,9 +2468,9 @@ dependencies = [ [[package]] name = "serde_repr" -version = "0.1.9" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fe39d9fbb0ebf5eb2c7cb7e2a47e4f462fad1379f1166b8ae49ad9eae89a7ca" +checksum = "9a5ec9fa74a20ebbe5d9ac23dac1fc96ba0ecfe9f50f2843b52e537b10fbcb4e" dependencies = [ "proc-macro2", "quote", @@ -3049,6 +3049,7 @@ dependencies = [ "semver 1.0.14", "serde", "serde-wasm-bindgen", + "serde_repr", "tauri-sys", "thiserror", "url", diff --git a/Cargo.toml b/Cargo.toml index 7d1815c..c602141 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ log = "0.4.17" semver = {version = "1.0.14", optional = true, features = ["serde"]} serde = {version = "1.0.140", features = ["derive"]} serde-wasm-bindgen = "0.4.3" +serde_repr = "0.1.10" thiserror = "1.0.37" url = {version = "2.3.1", optional = true, features = ["serde"]} wasm-bindgen = {version = "0.2.82", features = ["serde_json"]} @@ -25,11 +26,12 @@ wasm-bindgen-test = "0.3.33" all-features = true [features] -all = ["app", "clipboard", "event", "mocks", "tauri", "window", "process", "dialog", "os", "notification", "path", "updater", "global_shortcut"] +all = ["app", "clipboard", "event", "fs", "mocks", "tauri", "window", "process", "dialog", "os", "notification", "path", "updater", "global_shortcut"] app = ["dep:semver"] clipboard = [] dialog = [] event = ["dep:futures"] +fs = [] global_shortcut = [] mocks = [] notification = [] diff --git a/README.md b/README.md index 0acd7e2..96f3024 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,7 @@ All modules are gated by accordingly named Cargo features. It is recommended you - **clipboard**: Enables the `clipboard` module. - **dialog**: Enables the `dialog` module. - **event**: Enables the `event` module. +- **fs**: Enables the `fs` module. - **mocks**: Enables the `mocks` module. - **tauri**: Enables the `tauri` module. @@ -65,7 +66,7 @@ These API bindings are not completely on-par with `@tauri-apps/api` yet, but her - [x] `clipboard` - [x] `dialog` - [x] `event` -- [ ] `fs` +- [x] `fs` - [x] `global_shortcut` - [ ] `http` - [x] `mocks` diff --git a/src/dialog.rs b/src/dialog.rs index 9f5bb7f..5d320fa 100644 --- a/src/dialog.rs +++ b/src/dialog.rs @@ -1,6 +1,6 @@ //! Native system dialogs for opening and saving files. //! -//! The APIs must be added to tauri.allowlist.dialog in tauri.conf.json: +//! The APIs must be added to `tauri.allowlist.dialog` in `tauri.conf.json`: //! ```json //! { //! "tauri": { diff --git a/src/error.rs b/src/error.rs index e6ade40..65d4389 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,6 +1,6 @@ +use std::path::PathBuf; use wasm_bindgen::JsValue; - #[derive(Clone, Eq, PartialEq, Debug, thiserror::Error)] pub enum Error { #[error("JS Binding: {0}")] @@ -9,7 +9,10 @@ pub enum Error { Serde(String), #[cfg(any(feature = "event", feature = "window"))] #[error("Oneshot cancelled: {0}")] - OneshotCanceled(#[from] futures::channel::oneshot::Canceled) + OneshotCanceled(#[from] futures::channel::oneshot::Canceled), + #[cfg(feature = "fs")] + #[error("could not convert path to string")] + Utf8(PathBuf), } impl From for Error { @@ -22,4 +25,4 @@ impl From for Error { fn from(e: JsValue) -> Self { Self::Binding(format!("{:?}", e)) } -} \ No newline at end of file +} diff --git a/src/fs.rs b/src/fs.rs new file mode 100644 index 0000000..269b532 --- /dev/null +++ b/src/fs.rs @@ -0,0 +1,513 @@ +//! Access the file system. +//! +//! The APIs must be added to `tauri.allowlist.fs` in `tauri.conf.json`: +//! ```json +//! { +//! "tauri": { +//! "allowlist": { +//! "fs": { +//! "all": true, // enable all FS APIs +//! "readFile": true, +//! "writeFile": true, +//! "readDir": true, +//! "copyFile": true, +//! "createDir": true, +//! "removeDir": true, +//! "removeFile": true, +//! "renameFile": true, +//! "exists": true +//! } +//! } +//! } +//! } +//! ``` +//! It is recommended to allowlist only the APIs you use for optimal bundle size and security. +use crate::Error; +use js_sys::ArrayBuffer; +use serde::{Deserialize, Serialize}; +use serde_repr::*; +use std::path::{Path, PathBuf}; +use std::str; + +#[derive(Serialize_repr, Clone, PartialEq, Eq, Debug)] +#[repr(u16)] +pub enum BaseDirectory { + Audio = 1, + Cache = 2, + Config = 3, + Data = 4, + LocalData = 5, + Desktop = 6, + Document = 7, + Download = 8, + Executable = 9, + Font = 10, + Home = 11, + Picture = 12, + Public = 13, + Runtime = 14, + Template = 15, + Video = 16, + Resource = 17, + App = 18, + Log = 19, + Temp = 20, + AppConfig = 21, + AppData = 22, + AppLocalData = 23, + AppCache = 24, + AppLog = 25, +} + +#[derive(Deserialize, Clone, PartialEq, Debug)] +pub struct FileEntry { + pub path: PathBuf, + pub name: Option, + pub children: Option>, +} + +#[derive(Serialize, Clone, PartialEq, Debug)] +struct FsDirOptions { + pub dir: Option, + pub recursive: Option, +} + +#[derive(Serialize, Clone, PartialEq, Debug)] +struct FsOptions { + pub dir: Option, +} + +#[derive(Serialize, Clone, PartialEq, Debug)] +struct FsTextFileOption { + pub contents: String, + path: PathBuf, +} + +/// Copies a file to a destination. +/// +/// # Example +/// +/// ```rust,no_run +/// use tauri_sys::fs; +/// +/// fs::copy_file(source, destination, BaseDirectory::Download).expect("could not copy file"); +/// ``` +/// +/// Requires [`allowlist > fs > copyFile`](https://tauri.app/v1/api/js/fs) to be enabled. +pub async fn copy_file(source: &Path, destination: &Path, dir: BaseDirectory) -> crate::Result<()> { + let Some(source) = source.to_str() else { + return Err(Error::Utf8(source.to_path_buf())); + }; + + let Some(destination) = destination.to_str() else { + return Err(Error::Utf8(destination.to_path_buf())); + }; + + let raw = inner::copyFile( + source, + destination, + serde_wasm_bindgen::to_value(&FsOptions { dir: Some(dir) })?, + ) + .await?; + + Ok(serde_wasm_bindgen::from_value(raw)?) +} + +/// Creates a directory. +/// If one of the path's parent components doesn't exist the promise will be rejected. +/// +/// # Example +/// +/// ```rust,no_run +/// use tauri_sys::fs; +/// +/// fs::create_dir(dir, BaseDirectory::Download).expect("could not create directory"); +/// ``` +/// +/// Requires [`allowlist > fs > createDir`](https://tauri.app/v1/api/js/fs) to be enabled. +pub async fn create_dir(dir: &Path, base_dir: BaseDirectory) -> crate::Result<()> { + let recursive = Some(false); + + let Some(dir) = dir.to_str() else { + return Err(Error::Utf8(dir.to_path_buf())); + }; + + Ok(inner::createDir( + dir, + serde_wasm_bindgen::to_value(&FsDirOptions { + dir: Some(base_dir), + recursive, + })?, + ) + .await?) +} + +/// Creates a directory recursively. +/// +/// # Example +/// +/// ```rust,no_run +/// use tauri_sys::fs; +/// +/// fs::create_dir_all(dir, BaseDirectory::Download).expect("could not create directory"); +/// ``` +/// +/// Requires [`allowlist > fs > createDir`](https://tauri.app/v1/api/js/fs) to be enabled. +pub async fn create_dir_all(dir: &Path, base_dir: BaseDirectory) -> crate::Result<()> { + let recursive = Some(true); + + let Some(dir) = dir.to_str() else { + return Err(Error::Utf8(dir.to_path_buf())); + }; + + Ok(inner::createDir( + dir, + serde_wasm_bindgen::to_value(&FsDirOptions { + dir: Some(base_dir), + recursive, + })?, + ) + .await?) +} + +/// Checks if a path exists. +/// +/// # Example +/// +/// ```rust,no_run +/// use tauri_sys::fs; +/// +/// let file_exists = fs::exists(path, BaseDirectory::Download).expect("could not check if path exists"); +/// ``` +/// +/// Requires [`allowlist > fs > exists`](https://tauri.app/v1/api/js/fs) to be enabled. +pub async fn exists(path: &Path, dir: BaseDirectory) -> crate::Result { + let Some(path) = path.to_str() else { + return Err(Error::Utf8(path.to_path_buf())); + }; + + let raw = inner::exists( + path, + serde_wasm_bindgen::to_value(&FsOptions { dir: Some(dir) })?, + ) + .await?; + + Ok(serde_wasm_bindgen::from_value(raw)?) +} + +/// Reads a file as a byte array. +/// +/// # Example +/// +/// ```rust,no_run +/// use tauri_sys::fs; +/// +/// let contents = fs::read_binary_file(filePath, BaseDirectory::Download).expect("could not read file contents"); +/// ``` +/// +/// Requires [`allowlist > fs > readBinaryFile`](https://tauri.app/v1/api/js/fs) to be enabled. +pub async fn read_binary_file(path: &Path, dir: BaseDirectory) -> crate::Result> { + let Some(path) = path.to_str() else { + return Err(Error::Utf8(path.to_path_buf())); + }; + + let raw = inner::readBinaryFile( + path, + serde_wasm_bindgen::to_value(&FsOptions { dir: Some(dir) })?, + ) + .await?; + + Ok(serde_wasm_bindgen::from_value(raw)?) +} + +/// List directory files. +/// +/// # Example +/// +/// ```rust,no_run +/// use tauri_sys::fs; +/// +/// let files = fs::read_dir(path, BaseDirectory::Download).expect("could not read directory"); +/// ``` +/// +/// Requires [`allowlist > fs > readDir`](https://tauri.app/v1/api/js/fs) to be enabled. +pub async fn read_dir(path: &Path, dir: BaseDirectory) -> crate::Result> { + let recursive = Some(false); + let Some(path) = path.to_str() else { + return Err(Error::Utf8(path.to_path_buf())); + }; + + let raw = inner::readDir( + path, + serde_wasm_bindgen::to_value(&FsDirOptions { + dir: Some(dir), + recursive, + })?, + ) + .await?; + + Ok(serde_wasm_bindgen::from_value(raw)?) +} + +/// List directory files recursively. +/// +/// # Example +/// +/// ```rust,no_run +/// use tauri_sys::fs; +/// +/// let files = fs::read_dir_all(path, BaseDirectory::Download).expect("could not read directory"); +/// ``` +/// +/// Requires [`allowlist > fs > readDir`](https://tauri.app/v1/api/js/fs) to be enabled. +pub async fn read_dir_all(path: &Path, dir: BaseDirectory) -> crate::Result> { + let recursive = Some(true); + let Some(path) = path.to_str() else { + return Err(Error::Utf8(path.to_path_buf())); + }; + + let raw = inner::readDir( + path, + serde_wasm_bindgen::to_value(&FsDirOptions { + dir: Some(dir), + recursive, + })?, + ) + .await?; + + Ok(serde_wasm_bindgen::from_value(raw)?) +} + +/// Read a file as an UTF-8 encoded string. +/// +/// # Example +/// +/// ```rust,no_run +/// use tauri_sys::fs; +/// +/// let contents = fs::readTextFile(path, BaseDirectory::Download).expect("could not read file as text"); +/// ``` +/// +/// Requires [`allowlist > fs > readTextFile`](https://tauri.app/v1/api/js/fs) to be enabled. +pub async fn read_text_file(path: &Path, dir: BaseDirectory) -> crate::Result { + let Some(path) = path.to_str() else { + return Err(Error::Utf8(path.to_path_buf())); + }; + + let raw = inner::readTextFile( + path, + serde_wasm_bindgen::to_value(&FsOptions { dir: Some(dir) })?, + ) + .await?; + + Ok(serde_wasm_bindgen::from_value(raw)?) +} + +/// Removes a directory. +/// If the directory is not empty the promise will be rejected. +/// +/// # Example +/// +/// ```rust,no_run +/// use tauri_sys::fs; +/// +/// fs::remove_dir(path, BaseDirectory::Download).expect("could not remove directory"); +/// ``` +/// +/// Requires [`allowlist > fs > removeDir`](https://tauri.app/v1/api/js/fs) to be enabled. +pub async fn remove_dir(dir: &Path, base_dir: BaseDirectory) -> crate::Result<()> { + let recursive = Some(false); + let Some(dir) = dir.to_str() else { + return Err(Error::Utf8(dir.to_path_buf())); + }; + + Ok(inner::removeDir( + dir, + serde_wasm_bindgen::to_value(&FsDirOptions { + dir: Some(base_dir), + recursive, + })?, + ) + .await?) +} + +/// Removes a directory and its contents. +/// +/// # Example +/// +/// ```rust,no_run +/// use tauri_sys::fs; +/// +/// fs::remove_dir_all(path, BaseDirectory::Download).expect("could not remove directory"); +/// ``` +/// +/// Requires [`allowlist > fs > removeDir`](https://tauri.app/v1/api/js/fs) to be enabled. +pub async fn remove_dir_all(dir: &Path, base_dir: BaseDirectory) -> crate::Result<()> { + let recursive = Some(true); + let Some(dir) = dir.to_str() else { + return Err(Error::Utf8(dir.to_path_buf())); + }; + + Ok(inner::removeDir( + dir, + serde_wasm_bindgen::to_value(&FsDirOptions { + dir: Some(base_dir), + recursive, + })?, + ) + .await?) +} + +/// Removes a file. +/// +/// # Example +/// +/// ```rust,no_run +/// use tauri_sys::fs; +/// +/// fs::remove_file(path, BaseDirectory::Download).expect("could not remove file"); +/// ``` +/// +/// Requires [`allowlist > fs > removeFile`](https://tauri.app/v1/api/js/fs) to be enabled. +pub async fn remove_file(file: &Path, dir: BaseDirectory) -> crate::Result<()> { + let Some(file) = file.to_str() else { + return Err(Error::Utf8(file.to_path_buf())); + }; + + Ok(inner::removeFile( + file, + serde_wasm_bindgen::to_value(&FsOptions { dir: Some(dir) })?, + ) + .await?) +} + +/// Renames a file. +/// +/// # Example +/// +/// ```rust,no_run +/// use tauri_sys::fs; +/// +/// fs::rename_file(old_path, new_path, BaseDirectory::Download).expect("could not rename file"); +/// ``` +/// +/// Requires [`allowlist > fs > renameFile`](https://tauri.app/v1/api/js/fs) to be enabled. +pub async fn rename_file( + old_path: &Path, + new_path: &Path, + dir: BaseDirectory, +) -> crate::Result<()> { + let Some(old_path) = old_path.to_str() else { + return Err(Error::Utf8(old_path.to_path_buf())); + }; + + let Some(new_path) = new_path.to_str() else { + return Err(Error::Utf8(new_path.to_path_buf())); + }; + + Ok(inner::renameFile( + old_path, + new_path, + serde_wasm_bindgen::to_value(&FsOptions { dir: Some(dir) })?, + ) + .await?) +} + +/// Writes a byte array content to a file. +/// +/// # Example +/// +/// ```rust,no_run +/// use tauri_sys::fs; +/// +/// fs::write_binary_file(path, contents, BaseDirectory::Download).expect("could not writet binary file"); +/// ``` +/// +/// Requires [`allowlist > fs > writeBinaryFile`](https://tauri.app/v1/api/js/fs) to be enabled. +pub async fn write_binary_file( + path: &Path, + contents: ArrayBuffer, + dir: BaseDirectory, +) -> crate::Result<()> { + let Some(path) = path.to_str() else { + return Err(Error::Utf8(path.to_path_buf())); + }; + + Ok(inner::writeBinaryFile( + path, + contents, + serde_wasm_bindgen::to_value(&FsOptions { dir: Some(dir) })?, + ) + .await?) +} + +/// Writes a UTF-8 text file. +/// +/// # Example +/// +/// ```rust,no_run +/// use tauri_sys::fs; +/// +/// fs::write_text_file(path, contents, BaseDirectory::Download).expect("could not writet binary file"); +/// ``` +/// +/// Requires [`allowlist > fs > writeTextFile`](https://tauri.app/v1/api/js/fs) to be enabled. +pub async fn write_text_file(path: &Path, contents: &str, dir: BaseDirectory) -> crate::Result<()> { + let Some(path) = path.to_str() else { + return Err(Error::Utf8(path.to_path_buf())); + }; + + Ok(inner::writeTextFile( + path, + &contents, + serde_wasm_bindgen::to_value(&FsOptions { dir: Some(dir) })?, + ) + .await?) +} + +mod inner { + use super::ArrayBuffer; + use wasm_bindgen::{prelude::wasm_bindgen, JsValue}; + + #[wasm_bindgen(module = "/src/fs.js")] + extern "C" { + #[wasm_bindgen(catch)] + pub async fn copyFile( + source: &str, + destination: &str, + options: JsValue, + ) -> Result; + #[wasm_bindgen(catch)] + pub async fn createDir(dir: &str, options: JsValue) -> Result<(), JsValue>; + #[wasm_bindgen(catch)] + pub async fn exists(path: &str, options: JsValue) -> Result; + #[wasm_bindgen(catch)] + pub async fn readBinaryFile(filePath: &str, options: JsValue) -> Result; + #[wasm_bindgen(catch)] + pub async fn readTextFile(filePath: &str, options: JsValue) -> Result; + #[wasm_bindgen(catch)] + pub async fn readDir(dir: &str, options: JsValue) -> Result; + #[wasm_bindgen(catch)] + pub async fn removeDir(dir: &str, options: JsValue) -> Result<(), JsValue>; + #[wasm_bindgen(catch)] + pub async fn removeFile(source: &str, options: JsValue) -> Result<(), JsValue>; + #[wasm_bindgen(catch)] + pub async fn renameFile( + oldPath: &str, + newPath: &str, + options: JsValue, + ) -> Result<(), JsValue>; + #[wasm_bindgen(catch)] + pub async fn writeBinaryFile( + filePath: &str, + contents: ArrayBuffer, + options: JsValue, + ) -> Result<(), JsValue>; + #[wasm_bindgen(catch)] + pub async fn writeTextFile( + filePath: &str, + contents: &str, + options: JsValue, + ) -> Result<(), JsValue>; + } +} diff --git a/src/lib.rs b/src/lib.rs index 5c099dc..51f5244 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -127,6 +127,8 @@ pub mod dialog; mod error; #[cfg(feature = "event")] pub mod event; +#[cfg(feature = "fs")] +pub mod fs; #[cfg(feature = "global_shortcut")] pub mod global_shortcut; #[cfg(feature = "mocks")] @@ -155,24 +157,24 @@ pub(crate) mod utils { pos: u32, arr: js_sys::Array, } - + impl ArrayIterator { pub fn new(arr: js_sys::Array) -> Self { Self { pos: 0, arr } } } - + impl Iterator for ArrayIterator { type Item = wasm_bindgen::JsValue; - + fn next(&mut self) -> Option { let raw = self.arr.get(self.pos); - + if raw.is_undefined() { None } else { self.pos += 1; - + Some(raw) } }