Implemented fs module (#19)

This commit is contained in:
bicarlsen 2023-09-10 09:24:27 +02:00 committed by GitHub
parent 36c873b825
commit 55fe1d144f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 535 additions and 13 deletions

5
Cargo.lock generated
View file

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

View file

@ -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 = []

View file

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

View file

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

View file

@ -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<serde_wasm_bindgen::Error> for Error {
@ -22,4 +25,4 @@ impl From<JsValue> for Error {
fn from(e: JsValue) -> Self {
Self::Binding(format!("{:?}", e))
}
}
}

513
src/fs.rs Normal file
View file

@ -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<String>,
pub children: Option<Vec<FileEntry>>,
}
#[derive(Serialize, Clone, PartialEq, Debug)]
struct FsDirOptions {
pub dir: Option<BaseDirectory>,
pub recursive: Option<bool>,
}
#[derive(Serialize, Clone, PartialEq, Debug)]
struct FsOptions {
pub dir: Option<BaseDirectory>,
}
#[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<bool> {
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<Vec<u8>> {
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<Vec<FileEntry>> {
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<Vec<FileEntry>> {
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<String> {
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<JsValue, JsValue>;
#[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<JsValue, JsValue>;
#[wasm_bindgen(catch)]
pub async fn readBinaryFile(filePath: &str, options: JsValue) -> Result<JsValue, JsValue>;
#[wasm_bindgen(catch)]
pub async fn readTextFile(filePath: &str, options: JsValue) -> Result<JsValue, JsValue>;
#[wasm_bindgen(catch)]
pub async fn readDir(dir: &str, options: JsValue) -> Result<JsValue, JsValue>;
#[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>;
}
}

View file

@ -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<Self::Item> {
let raw = self.arr.get(self.pos);
if raw.is_undefined() {
None
} else {
self.pos += 1;
Some(raw)
}
}