diff --git a/src/js/node/fs.promises.ts b/src/js/node/fs.promises.ts index 5b7e9b16be..c9f2d0d376 100644 --- a/src/js/node/fs.promises.ts +++ b/src/js/node/fs.promises.ts @@ -218,267 +218,292 @@ const real_export = { }; export default real_export; -// Partially taken from https://github.com/nodejs/node/blob/c25878d370/lib/internal/fs/promises.js#L148 -class FileHandle extends EventEmitter { - constructor(fd) { - super(); - this[kFd] = fd ? fd : -1; - this[kRefs] = 1; - this[kClosePromise] = null; - } - - getAsyncId() { - throw new Error("BUN TODO FileHandle.getAsyncId"); - } - - get fd() { - return this[kFd]; - } - - appendFile(data, options) { - const fd = this[kFd]; - throwEBADFIfNecessary(real_export.writeFile, fd); - - try { - this[kRef](); - return real_export.writeFile(fd, data, options); - } finally { - this[kUnref](); - } - } - - chmod(mode) { - const fd = this[kFd]; - throwEBADFIfNecessary(real_export.fchmod, fd); - - try { - this[kRef](); - return real_export.fchmod(fd, mode); - } finally { - this[kUnref](); - } - } - - chown(uid, gid) { - const fd = this[kFd]; - throwEBADFIfNecessary(real_export.fchown, fd); - - try { - this[kRef](); - return real_export.fchown(fd, uid, gid); - } finally { - this[kUnref](); - } - } - - datasync() { - const fd = this[kFd]; - throwEBADFIfNecessary(real_export.fdatasync, fd); - - try { - this[kRef](); - return real_export.fdatasync(fd); - } finally { - this[kUnref](); - } - } - - sync() { - const fd = this[kFd]; - throwEBADFIfNecessary(real_export.fsync, fd); - - try { - this[kRef](); - return real_export.fsync(fd); - } finally { - this[kUnref](); - } - } - - read(buffer, offset, length, position) { - const fd = this[kFd]; - throwEBADFIfNecessary(real_export.read, fd); - - try { - this[kRef](); - const bytesRead = real_export.read(fd, buffer, offset, length, position); - return { bytesRead, buffer }; - } finally { - this[kUnref](); - } - } - - readv(buffers, position) { - const fd = this[kFd]; - throwEBADFIfNecessary(real_export.readv, fd); - - try { - this[kRef](); - const bytesRead = real_export.readv(fd, buffers, position); - return { bytesRead, buffers }; - } finally { - this[kUnref](); - } - } - - readFile(options) { - const fd = this[kFd]; - throwEBADFIfNecessary(real_export.readFile, fd); - - try { - this[kRef](); - return real_export.readFile(fd, options); - } finally { - this[kUnref](); - } - } - - readLines(options = undefined) { - throw new Error("BUN TODO FileHandle.readLines"); - } - - stat(options) { - const fd = this[kFd]; - throwEBADFIfNecessary(real_export.fstat, fd); - - try { - this[kRef](); - return real_export.fstat(fd, options); - } finally { - this[kUnref](); - } - } - - truncate(len = 0) { - const fd = this[kFd]; - throwEBADFIfNecessary(real_export.ftruncate, fd); - - try { - this[kRef](); - return real_export.ftruncate(fd, len); - } finally { - this[kUnref](); - } - } - - utimes(atime, mtime) { - const fd = this[kFd]; - throwEBADFIfNecessary(real_export.futimes, fd); - - try { - this[kRef](); - return real_export.futimes(fd, atime, mtime); - } finally { - this[kUnref](); - } - } - - write(buffer, offset, length, position) { - const fd = this[kFd]; - throwEBADFIfNecessary(real_export.write, fd); - - try { - this[kRef](); - return real_export.write(fd, buffer, offset, length, position); - } finally { - this[kUnref](); - } - } - - writev(buffers, position) { - const fd = this[kFd]; - throwEBADFIfNecessary(real_export.writev, fd); - - try { - this[kRef](); - return real_export.writev(fd, buffers, position); - } finally { - this[kUnref](); - } - } - - writeFile(data, options) { - const fd = this[kFd]; - throwEBADFIfNecessary(real_export.writeFile, fd); - - try { - this[kRef](); - return real_export.writeFile(fd, data, options); - } finally { - this[kUnref](); - } - } - - close() { - if (this[kFd] === -1) { - return PromiseResolve(); +{ + const { + writeFile, + readFile, + fchmod, + fchown, + fdatasync, + fsync, + read, + readv, + fstat, + ftruncate, + futimes, + write, + writev, + close, + } = real_export; + // Partially taken from https://github.com/nodejs/node/blob/c25878d370/lib/internal/fs/promises.js#L148 + // These functions await the result so that errors propagate correctly with + // async stack traces and so that the ref counting is correct. + var FileHandle = class FileHandle extends EventEmitter { + constructor(fd) { + super(); + this[kFd] = fd ? fd : -1; + this[kRefs] = 1; + this[kClosePromise] = null; } - if (this[kClosePromise]) { + getAsyncId() { + throw new Error("BUN TODO FileHandle.getAsyncId"); + } + + get fd() { + return this[kFd]; + } + + async appendFile(data, options) { + const fd = this[kFd]; + throwEBADFIfNecessary(writeFile, fd); + + try { + this[kRef](); + return await writeFile(fd, data, options); + } finally { + this[kUnref](); + } + } + + async chmod(mode) { + const fd = this[kFd]; + throwEBADFIfNecessary(fchmod, fd); + + try { + this[kRef](); + return await fchmod(fd, mode); + } finally { + this[kUnref](); + } + } + + async chown(uid, gid) { + const fd = this[kFd]; + throwEBADFIfNecessary(fchown, fd); + + try { + this[kRef](); + return await fchown(fd, uid, gid); + } finally { + this[kUnref](); + } + } + + async datasync() { + const fd = this[kFd]; + throwEBADFIfNecessary(fdatasync, fd); + + try { + this[kRef](); + return await fdatasync(fd); + } finally { + this[kUnref](); + } + } + + async sync() { + const fd = this[kFd]; + throwEBADFIfNecessary(fsync, fd); + + try { + this[kRef](); + return await fsync(fd); + } finally { + this[kUnref](); + } + } + + async read(buffer, offset, length, position) { + const fd = this[kFd]; + throwEBADFIfNecessary(read, fd); + + try { + this[kRef](); + return { buffer, bytesRead: await read(fd, buffer, offset, length, position) }; + } finally { + this[kUnref](); + } + } + + async readv(buffers, position) { + const fd = this[kFd]; + throwEBADFIfNecessary(readv, fd); + + try { + this[kRef](); + return await readv(fd, buffers, position); + } finally { + this[kUnref](); + } + } + + async readFile(options) { + const fd = this[kFd]; + throwEBADFIfNecessary(readFile, fd); + + try { + this[kRef](); + return await readFile(fd, options); + } finally { + this[kUnref](); + } + } + + readLines(options = undefined) { + throw new Error("BUN TODO FileHandle.readLines"); + } + + async stat(options) { + const fd = this[kFd]; + throwEBADFIfNecessary(fstat, fd); + + try { + this[kRef](); + return await fstat(fd, options); + } finally { + this[kUnref](); + } + } + + async truncate(len = 0) { + const fd = this[kFd]; + throwEBADFIfNecessary(ftruncate, fd); + + try { + this[kRef](); + return await ftruncate(fd, len); + } finally { + this[kUnref](); + } + } + + async utimes(atime, mtime) { + const fd = this[kFd]; + throwEBADFIfNecessary(futimes, fd); + + try { + this[kRef](); + return await futimes(fd, atime, mtime); + } finally { + this[kUnref](); + } + } + + async write(buffer, offset, length, position) { + const fd = this[kFd]; + throwEBADFIfNecessary(write, fd); + + try { + this[kRef](); + return { buffer, bytesWritten: await write(fd, buffer, offset, length, position) }; + } finally { + this[kUnref](); + } + } + + async writev(buffers, position) { + const fd = this[kFd]; + throwEBADFIfNecessary(writev, fd); + + try { + this[kRef](); + return await writev(fd, buffers, position); + } finally { + this[kUnref](); + } + } + + async writeFile(data, options) { + const fd = this[kFd]; + throwEBADFIfNecessary(writeFile, fd); + + try { + this[kRef](); + return await writeFile(fd, data, options); + } finally { + this[kUnref](); + } + } + + async close() { + if (this[kFd] === -1) { + return; + } + + if (this[kClosePromise]) { + return this[kClosePromise]; + } + + this[kRefs]--; + if (this[kRefs] === 0) { + this[kClosePromise] = SafePromisePrototypeFinally.$call(close(this[kFd]), () => { + this[kClosePromise] = undefined; + }); + } else { + this[kClosePromise] = SafePromisePrototypeFinally.$call( + new Promise((resolve, reject) => { + this[kCloseResolve] = resolve; + this[kCloseReject] = reject; + }), + () => { + this[kClosePromise] = undefined; + this[kCloseReject] = undefined; + this[kCloseResolve] = undefined; + }, + ); + } + + this.emit("close"); return this[kClosePromise]; } - this[kRefs]--; - if (this[kRefs] === 0) { - this[kClosePromise] = SafePromisePrototypeFinally.$call(real_export.close(this[kFd]), () => { - this[kClosePromise] = undefined; - }); - } else { - this[kClosePromise] = SafePromisePrototypeFinally.$call( - new Promise((resolve, reject) => { - this[kCloseResolve] = resolve; - this[kCloseReject] = reject; - }), - () => { - this[kClosePromise] = undefined; - this[kCloseReject] = undefined; - this[kCloseResolve] = undefined; - }, - ); + async [SymbolAsyncDispose]() { + return this.close(); } - this.emit("close"); - return this[kClosePromise]; - } + readableWebStream(options = kEmptyObject) { + const fd = this[kFd]; + throwEBADFIfNecessary(fs.createReadStream, fd); - async [SymbolAsyncDispose]() { - return this.close(); - } - - readableWebStream(options = kEmptyObject) { - throw new Error("BUN TODO FileHandle.readableWebStream"); - } - - createReadStream(options = undefined) { - throw new Error("BUN TODO FileHandle.createReadStream"); - } - - createWriteStream(options = undefined) { - throw new Error("BUN TODO FileHandle.createWriteStream"); - } - - [kTransfer]() { - throw new Error("BUN TODO FileHandle.kTransfer"); - } - - [kTransferList]() { - throw new Error("BUN TODO FileHandle.kTransferList"); - } - - [kDeserialize]({ handle }) { - throw new Error("BUN TODO FileHandle.kDeserialize"); - } - - [kRef]() { - this[kRefs]++; - } - - [kUnref]() { - this[kRefs]--; - if (this[kRefs] === 0) { - PromisePrototypeThen(this.close(), this[kCloseResolve], this[kCloseReject]); + return Bun.file(fd).stream(); } - } + + createReadStream(options) { + const fd = this[kFd]; + throwEBADFIfNecessary(fs.createReadStream, fd); + return require("node:fs").createReadStream("", { fd, highWaterMark: 64 * 1024, ...(options || {}) }); + } + + createWriteStream(options) { + const fd = this[kFd]; + throwEBADFIfNecessary(fs.createWriteStream, fd); + return require("node:fs").createWriteStream("", { fd, ...(options || {}) }); + } + + [kTransfer]() { + throw new Error("BUN TODO FileHandle.kTransfer"); + } + + [kTransferList]() { + throw new Error("BUN TODO FileHandle.kTransferList"); + } + + [kDeserialize]({ handle }) { + throw new Error("BUN TODO FileHandle.kDeserialize"); + } + + [kRef]() { + this[kRefs]++; + } + + [kUnref]() { + const refCount = this[kRefs]--; + if (refCount === 1) { + PromisePrototypeThen.$call(this.close(), this[kCloseResolve], this[kCloseReject]); + } + } + }; } function throwEBADFIfNecessary(fn, fd) { diff --git a/test/js/node/fs/fs.test.ts b/test/js/node/fs/fs.test.ts index c3bc89de25..729600bf5f 100644 --- a/test/js/node/fs/fs.test.ts +++ b/test/js/node/fs/fs.test.ts @@ -1,5 +1,5 @@ // @known-failing-on-windows: 1 failing -import { describe, expect, it } from "bun:test"; +import { describe, expect, it, spyOn } from "bun:test"; import { dirname, resolve, relative } from "node:path"; import { promisify } from "node:util"; import { bunEnv, bunExe, gc, getMaxFD, isIntelMacOS, isWindows } from "harness"; @@ -40,7 +40,7 @@ import fs, { fdatasyncSync, } from "node:fs"; -import _promises from "node:fs/promises"; +import _promises, { type FileHandle } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; @@ -60,6 +60,108 @@ function mkdirForce(path: string) { if (!existsSync(path)) mkdirSync(path, { recursive: true }); } +describe("FileHandle", () => { + it("FileHandle#read returns object", async () => { + await using fd = await fs.promises.open(__filename); + const buf = Buffer.alloc(10); + expect(await fd.read(buf, 0, 10, 0)).toEqual({ bytesRead: 10, buffer: buf }); + }); + + it("FileHandle#readv returns object", async () => { + await using fd = await fs.promises.open(__filename); + const buffers = [Buffer.alloc(10), Buffer.alloc(10)]; + expect(await fd.readv(buffers, 0)).toEqual({ bytesRead: 20, buffers }); + }); + + it("FileHandle#write throws EBADF when closed", async () => { + let handle: FileHandle; + let spy; + { + await using fd = await fs.promises.open(__filename); + handle = fd; + spy = spyOn(handle, "close"); + const buffers = [Buffer.alloc(10), Buffer.alloc(10)]; + expect(await fd.readv(buffers, 0)).toEqual({ bytesRead: 20, buffers }); + } + expect(handle.close).toHaveBeenCalled(); + expect(async () => await handle.read(Buffer.alloc(10))).toThrow("Bad file descriptor"); + }); + + it("FileHandle#write returns object", async () => { + await using fd = await fs.promises.open(`${tmpdir()}/${Date.now()}.writeFile.txt`, "w"); + const buf = Buffer.from("test"); + expect(await fd.write(buf, 0, 4, 0)).toEqual({ bytesWritten: 4, buffer: buf }); + }); + + it("FileHandle#writev returns object", async () => { + await using fd = await fs.promises.open(`${tmpdir()}/${Date.now()}.writeFile.txt`, "w"); + const buffers = [Buffer.from("test"), Buffer.from("test")]; + expect(await fd.writev(buffers, 0)).toEqual({ bytesWritten: 8, buffers }); + }); + + it("FileHandle#readFile returns buffer", async () => { + await using fd = await fs.promises.open(__filename); + const buf = await fd.readFile(); + expect(buf instanceof Buffer).toBe(true); + }); + + it("FileHandle#readableWebStream", async () => { + await using fd = await fs.promises.open(__filename); + const stream = fd.readableWebStream(); + const reader = stream.getReader(); + const chunk = await reader.read(); + expect(chunk.value instanceof Uint8Array).toBe(true); + reader.releaseLock(); + await stream.cancel(); + }); + + it("FileHandle#createReadStream", async () => { + await using fd = await fs.promises.open(__filename); + const readable = fd.createReadStream(); + const data = await new Promise(resolve => { + let data = ""; + readable.on("data", chunk => { + data += chunk; + }); + readable.on("end", () => { + resolve(data); + }); + }); + + expect(data).toBe(readFileSync(__filename, "utf8")); + }); + + it("FileHandle#writeFile", async () => { + const path = `${tmpdir()}/${Date.now()}.writeFile.txt`; + await using fd = await fs.promises.open(path, "w"); + await fd.writeFile("File written successfully"); + expect(readFileSync(path, "utf8")).toBe("File written successfully"); + }); + + it("FileHandle#createWriteStream", async () => { + const path = `${tmpdir()}/${Date.now()}.createWriteStream.txt`; + { + await using fd = await fs.promises.open(path, "w"); + const stream = fd.createWriteStream(); + stream.write("Test file written successfully"); + stream.end(); + + await new Promise((resolve, reject) => { + stream.on("error", e => { + reject(e); + }); + + stream.on("finish", () => { + expect(readFileSync(path, "utf8")).toBe("Test file written successfully"); + resolve(true); + }); + }); + } + + expect(readFileSync(path, "utf8")).toBe("Test file written successfully"); + }); +}); + it("fdatasyncSync", () => { const fd = openSync(import.meta.path, "w", 0o664); fdatasyncSync(fd); @@ -2159,18 +2261,6 @@ describe("fs/promises", () => { it("opendir should have a path property, issue#4995", async () => { expect((await fs.promises.opendir(".")).path).toBe("."); }); - - it("FileHandle#read returns object", async () => { - const fd = await fs.promises.open(__filename); - const buf = Buffer.alloc(10); - expect(await fd.read(buf, 0, 10, 0)).toEqual({ bytesRead: 10, buffer: buf }); - }); - - it("FileHandle#readv returns object", async () => { - const fd = await fs.promises.open(__filename); - const buffers = [Buffer.alloc(10), Buffer.alloc(10)]; - expect(await fd.readv(buffers, 0, 20, 0)).toEqual({ bytesRead: 20, buffers }); - }); }); it("stat on a large file", () => {