diff options
Diffstat (limited to 'std/node')
-rw-r--r-- | std/node/_fs_dir.ts | 114 | ||||
-rw-r--r-- | std/node/_fs_dir_test.ts | 156 | ||||
-rw-r--r-- | std/node/_fs_dirent.ts | 41 | ||||
-rw-r--r-- | std/node/_fs_dirent_test.ts | 123 |
4 files changed, 434 insertions, 0 deletions
diff --git a/std/node/_fs_dir.ts b/std/node/_fs_dir.ts new file mode 100644 index 000000000..47d0a6511 --- /dev/null +++ b/std/node/_fs_dir.ts @@ -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(); + } + } +} diff --git a/std/node/_fs_dir_test.ts b/std/node/_fs_dir_test.ts new file mode 100644 index 000000000..f2ee814c2 --- /dev/null +++ b/std/node/_fs_dir_test.ts @@ -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 }); + } + } +}); diff --git a/std/node/_fs_dirent.ts b/std/node/_fs_dirent.ts new file mode 100644 index 000000000..74447f404 --- /dev/null +++ b/std/node/_fs_dirent.ts @@ -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; + } +} diff --git a/std/node/_fs_dirent_test.ts b/std/node/_fs_dirent_test.ts new file mode 100644 index 000000000..acbaffc50 --- /dev/null +++ b/std/node/_fs_dirent_test.ts @@ -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" + ); + } +}); |