Repository: denoland/deno
Pull request: fix(ext/node): add `fs.promises.fstat` and `FileHandle#stat`

<!--
Before submitting a PR, please read https://docs.deno.com/runtime/manual/references/contributing

1. Give the PR a descriptive title.

  Examples of good title:
    - fix(std/http): Fix race condition in server
    - docs(console): Update docstrings
    - feat(doc): Handle nested reexports

  Examples of bad title:
    - fix #7123
    - update docs
    - fix bugs

2. Ensure there is a related issue and it is referenced in the PR text.
3. Ensure there are tests that cover the changes.
4. Ensure `cargo test` passes.
5. Ensure `./tools/format.js` passes without changing files.
6. Ensure `./tools/lint.js` passes.
7. Open as a draft PR if your work is still in progress. The CI won't run
   all steps, but you can add '[ci]' to a commit message to force it to.
8. If you would like to run the benchmarks on the CI, add the 'ci-bench' label.
-->

Fixes #23301

Implements `FileHandle.stat`. This PR also adds a promisified `fs.fstat` under 'fs/promises', which is used internally by `FileHandle.stat`.

part of #25554


Changed files:
filename: ext/node/polyfills/_fs/_fs_fstat.ts
patch: @@ -63,3 +63,24 @@ export function fstatSync(
   const origin = new FsFile(fd, Symbol.for("Deno.internal.FsFile")).statSync();
   return CFISBIS(origin, options?.bigint || false);
 }
+
+export function fstatPromise(fd: number): Promise<Stats>;
+export function fstatPromise(
+  fd: number,
+  options: { bigint: false },
+): Promise<Stats>;
+export function fstatPromise(
+  fd: number,
+  options: { bigint: true },
+): Promise<BigIntStats>;
+export function fstatPromise(
+  fd: number,
+  options?: statOptions,
+): Stats | BigIntStats {
+  return new Promise((resolve, reject) => {
+    fstat(fd, options, (err, stats) => {
+      if (err) reject(err);
+      else resolve(stats);
+    });
+  });
+}

filename: ext/node/polyfills/fs.ts
patch: @@ -23,7 +23,7 @@ import Dir from "ext:deno_node/_fs/_fs_dir.ts";
 import Dirent from "ext:deno_node/_fs/_fs_dirent.ts";
 import { exists, existsSync } from "ext:deno_node/_fs/_fs_exists.ts";
 import { fdatasync, fdatasyncSync } from "ext:deno_node/_fs/_fs_fdatasync.ts";
-import { fstat, fstatSync } from "ext:deno_node/_fs/_fs_fstat.ts";
+import { fstat, fstatPromise, fstatSync } from "ext:deno_node/_fs/_fs_fstat.ts";
 import { fsync, fsyncSync } from "ext:deno_node/_fs/_fs_fsync.ts";
 import { ftruncate, ftruncateSync } from "ext:deno_node/_fs/_fs_ftruncate.ts";
 import { futimes, futimesSync } from "ext:deno_node/_fs/_fs_futimes.ts";
@@ -174,6 +174,7 @@ const promises = {
   lstat: lstatPromise,
   stat: statPromise,
   statfs: statfsPromise,
+  fstat: fstatPromise,
   link: linkPromise,
   unlink: unlinkPromise,
   chmod: chmodPromise,

filename: ext/node/polyfills/fs/promises.ts
patch: @@ -16,6 +16,7 @@ export const readlink = fsPromises.readlink;
 export const symlink = fsPromises.symlink;
 export const lstat = fsPromises.lstat;
 export const stat = fsPromises.stat;
+export const fstat = fsPromises.fstat;
 export const link = fsPromises.link;
 export const unlink = fsPromises.unlink;
 export const chmod = fsPromises.chmod;

filename: ext/node/polyfills/internal/fs/handle.ts
patch: @@ -6,6 +6,7 @@
 import { EventEmitter } from "node:events";
 import { Buffer } from "node:buffer";
 import { promises, read, write } from "node:fs";
+export type { BigIntStats, Stats } from "ext:deno_node/_fs/_fs_stat.ts";
 import {
   BinaryOptionsArgument,
   FileOptionsArgument,
@@ -141,6 +142,13 @@ export class FileHandle extends EventEmitter {
     // Note that Deno.close is not async
     return Promise.resolve(core.close(this.fd));
   }
+
+  stat(): Promise<Stats>;
+  stat(options: { bigint: false }): Promise<Stats>;
+  stat(options: { bigint: true }): Promise<BigIntStats>;
+  stat(options?: { bigint: boolean }): Promise<Stats | BigIntStats> {
+    return fsCall(promises.fstat, this, options);
+  }
 }
 
 function fsCall(fn, handle, ...args) {
@@ -152,7 +160,7 @@ function fsCall(fn, handle, ...args) {
     });
   }
 
-  return fn(handle, ...args);
+  return fn(handle.fd, ...args);
 }
 
 export default {

filename: tests/unit_node/_fs/_fs_handle_test.ts
patch: @@ -93,3 +93,27 @@ Deno.test("[node/fs filehandle.write] Write from string", async function () {
   assertEquals(res.bytesWritten, 11);
   assertEquals(decoder.decode(data), "hello world");
 });
+
+Deno.test("[node/fs filehandle.stat] Get file status", async function () {
+  const fileHandle = await fs.open(testData);
+  const stat = await fileHandle.stat();
+
+  assertEquals(stat.isFile(), true);
+  assertEquals(stat.size, "hello world".length);
+
+  await fileHandle.close();
+});
+
+Deno.test("[node/fs filehandle.writeFile] Write to file", async function () {
+  const tempFile: string = await Deno.makeTempFile();
+  const fileHandle = await fs.open(tempFile, "w");
+
+  const str = "hello world";
+  await fileHandle.writeFile(str);
+
+  const data = Deno.readFileSync(tempFile);
+  await Deno.remove(tempFile);
+  await fileHandle.close();
+
+  assertEquals(decoder.decode(data), "hello world");
+});