refactor: unify fs read and write cmds for binary/text data [TRI-009] (#34)

This commit is contained in:
Lucas Nogueira 2022-01-09 16:24:44 -03:00
parent bf5667f21c
commit 766c4f2c57
No known key found for this signature in database
GPG Key ID: 2714B66BCFB01F7F
9 changed files with 50 additions and 180 deletions

View File

@ -602,18 +602,12 @@ pub struct FsAllowlistConfig {
/// Use this flag to enable all file system API features. /// Use this flag to enable all file system API features.
#[serde(default)] #[serde(default)]
pub all: bool, pub all: bool,
/// Read text file from local filesystem. /// Read file from local filesystem.
#[serde(default)] #[serde(default)]
pub read_text_file: bool, pub read_file: bool,
/// Read binary file from local filesystem. /// Write file to local filesystem.
#[serde(default)]
pub read_binary_file: bool,
/// Write text file to local filesystem.
#[serde(default)] #[serde(default)]
pub write_file: bool, pub write_file: bool,
/// Write binary file to local filesystem.
#[serde(default)]
pub write_binary_file: bool,
/// Read directory from local filesystem. /// Read directory from local filesystem.
#[serde(default)] #[serde(default)]
pub read_dir: bool, pub read_dir: bool,
@ -639,10 +633,8 @@ impl Allowlist for FsAllowlistConfig {
let allowlist = Self { let allowlist = Self {
scope: Default::default(), scope: Default::default(),
all: false, all: false,
read_text_file: true, read_file: true,
read_binary_file: true,
write_file: true, write_file: true,
write_binary_file: true,
read_dir: true, read_dir: true,
copy_file: true, copy_file: true,
create_dir: true, create_dir: true,
@ -660,10 +652,8 @@ impl Allowlist for FsAllowlistConfig {
vec!["fs-all"] vec!["fs-all"]
} else { } else {
let mut features = Vec::new(); let mut features = Vec::new();
check_feature!(self, features, read_text_file, "fs-read-text-file"); check_feature!(self, features, read_file, "fs-read-file");
check_feature!(self, features, read_binary_file, "fs-read-binary-file");
check_feature!(self, features, write_file, "fs-write-file"); check_feature!(self, features, write_file, "fs-write-file");
check_feature!(self, features, write_binary_file, "fs-write-binary-file");
check_feature!(self, features, read_dir, "fs-read-dir"); check_feature!(self, features, read_dir, "fs-read-dir");
check_feature!(self, features, copy_file, "fs-copy-file"); check_feature!(self, features, copy_file, "fs-copy-file");
check_feature!(self, features, create_dir, "fs-create-dir"); check_feature!(self, features, create_dir, "fs-create-dir");

View File

@ -140,24 +140,20 @@ dialog-save = ["dialog"]
fs-all = [ fs-all = [
"fs-copy-file", "fs-copy-file",
"fs-create-dir", "fs-create-dir",
"fs-read-binary-file", "fs-read-file",
"fs-read-dir", "fs-read-dir",
"fs-read-text-file",
"fs-remove-dir", "fs-remove-dir",
"fs-remove-file", "fs-remove-file",
"fs-rename-file", "fs-rename-file",
"fs-write-binary-file",
"fs-write-file" "fs-write-file"
] ]
fs-copy-file = [] fs-copy-file = []
fs-create-dir = [] fs-create-dir = []
fs-read-binary-file = [] fs-read-file = []
fs-read-dir = [] fs-read-dir = []
fs-read-text-file = []
fs-remove-dir = [] fs-remove-dir = []
fs-remove-file = [] fs-remove-file = []
fs-rename-file = [] fs-rename-file = []
fs-write-binary-file = ["base64"]
fs-write-file = [] fs-write-file = []
global-shortcut-all = [] global-shortcut-all = []
http-all = ["http-request"] http-all = ["http-request"]

File diff suppressed because one or more lines are too long

View File

@ -25,7 +25,7 @@ pub enum Error {
#[error("user cancelled the dialog")] #[error("user cancelled the dialog")]
DialogCancelled, DialogCancelled,
/// The network error. /// The network error.
#[cfg(all(feature = "http", not(feature = "reqwest-client")))] #[cfg(all(feature = "http-api", not(feature = "reqwest-client")))]
#[error("Network Error: {0}")] #[error("Network Error: {0}")]
Network(#[from] attohttpc::Error), Network(#[from] attohttpc::Error),
/// The network error. /// The network error.

View File

@ -44,25 +44,14 @@ pub struct FileOperationOptions {
#[serde(tag = "cmd", rename_all = "camelCase")] #[serde(tag = "cmd", rename_all = "camelCase")]
pub enum Cmd { pub enum Cmd {
/// The read text file API. /// The read text file API.
ReadTextFile { ReadFile {
path: PathBuf,
options: Option<FileOperationOptions>,
},
/// The read binary file API.
ReadBinaryFile {
path: PathBuf, path: PathBuf,
options: Option<FileOperationOptions>, options: Option<FileOperationOptions>,
}, },
/// The write file API. /// The write file API.
WriteFile { WriteFile {
path: PathBuf, path: PathBuf,
contents: String, contents: Vec<u8>,
options: Option<FileOperationOptions>,
},
/// The write binary file API.
WriteBinaryFile {
path: PathBuf,
contents: String,
options: Option<FileOperationOptions>, options: Option<FileOperationOptions>,
}, },
/// The read dir API. /// The read dir API.
@ -101,24 +90,8 @@ pub enum Cmd {
} }
impl Cmd { impl Cmd {
#[module_command_handler(fs_read_text_file, "fs > readTextFile")] #[module_command_handler(fs_read_file, "fs > readFile")]
fn read_text_file<R: Runtime>( fn read_file<R: Runtime>(
context: InvokeContext<R>,
path: PathBuf,
options: Option<FileOperationOptions>,
) -> crate::Result<String> {
file::read_string(resolve_path(
&context.config,
&context.package_info,
&context.window,
path,
options.and_then(|o| o.dir),
)?)
.map_err(crate::Error::FailedToExecuteApi)
}
#[module_command_handler(fs_read_binary_file, "fs > readBinaryFile")]
fn read_binary_file<R: Runtime>(
context: InvokeContext<R>, context: InvokeContext<R>,
path: PathBuf, path: PathBuf,
options: Option<FileOperationOptions>, options: Option<FileOperationOptions>,
@ -137,7 +110,7 @@ impl Cmd {
fn write_file<R: Runtime>( fn write_file<R: Runtime>(
context: InvokeContext<R>, context: InvokeContext<R>,
path: PathBuf, path: PathBuf,
contents: String, contents: Vec<u8>,
options: Option<FileOperationOptions>, options: Option<FileOperationOptions>,
) -> crate::Result<()> { ) -> crate::Result<()> {
File::create(resolve_path( File::create(resolve_path(
@ -147,32 +120,8 @@ impl Cmd {
path, path,
options.and_then(|o| o.dir), options.and_then(|o| o.dir),
)?) )?)
.map_err(crate::Error::Io) .map_err(Into::into)
.and_then(|mut f| f.write_all(contents.as_bytes()).map_err(|err| err.into()))?; .and_then(|mut f| f.write_all(&contents).map_err(|err| err.into()))
Ok(())
}
#[module_command_handler(fs_write_binary_file, "fs > writeBinaryFile")]
fn write_binary_file<R: Runtime>(
context: InvokeContext<R>,
path: PathBuf,
contents: String,
options: Option<FileOperationOptions>,
) -> crate::Result<()> {
base64::decode(contents)
.map_err(crate::Error::Base64Decode)
.and_then(|c| {
File::create(resolve_path(
&context.config,
&context.package_info,
&context.window,
path,
options.and_then(|o| o.dir),
)?)
.map_err(Into::into)
.and_then(|mut f| f.write_all(&c).map_err(|err| err.into()))
})?;
Ok(())
} }
#[module_command_handler(fs_read_dir, "fs > readDir")] #[module_command_handler(fs_read_dir, "fs > readDir")]
@ -385,35 +334,20 @@ mod tests {
} }
} }
#[tauri_macros::module_command_test(fs_read_text_file, "fs > readTextFile")] #[tauri_macros::module_command_test(fs_read_file, "fs > readFile")]
#[quickcheck_macros::quickcheck] #[quickcheck_macros::quickcheck]
fn read_text_file(path: PathBuf, options: Option<FileOperationOptions>) { fn read_file(path: PathBuf, options: Option<FileOperationOptions>) {
let res = super::Cmd::read_text_file(crate::test::mock_invoke_context(), path, options); let res = super::Cmd::read_text_file(crate::test::mock_invoke_context(), path, options);
assert!(!matches!(res, Err(crate::Error::ApiNotAllowlisted(_)))); assert!(!matches!(res, Err(crate::Error::ApiNotAllowlisted(_))));
} }
#[tauri_macros::module_command_test(fs_read_binary_file, "fs > readBinaryFile")]
#[quickcheck_macros::quickcheck]
fn read_binary_file(path: PathBuf, options: Option<FileOperationOptions>) {
let res = super::Cmd::read_binary_file(crate::test::mock_invoke_context(), path, options);
assert!(!matches!(res, Err(crate::Error::ApiNotAllowlisted(_))));
}
#[tauri_macros::module_command_test(fs_write_file, "fs > writeFile")] #[tauri_macros::module_command_test(fs_write_file, "fs > writeFile")]
#[quickcheck_macros::quickcheck] #[quickcheck_macros::quickcheck]
fn write_file(path: PathBuf, contents: String, options: Option<FileOperationOptions>) { fn write_file(path: PathBuf, contents: Vec<u8>, options: Option<FileOperationOptions>) {
let res = super::Cmd::write_file(crate::test::mock_invoke_context(), path, contents, options); let res = super::Cmd::write_file(crate::test::mock_invoke_context(), path, contents, options);
assert!(!matches!(res, Err(crate::Error::ApiNotAllowlisted(_)))); assert!(!matches!(res, Err(crate::Error::ApiNotAllowlisted(_))));
} }
#[tauri_macros::module_command_test(fs_read_binary_file, "fs > writeBinaryFile")]
#[quickcheck_macros::quickcheck]
fn write_binary_file(path: PathBuf, contents: String, options: Option<FileOperationOptions>) {
let res =
super::Cmd::write_binary_file(crate::test::mock_invoke_context(), path, contents, options);
assert!(!matches!(res, Err(crate::Error::ApiNotAllowlisted(_))));
}
#[tauri_macros::module_command_test(fs_read_dir, "fs > readDir")] #[tauri_macros::module_command_test(fs_read_dir, "fs > readDir")]
#[quickcheck_macros::quickcheck] #[quickcheck_macros::quickcheck]
fn read_dir(path: PathBuf, options: Option<DirOperationOptions>) { fn read_dir(path: PathBuf, options: Option<DirOperationOptions>) {

View File

@ -42,7 +42,7 @@ pub enum Error {
#[error("{0}")] #[error("{0}")]
Io(#[from] std::io::Error), Io(#[from] std::io::Error),
/// Failed to decode base64. /// Failed to decode base64.
#[cfg(any(fs_write_binary_file, feature = "updater"))] #[cfg(feature = "updater")]
#[error("Failed to decode base64 string: {0}")] #[error("Failed to decode base64 string: {0}")]
Base64Decode(#[from] base64::DecodeError), Base64Decode(#[from] base64::DecodeError),
/// Failed to load window icon. /// Failed to load window icon.

View File

@ -50,14 +50,12 @@
//! - **fs-all**: Enables all [Filesystem APIs](https://tauri.studio/en/docs/api/js/modules/fs). //! - **fs-all**: Enables all [Filesystem APIs](https://tauri.studio/en/docs/api/js/modules/fs).
//! - **fs-copy-file**: Enables the [`copyFile` API](https://tauri.studio/en/docs/api/js/modules/fs#copyfile). //! - **fs-copy-file**: Enables the [`copyFile` API](https://tauri.studio/en/docs/api/js/modules/fs#copyfile).
//! - **fs-create-dir**: Enables the [`createDir` API](https://tauri.studio/en/docs/api/js/modules/fs#createdir). //! - **fs-create-dir**: Enables the [`createDir` API](https://tauri.studio/en/docs/api/js/modules/fs#createdir).
//! - **fs-read-binary-file**: Enables the [`readBinaryFile` API](https://tauri.studio/en/docs/api/js/modules/fs#readbinaryfile).
//! - **fs-read-dir**: Enables the [`readDir` API](https://tauri.studio/en/docs/api/js/modules/fs#readdir). //! - **fs-read-dir**: Enables the [`readDir` API](https://tauri.studio/en/docs/api/js/modules/fs#readdir).
//! - **fs-read-text-file**: Enables the [`readTextFile` API](https://tauri.studio/en/docs/api/js/modules/fs#readtextfile). //! - **fs-read-file**: Enables the [`readTextFile` API](https://tauri.studio/en/docs/api/js/modules/fs#readtextfile) and the [`readBinaryFile` API](https://tauri.studio/en/docs/api/js/modules/fs#readbinaryfile).
//! - **fs-remove-dir**: Enables the [`removeDir` API](https://tauri.studio/en/docs/api/js/modules/fs#removedir). //! - **fs-remove-dir**: Enables the [`removeDir` API](https://tauri.studio/en/docs/api/js/modules/fs#removedir).
//! - **fs-remove-file**: Enables the [`removeFile` API](https://tauri.studio/en/docs/api/js/modules/fs#removefile). //! - **fs-remove-file**: Enables the [`removeFile` API](https://tauri.studio/en/docs/api/js/modules/fs#removefile).
//! - **fs-rename-file**: Enables the [`renameFile` API](https://tauri.studio/en/docs/api/js/modules/fs#renamefile). //! - **fs-rename-file**: Enables the [`renameFile` API](https://tauri.studio/en/docs/api/js/modules/fs#renamefile).
//! - **fs-write-binary-file**: Enables the [`writeBinaryFile` API](https://tauri.studio/en/docs/api/js/modules/fs#writebinaryfile). //! - **fs-write-file**: Enables the [`writeFile` API](https://tauri.studio/en/docs/api/js/modules/fs#writefile) and the [`writeBinaryFile` API](https://tauri.studio/en/docs/api/js/modules/fs#writebinaryfile).
//! - **fs-write-file**: Enables the [`writeFile` API](https://tauri.studio/en/docs/api/js/modules/fs#writefile).
//! //!
//! ### Global shortcut allowlist //! ### Global shortcut allowlist
//! //!

View File

@ -14,10 +14,8 @@
* "allowlist": { * "allowlist": {
* "fs": { * "fs": {
* "all": true, // enable all FS APIs * "all": true, // enable all FS APIs
* "readTextFile": true, * "readFile": true,
* "readBinaryFile": true,
* "writeFile": true, * "writeFile": true,
* "writeBinaryFile": true,
* "readDir": true, * "readDir": true,
* "copyFile": true, * "copyFile": true,
* "createDir": true, * "createDir": true,
@ -67,14 +65,20 @@ interface FsDirOptions {
recursive?: boolean recursive?: boolean
} }
/** Options object used to write a UTF-8 string to a file. */
interface FsTextFileOption { interface FsTextFileOption {
/** Path to the file to write. */
path: string path: string
/** The UTF-8 string to write to the file. */
contents: string contents: string
} }
/** Options object used to write a binary data to a file. */
interface FsBinaryFileOption { interface FsBinaryFileOption {
/** Path to the file to write. */
path: string path: string
contents: ArrayBuffer /** The byte array contents. */
contents: Iterable<number> | ArrayLike<number>
} }
interface FileEntry { interface FileEntry {
@ -89,7 +93,7 @@ interface FileEntry {
} }
/** /**
* Reads a file as UTF-8 encoded string. * Reads a file as an UTF-8 encoded string.
* *
* @param filePath Path to the file. * @param filePath Path to the file.
* @param options Configuration object. * @param options Configuration object.
@ -99,14 +103,14 @@ async function readTextFile(
filePath: string, filePath: string,
options: FsOptions = {} options: FsOptions = {}
): Promise<string> { ): Promise<string> {
return invokeTauriCommand<string>({ return invokeTauriCommand<number[]>({
__tauriModule: 'Fs', __tauriModule: 'Fs',
message: { message: {
cmd: 'readTextFile', cmd: 'readFile',
path: filePath, path: filePath,
options options
} }
}) }).then((data) => new TextDecoder().decode(new Uint8Array(data)))
} }
/** /**
@ -123,7 +127,7 @@ async function readBinaryFile(
return invokeTauriCommand<number[]>({ return invokeTauriCommand<number[]>({
__tauriModule: 'Fs', __tauriModule: 'Fs',
message: { message: {
cmd: 'readBinaryFile', cmd: 'readFile',
path: filePath, path: filePath,
options options
} }
@ -131,7 +135,7 @@ async function readBinaryFile(
} }
/** /**
* Writes a text file. * Writes a UTF-8 text file.
* *
* @param file File configuration object. * @param file File configuration object.
* @param options Configuration object. * @param options Configuration object.
@ -153,50 +157,14 @@ async function writeFile(
message: { message: {
cmd: 'writeFile', cmd: 'writeFile',
path: file.path, path: file.path,
contents: file.contents, contents: Array.from(new TextEncoder().encode(file.contents)),
options options
} }
}) })
} }
/** @ignore */
const CHUNK_SIZE = 65536
/** /**
* Convert an Uint8Array to ascii string. * Writes a byte array content to a file.
*
* @ignore
* @param arr
* @returns An ASCII string.
*/
function uint8ArrayToString(arr: Uint8Array): string {
if (arr.length < CHUNK_SIZE) {
return String.fromCharCode.apply(null, Array.from(arr))
}
let result = ''
const arrLen = arr.length
for (let i = 0; i < arrLen; i++) {
const chunk = arr.subarray(i * CHUNK_SIZE, (i + 1) * CHUNK_SIZE)
result += String.fromCharCode.apply(null, Array.from(chunk))
}
return result
}
/**
* Convert an ArrayBuffer to base64 encoded string.
*
* @ignore
* @param buffer
* @returns A base64 encoded string.
*/
function arrayBufferToBase64(buffer: ArrayBuffer): string {
const str = uint8ArrayToString(new Uint8Array(buffer))
return btoa(str)
}
/**
* Writes a binary file.
* *
* @param file Write configuration object. * @param file Write configuration object.
* @param options Configuration object. * @param options Configuration object.
@ -216,9 +184,9 @@ async function writeBinaryFile(
return invokeTauriCommand({ return invokeTauriCommand({
__tauriModule: 'Fs', __tauriModule: 'Fs',
message: { message: {
cmd: 'writeBinaryFile', cmd: 'writeFile',
path: file.path, path: file.path,
contents: arrayBufferToBase64(file.contents), contents: Array.from(file.contents),
options options
} }
}) })

View File

@ -60,16 +60,14 @@
"all": false, "all": false,
"copyFile": false, "copyFile": false,
"createDir": false, "createDir": false,
"readBinaryFile": false,
"readDir": false, "readDir": false,
"readTextFile": false, "readFile": false,
"removeDir": false, "removeDir": false,
"removeFile": false, "removeFile": false,
"renameFile": false, "renameFile": false,
"scope": [ "scope": [
"$APP/**" "$APP/**"
], ],
"writeBinaryFile": false,
"writeFile": false "writeFile": false
}, },
"globalShortcut": { "globalShortcut": {
@ -234,16 +232,14 @@
"all": false, "all": false,
"copyFile": false, "copyFile": false,
"createDir": false, "createDir": false,
"readBinaryFile": false,
"readDir": false, "readDir": false,
"readTextFile": false, "readFile": false,
"removeDir": false, "removeDir": false,
"removeFile": false, "removeFile": false,
"renameFile": false, "renameFile": false,
"scope": [ "scope": [
"$APP/**" "$APP/**"
], ],
"writeBinaryFile": false,
"writeFile": false "writeFile": false
}, },
"allOf": [ "allOf": [
@ -972,18 +968,13 @@
"default": false, "default": false,
"type": "boolean" "type": "boolean"
}, },
"readBinaryFile": {
"description": "Read binary file from local filesystem.",
"default": false,
"type": "boolean"
},
"readDir": { "readDir": {
"description": "Read directory from local filesystem.", "description": "Read directory from local filesystem.",
"default": false, "default": false,
"type": "boolean" "type": "boolean"
}, },
"readTextFile": { "readFile": {
"description": "Read text file from local filesystem.", "description": "Read file from local filesystem.",
"default": false, "default": false,
"type": "boolean" "type": "boolean"
}, },
@ -1013,13 +1004,8 @@
} }
] ]
}, },
"writeBinaryFile": {
"description": "Write binary file to local filesystem.",
"default": false,
"type": "boolean"
},
"writeFile": { "writeFile": {
"description": "Write text file to local filesystem.", "description": "Write file to local filesystem.",
"default": false, "default": false,
"type": "boolean" "type": "boolean"
} }
@ -1256,14 +1242,14 @@
"type": "object", "type": "object",
"properties": { "properties": {
"csp": { "csp": {
"description": "The Content Security Policy that will be injected on all HTML files on the built application. If [`dev_csp`](SecurityConfig.dev_csp) is not specified, this value is also injected on dev.\n\nThis is a really important part of the configuration since it helps you ensure your WebView is secured. See https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP.", "description": "The Content Security Policy that will be injected on all HTML files on the built application. If [`dev_csp`](SecurityConfig.dev_csp) is not specified, this value is also injected on dev.\n\nThis is a really important part of the configuration since it helps you ensure your WebView is secured. See <https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP>.",
"type": [ "type": [
"string", "string",
"null" "null"
] ]
}, },
"devCsp": { "devCsp": {
"description": "The Content Security Policy that will be injected on all HTML files on development.\n\nThis is a really important part of the configuration since it helps you ensure your WebView is secured. See https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP.", "description": "The Content Security Policy that will be injected on all HTML files on development.\n\nThis is a really important part of the configuration since it helps you ensure your WebView is secured. See <https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP>.",
"type": [ "type": [
"string", "string",
"null" "null"
@ -1292,7 +1278,7 @@
"type": "boolean" "type": "boolean"
}, },
"sidecar": { "sidecar": {
"description": "Enable sidecar execution, allowing the JavaScript layer to spawn a sidecar program, an executable that is shipped with the application. For more information see https://tauri.studio/en/docs/usage/guides/bundler/sidecar.", "description": "Enable sidecar execution, allowing the JavaScript layer to spawn a sidecar program, an executable that is shipped with the application. For more information see <https://tauri.studio/en/docs/usage/guides/bundler/sidecar>.",
"default": false, "default": false,
"type": "boolean" "type": "boolean"
} }
@ -1343,16 +1329,14 @@
"all": false, "all": false,
"copyFile": false, "copyFile": false,
"createDir": false, "createDir": false,
"readBinaryFile": false,
"readDir": false, "readDir": false,
"readTextFile": false, "readFile": false,
"removeDir": false, "removeDir": false,
"removeFile": false, "removeFile": false,
"renameFile": false, "renameFile": false,
"scope": [ "scope": [
"$APP/**" "$APP/**"
], ],
"writeBinaryFile": false,
"writeFile": false "writeFile": false
}, },
"globalShortcut": { "globalShortcut": {
@ -1960,7 +1944,7 @@
} }
}, },
"language": { "language": {
"description": "The installer language. See https://docs.microsoft.com/en-us/windows/win32/msi/localizing-the-error-and-actiontext-tables.", "description": "The installer language. See <https://docs.microsoft.com/en-us/windows/win32/msi/localizing-the-error-and-actiontext-tables>.",
"default": "en-US", "default": "en-US",
"type": "string" "type": "string"
}, },