Repository: denoland/deno
Pull request: fix(ext/node): add a createReadStream & createWriteStream methods to the FileHandle class

This PR implements the missing support for `createReadStream` and `createWriteStream` on the `FileHandler` class (as outlined in #25554)

Notably, it does so by building on top of #27591 which was presumably abandoned

Changed files:
filename: ext/node/polyfills/internal/fs/handle.ts
patch: @@ -5,8 +5,9 @@
 
 import { EventEmitter } from "node:events";
 import { Buffer } from "node:buffer";
-import { Mode, promises, read, write } from "node:fs";
+import { Mode, promises, read, ReadStream, write, WriteStream } from "node:fs";
 import { core } from "ext:core/mod.js";
+export type { BigIntStats, Stats } from "ext:deno_node/_fs/_fs_stat.ts";
 import {
   BinaryOptionsArgument,
   FileOptionsArgument,
@@ -18,6 +19,10 @@ export type { BigIntStats, Stats } from "ext:deno_node/_fs/_fs_stat.ts";
 import { writevPromise, WriteVResult } from "ext:deno_node/_fs/_fs_writev.ts";
 import { fdatasyncPromise } from "ext:deno_node/_fs/_fs_fdatasync.ts";
 import { fsyncPromise } from "ext:deno_node/_fs/_fs_fsync.ts";
+import {
+  CreateReadStreamOptions,
+  CreateWriteStreamOptions,
+} from "node:fs/promises";
 
 interface WriteResult {
   bytesWritten: number;
@@ -180,6 +185,14 @@ export class FileHandle extends EventEmitter {
     assertNotClosed(this, promises.chown.name);
     return promises.chown(this.#path, uid, gid);
   }
+
+  createReadStream(options?: CreateReadStreamOptions): ReadStream {
+    return new ReadStream(undefined, { ...options, fd: this.fd });
+  }
+
+  createWriteStream(options?: CreateWriteStreamOptions): WriteStream {
+    return new WriteStream(undefined, { ...options, fd: this.fd });
+  }
 }
 
 function assertNotClosed(handle: FileHandle, syscall: string) {

filename: tests/unit_node/_fs/_fs_handle_test.ts
patch: @@ -317,3 +317,55 @@ Deno.test({
     await fileHandle.close();
   },
 });
+
+Deno.test(
+  "[node/fs filehandle.createReadStream] Create a read stream",
+  async function () {
+    const fileHandle = await fs.open(testData);
+    const stream = fileHandle.createReadStream();
+    const fileSize = (await fileHandle.stat()).size;
+
+    assertEquals(stream.bytesRead, 0);
+    assertEquals(stream.readable, true);
+
+    let bytesRead = 0;
+
+    stream.on("open", () => assertEquals(stream.bytesRead, 0));
+
+    stream.on("data", (data) => {
+      assertEquals(data instanceof Buffer, true);
+      assertEquals((data as Buffer).byteOffset % 8, 0);
+      bytesRead += data.length;
+      assertEquals(stream.bytesRead, bytesRead);
+    });
+
+    stream.on("end", () => {
+      assertEquals(stream.bytesRead, fileSize);
+      assertEquals(bytesRead, fileSize);
+    });
+
+    // Wait for the 'close' event so that the test won't finish prematurely
+    // note: stream automatically closes fd once all the data is read
+    await new Promise<void>((resolve) => {
+      stream.on("close", resolve);
+    });
+  },
+);
+
+Deno.test(
+  "[node/fs filehandle.createWriteStream] Create a write stream",
+  async function () {
+    const tempFile: string = await Deno.makeTempFile();
+    try {
+      const fileHandle = await fs.open(tempFile, "w");
+      const stream = fileHandle.createWriteStream();
+      const { promise, resolve } = Promise.withResolvers<void>();
+      stream.on("close", resolve);
+      stream.end("a\n", "utf8");
+      await promise;
+      assertEquals(await Deno.readTextFile(tempFile), "a\n");
+    } finally {
+      await Deno.remove(tempFile);
+    }
+  },
+);