diff --git a/packages/core/src/bindings/fs/node.ts b/packages/core/src/bindings/fs/node.ts index fb4b5963128..8feafd8c44b 100644 --- a/packages/core/src/bindings/fs/node.ts +++ b/packages/core/src/bindings/fs/node.ts @@ -1,3 +1,7 @@ +import { + fileHandleToReadableStream, + fileHandleToWritableStream, +} from "@zwave-js/shared"; import type { FSStats, FileHandle, @@ -56,18 +60,52 @@ export const fs: FileSystem = { mode = flags.read ? "w+" : "w"; } - return new NodeFileHandle(await fsp.open(path, mode)); + return new NodeFileHandle( + await fsp.open(path, mode), + { + read: flags.read, + write: flags.write, + }, + ); }, }; export class NodeFileHandle implements FileHandle { - public constructor(handle: fsp.FileHandle) { + public constructor( + handle: fsp.FileHandle, + flags: { read: boolean; write: boolean }, + ) { this.open = true; this.handle = handle; + this.flags = flags; } private open: boolean; private handle: fsp.FileHandle; + private flags: { read: boolean; write: boolean }; + + private _readable?: ReadableStream; + private _writable?: WritableStream; + + public get readable(): ReadableStream { + if (!this.flags.read) { + throw new Error("File is not readable"); + } + if (!this._readable) { + this._readable = fileHandleToReadableStream(this); + } + return this._readable; + } + + public get writable(): WritableStream { + if (!this.flags.write) { + throw new Error("File is not writable"); + } + if (!this._writable) { + this._writable = fileHandleToWritableStream(this); + } + return this._writable; + } async close(): Promise { if (!this.open) return; diff --git a/packages/shared/src/bindings.ts b/packages/shared/src/bindings.ts index cea7fd821f1..95bda1537e1 100644 --- a/packages/shared/src/bindings.ts +++ b/packages/shared/src/bindings.ts @@ -1,6 +1,8 @@ // Definitions for runtime-agnostic low level bindings like cryptography, // file system access, etc. +import { type ReadableWritablePair } from "node:stream/web"; + export interface CryptoPrimitives { randomBytes(length: number): Uint8Array; /** Encrypts a payload using AES-128-ECB */ @@ -61,7 +63,9 @@ export interface FSStats { size: number; } -export interface FileHandle { +export interface FileHandle + extends ReadableWritablePair +{ close(): Promise; read( position?: number | null, diff --git a/packages/shared/src/fs.ts b/packages/shared/src/fs.ts index 85f25f48f42..36a6916d4f6 100644 --- a/packages/shared/src/fs.ts +++ b/packages/shared/src/fs.ts @@ -1,7 +1,9 @@ +import { type ReadableWritablePair } from "node:stream/web"; import path from "pathe"; import { Bytes } from "./Bytes.js"; import { type CopyFile, + type FileHandle, type ManageDirectory, type ReadFile, type ReadFileSystemInfo, @@ -90,3 +92,36 @@ export async function pathExists( return false; } } + +export function fileHandleToWritableStream( + handle: Omit, +): WritableStream { + return new WritableStream({ + async write(chunk) { + while (chunk.length > 0) { + const { bytesWritten } = await handle.write(chunk); + chunk = chunk.subarray(bytesWritten); + } + }, + }); +} + +export function fileHandleToReadableStream( + handle: Omit, +): ReadableStream { + return new ReadableStream({ + async pull(controller) { + const { data } = await handle.read(null, 16 * 1024); + controller.enqueue(data); + }, + }); +} + +export function fileHandleToStreams( + handle: Omit, +): ReadableWritablePair { + return { + readable: fileHandleToReadableStream(handle), + writable: fileHandleToWritableStream(handle), + }; +} diff --git a/packages/zwave-js/src/lib/driver/Driver.ts b/packages/zwave-js/src/lib/driver/Driver.ts index 15861ab833b..7b868ff1cb2 100644 --- a/packages/zwave-js/src/lib/driver/Driver.ts +++ b/packages/zwave-js/src/lib/driver/Driver.ts @@ -1225,6 +1225,7 @@ export class Driver extends TypedEventTarget const symlinkedPorts: string[] = []; const localPorts: string[] = []; const remotePorts: string[] = []; + // FIXME: Move this into the serial bindings if (local) { // Put symlinks to the serial ports first if possible if (os.platform() === "linux") { diff --git a/packages/zwave-js/src/lib/driver/UpdateConfig.ts b/packages/zwave-js/src/lib/driver/UpdateConfig.ts index 00ef6b0543c..8f037c959f5 100644 --- a/packages/zwave-js/src/lib/driver/UpdateConfig.ts +++ b/packages/zwave-js/src/lib/driver/UpdateConfig.ts @@ -7,7 +7,9 @@ import { } from "@zwave-js/shared"; import { type CopyFile, + type FileHandle, type ManageDirectory, + type OpenFile, type ReadFileSystemInfo, type WriteFile, } from "@zwave-js/shared/bindings"; @@ -70,7 +72,7 @@ export async function checkForConfigUpdates( * This only works if an external configuation directory is used. */ export async function installConfigUpdate( - fs: ManageDirectory & ReadFileSystemInfo & WriteFile & CopyFile, + fs: ManageDirectory & ReadFileSystemInfo & WriteFile & CopyFile & OpenFile, newVersion: string, external: { configDir: string; @@ -118,17 +120,17 @@ export async function installConfigUpdate( // Download the package tarball into the temporary directory const tarFilename = path.join(tmpDir, "zjs-config-update.tgz"); - let fileHandle: fsp.FileHandle | undefined; + let fileHandle: FileHandle | undefined; try { - fileHandle = await fsp.open(tarFilename, "w"); - const writable = new WritableStream({ - async write(chunk) { - await fileHandle!.write(chunk); - }, + fileHandle = await fs.open(tarFilename, { + read: false, + write: true, + create: true, + truncate: true, }); const response = await ky.get(url); - await response.body?.pipeTo(writable); + await response.body?.pipeTo(fileHandle.writable); } catch (e) { throw new ZWaveError( `Config update failed: Could not download tarball. Reason: ${