0
0
Fork 0
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:
Chris Knight 2020-03-03 13:56:10 +00:00 committed by GitHub
parent eafd40feab
commit 3968308886
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 434 additions and 0 deletions

114
std/node/_fs_dir.ts Normal file
View 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
View 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
View 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
View 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"
);
}
});