feat(fs): support `ReadableStream<Unit8Array>` for `writeFile` API (#1964)

pull/1962/head^2
Amr Bashir 7 months ago committed by GitHub
parent ac2edc2159
commit 5092ea5e89
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,7 @@
---
"fs": "patch"
"fs-js": "patch"
---
Add support for using `ReadableStream<Unit8Array>` with `writeFile` API.

2
Cargo.lock generated

@ -6565,7 +6565,9 @@ dependencies = [
"serde_repr", "serde_repr",
"tauri", "tauri",
"tauri-plugin", "tauri-plugin",
"tauri-utils",
"thiserror 2.0.3", "thiserror 2.0.3",
"toml 0.8.19",
"url", "url",
"uuid", "uuid",
] ]

@ -24,6 +24,8 @@ ios = { level = "partial", notes = "Access is restricted to Application folder b
tauri-plugin = { workspace = true, features = ["build"] } tauri-plugin = { workspace = true, features = ["build"] }
schemars = { workspace = true } schemars = { workspace = true }
serde = { workspace = true } serde = { workspace = true }
toml = "0.8"
tauri-utils = { workspace = true, features = ["build"] }
[dependencies] [dependencies]
serde = { workspace = true } serde = { workspace = true }

File diff suppressed because one or more lines are too long

@ -7,6 +7,8 @@ use std::{
path::{Path, PathBuf}, path::{Path, PathBuf},
}; };
use tauri_utils::acl::manifest::PermissionFile;
#[path = "src/scope.rs"] #[path = "src/scope.rs"]
#[allow(dead_code)] #[allow(dead_code)]
mod scope; mod scope;
@ -75,31 +77,31 @@ const BASE_DIR_VARS: &[&str] = &[
"APPCACHE", "APPCACHE",
"APPLOG", "APPLOG",
]; ];
const COMMANDS: &[&str] = &[ const COMMANDS: &[(&str, &[&str])] = &[
"mkdir", ("mkdir", &[]),
"create", ("create", &[]),
"copy_file", ("copy_file", &[]),
"remove", ("remove", &[]),
"rename", ("rename", &[]),
"truncate", ("truncate", &[]),
"ftruncate", ("ftruncate", &[]),
"write", ("write", &[]),
"write_file", ("write_file", &["open", "write"]),
"write_text_file", ("write_text_file", &[]),
"read_dir", ("read_dir", &[]),
"read_file", ("read_file", &[]),
"read", ("read", &[]),
"open", ("open", &[]),
"read_text_file", ("read_text_file", &[]),
"read_text_file_lines", ("read_text_file_lines", &["read_text_file_lines_next"]),
"read_text_file_lines_next", ("read_text_file_lines_next", &[]),
"seek", ("seek", &[]),
"stat", ("stat", &[]),
"lstat", ("lstat", &[]),
"fstat", ("fstat", &[]),
"exists", ("exists", &[]),
"watch", ("watch", &[]),
"unwatch", ("unwatch", &[]),
]; ];
fn main() { fn main() {
@ -205,9 +207,47 @@ permissions = [
} }
} }
tauri_plugin::Builder::new(COMMANDS) tauri_plugin::Builder::new(&COMMANDS.iter().map(|c| c.0).collect::<Vec<_>>())
.global_api_script_path("./api-iife.js") .global_api_script_path("./api-iife.js")
.global_scope_schema(schemars::schema_for!(FsScopeEntry)) .global_scope_schema(schemars::schema_for!(FsScopeEntry))
.android_path("android") .android_path("android")
.build(); .build();
// workaround to include nested permissions as `tauri_plugin` doesn't support it
let permissions_dir = autogenerated.join("commands");
for (command, nested_commands) in COMMANDS {
if nested_commands.is_empty() {
continue;
}
let permission_path = permissions_dir.join(format!("{command}.toml"));
let content = std::fs::read_to_string(&permission_path)
.unwrap_or_else(|_| panic!("failed to read {command}.toml"));
let mut permission_file = toml::from_str::<PermissionFile>(&content)
.unwrap_or_else(|_| panic!("failed to deserialize {command}.toml"));
for p in permission_file
.permission
.iter_mut()
.filter(|p| p.identifier.starts_with("allow"))
{
p.commands
.allow
.extend(nested_commands.iter().map(|s| s.to_string()));
}
let out = toml::to_string_pretty(&permission_file)
.unwrap_or_else(|_| panic!("failed to serialize {command}.toml"));
let out = format!(
r#"# Automatically generated - DO NOT EDIT!
"$schema" = "../../schemas/schema.json"
{out}"#
);
std::fs::write(permission_path, out)
.unwrap_or_else(|_| panic!("failed to write {command}.toml"));
}
} }

