fix(fs): writeFile with ReadableStream throws (#2907)

This commit is contained in:
Tony
2025-08-10 18:44:33 +08:00
committed by GitHub
parent b8056f484c
commit 4eb36b0ff5
4 changed files with 103 additions and 96 deletions
+6
View File
@@ -0,0 +1,6 @@
---
fs: patch
fs-js: patch
---
Fixed calling `writeFile` with `data: ReadableStream` throws `Invalid argument`
+90 -94
View File
@@ -1,194 +1,190 @@
<script> <script>
import * as fs from "@tauri-apps/plugin-fs"; import * as fs from '@tauri-apps/plugin-fs'
import { convertFileSrc } from "@tauri-apps/api/core"; import { convertFileSrc } from '@tauri-apps/api/core'
import { arrayBufferToBase64 } from "../lib/utils"; import { arrayBufferToBase64 } from '../lib/utils'
import { onDestroy } from "svelte"; import { onDestroy } from 'svelte'
export let onMessage; const { onMessage, insecureRenderHtml } = $props()
export let insecureRenderHtml;
let path = ""; let path = $state('')
let img; let img
/** @type {fs.FileHandle} */ /** @type {fs.FileHandle} */
let file; let file = $state()
let renameTo; let renameTo = $state()
let watchPath = ""; let watchPath = $state('')
let watchDebounceDelay = "0"; let watchDebounceDelay = $state(0)
let watchRecursive = false; let watchRecursive = $state(false)
let unwatchFn; /** @type {fs.BaseDirectory | undefined} */
let unwatchPath = ""; let baseDir = $state()
let unwatchFn
let unwatchPath = ''
function getDir() { const dirOptions = Object.keys(fs.BaseDirectory).filter((key) =>
const dirSelect = document.getElementById("dir"); isNaN(parseInt(key))
return dirSelect.value ? parseInt(dir.value) : null; )
}
const DirOptions = Object.keys(fs.BaseDirectory)
.filter((key) => isNaN(parseInt(key)))
.map((dir) => [dir, fs.BaseDirectory[dir]]);
function open() { function open() {
fs.open(path, { fs.open(path, {
baseDir: getDir(), baseDir,
read: true, read: true,
write: true, write: true,
create: true, create: true
}) })
.then((f) => { .then((f) => {
file = f; file = f
onMessage(`Opened ${path}`); onMessage(`Opened ${path}`)
}) })
.catch(onMessage); .catch(onMessage)
} }
function mkdir() { function mkdir() {
fs.mkdir(path, { baseDir: getDir() }) fs.mkdir(path, { baseDir })
.then(() => { .then(() => {
onMessage(`Created dir ${path}`); onMessage(`Created dir ${path}`)
}) })
.catch(onMessage); .catch(onMessage)
} }
function remove() { function remove() {
fs.remove(path, { baseDir: getDir() }) fs.remove(path, { baseDir })
.then(() => { .then(() => {
onMessage(`Removed ${path}`); onMessage(`Removed ${path}`)
}) })
.catch(onMessage); .catch(onMessage)
} }
function rename() { function rename() {
fs.rename(path, renameTo, { fs.rename(path, renameTo, {
oldPathBaseDir: getDir(), oldPathBaseDir,
newPathBaseDir: getDir(), newPathBaseDir
}) })
.then(() => { .then(() => {
onMessage(`Renamed ${path} to ${renameTo}`); onMessage(`Renamed ${path} to ${renameTo}`)
}) })
.catch(onMessage); .catch(onMessage)
} }
function truncate() { function truncate() {
file file
.truncate(0) .truncate(0)
.then(() => { .then(() => {
onMessage(`Truncated file`); onMessage(`Truncated file`)
}) })
.catch(onMessage); .catch(onMessage)
} }
function stat() { function stat() {
file file
.stat() .stat()
.then((stat) => { .then((stat) => {
onMessage(`File stat ${JSON.stringify(stat)}`); onMessage(`File stat ${JSON.stringify(stat)}`)
}) })
.catch(onMessage); .catch(onMessage)
} }
function read() { function read() {
const opts = { const opts = {
baseDir: getDir(), baseDir
}; }
fs.stat(path, opts) fs.stat(path, opts)
.then((stat) => { .then((stat) => {
const isFile = stat.isFile; const isFile = stat.isFile
const promise = isFile const promise = isFile
? fs.readFile(path, opts) ? fs.readFile(path, opts)
: fs.readDir(path, opts); : fs.readDir(path, opts)
promise promise
.then(function (response) { .then(function (response) {
if (isFile) { if (isFile) {
if (path.includes(".png") || path.includes(".jpg")) { if (path.includes('.png') || path.includes('.jpg')) {
arrayBufferToBase64( arrayBufferToBase64(
new Uint8Array(response), new Uint8Array(response),
function (base64) { function (base64) {
const src = "data:image/png;base64," + base64; const src = 'data:image/png;base64,' + base64
insecureRenderHtml('<img src="' + src + '"></img>'); insecureRenderHtml('<img src="' + src + '"></img>')
} }
); )
} else { } else {
const value = String.fromCharCode.apply(null, response); const value = String.fromCharCode.apply(null, response)
insecureRenderHtml( insecureRenderHtml(
'<textarea id="file-response"></textarea><button id="file-save">Save</button>' '<textarea id="file-response"></textarea><button id="file-save">Save</button>'
); )
setTimeout(() => { setTimeout(() => {
const fileInput = document.getElementById("file-response"); const fileInput = document.getElementById('file-response')
fileInput.value = value; fileInput.value = value
document document
.getElementById("file-save") .getElementById('file-save')
.addEventListener("click", function () { .addEventListener('click', function () {
fs.writeTextFile(path, fileInput.value, { fs.writeTextFile(path, fileInput.value, {
baseDir: getDir(), baseDir
}).catch(onMessage); }).catch(onMessage)
}); })
}); })
} }
} else { } else {
onMessage(response); onMessage(response)
} }
}) })
.catch(onMessage); .catch(onMessage)
}) })
.catch(onMessage); .catch(onMessage)
} }
function setSrc() { function setSrc() {
img.src = convertFileSrc(path); img.src = convertFileSrc(path)
} }
function watch() { function watch() {
unwatch(); unwatch()
if (watchPath) { if (watchPath) {
onMessage(`Watching ${watchPath} for changes`); onMessage(`Watching ${watchPath} for changes`)
let options = { let options = {
recursive: watchRecursive, recursive: watchRecursive,
delayMs: parseInt(watchDebounceDelay), delayMs: watchDebounceDelay
}; }
if (options.delayMs === 0) { if (options.delayMs === 0) {
fs.watchImmediate(watchPath, onMessage, options) fs.watchImmediate(watchPath, onMessage, options)
.then((fn) => { .then((fn) => {
unwatchFn = fn; unwatchFn = fn
unwatchPath = watchPath; unwatchPath = watchPath
}) })
.catch(onMessage); .catch(onMessage)
} else { } else {
fs.watch(watchPath, onMessage, options) fs.watch(watchPath, onMessage, options)
.then((fn) => { .then((fn) => {
unwatchFn = fn; unwatchFn = fn
unwatchPath = watchPath; unwatchPath = watchPath
}) })
.catch(onMessage); .catch(onMessage)
} }
} }
} }
function unwatch() { function unwatch() {
if (unwatchFn) { if (unwatchFn) {
onMessage(`Stopped watching ${unwatchPath} for changes`); onMessage(`Stopped watching ${unwatchPath} for changes`)
unwatchFn(); unwatchFn()
} }
unwatchFn = undefined; unwatchFn = undefined
unwatchPath = undefined; unwatchPath = undefined
} }
onDestroy(() => { onDestroy(() => {
if (file) { if (file) {
file.close(); file.close()
} }
if (unwatchFn) { if (unwatchFn) {
unwatchFn(); unwatchFn()
} }
}) })
</script> </script>
<div class="flex flex-col"> <div class="flex flex-col">
<div class="flex gap-1"> <div class="flex gap-1">
<select class="input" id="dir"> <select class="input" bind:value={baseDir}>
<option value="">None</option> <option value={undefined} selected>None</option>
{#each DirOptions as dir} {#each dirOptions as dir}
<option value={dir[1]}>{dir[0]}</option> <option value={fs.BaseDirectory[dir]}>{dir}</option>
{/each} {/each}
</select> </select>
<input <input
@@ -199,20 +195,20 @@
</div> </div>
<br /> <br />
<div> <div>
<button class="btn" on:click={open}>Open</button> <button class="btn" onclick={open}>Open</button>
<button class="btn" on:click={read}>Read</button> <button class="btn" onclick={read}>Read</button>
<button class="btn" on:click={mkdir}>Mkdir</button> <button class="btn" onclick={mkdir}>Mkdir</button>
<button class="btn" on:click={remove}>Remove</button> <button class="btn" onclick={remove}>Remove</button>
<div class="flex flex-row"> <div class="flex flex-row">
<button class="btn" on:click={rename}>Rename</button> <button class="btn" onclick={rename}>Rename</button>
<input class="input" bind:value={renameTo} placeholder="To" /> <input class="input" bind:value={renameTo} placeholder="To" />
</div> </div>
<button class="btn" type="button" on:click={setSrc}>Use as img src</button> <button class="btn" type="button" onclick={setSrc}>Use as img src</button>
</div> </div>
{#if file} {#if file}
<div> <div>
<button class="btn" on:click={truncate}>Truncate</button> <button class="btn" onclick={truncate}>Truncate</button>
<button class="btn" on:click={stat}>Stat</button> <button class="btn" onclick={stat}>Stat</button>
</div> </div>
{/if} {/if}
@@ -241,8 +237,8 @@
</div> </div>
<br /> <br />
<div> <div>
<button class="btn" on:click={watch}>Watch</button> <button class="btn" onclick={watch}>Watch</button>
<button class="btn" on:click={unwatch}>Unwatch</button> <button class="btn" onclick={unwatch}>Unwatch</button>
</div> </div>
</div> </div>
File diff suppressed because one or more lines are too long
+6 -1
View File
@@ -1074,7 +1074,12 @@ async function writeFile(
} }
if (data instanceof ReadableStream) { if (data instanceof ReadableStream) {
const file = await open(path, { create: true, ...options }) const file = await open(path, {
read: false,
create: true,
write: true,
...options
})
const reader = data.getReader() const reader = data.getReader()
try { try {