1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-12-18 05:14:21 -05:00

fix(node/fs): support recursive option in readdir (#27179)

We didn't support the `recursive` option of
`fs.readdir()/fs.readdirSync()`.

Fixes https://github.com/denoland/deno/issues/27175
This commit is contained in:
Marvin Hagemeister 2024-12-03 10:28:20 +01:00 committed by GitHub
parent b78c851a94
commit 2fbc5fea83
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 116 additions and 32 deletions

View file

@ -4,12 +4,13 @@
// deno-lint-ignore-file prefer-primordials // deno-lint-ignore-file prefer-primordials
import { TextDecoder, TextEncoder } from "ext:deno_web/08_text_encoding.js"; import { TextDecoder, TextEncoder } from "ext:deno_web/08_text_encoding.js";
import { asyncIterableToCallback } from "ext:deno_node/_fs/_fs_watch.ts";
import Dirent from "ext:deno_node/_fs/_fs_dirent.ts"; import Dirent from "ext:deno_node/_fs/_fs_dirent.ts";
import { denoErrorToNodeError } from "ext:deno_node/internal/errors.ts"; import { denoErrorToNodeError } from "ext:deno_node/internal/errors.ts";
import { getValidatedPath } from "ext:deno_node/internal/fs/utils.mjs"; import { getValidatedPath } from "ext:deno_node/internal/fs/utils.mjs";
import { Buffer } from "node:buffer"; import { Buffer } from "node:buffer";
import { promisify } from "ext:deno_node/internal/util.mjs"; import { promisify } from "ext:deno_node/internal/util.mjs";
import { op_fs_read_dir_async, op_fs_read_dir_sync } from "ext:core/ops";
import { join, relative } from "node:path";
function toDirent(val: Deno.DirEntry & { parentPath: string }): Dirent { function toDirent(val: Deno.DirEntry & { parentPath: string }): Dirent {
return new Dirent(val); return new Dirent(val);
@ -18,6 +19,7 @@ function toDirent(val: Deno.DirEntry & { parentPath: string }): Dirent {
type readDirOptions = { type readDirOptions = {
encoding?: string; encoding?: string;
withFileTypes?: boolean; withFileTypes?: boolean;
recursive?: boolean;
}; };
type readDirCallback = (err: Error | null, files: string[]) => void; type readDirCallback = (err: Error | null, files: string[]) => void;
@ -30,12 +32,12 @@ type readDirBoth = (
export function readdir( export function readdir(
path: string | Buffer | URL, path: string | Buffer | URL,
options: { withFileTypes?: false; encoding?: string }, options: readDirOptions,
callback: readDirCallback, callback: readDirCallback,
): void; ): void;
export function readdir( export function readdir(
path: string | Buffer | URL, path: string | Buffer | URL,
options: { withFileTypes: true; encoding?: string }, options: readDirOptions,
callback: readDirCallbackDirent, callback: readDirCallbackDirent,
): void; ): void;
export function readdir(path: string | URL, callback: readDirCallback): void; export function readdir(path: string | URL, callback: readDirCallback): void;
@ -51,8 +53,7 @@ export function readdir(
const options = typeof optionsOrCallback === "object" const options = typeof optionsOrCallback === "object"
? optionsOrCallback ? optionsOrCallback
: null; : null;
const result: Array<string | Dirent> = []; path = getValidatedPath(path).toString();
path = getValidatedPath(path);
if (!callback) throw new Error("No callback function supplied"); if (!callback) throw new Error("No callback function supplied");
@ -66,24 +67,44 @@ export function readdir(
} }
} }
try { const result: Array<string | Dirent> = [];
path = path.toString(); const dirs = [path];
asyncIterableToCallback(Deno.readDir(path), (val, done) => { let current: string | undefined;
if (typeof path !== "string") return; (async () => {
if (done) { while ((current = dirs.shift()) !== undefined) {
callback(null, result); try {
const entries = await op_fs_read_dir_async(current);
for (let i = 0; i < entries.length; i++) {
const entry = entries[i];
if (options?.recursive && entry.isDirectory) {
dirs.push(join(current, entry.name));
}
if (options?.withFileTypes) {
entry.parentPath = current;
result.push(toDirent(entry));
} else {
let name = decode(entry.name, options?.encoding);
if (options?.recursive) {
name = relative(path, join(current, name));
}
result.push(name);
}
}
} catch (err) {
callback(
denoErrorToNodeError(err as Error, {
syscall: "readdir",
path: current,
}),
);
return; return;
} }
if (options?.withFileTypes) { }
val.parentPath = path;
result.push(toDirent(val)); callback(null, result);
} else result.push(decode(val.name)); })();
}, (e) => {
callback(denoErrorToNodeError(e as Error, { syscall: "readdir" }));
});
} catch (e) {
callback(denoErrorToNodeError(e as Error, { syscall: "readdir" }));
}
} }
function decode(str: string, encoding?: string): string { function decode(str: string, encoding?: string): string {
@ -118,8 +139,7 @@ export function readdirSync(
path: string | Buffer | URL, path: string | Buffer | URL,
options?: readDirOptions, options?: readDirOptions,
): Array<string | Dirent> { ): Array<string | Dirent> {
const result = []; path = getValidatedPath(path).toString();
path = getValidatedPath(path);
if (options?.encoding) { if (options?.encoding) {
try { try {
@ -131,16 +151,37 @@ export function readdirSync(
} }
} }
try { const result: Array<string | Dirent> = [];
path = path.toString(); const dirs = [path];
for (const file of Deno.readDirSync(path)) { let current: string | undefined;
if (options?.withFileTypes) { while ((current = dirs.shift()) !== undefined) {
file.parentPath = path; try {
result.push(toDirent(file)); const entries = op_fs_read_dir_sync(current);
} else result.push(decode(file.name));
for (let i = 0; i < entries.length; i++) {
const entry = entries[i];
if (options?.recursive && entry.isDirectory) {
dirs.push(join(current, entry.name));
}
if (options?.withFileTypes) {
entry.parentPath = current;
result.push(toDirent(entry));
} else {
let name = decode(entry.name, options?.encoding);
if (options?.recursive) {
name = relative(path, join(current, name));
}
result.push(name);
}
}
} catch (e) {
throw denoErrorToNodeError(e as Error, {
syscall: "readdir",
path: current,
});
} }
} catch (e) {
throw denoErrorToNodeError(e as Error, { syscall: "readdir" });
} }
return result; return result;
} }

View file

@ -53,6 +53,29 @@ Deno.test({
}, },
}); });
Deno.test("ASYNC: read dirs recursively", async () => {
const dir = Deno.makeTempDirSync();
Deno.writeTextFileSync(join(dir, "file1.txt"), "hi");
Deno.mkdirSync(join(dir, "sub"));
Deno.writeTextFileSync(join(dir, "sub", "file2.txt"), "hi");
try {
const files = await new Promise<string[]>((resolve, reject) => {
readdir(dir, { recursive: true }, (err, files) => {
if (err) reject(err);
resolve(files.map((f) => f.toString()));
});
});
assertEqualsArrayAnyOrder(
files,
["file1.txt", "sub", join("sub", "file2.txt")],
);
} finally {
Deno.removeSync(dir, { recursive: true });
}
});
Deno.test({ Deno.test({
name: "SYNC: reading empty the directory", name: "SYNC: reading empty the directory",
fn() { fn() {
@ -75,6 +98,26 @@ Deno.test({
}, },
}); });
Deno.test("SYNC: read dirs recursively", () => {
const dir = Deno.makeTempDirSync();
Deno.writeTextFileSync(join(dir, "file1.txt"), "hi");
Deno.mkdirSync(join(dir, "sub"));
Deno.writeTextFileSync(join(dir, "sub", "file2.txt"), "hi");
try {
const files = readdirSync(dir, { recursive: true }).map((f) =>
f.toString()
);
assertEqualsArrayAnyOrder(
files,
["file1.txt", "sub", join("sub", "file2.txt")],
);
} finally {
Deno.removeSync(dir, { recursive: true });
}
});
Deno.test("[std/node/fs] readdir callback isn't called twice if error is thrown", async () => { Deno.test("[std/node/fs] readdir callback isn't called twice if error is thrown", async () => {
// The correct behaviour is not to catch any errors thrown, // The correct behaviour is not to catch any errors thrown,
// but that means there'll be an uncaught error and the test will fail. // but that means there'll be an uncaught error and the test will fail.