@ -266,6 +266,7 @@ function fromBytes(buffer: FixedSizeArray<number, 8>): number {
const size = bytes.byteLength const size = bytes.byteLength
let x = 0 let x = 0
for (let i = 0; i < size; i++) { for (let i = 0; i < size; i++) {
// eslint-disable-next-line security/detect-object-injection
const byte = bytes[i] const byte = bytes[i]
x *= 0x100 x *= 0x100
x += byte x += byte
@ -427,11 +428,11 @@ class FileHandle extends Resource {
} }
/** /**
* Writes `p.byteLength` bytes from `p` to the underlying data stream. It * Writes `data.byteLength` bytes from `data` to the underlying data stream. It
* resolves to the number of bytes written from `p` (`0` <= `n` <= * resolves to the number of bytes written from `data` (`0` <= `n` <=
* `p.byteLength`) or reject with the error encountered that caused the * `data.byteLength`) or reject with the error encountered that caused the
* write to stop early. `write()` must reject with a non-null error if * write to stop early. `write()` must reject with a non-null error if
* would resolve to `n` < `p.byteLength`. `write()` must not modify the * would resolve to `n` < `data.byteLength`. `write()` must not modify the
* slice data, even temporarily. * slice data, even temporarily.
* *
* @example * @example
@ -1044,19 +1045,27 @@ interface WriteFileOptions {
*/ */
async function writeFile( async function writeFile(
path: string | URL, path: string | URL,
data: Uint8Array, data: Uint8Array | ReadableStream<Uint8Array>,
options?: WriteFileOptions options?: WriteFileOptions
): Promise<void> { ): Promise<void> {
if (path instanceof URL && path.protocol !== 'file:') { if (path instanceof URL && path.protocol !== 'file:') {
throw new TypeError('Must be a file URL.') throw new TypeError('Must be a file URL.')
} }
await invoke('plugin:fs|write_file', data, { if (data instanceof ReadableStream) {
headers: { const file = await open(path, options)
path: encodeURIComponent(path instanceof URL ? path.toString() : path), for await (const chunk of data) {
options: JSON.stringify(options) await file.write(chunk)
} }
}) await file.close()
} else {
await invoke('plugin:fs|write_file', data, {
headers: {
path: encodeURIComponent(path instanceof URL ? path.toString() : path),
options: JSON.stringify(options)
}
})
}
} }
/** /**

@ -5,9 +5,18 @@
[[permission]] [[permission]]
identifier = "allow-read-text-file-lines" identifier = "allow-read-text-file-lines"
description = "Enables the read_text_file_lines command without any pre-configured scope." description = "Enables the read_text_file_lines command without any pre-configured scope."
commands.allow = ["read_text_file_lines"]
[permission.commands]
allow = [
"read_text_file_lines",
"read_text_file_lines_next",
]
deny = []
[[permission]] [[permission]]
identifier = "deny-read-text-file-lines" identifier = "deny-read-text-file-lines"
description = "Denies the read_text_file_lines command without any pre-configured scope." description = "Denies the read_text_file_lines command without any pre-configured scope."
commands.deny = ["read_text_file_lines"]
[permission.commands]
allow = []
deny = ["read_text_file_lines"]

@ -5,9 +5,19 @@
[[permission]] [[permission]]
identifier = "allow-write-file" identifier = "allow-write-file"
description = "Enables the write_file command without any pre-configured scope." description = "Enables the write_file command without any pre-configured scope."
commands.allow = ["write_file"]
[permission.commands]
allow = [
"write_file",
"open",
"write",
]
deny = []
[[permission]] [[permission]]
identifier = "deny-write-file" identifier = "deny-write-file"
description = "Denies the write_file command without any pre-configured scope." description = "Denies the write_file command without any pre-configured scope."
commands.deny = ["write_file"]
[permission.commands]
allow = []
deny = ["write_file"]

Loading…
Cancel
Save