summaryrefslogtreecommitdiff
path: root/std/node
diff options
context:
space:
mode:
Diffstat (limited to 'std/node')
-rw-r--r--std/node/_fs_dir.ts114
-rw-r--r--std/node/_fs_dir_test.ts156
-rw-r--r--std/node/_fs_dirent.ts41
-rw-r--r--std/node/_fs_dirent_test.ts123
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"
+ );
+ }
+});