From 5bed06fb94214db70a27cca8fa8eff717d537dba Mon Sep 17 00:00:00 2001 From: ali ahmed <48116123+AliBasicCoder@users.noreply.github.com> Date: Wed, 14 Oct 2020 17:59:28 +0200 Subject: [PATCH] feat(std/fs/node): adding some functions (#7921) --- std/node/_fs/_fs_lstat.ts | 59 +++++++ std/node/_fs/_fs_lstat_test.ts | 56 ++++++ std/node/_fs/_fs_open.ts | 103 +++++++++++ std/node/_fs/_fs_open_test.ts | 209 ++++++++++++++++++++++ std/node/_fs/_fs_readdir.ts | 117 +++++++++++++ std/node/_fs/_fs_readdir_test.ts | 71 ++++++++ std/node/_fs/_fs_rename.ts | 23 +++ std/node/_fs/_fs_rename_test.ts | 38 ++++ std/node/_fs/_fs_rmdir.ts | 36 ++++ std/node/_fs/_fs_rmdir_test.ts | 88 ++++++++++ std/node/_fs/_fs_stat.ts | 289 +++++++++++++++++++++++++++++++ std/node/_fs/_fs_stat_test.ts | 107 ++++++++++++ std/node/_fs/_fs_unlink.ts | 10 ++ std/node/_fs/_fs_unlink_test.ts | 30 ++++ std/node/_fs/_fs_watch.ts | 111 ++++++++++++ std/node/_fs/_fs_watch_test.ts | 32 ++++ std/node/fs.ts | 24 +++ 17 files changed, 1403 insertions(+) create mode 100644 std/node/_fs/_fs_lstat.ts create mode 100644 std/node/_fs/_fs_lstat_test.ts create mode 100644 std/node/_fs/_fs_open.ts create mode 100644 std/node/_fs/_fs_open_test.ts create mode 100644 std/node/_fs/_fs_readdir.ts create mode 100644 std/node/_fs/_fs_readdir_test.ts create mode 100644 std/node/_fs/_fs_rename.ts create mode 100644 std/node/_fs/_fs_rename_test.ts create mode 100644 std/node/_fs/_fs_rmdir.ts create mode 100644 std/node/_fs/_fs_rmdir_test.ts create mode 100644 std/node/_fs/_fs_stat.ts create mode 100644 std/node/_fs/_fs_stat_test.ts create mode 100644 std/node/_fs/_fs_unlink.ts create mode 100644 std/node/_fs/_fs_unlink_test.ts create mode 100644 std/node/_fs/_fs_watch.ts create mode 100644 std/node/_fs/_fs_watch_test.ts diff --git a/std/node/_fs/_fs_lstat.ts b/std/node/_fs/_fs_lstat.ts new file mode 100644 index 0000000000..0b79fb665d --- /dev/null +++ b/std/node/_fs/_fs_lstat.ts @@ -0,0 +1,59 @@ +import { + BigIntStats, + CFISBIS, + statCallback, + statCallbackBigInt, + statOptions, + Stats, +} from "./_fs_stat.ts"; + +export function lstat(path: string | URL, callback: statCallback): void; +export function lstat( + path: string | URL, + options: { bigint: false }, + callback: statCallback, +): void; +export function lstat( + path: string | URL, + options: { bigint: true }, + callback: statCallbackBigInt, +): void; +export function lstat( + path: string | URL, + optionsOrCallback: statCallback | statCallbackBigInt | statOptions, + maybeCallback?: statCallback | statCallbackBigInt, +) { + const callback = + (typeof optionsOrCallback === "function" + ? optionsOrCallback + : maybeCallback) as ( + err: Error | undefined, + stat: BigIntStats | Stats, + ) => void; + const options = typeof optionsOrCallback === "object" + ? optionsOrCallback + : { bigint: false }; + + if (!callback) throw new Error("No callback function supplied"); + + Deno.lstat(path) + .then((stat) => callback(undefined, CFISBIS(stat, options.bigint))) + .catch((err) => callback(err, err)); +} + +export function lstatSync(path: string | URL): Stats; +export function lstatSync( + path: string | URL, + options: { bigint: false }, +): Stats; +export function lstatSync( + path: string | URL, + options: { bigint: true }, +): BigIntStats; +export function lstatSync( + path: string | URL, + options?: statOptions, +): Stats | BigIntStats { + const origin = Deno.lstatSync(path); + return CFISBIS(origin, options?.bigint || false); +} diff --git a/std/node/_fs/_fs_lstat_test.ts b/std/node/_fs/_fs_lstat_test.ts new file mode 100644 index 0000000000..1da0a562f4 --- /dev/null +++ b/std/node/_fs/_fs_lstat_test.ts @@ -0,0 +1,56 @@ +import { lstat, lstatSync } from "./_fs_lstat.ts"; +import { fail } from "../../testing/asserts.ts"; +import { assertStats, assertStatsBigInt } from "./_fs_stat_test.ts"; +import type { BigIntStats, Stats } from "./_fs_stat.ts"; + +Deno.test({ + name: "ASYNC: get a file Stats (lstat)", + async fn() { + const file = Deno.makeTempFileSync(); + await new Promise((resolve, reject) => { + lstat(file, (err, stat) => { + if (err) reject(err); + resolve(stat); + }); + }) + .then((stat) => { + assertStats(stat, Deno.lstatSync(file)); + }) + .catch(() => fail()) + .finally(() => { + Deno.removeSync(file); + }); + }, +}); + +Deno.test({ + name: "SYNC: get a file Stats (lstat)", + fn() { + const file = Deno.makeTempFileSync(); + assertStats(lstatSync(file), Deno.lstatSync(file)); + }, +}); + +Deno.test({ + name: "ASYNC: get a file BigInt Stats (lstat)", + async fn() { + const file = Deno.makeTempFileSync(); + await new Promise((resolve, reject) => { + lstat(file, { bigint: true }, (err, stat) => { + if (err) reject(err); + resolve(stat); + }); + }) + .then((stat) => assertStatsBigInt(stat, Deno.lstatSync(file))) + .catch(() => fail()) + .finally(() => Deno.removeSync(file)); + }, +}); + +Deno.test({ + name: "SYNC: BigInt Stats (lstat)", + fn() { + const file = Deno.makeTempFileSync(); + assertStatsBigInt(lstatSync(file, { bigint: true }), Deno.lstatSync(file)); + }, +}); diff --git a/std/node/_fs/_fs_open.ts b/std/node/_fs/_fs_open.ts new file mode 100644 index 0000000000..bf53115de6 --- /dev/null +++ b/std/node/_fs/_fs_open.ts @@ -0,0 +1,103 @@ +import { existsSync } from "../../fs/mod.ts"; +import { fromFileUrl } from "../path.ts"; +import { getOpenOptions } from "./_fs_common.ts"; + +type openFlags = + | "a" + | "ax" + | "a+" + | "ax+" + | "as" + | "as+" + | "r" + | "r+" + | "rs+" + | "w" + | "wx" + | "w+" + | "wx+"; + +type openCallback = (err: Error | undefined, fd: number) => void; + +function convertFlagAndModeToOptions( + flag?: openFlags, + mode?: number, +): Deno.OpenOptions | undefined { + if (!flag && !mode) return undefined; + if (!flag && mode) return { mode }; + return { ...getOpenOptions(flag), mode }; +} + +export function open(path: string | URL, callback: openCallback): void; +export function open( + path: string | URL, + flags: openFlags, + callback: openCallback, +): void; +export function open( + path: string | URL, + flags: openFlags, + mode: number, + callback: openCallback, +): void; +export function open( + path: string | URL, + flagsOrCallback: openCallback | openFlags, + callbackOrMode?: openCallback | number, + maybeCallback?: openCallback, +) { + const flags = typeof flagsOrCallback === "string" + ? flagsOrCallback + : undefined; + const callback = typeof flagsOrCallback === "function" + ? flagsOrCallback + : typeof callbackOrMode === "function" + ? callbackOrMode + : maybeCallback; + const mode = typeof callbackOrMode === "number" ? callbackOrMode : undefined; + path = path instanceof URL ? fromFileUrl(path) : path; + + if (!callback) throw new Error("No callback function supplied"); + + if (["ax", "ax+", "wx", "wx+"].includes(flags || "") && existsSync(path)) { + const err = new Error(`EEXIST: file already exists, open '${path}'`); + callback(err, 0); + } else { + if (flags === "as" || flags === "as+") { + try { + const res = openSync(path, flags, mode); + callback(undefined, res); + } catch (error) { + callback(error, error); + } + return; + } + Deno.open(path, convertFlagAndModeToOptions(flags, mode)) + .then((file) => callback(undefined, file.rid)) + .catch((err) => callback(err, err)); + } +} + +export function openSync(path: string | URL): number; +export function openSync(path: string | URL, flags?: openFlags): number; +export function openSync(path: string | URL, mode?: number): number; +export function openSync( + path: string | URL, + flags?: openFlags, + mode?: number, +): number; +export function openSync( + path: string | URL, + flagsOrMode?: openFlags | number, + maybeMode?: number, +) { + const flags = typeof flagsOrMode === "string" ? flagsOrMode : undefined; + const mode = typeof flagsOrMode === "number" ? flagsOrMode : maybeMode; + path = path instanceof URL ? fromFileUrl(path) : path; + + if (["ax", "ax+", "wx", "wx+"].includes(flags || "") && existsSync(path)) { + throw new Error(`EEXIST: file already exists, open '${path}'`); + } + + return Deno.openSync(path, convertFlagAndModeToOptions(flags, mode)).rid; +} diff --git a/std/node/_fs/_fs_open_test.ts b/std/node/_fs/_fs_open_test.ts new file mode 100644 index 0000000000..4cbd58d023 --- /dev/null +++ b/std/node/_fs/_fs_open_test.ts @@ -0,0 +1,209 @@ +import { + assert, + assertEquals, + assertThrows, + fail, +} from "../../testing/asserts.ts"; +import { open, openSync } from "./_fs_open.ts"; +import { join, parse } from "../../path/mod.ts"; +import { existsSync } from "../../fs/mod.ts"; +import { closeSync } from "./_fs_close.ts"; + +const temp_dir = parse(Deno.makeTempFileSync()).dir; + +Deno.test({ + name: "ASYNC: open file", + async fn() { + const file = Deno.makeTempFileSync(); + let fd1: number; + await new Promise((resolve, reject) => { + open(file, (err, fd) => { + if (err) reject(err); + resolve(fd); + }); + }) + .then((fd) => { + fd1 = fd; + assert(Deno.resources()[fd], `${fd}`); + }) + .catch(() => fail()) + .finally(() => closeSync(fd1)); + }, +}); + +Deno.test({ + name: "SYNC: open file", + fn() { + const file = Deno.makeTempFileSync(); + const fd = openSync(file); + assert(Deno.resources()[fd]); + closeSync(fd); + }, +}); + +Deno.test({ + name: "open with flag 'a'", + fn() { + const file = join(temp_dir, "some_random_file"); + const fd = openSync(file, "a"); + assertEquals(typeof fd, "number"); + assertEquals(existsSync(file), true); + assert(Deno.resources()[fd]); + closeSync(fd); + }, +}); + +Deno.test({ + name: "open with flag 'ax'", + fn() { + const file = Deno.makeTempFileSync(); + assertThrows( + () => { + openSync(file, "ax"); + }, + Error, + `EEXIST: file already exists, open '${file}'`, + ); + Deno.removeSync(file); + }, +}); + +Deno.test({ + name: "open with flag 'a+'", + fn() { + const file = join(temp_dir, "some_random_file2"); + const fd = openSync(file, "a+"); + assertEquals(typeof fd, "number"); + assertEquals(existsSync(file), true); + closeSync(fd); + }, +}); + +Deno.test({ + name: "open with flag 'ax+'", + fn() { + const file = Deno.makeTempFileSync(); + assertThrows( + () => { + openSync(file, "ax+"); + }, + Error, + `EEXIST: file already exists, open '${file}'`, + ); + Deno.removeSync(file); + }, +}); + +Deno.test({ + name: "open with flag 'as'", + fn() { + const file = join(temp_dir, "some_random_file10"); + const fd = openSync(file, "as"); + assertEquals(existsSync(file), true); + assertEquals(typeof fd, "number"); + closeSync(fd); + }, +}); + +Deno.test({ + name: "open with flag 'as+'", + fn() { + const file = join(temp_dir, "some_random_file10"); + const fd = openSync(file, "as+"); + assertEquals(existsSync(file), true); + assertEquals(typeof fd, "number"); + closeSync(fd); + }, +}); + +Deno.test({ + name: "open with flag 'r'", + fn() { + const file = join(temp_dir, "some_random_file3"); + assertThrows(() => { + openSync(file, "r"); + }, Error); + }, +}); + +Deno.test({ + name: "open with flag 'r+'", + fn() { + const file = join(temp_dir, "some_random_file4"); + assertThrows(() => { + openSync(file, "r+"); + }, Error); + }, +}); + +Deno.test({ + name: "open with flag 'w'", + fn() { + const file = Deno.makeTempFileSync(); + Deno.writeTextFileSync(file, "hi there"); + const fd = openSync(file, "w"); + assertEquals(typeof fd, "number"); + assertEquals(Deno.readTextFileSync(file), ""); + closeSync(fd); + + const file2 = join(temp_dir, "some_random_file5"); + const fd2 = openSync(file2, "w"); + assertEquals(typeof fd2, "number"); + assertEquals(existsSync(file2), true); + closeSync(fd2); + }, +}); + +Deno.test({ + name: "open with flag 'wx'", + fn() { + const file = Deno.makeTempFileSync(); + Deno.writeTextFileSync(file, "hi there"); + const fd = openSync(file, "w"); + assertEquals(typeof fd, "number"); + assertEquals(Deno.readTextFileSync(file), ""); + closeSync(fd); + + const file2 = Deno.makeTempFileSync(); + assertThrows( + () => { + openSync(file2, "wx"); + }, + Error, + `EEXIST: file already exists, open '${file2}'`, + ); + }, +}); + +Deno.test({ + name: "open with flag 'w+'", + fn() { + const file = Deno.makeTempFileSync(); + Deno.writeTextFileSync(file, "hi there"); + const fd = openSync(file, "w+"); + assertEquals(typeof fd, "number"); + assertEquals(Deno.readTextFileSync(file), ""); + closeSync(fd); + + const file2 = join(temp_dir, "some_random_file6"); + const fd2 = openSync(file2, "w+"); + assertEquals(typeof fd2, "number"); + assertEquals(existsSync(file2), true); + closeSync(fd2); + }, +}); + +Deno.test({ + name: "open with flag 'wx+'", + fn() { + const file = Deno.makeTempFileSync(); + assertThrows( + () => { + openSync(file, "wx+"); + }, + Error, + `EEXIST: file already exists, open '${file}'`, + ); + Deno.removeSync(file); + }, +}); diff --git a/std/node/_fs/_fs_readdir.ts b/std/node/_fs/_fs_readdir.ts new file mode 100644 index 0000000000..9034eccf82 --- /dev/null +++ b/std/node/_fs/_fs_readdir.ts @@ -0,0 +1,117 @@ +import { asyncIterableToCallback } from "./_fs_watch.ts"; +import Dirent from "./_fs_dirent.ts"; +import { fromFileUrl } from "../path.ts"; + +function toDirent(val: Deno.DirEntry): Dirent { + return new Dirent(val); +} + +type readDirOptions = { + encoding?: string; + withFileTypes?: boolean; +}; + +type readDirCallback = (err: Error | undefined, files: string[]) => void; + +type readDirCallbackDirent = (err: Error | undefined, files: Dirent[]) => void; + +type readDirBoth = ( + err: Error | undefined, + files: string[] | Dirent[] | Array, +) => void; + +export function readdir( + path: string | URL, + options: { withFileTypes?: false; encoding?: string }, + callback: readDirCallback, +): void; +export function readdir( + path: string | URL, + options: { withFileTypes: true; encoding?: string }, + callback: readDirCallbackDirent, +): void; +export function readdir(path: string | URL, callback: readDirCallback): void; +export function readdir( + path: string | URL, + optionsOrCallback: readDirOptions | readDirCallback | readDirCallbackDirent, + maybeCallback?: readDirCallback | readDirCallbackDirent, +) { + const callback = + (typeof optionsOrCallback === "function" + ? optionsOrCallback + : maybeCallback) as readDirBoth | undefined; + const options = typeof optionsOrCallback === "object" + ? optionsOrCallback + : null; + const result: Array = []; + path = path instanceof URL ? fromFileUrl(path) : path; + + if (!callback) throw new Error("No callback function supplied"); + + if (options?.encoding) { + try { + new TextDecoder(options.encoding); + } catch (error) { + throw new Error( + `TypeError [ERR_INVALID_OPT_VALUE_ENCODING]: The value "${options.encoding}" is invalid for option "encoding"`, + ); + } + } + + try { + asyncIterableToCallback(Deno.readDir(path), (val, done) => { + if (typeof path !== "string") return; + if (done) { + callback(undefined, result); + return; + } + if (options?.withFileTypes) { + result.push(toDirent(val)); + } else result.push(decode(val.name)); + }); + } catch (error) { + callback(error, result); + } +} + +function decode(str: string, encoding?: string): string { + if (!encoding) return str; + else { + const decoder = new TextDecoder(encoding); + const encoder = new TextEncoder(); + return decoder.decode(encoder.encode(str)); + } +} + +export function readdirSync( + path: string | URL, + options: { withFileTypes: true; encoding?: string }, +): Dirent[]; +export function readdirSync( + path: string | URL, + options?: { withFileTypes?: false; encoding?: string }, +): string[]; +export function readdirSync( + path: string | URL, + options?: readDirOptions, +): Array { + const result = []; + path = path instanceof URL ? fromFileUrl(path) : path; + + if (options?.encoding) { + try { + new TextDecoder(options.encoding); + } catch (error) { + throw new Error( + `TypeError [ERR_INVALID_OPT_VALUE_ENCODING]: The value "${options.encoding}" is invalid for option "encoding"`, + ); + } + } + + for (const file of Deno.readDirSync(path)) { + if (options?.withFileTypes) { + result.push(toDirent(file)); + } else result.push(decode(file.name)); + } + return result; +} diff --git a/std/node/_fs/_fs_readdir_test.ts b/std/node/_fs/_fs_readdir_test.ts new file mode 100644 index 0000000000..169e102455 --- /dev/null +++ b/std/node/_fs/_fs_readdir_test.ts @@ -0,0 +1,71 @@ +import { assertEquals, assertNotEquals, fail } from "../../testing/asserts.ts"; +import { readdir, readdirSync } from "./_fs_readdir.ts"; +import { join } from "../../path/mod.ts"; + +Deno.test({ + name: "ASYNC: reading empty directory", + async fn() { + const dir = Deno.makeTempDirSync(); + await new Promise((resolve, reject) => { + readdir(dir, (err, files) => { + if (err) reject(err); + resolve(files); + }); + }) + .then((files) => assertEquals(files, [])) + .catch(() => fail()) + .finally(() => Deno.removeSync(dir)); + }, +}); + +function assertEqualsArrayAnyOrder(actual: T[], expected: T[]) { + assertEquals(actual.length, expected.length); + for (const item of expected) { + const index = actual.indexOf(item); + assertNotEquals(index, -1); + expected = expected.splice(index, 1); + } +} + +Deno.test({ + name: "ASYNC: reading non-empty directory", + async fn() { + const dir = Deno.makeTempDirSync(); + Deno.writeTextFileSync(join(dir, "file1.txt"), "hi"); + Deno.writeTextFileSync(join(dir, "file2.txt"), "hi"); + Deno.mkdirSync(join(dir, "some_dir")); + await new Promise((resolve, reject) => { + readdir(dir, (err, files) => { + if (err) reject(err); + resolve(files); + }); + }) + .then((files) => + assertEqualsArrayAnyOrder(files, ["file1.txt", "some_dir", "file2.txt"]) + ) + .catch(() => fail()) + .finally(() => Deno.removeSync(dir, { recursive: true })); + }, +}); + +Deno.test({ + name: "SYNC: reading empty the directory", + fn() { + const dir = Deno.makeTempDirSync(); + assertEquals(readdirSync(dir), []); + }, +}); + +Deno.test({ + name: "SYNC: reading non-empty directory", + fn() { + const dir = Deno.makeTempDirSync(); + Deno.writeTextFileSync(join(dir, "file1.txt"), "hi"); + Deno.writeTextFileSync(join(dir, "file2.txt"), "hi"); + Deno.mkdirSync(join(dir, "some_dir")); + assertEqualsArrayAnyOrder( + readdirSync(dir), + ["file1.txt", "some_dir", "file2.txt"], + ); + }, +}); diff --git a/std/node/_fs/_fs_rename.ts b/std/node/_fs/_fs_rename.ts new file mode 100644 index 0000000000..ee7c009770 --- /dev/null +++ b/std/node/_fs/_fs_rename.ts @@ -0,0 +1,23 @@ +import { fromFileUrl } from "../path.ts"; + +export function rename( + oldPath: string | URL, + newPath: string | URL, + callback: (err?: Error) => void, +) { + oldPath = oldPath instanceof URL ? fromFileUrl(oldPath) : oldPath; + newPath = newPath instanceof URL ? fromFileUrl(newPath) : newPath; + + if (!callback) throw new Error("No callback function supplied"); + + Deno.rename(oldPath, newPath) + .then((_) => callback()) + .catch(callback); +} + +export function renameSync(oldPath: string | URL, newPath: string | URL) { + oldPath = oldPath instanceof URL ? fromFileUrl(oldPath) : oldPath; + newPath = newPath instanceof URL ? fromFileUrl(newPath) : newPath; + + Deno.renameSync(oldPath, newPath); +} diff --git a/std/node/_fs/_fs_rename_test.ts b/std/node/_fs/_fs_rename_test.ts new file mode 100644 index 0000000000..d0084d0d12 --- /dev/null +++ b/std/node/_fs/_fs_rename_test.ts @@ -0,0 +1,38 @@ +import { assertEquals, fail } from "../../testing/asserts.ts"; +import { rename, renameSync } from "./_fs_rename.ts"; +import { existsSync } from "../../fs/mod.ts"; +import { join, parse } from "../../path/mod.ts"; + +Deno.test({ + name: "ASYNC: renaming a file", + async fn() { + const file = Deno.makeTempFileSync(); + const newPath = join(parse(file).dir, `${parse(file).base}_renamed`); + await new Promise((resolve, reject) => { + rename(file, newPath, (err) => { + if (err) reject(err); + resolve(); + }); + }) + .then(() => { + assertEquals(existsSync(newPath), true); + assertEquals(existsSync(file), false); + }) + .catch(() => fail()) + .finally(() => { + if (existsSync(file)) Deno.removeSync(file); + if (existsSync(newPath)) Deno.removeSync(newPath); + }); + }, +}); + +Deno.test({ + name: "SYNC: renaming a file", + fn() { + const file = Deno.makeTempFileSync(); + const newPath = join(parse(file).dir, `${parse(file).base}_renamed`); + renameSync(file, newPath); + assertEquals(existsSync(newPath), true); + assertEquals(existsSync(file), false); + }, +}); diff --git a/std/node/_fs/_fs_rmdir.ts b/std/node/_fs/_fs_rmdir.ts new file mode 100644 index 0000000000..2035a1e717 --- /dev/null +++ b/std/node/_fs/_fs_rmdir.ts @@ -0,0 +1,36 @@ +type rmdirOptions = { + maxRetries?: number; + recursive?: boolean; + retryDelay?: number; +}; + +type rmdirCallback = (err?: Error) => void; + +export function rmdir(path: string | URL, callback: rmdirCallback): void; +export function rmdir( + path: string | URL, + options: rmdirOptions, + callback: rmdirCallback, +): void; +export function rmdir( + path: string | URL, + optionsOrCallback: rmdirOptions | rmdirCallback, + maybeCallback?: rmdirCallback, +) { + const callback = typeof optionsOrCallback === "function" + ? optionsOrCallback + : maybeCallback; + const options = typeof optionsOrCallback === "object" + ? optionsOrCallback + : undefined; + + if (!callback) throw new Error("No callback function supplied"); + + Deno.remove(path, { recursive: options?.recursive }) + .then((_) => callback()) + .catch(callback); +} + +export function rmdirSync(path: string | URL, options?: rmdirOptions) { + Deno.removeSync(path, { recursive: options?.recursive }); +} diff --git a/std/node/_fs/_fs_rmdir_test.ts b/std/node/_fs/_fs_rmdir_test.ts new file mode 100644 index 0000000000..884d4912ac --- /dev/null +++ b/std/node/_fs/_fs_rmdir_test.ts @@ -0,0 +1,88 @@ +import { assertEquals, fail } from "../../testing/asserts.ts"; +import { rmdir, rmdirSync } from "./_fs_rmdir.ts"; +import { closeSync } from "./_fs_close.ts"; +import { existsSync } from "../../fs/mod.ts"; +import { join } from "../../path/mod.ts"; + +Deno.test({ + name: "ASYNC: removing empty folder", + async fn() { + const dir = Deno.makeTempDirSync(); + await new Promise((resolve, reject) => { + rmdir(dir, (err) => { + if (err) reject(err); + resolve(); + }); + }) + .then(() => assertEquals(existsSync(dir), false)) + .catch(() => fail()) + .finally(() => { + if (existsSync(dir)) Deno.removeSync(dir); + }); + }, +}); + +Deno.test({ + name: "SYNC: removing empty folder", + fn() { + const dir = Deno.makeTempDirSync(); + rmdirSync(dir); + assertEquals(existsSync(dir), false); + }, +}); + +function closeRes(before: Deno.ResourceMap, after: Deno.ResourceMap) { + for (const key in after) { + if (!before[key]) { + try { + closeSync(Number(key)); + } catch (error) { + return error; + } + } + } +} + +Deno.test({ + name: "ASYNC: removing non-empty folder", + async fn() { + const rBefore = Deno.resources(); + const dir = Deno.makeTempDirSync(); + Deno.createSync(join(dir, "file1.txt")); + Deno.createSync(join(dir, "file2.txt")); + Deno.mkdirSync(join(dir, "some_dir")); + Deno.createSync(join(dir, "some_dir", "file.txt")); + await new Promise((resolve, reject) => { + rmdir(dir, { recursive: true }, (err) => { + if (err) reject(err); + resolve(); + }); + }) + .then(() => assertEquals(existsSync(dir), false)) + .catch(() => fail()) + .finally(() => { + if (existsSync(dir)) Deno.removeSync(dir, { recursive: true }); + const rAfter = Deno.resources(); + closeRes(rBefore, rAfter); + }); + }, + ignore: Deno.build.os === "windows", +}); + +Deno.test({ + name: "SYNC: removing non-empty folder", + fn() { + const rBefore = Deno.resources(); + const dir = Deno.makeTempDirSync(); + Deno.createSync(join(dir, "file1.txt")); + Deno.createSync(join(dir, "file2.txt")); + Deno.mkdirSync(join(dir, "some_dir")); + Deno.createSync(join(dir, "some_dir", "file.txt")); + rmdirSync(dir, { recursive: true }); + assertEquals(existsSync(dir), false); + // closing resources + const rAfter = Deno.resources(); + closeRes(rBefore, rAfter); + }, + ignore: Deno.build.os === "windows", +}); diff --git a/std/node/_fs/_fs_stat.ts b/std/node/_fs/_fs_stat.ts new file mode 100644 index 0000000000..d823f7ddbe --- /dev/null +++ b/std/node/_fs/_fs_stat.ts @@ -0,0 +1,289 @@ +export type statOptions = { + bigint: boolean; +}; + +export type Stats = { + /** ID of the device containing the file. + * + * _Linux/Mac OS only._ */ + dev: number | null; + /** Inode number. + * + * _Linux/Mac OS only._ */ + ino: number | null; + /** **UNSTABLE**: Match behavior with Go on Windows for `mode`. + * + * The underlying raw `st_mode` bits that contain the standard Unix + * permissions for this file/directory. */ + mode: number | null; + /** Number of hard links pointing to this file. + * + * _Linux/Mac OS only._ */ + nlink: number | null; + /** User ID of the owner of this file. + * + * _Linux/Mac OS only._ */ + uid: number | null; + /** Group ID of the owner of this file. + * + * _Linux/Mac OS only._ */ + gid: number | null; + /** Device ID of this file. + * + * _Linux/Mac OS only._ */ + rdev: number | null; + /** The size of the file, in bytes. */ + size: number; + /** Blocksize for filesystem I/O. + * + * _Linux/Mac OS only._ */ + blksize: number | null; + /** Number of blocks allocated to the file, in 512-byte units. + * + * _Linux/Mac OS only._ */ + blocks: number | null; + /** The last modification time of the file. This corresponds to the `mtime` + * field from `stat` on Linux/Mac OS and `ftLastWriteTime` on Windows. This + * may not be available on all platforms. */ + mtime: Date | null; + /** The last access time of the file. This corresponds to the `atime` + * field from `stat` on Unix and `ftLastAccessTime` on Windows. This may not + * be available on all platforms. */ + atime: Date | null; + /** The creation time of the file. This corresponds to the `birthtime` + * field from `stat` on Mac/BSD and `ftCreationTime` on Windows. This may + * not be available on all platforms. */ + birthtime: Date | null; + /** change time */ + ctime: Date | null; + /** atime in milliseconds */ + atimeMs: number | null; + /** atime in milliseconds */ + mtimeMs: number | null; + /** atime in milliseconds */ + ctimeMs: number | null; + /** atime in milliseconds */ + birthtimeMs: number | null; + isBlockDevice: () => boolean; + isCharacterDevice: () => boolean; + isDirectory: () => boolean; + isFIFO: () => boolean; + isFile: () => boolean; + isSocket: () => boolean; + isSymbolicLink: () => boolean; +}; + +export type BigIntStats = { + /** ID of the device containing the file. + * + * _Linux/Mac OS only._ */ + dev: BigInt | null; + /** Inode number. + * + * _Linux/Mac OS only._ */ + ino: BigInt | null; + /** **UNSTABLE**: Match behavior with Go on Windows for `mode`. + * + * The underlying raw `st_mode` bits that contain the standard Unix + * permissions for this file/directory. */ + mode: BigInt | null; + /** Number of hard links pointing to this file. + * + * _Linux/Mac OS only._ */ + nlink: BigInt | null; + /** User ID of the owner of this file. + * + * _Linux/Mac OS only._ */ + uid: BigInt | null; + /** Group ID of the owner of this file. + * + * _Linux/Mac OS only._ */ + gid: BigInt | null; + /** Device ID of this file. + * + * _Linux/Mac OS only._ */ + rdev: BigInt | null; + /** The size of the file, in bytes. */ + size: BigInt; + /** Blocksize for filesystem I/O. + * + * _Linux/Mac OS only._ */ + blksize: BigInt | null; + /** Number of blocks allocated to the file, in 512-byte units. + * + * _Linux/Mac OS only._ */ + blocks: BigInt | null; + /** The last modification time of the file. This corresponds to the `mtime` + * field from `stat` on Linux/Mac OS and `ftLastWriteTime` on Windows. This + * may not be available on all platforms. */ + mtime: Date | null; + /** The last access time of the file. This corresponds to the `atime` + * field from `stat` on Unix and `ftLastAccessTime` on Windows. This may not + * be available on all platforms. */ + atime: Date | null; + /** The creation time of the file. This corresponds to the `birthtime` + * field from `stat` on Mac/BSD and `ftCreationTime` on Windows. This may + * not be available on all platforms. */ + birthtime: Date | null; + /** change time */ + ctime: Date | null; + /** atime in milliseconds */ + atimeMs: BigInt | null; + /** atime in milliseconds */ + mtimeMs: BigInt | null; + /** atime in milliseconds */ + ctimeMs: BigInt | null; + /** atime in nanoseconds */ + birthtimeMs: BigInt | null; + /** atime in nanoseconds */ + atimeNs: BigInt | null; + /** atime in nanoseconds */ + mtimeNs: BigInt | null; + /** atime in nanoseconds */ + ctimeNs: BigInt | null; + /** atime in nanoseconds */ + birthtimeNs: BigInt | null; + isBlockDevice: () => boolean; + isCharacterDevice: () => boolean; + isDirectory: () => boolean; + isFIFO: () => boolean; + isFile: () => boolean; + isSocket: () => boolean; + isSymbolicLink: () => boolean; +}; + +export function convertFileInfoToStats(origin: Deno.FileInfo): Stats { + return { + dev: origin.dev, + ino: origin.ino, + mode: origin.mode, + nlink: origin.nlink, + uid: origin.uid, + gid: origin.gid, + rdev: origin.rdev, + size: origin.size, + blksize: origin.blksize, + blocks: origin.blocks, + mtime: origin.mtime, + atime: origin.atime, + birthtime: origin.birthtime, + mtimeMs: origin.mtime?.getTime() || null, + atimeMs: origin.atime?.getTime() || null, + birthtimeMs: origin.birthtime?.getTime() || null, + isFile: () => origin.isFile, + isDirectory: () => origin.isDirectory, + isSymbolicLink: () => origin.isSymlink, + // not sure about those + isBlockDevice: () => false, + isFIFO: () => false, + isCharacterDevice: () => false, + isSocket: () => false, + ctime: origin.mtime, + ctimeMs: origin.mtime?.getTime() || null, + }; +} + +function to_BigInt(number?: number | null) { + if (number === null || number === undefined) return null; + return BigInt(number); +} + +export function convertFileInfoToBigIntStats( + origin: Deno.FileInfo, +): BigIntStats { + return { + dev: to_BigInt(origin.dev), + ino: to_BigInt(origin.ino), + mode: to_BigInt(origin.mode), + nlink: to_BigInt(origin.nlink), + uid: to_BigInt(origin.uid), + gid: to_BigInt(origin.gid), + rdev: to_BigInt(origin.rdev), + size: to_BigInt(origin.size) || 0n, + blksize: to_BigInt(origin.blksize), + blocks: to_BigInt(origin.blocks), + mtime: origin.mtime, + atime: origin.atime, + birthtime: origin.birthtime, + mtimeMs: origin.mtime ? BigInt(origin.mtime.getTime()) : null, + atimeMs: origin.atime ? BigInt(origin.atime.getTime()) : null, + birthtimeMs: origin.birthtime ? BigInt(origin.birthtime.getTime()) : null, + mtimeNs: origin.mtime ? BigInt(origin.mtime.getTime()) * 1000000n : null, + atimeNs: origin.atime ? BigInt(origin.atime.getTime()) * 1000000n : null, + birthtimeNs: origin.birthtime + ? BigInt(origin.birthtime.getTime()) * 1000000n + : null, + isFile: () => origin.isFile, + isDirectory: () => origin.isDirectory, + isSymbolicLink: () => origin.isSymlink, + // not sure about those + isBlockDevice: () => false, + isFIFO: () => false, + isCharacterDevice: () => false, + isSocket: () => false, + ctime: origin.mtime, + ctimeMs: origin.mtime ? BigInt(origin.mtime.getTime()) : null, + ctimeNs: origin.mtime ? BigInt(origin.mtime.getTime()) * 1000000n : null, + }; +} + +// shortcut for Convert File Info to Stats or BigIntStats +export function CFISBIS(fileInfo: Deno.FileInfo, bigInt: boolean) { + if (bigInt) return convertFileInfoToBigIntStats(fileInfo); + return convertFileInfoToStats(fileInfo); +} + +export type statCallbackBigInt = ( + err: Error | undefined, + stat: BigIntStats, +) => void; + +export type statCallback = (err: Error | undefined, stat: Stats) => void; + +export function stat(path: string | URL, callback: statCallback): void; +export function stat( + path: string | URL, + options: { bigint: false }, + callback: statCallback, +): void; +export function stat( + path: string | URL, + options: { bigint: true }, + callback: statCallbackBigInt, +): void; +export function stat( + path: string | URL, + optionsOrCallback: statCallback | statCallbackBigInt | statOptions, + maybeCallback?: statCallback | statCallbackBigInt, +) { + const callback = + (typeof optionsOrCallback === "function" + ? optionsOrCallback + : maybeCallback) as ( + err: Error | undefined, + stat: BigIntStats | Stats, + ) => void; + const options = typeof optionsOrCallback === "object" + ? optionsOrCallback + : { bigint: false }; + + if (!callback) throw new Error("No callback function supplied"); + + Deno.stat(path) + .then((stat) => callback(undefined, CFISBIS(stat, options.bigint))) + .catch((err) => callback(err, err)); +} + +export function statSync(path: string | URL): Stats; +export function statSync(path: string | URL, options: { bigint: false }): Stats; +export function statSync( + path: string | URL, + options: { bigint: true }, +): BigIntStats; +export function statSync( + path: string | URL, + options: statOptions = { bigint: false }, +): Stats | BigIntStats { + const origin = Deno.statSync(path); + return CFISBIS(origin, options.bigint); +} diff --git a/std/node/_fs/_fs_stat_test.ts b/std/node/_fs/_fs_stat_test.ts new file mode 100644 index 0000000000..925a79be26 --- /dev/null +++ b/std/node/_fs/_fs_stat_test.ts @@ -0,0 +1,107 @@ +import { BigIntStats, stat, Stats, statSync } from "./_fs_stat.ts"; +import { assertEquals, fail } from "../../testing/asserts.ts"; + +export function assertStats(actual: Stats, expected: Deno.FileInfo) { + assertEquals(actual.dev, expected.dev); + assertEquals(actual.gid, expected.gid); + assertEquals(actual.size, expected.size); + assertEquals(actual.blksize, expected.blksize); + assertEquals(actual.blocks, expected.blocks); + assertEquals(actual.ino, expected.ino); + assertEquals(actual.gid, expected.gid); + assertEquals(actual.mode, expected.mode); + assertEquals(actual.nlink, expected.nlink); + assertEquals(actual.rdev, expected.rdev); + assertEquals(actual.uid, expected.uid); + assertEquals(actual.atime?.getTime(), expected.atime?.getTime()); + assertEquals(actual.mtime?.getTime(), expected.mtime?.getTime()); + assertEquals(actual.birthtime?.getTime(), expected.birthtime?.getTime()); + assertEquals(actual.atimeMs, expected.atime?.getTime()); + assertEquals(actual.mtimeMs, expected.mtime?.getTime()); + assertEquals(actual.birthtimeMs, expected.birthtime?.getTime()); + assertEquals(actual.isFile(), expected.isFile); + assertEquals(actual.isDirectory(), expected.isDirectory); + assertEquals(actual.isSymbolicLink(), expected.isSymlink); +} + +function to_BigInt(num?: number | null) { + if (num === undefined || num === null) return null; + return BigInt(num); +} + +export function assertStatsBigInt( + actual: BigIntStats, + expected: Deno.FileInfo, +) { + assertEquals(actual.dev, to_BigInt(expected.dev)); + assertEquals(actual.gid, to_BigInt(expected.gid)); + assertEquals(actual.size, to_BigInt(expected.size)); + assertEquals(actual.blksize, to_BigInt(expected.blksize)); + assertEquals(actual.blocks, to_BigInt(expected.blocks)); + assertEquals(actual.ino, to_BigInt(expected.ino)); + assertEquals(actual.gid, to_BigInt(expected.gid)); + assertEquals(actual.mode, to_BigInt(expected.mode)); + assertEquals(actual.nlink, to_BigInt(expected.nlink)); + assertEquals(actual.rdev, to_BigInt(expected.rdev)); + assertEquals(actual.uid, to_BigInt(expected.uid)); + assertEquals(actual.atime?.getTime(), expected.atime?.getTime()); + assertEquals(actual.mtime?.getTime(), expected.mtime?.getTime()); + assertEquals(actual.birthtime?.getTime(), expected.birthtime?.getTime()); + assertEquals(Number(actual.atimeMs), expected.atime?.getTime()); + assertEquals(Number(actual.mtimeMs), expected.mtime?.getTime()); + assertEquals(Number(actual.birthtimeMs), expected.birthtime?.getTime()); + assertEquals(actual.atimeNs === null, actual.atime === null); + assertEquals(actual.mtimeNs === null, actual.mtime === null); + assertEquals(actual.birthtimeNs === null, actual.birthtime === null); + assertEquals(actual.isFile(), expected.isFile); + assertEquals(actual.isDirectory(), expected.isDirectory); + assertEquals(actual.isSymbolicLink(), expected.isSymlink); +} + +Deno.test({ + name: "ASYNC: get a file Stats", + async fn() { + const file = Deno.makeTempFileSync(); + await new Promise((resolve, reject) => { + stat(file, (err, stat) => { + if (err) reject(err); + resolve(stat); + }); + }) + .then((stat) => assertStats(stat, Deno.statSync(file))) + .catch(() => fail()) + .finally(() => Deno.removeSync(file)); + }, +}); + +Deno.test({ + name: "SYNC: get a file Stats", + fn() { + const file = Deno.makeTempFileSync(); + assertStats(statSync(file), Deno.statSync(file)); + }, +}); + +Deno.test({ + name: "ASYNC: get a file BigInt Stats", + async fn() { + const file = Deno.makeTempFileSync(); + await new Promise((resolve, reject) => { + stat(file, { bigint: true }, (err, stat) => { + if (err) reject(err); + resolve(stat); + }); + }) + .then((stat) => assertStatsBigInt(stat, Deno.statSync(file))) + .catch(() => fail()) + .finally(() => Deno.removeSync(file)); + }, +}); + +Deno.test({ + name: "SYNC: get a file BigInt Stats", + fn() { + const file = Deno.makeTempFileSync(); + assertStatsBigInt(statSync(file, { bigint: true }), Deno.statSync(file)); + }, +}); diff --git a/std/node/_fs/_fs_unlink.ts b/std/node/_fs/_fs_unlink.ts new file mode 100644 index 0000000000..aba734fe13 --- /dev/null +++ b/std/node/_fs/_fs_unlink.ts @@ -0,0 +1,10 @@ +export function unlink(path: string | URL, callback: (err?: Error) => void) { + if (!callback) throw new Error("No callback function supplied"); + Deno.remove(path) + .then((_) => callback()) + .catch(callback); +} + +export function unlinkSync(path: string | URL) { + Deno.removeSync(path); +} diff --git a/std/node/_fs/_fs_unlink_test.ts b/std/node/_fs/_fs_unlink_test.ts new file mode 100644 index 0000000000..922a1a703b --- /dev/null +++ b/std/node/_fs/_fs_unlink_test.ts @@ -0,0 +1,30 @@ +import { assertEquals, fail } from "../../testing/asserts.ts"; +import { existsSync } from "../../fs/mod.ts"; +import { unlink, unlinkSync } from "./_fs_unlink.ts"; + +Deno.test({ + name: "ASYNC: deleting a file", + async fn() { + const file = Deno.makeTempFileSync(); + await new Promise((resolve, reject) => { + unlink(file, (err) => { + if (err) reject(err); + resolve(); + }); + }) + .then(() => assertEquals(existsSync(file), false)) + .catch(() => fail()) + .finally(() => { + if (existsSync(file)) Deno.removeSync(file); + }); + }, +}); + +Deno.test({ + name: "SYNC: Test deleting a file", + fn() { + const file = Deno.makeTempFileSync(); + unlinkSync(file); + assertEquals(existsSync(file), false); + }, +}); diff --git a/std/node/_fs/_fs_watch.ts b/std/node/_fs/_fs_watch.ts new file mode 100644 index 0000000000..a5f3bb9c17 --- /dev/null +++ b/std/node/_fs/_fs_watch.ts @@ -0,0 +1,111 @@ +import { fromFileUrl } from "../path.ts"; +import { EventEmitter } from "../events.ts"; +import { notImplemented } from "../_utils.ts"; + +export function asyncIterableIteratorToCallback( + iterator: AsyncIterableIterator, + callback: (val: T, done?: boolean) => void, +) { + function next() { + iterator.next().then((obj) => { + if (obj.done) { + callback(obj.value, true); + return; + } + callback(obj.value); + next(); + }); + } + next(); +} + +export function asyncIterableToCallback( + iter: AsyncIterable, + callback: (val: T, done?: boolean) => void, +) { + const iterator = iter[Symbol.asyncIterator](); + function next() { + iterator.next().then((obj) => { + if (obj.done) { + callback(obj.value, true); + return; + } + callback(obj.value); + next(); + }); + } + next(); +} + +type watchOptions = { + persistent?: boolean; + recursive?: boolean; + encoding?: string; +}; + +type watchListener = (eventType: string, filename: string) => void; + +export function watch( + filename: string | URL, + options: watchOptions, + listener: watchListener, +): FSWatcher; +export function watch( + filename: string | URL, + listener: watchListener, +): FSWatcher; +export function watch( + filename: string | URL, + options: watchOptions, +): FSWatcher; +export function watch(filename: string | URL): FSWatcher; +export function watch( + filename: string | URL, + optionsOrListener?: watchOptions | watchListener, + optionsOrListener2?: watchOptions | watchListener, +) { + const listener = typeof optionsOrListener === "function" + ? optionsOrListener + : typeof optionsOrListener2 === "function" + ? optionsOrListener2 + : undefined; + const options = typeof optionsOrListener === "object" + ? optionsOrListener + : typeof optionsOrListener2 === "object" + ? optionsOrListener2 + : undefined; + filename = filename instanceof URL ? fromFileUrl(filename) : filename; + + const iterator = Deno.watchFs(filename, { + recursive: options?.recursive || false, + }); + + if (!listener) throw new Error("No callback function supplied"); + + const fsWatcher = new FSWatcher(() => { + if (iterator.return) iterator.return(); + }); + + fsWatcher.on("change", listener); + + asyncIterableIteratorToCallback(iterator, (val, done) => { + if (done) return; + fsWatcher.emit("change", val.kind, val.paths[0]); + }); + + return fsWatcher; +} + +class FSWatcher extends EventEmitter { + close: () => void; + constructor(closer: () => void) { + super(); + this.close = closer; + } + ref() { + notImplemented("FSWatcher.ref() is not implemented"); + } + unref() { + notImplemented("FSWatcher.unref() is not implemented"); + } +} diff --git a/std/node/_fs/_fs_watch_test.ts b/std/node/_fs/_fs_watch_test.ts new file mode 100644 index 0000000000..e85b4c9bc1 --- /dev/null +++ b/std/node/_fs/_fs_watch_test.ts @@ -0,0 +1,32 @@ +import { watch } from "./_fs_watch.ts"; +import { assertEquals, fail } from "../../testing/asserts.ts"; + +function wait(time: number) { + return new Promise((resolve) => { + setTimeout(resolve, time); + }); +} + +Deno.test({ + name: "watching a file", + async fn() { + const file = Deno.makeTempFileSync(); + const result: Array<[string, string]> = []; + await new Promise((resolve) => { + const watcher = watch( + file, + (eventType, filename) => result.push([eventType, filename]), + ); + wait(100) + .then(() => Deno.writeTextFileSync(file, "something")) + .then(() => wait(100)) + .then(() => watcher.close()) + .then(() => wait(100)) + .then(resolve); + }) + .then(() => { + assertEquals(result.length >= 1, true); + }) + .catch(() => fail()); + }, +}); diff --git a/std/node/fs.ts b/std/node/fs.ts index adb3a7c63c..91e24728bd 100644 --- a/std/node/fs.ts +++ b/std/node/fs.ts @@ -11,6 +11,15 @@ import { exists, existsSync } from "./_fs/_fs_exists.ts"; import { mkdir, mkdirSync } from "./_fs/_fs_mkdir.ts"; import { copyFile, copyFileSync } from "./_fs/_fs_copy.ts"; import { writeFile, writeFileSync } from "./_fs/_fs_writeFile.ts"; +import { readdir, readdirSync } from "./_fs/_fs_readdir.ts"; +import { rename, renameSync } from "./_fs/_fs_rename.ts"; +import { rmdir, rmdirSync } from "./_fs/_fs_rmdir.ts"; +import { unlink, unlinkSync } from "./_fs/_fs_unlink.ts"; +import { watch } from "./_fs/_fs_watch.ts"; +import { open, openSync } from "./_fs/_fs_open.ts"; +import { stat, statSync } from "./_fs/_fs_stat.ts"; +import { lstat, lstatSync } from "./_fs/_fs_lstat.ts"; + import * as promises from "./_fs/promises/mod.ts"; export { @@ -29,13 +38,28 @@ export { copyFileSync, exists, existsSync, + lstat, + lstatSync, mkdir, mkdirSync, + open, + openSync, promises, + readdir, + readdirSync, readFile, readFileSync, readlink, readlinkSync, + rename, + renameSync, + rmdir, + rmdirSync, + stat, + statSync, + unlink, + unlinkSync, + watch, writeFile, writeFileSync, };