Repository: denoland/deno
Pull request: feat(node/fs): Add a chmod method to the FileHandle class

<!--
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.
-->
Add the chmod method to the FileHandle class in node compat as part of #25554 

Changed files:
filename: ext/node/polyfills/_fs/_fs_open.ts
patch: @@ -153,7 +153,7 @@ export function openPromise(
   return new Promise((resolve, reject) => {
     open(path, flags, mode, (err, fd) => {
       if (err) reject(err);
-      else resolve(new FileHandle(fd));
+      else resolve(new FileHandle(fd, path));
     });
   });
 }

filename: ext/node/polyfills/internal/fs/handle.ts
patch: @@ -5,7 +5,7 @@
 
 import { EventEmitter } from "node:events";
 import { Buffer } from "node:buffer";
-import { promises, read, write } from "node:fs";
+import { Mode, promises, read, write } from "node:fs";
 export type { BigIntStats, Stats } from "ext:deno_node/_fs/_fs_stat.ts";
 import {
   BinaryOptionsArgument,
@@ -26,11 +26,15 @@ interface ReadResult {
   buffer: Buffer;
 }
 
+type Path = string | Buffer | URL;
 export class FileHandle extends EventEmitter {
   #rid: number;
-  constructor(rid: number) {
+  #path: Path;
+
+  constructor(rid: number, path: Path) {
     super();
     this.#rid = rid;
+    this.#path = path;
   }
 
   get fd() {
@@ -144,17 +148,24 @@ export class FileHandle extends EventEmitter {
   stat(options?: { bigint: boolean }): Promise<Stats | BigIntStats> {
     return fsCall(promises.fstat, this, options);
   }
+  chmod(mode: Mode): Promise<void> {
+    assertNotClosed(this, promises.chmod.name);
+    return promises.chmod(this.#path, mode);
+  }
 }
 
-function fsCall(fn, handle, ...args) {
+function assertNotClosed(handle: FileHandle, syscall: string) {
   if (handle.fd === -1) {
     const err = new Error("file closed");
     throw Object.assign(err, {
       code: "EBADF",
-      syscall: fn.name,
+      syscall,
     });
   }
+}
 
+function fsCall(fn, handle, ...args) {
+  assertNotClosed(handle, fn.name);
   return fn(handle.fd, ...args);
 }
 

filename: tests/unit_node/_fs/_fs_handle_test.ts
patch: @@ -199,3 +199,21 @@ Deno.test(
     assertEquals(data.length, 0);
   },
 );
+
+Deno.test({
+  name: "[node/fs filehandle.chmod] Change the permissions of the file",
+  ignore: Deno.build.os === "windows",
+  async fn() {
+    const fileHandle = await fs.open(testData);
+
+    const readOnly = 0o444;
+    await fileHandle.chmod(readOnly.toString(8));
+    assertEquals(Deno.statSync(testData).mode! & 0o777, readOnly);
+
+    const readWrite = 0o666;
+    await fileHandle.chmod(readWrite.toString(8));
+    assertEquals(Deno.statSync(testData).mode! & 0o777, readWrite);
+
+    await fileHandle.close();
+  },
+});