Skip to content

Commit

Permalink
feat(fs/unstable): add fs.stat (#6258)
Browse files Browse the repository at this point in the history
  • Loading branch information
kt3k authored Dec 16, 2024
1 parent cc9a7c0 commit 34abfeb
Show file tree
Hide file tree
Showing 10 changed files with 325 additions and 0 deletions.
1 change: 1 addition & 0 deletions _tools/node_test_runner/run_test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
27 changes: 27 additions & 0 deletions fs/_map_error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.

import * as errors from "./unstable_errors.js";

type Class<T> = new (...params: unknown[]) => T;

type ClassOrT<T> = T extends Class<infer U> ? 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<typeof Ctor>;

const map: Record<string, ReturnType<typeof mapper>> = {
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: E) {
if (!isNodeErr(e)) return e;
return map[e.code]?.(e) || e;
}
31 changes: 31 additions & 0 deletions fs/_to_file_info.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// 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,
// 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,
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,
};
}
26 changes: 26 additions & 0 deletions fs/_utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
// deno-lint-ignore-file no-explicit-any

/**
* 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 (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");
}
1 change: 1 addition & 0 deletions fs/deno.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"./exists": "./exists.ts",
"./expand-glob": "./expand_glob.ts",
"./move": "./move.ts",
"./unstable-stat": "./unstable_stat.ts",
"./walk": "./walk.ts"
}
}
61 changes: 61 additions & 0 deletions fs/unstable_errors.d.ts
Original file line number Diff line number Diff line change
@@ -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 {}
38 changes: 38 additions & 0 deletions fs/unstable_errors.js
Original file line number Diff line number Diff line change
@@ -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 {};
35 changes: 35 additions & 0 deletions fs/unstable_stat.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.

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";

/** Resolves to a {@linkcode FileInfo} for the specified `path`. Will
* always follow symlinks.
*
* ```ts
* import { assert } from "@std/assert";
* import { stat } from "@std/fs/unstable-stat";
* const fileInfo = await Deno.stat("README.md");
* assert(fileInfo.isFile);
* ```
*
* Requires `allow-read` permission.
*
* @tags allow-read
* @category File System
*/
export async function stat(path: string | URL): Promise<FileInfo> {
if (isDeno) {
return Deno.stat(path);
} else {
const fsPromises = getNodeFsPromises();
try {
const stat = await fsPromises.stat(path);
return toFileInfo(stat);
} catch (error) {
throw mapError(error);
}
}
}
23 changes: 23 additions & 0 deletions fs/unstable_stat_test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
82 changes: 82 additions & 0 deletions fs/unstable_types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// 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. */
// 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.
*
* _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;
}

0 comments on commit 34abfeb

Please sign in to comment.