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:
parent
b78c851a94
commit
2fbc5fea83
2 changed files with 116 additions and 32 deletions
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.
|
||||||
|
|
Loading…
Reference in a new issue