mirror of
https://github.com/denoland/deno.git
synced 2025-03-03 09:31:22 -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