Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(fs/unstable): add fs.stat #6258

Merged
merged 3 commits into from
Dec 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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>;

Check warning on line 12 in fs/_map_error.ts

View check run for this annotation

Codecov / codecov/patch

fs/_map_error.ts#L9-L12

Added lines #L9 - L12 were not covered by tests

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

Check warning on line 22 in fs/_map_error.ts

View check run for this annotation

Codecov / codecov/patch

fs/_map_error.ts#L20-L22

Added lines #L20 - L22 were not covered by tests

export function mapError<E>(e: E) {
if (!isNodeErr(e)) return e;
return map[e.code]?.(e) || e;
}

Check warning on line 27 in fs/_map_error.ts

View check run for this annotation

Codecov / codecov/patch

fs/_map_error.ts#L24-L27

Added lines #L24 - L27 were not covered by tests
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,
};
}

Check warning on line 31 in fs/_to_file_info.ts

View check run for this annotation

Codecov / codecov/patch

fs/_to_file_info.ts#L6-L31

Added lines #L6 - L31 were not covered by tests
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");

Check warning on line 17 in fs/_utils.ts

View check run for this annotation

Codecov / codecov/patch

fs/_utils.ts#L17

Added line #L17 was not covered by tests
} 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");
}

Check warning on line 26 in fs/_utils.ts

View check run for this annotation

Codecov / codecov/patch

fs/_utils.ts#L24-L26

Added lines #L24 - L26 were not covered by tests
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 {};

Check warning on line 10 in fs/unstable_errors.js

View check run for this annotation

Codecov / codecov/patch

fs/unstable_errors.js#L10

Added line #L10 was not covered by tests
export const BadResource = isDeno
? Deno.errors.BadResource
: class BadResource extends Error {};

Check warning on line 13 in fs/unstable_errors.js

View check run for this annotation

Codecov / codecov/patch

fs/unstable_errors.js#L13

Added line #L13 was not covered by tests
export const BrokenPipe = isDeno
? Deno.errors.BrokenPipe
: class BrokenPipe extends Error {};
export const Busy = isDeno ? Deno.errors.Busy : class Busy extends Error {};

Check warning on line 17 in fs/unstable_errors.js

View check run for this annotation

Codecov / codecov/patch

fs/unstable_errors.js#L16-L17

Added lines #L16 - L17 were not covered by tests
export const Interrupted = isDeno
? Deno.errors.Interrupted
: class Interrupted extends Error {};

Check warning on line 20 in fs/unstable_errors.js

View check run for this annotation

Codecov / codecov/patch

fs/unstable_errors.js#L20

Added line #L20 was not covered by tests
export const InvalidData = isDeno
? Deno.errors.InvalidData
: class InvalidData extends Error {};

Check warning on line 23 in fs/unstable_errors.js

View check run for this annotation

Codecov / codecov/patch

fs/unstable_errors.js#L23

Added line #L23 was not covered by tests
export const NotFound = isDeno
? Deno.errors.NotFound
: class NotFound extends Error {};

Check warning on line 26 in fs/unstable_errors.js

View check run for this annotation

Codecov / codecov/patch

fs/unstable_errors.js#L26

Added line #L26 was not covered by tests
export const PermissionDenied = isDeno
? Deno.errors.PermissionDenied
: class PermissionDenied extends Error {};

Check warning on line 29 in fs/unstable_errors.js

View check run for this annotation

Codecov / codecov/patch

fs/unstable_errors.js#L29

Added line #L29 was not covered by tests
export const TimedOut = isDeno
? Deno.errors.TimedOut
: class TimedOut extends Error {};

Check warning on line 32 in fs/unstable_errors.js

View check run for this annotation

Codecov / codecov/patch

fs/unstable_errors.js#L32

Added line #L32 was not covered by tests
export const UnexpectedEof = isDeno
? Deno.errors.UnexpectedEof
: class UnexpectedEof extends Error {};

Check warning on line 35 in fs/unstable_errors.js

View check run for this annotation

Codecov / codecov/patch

fs/unstable_errors.js#L35

Added line #L35 was not covered by tests
export const WriteZero = isDeno
? Deno.errors.WriteZero
: class WriteZero extends Error {};

Check warning on line 38 in fs/unstable_errors.js

View check run for this annotation

Codecov / codecov/patch

fs/unstable_errors.js#L38

Added line #L38 was not covered by tests
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);
}
}

Check warning on line 34 in fs/unstable_stat.ts

View check run for this annotation

Codecov / codecov/patch

fs/unstable_stat.ts#L27-L34

Added lines #L27 - L34 were not covered by tests
}
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;
}
Loading