From 53cc503d662873e26d2281065f54a1f78a4f9954 Mon Sep 17 00:00:00 2001 From: Yoshiya Hinosawa Date: Thu, 12 Dec 2024 21:56:35 +0900 Subject: [PATCH 1/3] feat(fs/unstable): add fs.stat --- _tools/node_test_runner/run_test.mjs | 1 + fs/_map_error.ts | 27 ++++++++++ fs/_to_file_info.ts | 30 +++++++++++ fs/_utils.ts | 22 ++++++++ fs/deno.json | 1 + fs/unstable_errors.d.ts | 61 +++++++++++++++++++++ fs/unstable_errors.js | 38 +++++++++++++ fs/unstable_stat.ts | 35 ++++++++++++ fs/unstable_stat_test.ts | 23 ++++++++ fs/unstable_types.ts | 81 ++++++++++++++++++++++++++++ 10 files changed, 319 insertions(+) create mode 100644 fs/_map_error.ts create mode 100644 fs/_to_file_info.ts create mode 100644 fs/_utils.ts create mode 100644 fs/unstable_errors.d.ts create mode 100644 fs/unstable_errors.js create mode 100644 fs/unstable_stat.ts create mode 100644 fs/unstable_stat_test.ts create mode 100644 fs/unstable_types.ts diff --git a/_tools/node_test_runner/run_test.mjs b/_tools/node_test_runner/run_test.mjs index a3337ce40086..31b521f0f6c1 100644 --- a/_tools/node_test_runner/run_test.mjs +++ b/_tools/node_test_runner/run_test.mjs @@ -49,6 +49,7 @@ import "../../collections/union_test.ts"; import "../../collections/unzip_test.ts"; import "../../collections/without_all_test.ts"; import "../../collections/zip_test.ts"; +import "../../fs/unstable_stat_test.ts"; for (const testDef of testDefinitions) { test(testDef.name, testDef.fn); diff --git a/fs/_map_error.ts b/fs/_map_error.ts new file mode 100644 index 000000000000..9b8a4a515c72 --- /dev/null +++ b/fs/_map_error.ts @@ -0,0 +1,27 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +import * as errors from "./unstable_errors.js"; + +type Class = new (...params: unknown[]) => T; + +type ClassOrT = T extends Class ? U : T; + +const mapper = (Ctor: typeof errors[keyof typeof errors]) => (err: Error) => + Object.assign(new Ctor(err.message), { + stack: err.stack, + }) as unknown as ClassOrT; + +const map: Record> = { + EEXIST: mapper(errors.AlreadyExists), + ENOENT: mapper(errors.NotFound), + EBADF: mapper(errors.BadResource), +}; + +const isNodeErr = (e: unknown): e is Error & { code: string } => { + return e instanceof Error && "code" in e; +}; + +export function mapError(e: E) { + if (!isNodeErr(e)) return e; + return map[e.code]?.(e) || e; +} diff --git a/fs/_to_file_info.ts b/fs/_to_file_info.ts new file mode 100644 index 000000000000..369ca31442ab --- /dev/null +++ b/fs/_to_file_info.ts @@ -0,0 +1,30 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +import type { FileInfo } from "./unstable_types.ts"; +import { isWindows } from "./_utils.ts"; + +export function toFileInfo(s: import("node:fs").Stats): FileInfo { + return { + atime: s.atime, + ctime: s.ctime, + birthtime: s.birthtime, + blksize: isWindows ? null : s.blksize, + blocks: isWindows ? null : s.blocks, + dev: s.dev, + gid: isWindows ? null : s.gid, + ino: isWindows ? null : s.ino, + isDirectory: s.isDirectory(), + isFile: s.isFile(), + isSymlink: s.isSymbolicLink(), + isBlockDevice: isWindows ? null : s.isBlockDevice(), + isCharDevice: isWindows ? null : s.isCharacterDevice(), + isFifo: isWindows ? null : s.isFIFO(), + isSocket: isWindows ? null : s.isSocket(), + mode: isWindows ? null : s.mode, + mtime: s.mtime, + nlink: isWindows ? null : s.nlink, + rdev: isWindows ? null : s.rdev, + size: s.size, + uid: isWindows ? null : s.uid, + }; +} diff --git a/fs/_utils.ts b/fs/_utils.ts new file mode 100644 index 000000000000..33f0e9c0ec0c --- /dev/null +++ b/fs/_utils.ts @@ -0,0 +1,22 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. +// deno-lint-ignore-file no-explicit-any no-process-globals + +/** + * True if the runtime is Deno, false otherwise. + */ +export const isDeno = navigator.userAgent?.includes("Deno"); + +/** True if the platform is windows, false otherwise */ +export const isWindows = checkWindows(); + +/** + * @returns true if the platform is Windows, false otherwise. + */ +function checkWindows(): boolean { + if (typeof navigator !== "undefined" && (navigator as any).platform) { + return (navigator as any).platform.startsWith("Win"); + } else if (typeof process !== "undefined") { + return process.platform === "win32"; + } + return false; +} diff --git a/fs/deno.json b/fs/deno.json index 7ab2aca920d2..4d044652921b 100644 --- a/fs/deno.json +++ b/fs/deno.json @@ -13,6 +13,7 @@ "./exists": "./exists.ts", "./expand-glob": "./expand_glob.ts", "./move": "./move.ts", + "./unstable-stat": "./unstable_stat.ts", "./walk": "./walk.ts" } } diff --git a/fs/unstable_errors.d.ts b/fs/unstable_errors.d.ts new file mode 100644 index 000000000000..32fa44a118c8 --- /dev/null +++ b/fs/unstable_errors.d.ts @@ -0,0 +1,61 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +/** + * Raised when trying to create a resource, like a file, that already + * exits. + */ +export class AlreadyExists extends Error {} +/** + * The underlying IO resource is invalid or closed, and so the operation + * could not be performed. + */ +export class BadResource extends Error {} +/** + * Raised when trying to write to a resource and a broken pipe error occurs. + * This can happen when trying to write directly to `stdout` or `stderr` + * and the operating system is unable to pipe the output for a reason + * external to the Deno runtime. + */ +export class BrokenPipe extends Error {} +/** + * Raised when the underlying IO resource is not available because it is + * being awaited on in another block of code. + */ +export class Busy extends Error {} +/** + * Raised when an operation to returns data that is invalid for the + * operation being performed. + */ +export class InvalidData extends Error {} +/** + * Raised when the underlying operating system reports an `EINTR` error. In + * many cases, this underlying IO error will be handled internally within + * Deno, or result in an {@linkcode BadResource} error instead. + */ +export class Interrupted extends Error {} +/** + * Raised when the underlying operating system indicates that the file + * was not found. + */ +export class NotFound extends Error {} +/** + * Raised when the underlying operating system indicates the current user + * which the Deno process is running under does not have the appropriate + * permissions to a file or resource. + */ +export class PermissionDenied extends Error {} +/** + * Raised when the underlying operating system reports that an I/O operation + * has timed out (`ETIMEDOUT`). + */ +export class TimedOut extends Error {} +/** + * Raised when attempting to read bytes from a resource, but the EOF was + * unexpectedly encountered. + */ +export class UnexpectedEof extends Error {} +/** + * Raised when expecting to write to a IO buffer resulted in zero bytes + * being written. + */ +export class WriteZero extends Error {} diff --git a/fs/unstable_errors.js b/fs/unstable_errors.js new file mode 100644 index 000000000000..2a444c670703 --- /dev/null +++ b/fs/unstable_errors.js @@ -0,0 +1,38 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +// @ts-self-types="./unstable_errors.d.ts" + +import { isDeno } from "./_utils.ts"; + +// please keep sorted +export const AlreadyExists = isDeno + ? Deno.errors.AlreadyExists + : class AlreadyExists extends Error {}; +export const BadResource = isDeno + ? Deno.errors.BadResource + : class BadResource extends Error {}; +export const BrokenPipe = isDeno + ? Deno.errors.BrokenPipe + : class BrokenPipe extends Error {}; +export const Busy = isDeno ? Deno.errors.Busy : class Busy extends Error {}; +export const Interrupted = isDeno + ? Deno.errors.Interrupted + : class Interrupted extends Error {}; +export const InvalidData = isDeno + ? Deno.errors.InvalidData + : class InvalidData extends Error {}; +export const NotFound = isDeno + ? Deno.errors.NotFound + : class NotFound extends Error {}; +export const PermissionDenied = isDeno + ? Deno.errors.PermissionDenied + : class PermissionDenied extends Error {}; +export const TimedOut = isDeno + ? Deno.errors.TimedOut + : class TimedOut extends Error {}; +export const UnexpectedEof = isDeno + ? Deno.errors.UnexpectedEof + : class UnexpectedEof extends Error {}; +export const WriteZero = isDeno + ? Deno.errors.WriteZero + : class WriteZero extends Error {}; diff --git a/fs/unstable_stat.ts b/fs/unstable_stat.ts new file mode 100644 index 000000000000..093b34ef478f --- /dev/null +++ b/fs/unstable_stat.ts @@ -0,0 +1,35 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. +// deno-lint-ignore-file no-process-globals + +import { isDeno } from "./_utils.ts"; +import { mapError } from "./_map_error.ts"; +import { toFileInfo } from "./_to_file_info.ts"; +import type { FileInfo } from "./unstable_types.ts"; + +/** Resolves to a {@linkcode FileInfo} for the specified `path`. Will + * always follow symlinks. + * + * ```ts + * import { assert } from "jsr:@std/assert"; + * const fileInfo = await Deno.stat("hello.txt"); + * assert(fileInfo.isFile); + * ``` + * + * Requires `allow-read` permission. + * + * @tags allow-read + * @category File System + */ +export async function stat(path: string | URL): Promise { + if (isDeno) { + return Deno.stat(path); + } else { + const fsPromises = process.getBuiltinModule("node:fs/promises"); + try { + const stat = await fsPromises.stat(path); + return toFileInfo(stat); + } catch (error) { + throw mapError(error); + } + } +} diff --git a/fs/unstable_stat_test.ts b/fs/unstable_stat_test.ts new file mode 100644 index 000000000000..da9fb454288c --- /dev/null +++ b/fs/unstable_stat_test.ts @@ -0,0 +1,23 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +import { assert, assertRejects } from "@std/assert"; +import { stat } from "./unstable_stat.ts"; +import { NotFound } from "./unstable_errors.js"; + +Deno.test("stat() returns FileInfo for a file", async () => { + const fileInfo = await stat("README.md"); + + assert(fileInfo.isFile); +}); + +Deno.test("stat() returns FileInfo for a directory", async () => { + const fileInfo = await stat("fs"); + + assert(fileInfo.isDirectory); +}); + +Deno.test("stat() rejects with NotFound for a non-existent file", async () => { + await assertRejects(async () => { + await stat("non_existent_file"); + }, NotFound); +}); diff --git a/fs/unstable_types.ts b/fs/unstable_types.ts new file mode 100644 index 000000000000..5517d4b422cb --- /dev/null +++ b/fs/unstable_types.ts @@ -0,0 +1,81 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +export interface FileInfo { + /** True if this is info for a regular file. Mutually exclusive to + * `FileInfo.isDirectory` and `FileInfo.isSymlink`. */ + isFile: boolean; + /** True if this is info for a regular directory. Mutually exclusive to + * `FileInfo.isFile` and `FileInfo.isSymlink`. */ + isDirectory: boolean; + /** True if this is info for a symlink. Mutually exclusive to + * `FileInfo.isFile` and `FileInfo.isDirectory`. */ + isSymlink: boolean; + /** The size of the file, in bytes. */ + size: number; + /** The last modification time of the file. This corresponds to the `mtime` + * field from `stat` on Linux/Mac OS and `ftLastWriteTime` on Windows. This + * may not be available on all platforms. */ + mtime: Date | null; + /** The last access time of the file. This corresponds to the `atime` + * field from `stat` on Unix and `ftLastAccessTime` on Windows. This may not + * be available on all platforms. */ + atime: Date | null; + /** The creation time of the file. This corresponds to the `birthtime` + * field from `stat` on Mac/BSD and `ftCreationTime` on Windows. This may + * not be available on all platforms. */ + birthtime: Date | null; + /** The last change time of the file. This corresponds to the `ctime` + * field from `stat` on Mac/BSD and `ChangeTime` on Windows. This may + * not be available on all platforms. */ + ctime: Date | null; + /** ID of the device containing the file. */ + dev: number; + /** Inode number. + * + * _Linux/Mac OS only._ */ + ino: number | null; + /** The underlying raw `st_mode` bits that contain the standard Unix + * permissions for this file/directory. + */ + mode: number | null; + /** Number of hard links pointing to this file. + * + * _Linux/Mac OS only._ */ + nlink: number | null; + /** User ID of the owner of this file. + * + * _Linux/Mac OS only._ */ + uid: number | null; + /** Group ID of the owner of this file. + * + * _Linux/Mac OS only._ */ + gid: number | null; + /** Device ID of this file. + * + * _Linux/Mac OS only._ */ + rdev: number | null; + /** Blocksize for filesystem I/O. + * + * _Linux/Mac OS only._ */ + blksize: number | null; + /** Number of blocks allocated to the file, in 512-byte units. + * + * _Linux/Mac OS only._ */ + blocks: number | null; + /** True if this is info for a block device. + * + * _Linux/Mac OS only._ */ + isBlockDevice: boolean | null; + /** True if this is info for a char device. + * + * _Linux/Mac OS only._ */ + isCharDevice: boolean | null; + /** True if this is info for a fifo. + * + * _Linux/Mac OS only._ */ + isFifo: boolean | null; + /** True if this is info for a socket. + * + * _Linux/Mac OS only._ */ + isSocket: boolean | null; +} From dafa9a3188075c6c0a06ba8e97e5fed318b71150 Mon Sep 17 00:00:00 2001 From: Yoshiya Hinosawa Date: Thu, 12 Dec 2024 22:07:54 +0900 Subject: [PATCH 2/3] fix example --- fs/unstable_stat.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/fs/unstable_stat.ts b/fs/unstable_stat.ts index 093b34ef478f..7667e47e79fe 100644 --- a/fs/unstable_stat.ts +++ b/fs/unstable_stat.ts @@ -10,8 +10,9 @@ import type { FileInfo } from "./unstable_types.ts"; * always follow symlinks. * * ```ts - * import { assert } from "jsr:@std/assert"; - * const fileInfo = await Deno.stat("hello.txt"); + * import { assert } from "@std/assert"; + * import { stat } from "@std/fs/unstable-stat"; + * const fileInfo = await Deno.stat("README.md"); * assert(fileInfo.isFile); * ``` * From 306bb5c2a0bb4d06b739f9b04f71dee2e31c7d51 Mon Sep 17 00:00:00 2001 From: Yoshiya Hinosawa Date: Thu, 12 Dec 2024 22:16:03 +0900 Subject: [PATCH 3/3] fix for Deno 1.x --- fs/_to_file_info.ts | 3 ++- fs/_utils.ts | 10 +++++++--- fs/unstable_stat.ts | 5 ++--- fs/unstable_types.ts | 3 ++- 4 files changed, 13 insertions(+), 8 deletions(-) diff --git a/fs/_to_file_info.ts b/fs/_to_file_info.ts index 369ca31442ab..bb7bd0e9d16d 100644 --- a/fs/_to_file_info.ts +++ b/fs/_to_file_info.ts @@ -6,7 +6,8 @@ import { isWindows } from "./_utils.ts"; export function toFileInfo(s: import("node:fs").Stats): FileInfo { return { atime: s.atime, - ctime: s.ctime, + // TODO(kt3k): uncomment this when we drop support for Deno 1.x + // ctime: s.ctime, birthtime: s.birthtime, blksize: isWindows ? null : s.blksize, blocks: isWindows ? null : s.blocks, diff --git a/fs/_utils.ts b/fs/_utils.ts index 33f0e9c0ec0c..2ac664f79cd2 100644 --- a/fs/_utils.ts +++ b/fs/_utils.ts @@ -1,5 +1,5 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. -// deno-lint-ignore-file no-explicit-any no-process-globals +// deno-lint-ignore-file no-explicit-any /** * True if the runtime is Deno, false otherwise. @@ -15,8 +15,12 @@ export const isWindows = checkWindows(); function checkWindows(): boolean { if (typeof navigator !== "undefined" && (navigator as any).platform) { return (navigator as any).platform.startsWith("Win"); - } else if (typeof process !== "undefined") { - return process.platform === "win32"; + } else if (typeof (globalThis as any).process !== "undefined") { + return (globalThis as any).platform === "win32"; } return false; } + +export function getNodeFsPromises() { + return (globalThis as any).process.getBuiltinModule("node:fs/promises"); +} diff --git a/fs/unstable_stat.ts b/fs/unstable_stat.ts index 7667e47e79fe..f500fb671f02 100644 --- a/fs/unstable_stat.ts +++ b/fs/unstable_stat.ts @@ -1,7 +1,6 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. -// deno-lint-ignore-file no-process-globals -import { isDeno } from "./_utils.ts"; +import { getNodeFsPromises, isDeno } from "./_utils.ts"; import { mapError } from "./_map_error.ts"; import { toFileInfo } from "./_to_file_info.ts"; import type { FileInfo } from "./unstable_types.ts"; @@ -25,7 +24,7 @@ export async function stat(path: string | URL): Promise { if (isDeno) { return Deno.stat(path); } else { - const fsPromises = process.getBuiltinModule("node:fs/promises"); + const fsPromises = getNodeFsPromises(); try { const stat = await fsPromises.stat(path); return toFileInfo(stat); diff --git a/fs/unstable_types.ts b/fs/unstable_types.ts index 5517d4b422cb..bde3f14fac33 100644 --- a/fs/unstable_types.ts +++ b/fs/unstable_types.ts @@ -27,7 +27,8 @@ export interface FileInfo { /** The last change time of the file. This corresponds to the `ctime` * field from `stat` on Mac/BSD and `ChangeTime` on Windows. This may * not be available on all platforms. */ - ctime: Date | null; + // TODO(kt3k): uncomment this when we drop support for Deno 1.x + // ctime: Date | null; /** ID of the device containing the file. */ dev: number; /** Inode number.