mirror of
https://github.com/denoland/deno.git
synced 2025-03-03 17:34:47 -05:00
feat(std/node): add directory classes (#4087)
This commit is contained in:
parent
eafd40feab
commit
3968308886
4 changed files with 434 additions and 0 deletions
114
std/node/_fs_dir.ts
Normal file
114
std/node/_fs_dir.ts
Normal file
|
@ -0,0 +1,114 @@
|
||||||
|
import Dirent from "./_fs_dirent.ts";
|
||||||
|
|
||||||
|
export default class Dir {
|
||||||
|
private dirPath: string | Uint8Array;
|
||||||
|
private files: Dirent[] = [];
|
||||||
|
private filesReadComplete = false;
|
||||||
|
|
||||||
|
constructor(path: string | Uint8Array) {
|
||||||
|
this.dirPath = path;
|
||||||
|
}
|
||||||
|
|
||||||
|
get path(): string {
|
||||||
|
if (this.dirPath instanceof Uint8Array) {
|
||||||
|
return new TextDecoder().decode(this.dirPath);
|
||||||
|
}
|
||||||
|
return this.dirPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* NOTE: Deno doesn't provide an interface to the filesystem like readdir
|
||||||
|
* where each call to readdir returns the next file. This function simulates this
|
||||||
|
* behaviour by fetching all the entries on the first call, putting them on a stack
|
||||||
|
* and then popping them off the stack one at a time.
|
||||||
|
*
|
||||||
|
* TODO: Rework this implementation once https://github.com/denoland/deno/issues/4218
|
||||||
|
* is resolved.
|
||||||
|
*/
|
||||||
|
read(callback?: Function): Promise<Dirent | null> {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
try {
|
||||||
|
if (this.initializationOfDirectoryFilesIsRequired()) {
|
||||||
|
const denoFiles: Deno.FileInfo[] = await Deno.readDir(this.path);
|
||||||
|
this.files = denoFiles.map(file => new Dirent(file));
|
||||||
|
}
|
||||||
|
const nextFile = this.files.pop();
|
||||||
|
if (nextFile) {
|
||||||
|
resolve(nextFile);
|
||||||
|
this.filesReadComplete = this.files.length === 0;
|
||||||
|
} else {
|
||||||
|
this.filesReadComplete = true;
|
||||||
|
resolve(null);
|
||||||
|
}
|
||||||
|
if (callback) {
|
||||||
|
callback(null, !nextFile ? null : nextFile);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (callback) {
|
||||||
|
callback(err, null);
|
||||||
|
}
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
readSync(): Dirent | null {
|
||||||
|
if (this.initializationOfDirectoryFilesIsRequired()) {
|
||||||
|
this.files.push(
|
||||||
|
...Deno.readDirSync(this.path).map(file => new Dirent(file))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const dirent: Dirent | undefined = this.files.pop();
|
||||||
|
this.filesReadComplete = this.files.length === 0;
|
||||||
|
|
||||||
|
return !dirent ? null : dirent;
|
||||||
|
}
|
||||||
|
|
||||||
|
private initializationOfDirectoryFilesIsRequired(): boolean {
|
||||||
|
return this.files.length === 0 && !this.filesReadComplete;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unlike Node, Deno does not require managing resource ids for reading
|
||||||
|
* directories, and therefore does not need to close directories when
|
||||||
|
* finished reading.
|
||||||
|
*/
|
||||||
|
close(callback?: Function): Promise<void> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
try {
|
||||||
|
if (callback) {
|
||||||
|
callback(null);
|
||||||
|
}
|
||||||
|
resolve();
|
||||||
|
} catch (err) {
|
||||||
|
if (callback) {
|
||||||
|
callback(err);
|
||||||
|
}
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unlike Node, Deno does not require managing resource ids for reading
|
||||||
|
* directories, and therefore does not need to close directories when
|
||||||
|
* finished reading
|
||||||
|
*/
|
||||||
|
closeSync(): void {
|
||||||
|
//No op
|
||||||
|
}
|
||||||
|
|
||||||
|
async *[Symbol.asyncIterator](): AsyncIterableIterator<Dirent> {
|
||||||
|
try {
|
||||||
|
while (true) {
|
||||||
|
const dirent: Dirent | null = await this.read();
|
||||||
|
if (dirent === null) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
yield dirent;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
await this.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
156
std/node/_fs_dir_test.ts
Normal file
156
std/node/_fs_dir_test.ts
Normal file
|
@ -0,0 +1,156 @@
|
||||||
|
const { test } = Deno;
|
||||||
|
import { assert, assertEquals, fail } from "../testing/asserts.ts";
|
||||||
|
import Dir from "./_fs_dir.ts";
|
||||||
|
import Dirent from "./_fs_dirent.ts";
|
||||||
|
|
||||||
|
test({
|
||||||
|
name: "Closing current directory with callback is successful",
|
||||||
|
async fn() {
|
||||||
|
let calledBack = false;
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
new Dir(".").close((valOrErr: any) => {
|
||||||
|
assert(!valOrErr);
|
||||||
|
calledBack = true;
|
||||||
|
});
|
||||||
|
assert(calledBack);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test({
|
||||||
|
name: "Closing current directory without callback returns void Promise",
|
||||||
|
async fn() {
|
||||||
|
await new Dir(".").close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test({
|
||||||
|
name: "Closing current directory synchronously works",
|
||||||
|
async fn() {
|
||||||
|
new Dir(".").closeSync();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test({
|
||||||
|
name: "Path is correctly returned",
|
||||||
|
fn() {
|
||||||
|
assertEquals(new Dir("std/node").path, "std/node");
|
||||||
|
|
||||||
|
const enc: Uint8Array = new TextEncoder().encode("std/node");
|
||||||
|
assertEquals(new Dir(enc).path, "std/node");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test({
|
||||||
|
name: "read returns null for empty directory",
|
||||||
|
async fn() {
|
||||||
|
const testDir: string = Deno.makeTempDirSync();
|
||||||
|
try {
|
||||||
|
const file: Dirent | null = await new Dir(testDir).read();
|
||||||
|
assert(file === null);
|
||||||
|
|
||||||
|
let calledBack = false;
|
||||||
|
const fileFromCallback: Dirent | null = await new Dir(
|
||||||
|
testDir
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
).read((err: any, res: Dirent) => {
|
||||||
|
assert(res === null);
|
||||||
|
assert(err === null);
|
||||||
|
calledBack = true;
|
||||||
|
});
|
||||||
|
assert(fileFromCallback === null);
|
||||||
|
assert(calledBack);
|
||||||
|
|
||||||
|
assertEquals(new Dir(testDir).readSync(), null);
|
||||||
|
} finally {
|
||||||
|
Deno.removeSync(testDir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test({
|
||||||
|
name: "Async read returns one file at a time",
|
||||||
|
async fn() {
|
||||||
|
const testDir: string = Deno.makeTempDirSync();
|
||||||
|
Deno.createSync(testDir + "/foo.txt");
|
||||||
|
Deno.createSync(testDir + "/bar.txt");
|
||||||
|
|
||||||
|
try {
|
||||||
|
let secondCallback = false;
|
||||||
|
const dir: Dir = new Dir(testDir);
|
||||||
|
const firstRead: Dirent | null = await dir.read();
|
||||||
|
const secondRead: Dirent | null = await dir.read(
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
(err: any, secondResult: Dirent) => {
|
||||||
|
assert(
|
||||||
|
secondResult.name === "bar.txt" || secondResult.name === "foo.txt"
|
||||||
|
);
|
||||||
|
secondCallback = true;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const thirdRead: Dirent | null = await dir.read();
|
||||||
|
|
||||||
|
if (firstRead?.name === "foo.txt") {
|
||||||
|
assertEquals(secondRead?.name, "bar.txt");
|
||||||
|
} else if (firstRead?.name === "bar.txt") {
|
||||||
|
assertEquals(secondRead?.name, "foo.txt");
|
||||||
|
} else {
|
||||||
|
fail("File not found during read");
|
||||||
|
}
|
||||||
|
assert(secondCallback);
|
||||||
|
assert(thirdRead === null);
|
||||||
|
} finally {
|
||||||
|
Deno.removeSync(testDir, { recursive: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test({
|
||||||
|
name: "Sync read returns one file at a time",
|
||||||
|
fn() {
|
||||||
|
const testDir: string = Deno.makeTempDirSync();
|
||||||
|
Deno.createSync(testDir + "/foo.txt");
|
||||||
|
Deno.createSync(testDir + "/bar.txt");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const dir: Dir = new Dir(testDir);
|
||||||
|
const firstRead: Dirent | null = dir.readSync();
|
||||||
|
const secondRead: Dirent | null = dir.readSync();
|
||||||
|
const thirdRead: Dirent | null = dir.readSync();
|
||||||
|
|
||||||
|
if (firstRead?.name === "foo.txt") {
|
||||||
|
assertEquals(secondRead?.name, "bar.txt");
|
||||||
|
} else if (firstRead?.name === "bar.txt") {
|
||||||
|
assertEquals(secondRead?.name, "foo.txt");
|
||||||
|
} else {
|
||||||
|
fail("File not found during read");
|
||||||
|
}
|
||||||
|
assert(thirdRead === null);
|
||||||
|
} finally {
|
||||||
|
Deno.removeSync(testDir, { recursive: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test({
|
||||||
|
name: "Async iteration over existing directory",
|
||||||
|
async fn() {
|
||||||
|
const testDir: string = Deno.makeTempDirSync();
|
||||||
|
Deno.createSync(testDir + "/foo.txt");
|
||||||
|
Deno.createSync(testDir + "/bar.txt");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const dir: Dir = new Dir(testDir);
|
||||||
|
const results: Array<string | null> = [];
|
||||||
|
|
||||||
|
for await (const file of dir[Symbol.asyncIterator]()) {
|
||||||
|
results.push(file.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(results.length === 2);
|
||||||
|
assert(results.includes("foo.txt"));
|
||||||
|
assert(results.includes("bar.txt"));
|
||||||
|
} finally {
|
||||||
|
Deno.removeSync(testDir, { recursive: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
41
std/node/_fs_dirent.ts
Normal file
41
std/node/_fs_dirent.ts
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
import { notImplemented } from "./_utils.ts";
|
||||||
|
|
||||||
|
export default class Dirent {
|
||||||
|
constructor(private entry: Deno.FileInfo) {}
|
||||||
|
|
||||||
|
isBlockDevice(): boolean {
|
||||||
|
return this.entry.blocks != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
isCharacterDevice(): boolean {
|
||||||
|
return this.entry.blocks == null;
|
||||||
|
}
|
||||||
|
|
||||||
|
isDirectory(): boolean {
|
||||||
|
return this.entry.isDirectory();
|
||||||
|
}
|
||||||
|
|
||||||
|
isFIFO(): boolean {
|
||||||
|
notImplemented(
|
||||||
|
"Deno does not yet support identification of FIFO named pipes"
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
isFile(): boolean {
|
||||||
|
return this.entry.isFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
isSocket(): boolean {
|
||||||
|
notImplemented("Deno does not yet support identification of sockets");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
isSymbolicLink(): boolean {
|
||||||
|
return this.entry.isSymlink();
|
||||||
|
}
|
||||||
|
|
||||||
|
get name(): string | null {
|
||||||
|
return this.entry.name;
|
||||||
|
}
|
||||||
|
}
|
123
std/node/_fs_dirent_test.ts
Normal file
123
std/node/_fs_dirent_test.ts
Normal file
|
@ -0,0 +1,123 @@
|
||||||
|
const { test } = Deno;
|
||||||
|
import { assert, assertEquals, assertThrows } from "../testing/asserts.ts";
|
||||||
|
import Dirent from "./_fs_dirent.ts";
|
||||||
|
|
||||||
|
class FileInfoMock implements Deno.FileInfo {
|
||||||
|
len = -1;
|
||||||
|
modified = -1;
|
||||||
|
accessed = -1;
|
||||||
|
created = -1;
|
||||||
|
name = "";
|
||||||
|
dev = -1;
|
||||||
|
ino = -1;
|
||||||
|
mode = -1;
|
||||||
|
nlink = -1;
|
||||||
|
uid = -1;
|
||||||
|
gid = -1;
|
||||||
|
rdev = -1;
|
||||||
|
blksize = -1;
|
||||||
|
blocks: number | null = null;
|
||||||
|
|
||||||
|
isFileMock = false;
|
||||||
|
isDirectoryMock = false;
|
||||||
|
isSymlinkMock = false;
|
||||||
|
|
||||||
|
isFile(): boolean {
|
||||||
|
return this.isFileMock;
|
||||||
|
}
|
||||||
|
isDirectory(): boolean {
|
||||||
|
return this.isDirectoryMock;
|
||||||
|
}
|
||||||
|
isSymlink(): boolean {
|
||||||
|
return this.isSymlinkMock;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test({
|
||||||
|
name: "Block devices are correctly identified",
|
||||||
|
fn() {
|
||||||
|
const fileInfo: FileInfoMock = new FileInfoMock();
|
||||||
|
fileInfo.blocks = 5;
|
||||||
|
assert(new Dirent(fileInfo).isBlockDevice());
|
||||||
|
assert(!new Dirent(fileInfo).isCharacterDevice());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test({
|
||||||
|
name: "Character devices are correctly identified",
|
||||||
|
fn() {
|
||||||
|
const fileInfo: FileInfoMock = new FileInfoMock();
|
||||||
|
fileInfo.blocks = null;
|
||||||
|
assert(new Dirent(fileInfo).isCharacterDevice());
|
||||||
|
assert(!new Dirent(fileInfo).isBlockDevice());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test({
|
||||||
|
name: "Directories are correctly identified",
|
||||||
|
fn() {
|
||||||
|
const fileInfo: FileInfoMock = new FileInfoMock();
|
||||||
|
fileInfo.isDirectoryMock = true;
|
||||||
|
fileInfo.isFileMock = false;
|
||||||
|
fileInfo.isSymlinkMock = false;
|
||||||
|
assert(new Dirent(fileInfo).isDirectory());
|
||||||
|
assert(!new Dirent(fileInfo).isFile());
|
||||||
|
assert(!new Dirent(fileInfo).isSymbolicLink());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test({
|
||||||
|
name: "Files are correctly identified",
|
||||||
|
fn() {
|
||||||
|
const fileInfo: FileInfoMock = new FileInfoMock();
|
||||||
|
fileInfo.isDirectoryMock = false;
|
||||||
|
fileInfo.isFileMock = true;
|
||||||
|
fileInfo.isSymlinkMock = false;
|
||||||
|
assert(!new Dirent(fileInfo).isDirectory());
|
||||||
|
assert(new Dirent(fileInfo).isFile());
|
||||||
|
assert(!new Dirent(fileInfo).isSymbolicLink());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test({
|
||||||
|
name: "Symlinks are correctly identified",
|
||||||
|
fn() {
|
||||||
|
const fileInfo: FileInfoMock = new FileInfoMock();
|
||||||
|
fileInfo.isDirectoryMock = false;
|
||||||
|
fileInfo.isFileMock = false;
|
||||||
|
fileInfo.isSymlinkMock = true;
|
||||||
|
assert(!new Dirent(fileInfo).isDirectory());
|
||||||
|
assert(!new Dirent(fileInfo).isFile());
|
||||||
|
assert(new Dirent(fileInfo).isSymbolicLink());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test({
|
||||||
|
name: "File name is correct",
|
||||||
|
fn() {
|
||||||
|
const fileInfo: FileInfoMock = new FileInfoMock();
|
||||||
|
fileInfo.name = "my_file";
|
||||||
|
assertEquals(new Dirent(fileInfo).name, "my_file");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test({
|
||||||
|
name: "Socket and FIFO pipes aren't yet available",
|
||||||
|
fn() {
|
||||||
|
const fileInfo: FileInfoMock = new FileInfoMock();
|
||||||
|
assertThrows(
|
||||||
|
() => {
|
||||||
|
new Dirent(fileInfo).isFIFO();
|
||||||
|
},
|
||||||
|
Error,
|
||||||
|
"does not yet support"
|
||||||
|
);
|
||||||
|
assertThrows(
|
||||||
|
() => {
|
||||||
|
new Dirent(fileInfo).isSocket();
|
||||||
|
},
|
||||||
|
Error,
|
||||||
|
"does not yet support"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
Loading…
Add table
Reference in a new issue