123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452 |
- /**
- * @fileoverview The main file for the hfs package.
- * @author Nicholas C. Zakas
- */
- /* global Buffer:readonly, URL */
- //-----------------------------------------------------------------------------
- // Types
- //-----------------------------------------------------------------------------
- /** @typedef {import("@humanfs/types").HfsImpl} HfsImpl */
- /** @typedef {import("@humanfs/types").HfsDirectoryEntry} HfsDirectoryEntry */
- /** @typedef {import("node:fs/promises")} Fsp */
- /** @typedef {import("fs").Dirent} Dirent */
- //-----------------------------------------------------------------------------
- // Imports
- //-----------------------------------------------------------------------------
- import { Hfs } from "@humanfs/core";
- import path from "node:path";
- import { Retrier } from "@humanwhocodes/retry";
- import nativeFsp from "node:fs/promises";
- import { fileURLToPath } from "node:url";
- //-----------------------------------------------------------------------------
- // Constants
- //-----------------------------------------------------------------------------
- const RETRY_ERROR_CODES = new Set(["ENFILE", "EMFILE"]);
- //-----------------------------------------------------------------------------
- // Helpers
- //-----------------------------------------------------------------------------
- /**
- * A class representing a directory entry.
- * @implements {HfsDirectoryEntry}
- */
- class NodeHfsDirectoryEntry {
- /**
- * The name of the directory entry.
- * @type {string}
- */
- name;
- /**
- * True if the entry is a file.
- * @type {boolean}
- */
- isFile;
- /**
- * True if the entry is a directory.
- * @type {boolean}
- */
- isDirectory;
- /**
- * True if the entry is a symbolic link.
- * @type {boolean}
- */
- isSymlink;
- /**
- * Creates a new instance.
- * @param {Dirent} dirent The directory entry to wrap.
- */
- constructor(dirent) {
- this.name = dirent.name;
- this.isFile = dirent.isFile();
- this.isDirectory = dirent.isDirectory();
- this.isSymlink = dirent.isSymbolicLink();
- }
- }
- //-----------------------------------------------------------------------------
- // Exports
- //-----------------------------------------------------------------------------
- /**
- * A class representing the Node.js implementation of Hfs.
- * @implements {HfsImpl}
- */
- export class NodeHfsImpl {
- /**
- * The file system module to use.
- * @type {Fsp}
- */
- #fsp;
- /**
- * The retryer object used for retrying operations.
- * @type {Retrier}
- */
- #retrier;
- /**
- * Creates a new instance.
- * @param {object} [options] The options for the instance.
- * @param {Fsp} [options.fsp] The file system module to use.
- */
- constructor({ fsp = nativeFsp } = {}) {
- this.#fsp = fsp;
- this.#retrier = new Retrier(error => RETRY_ERROR_CODES.has(error.code));
- }
- /**
- * Reads a file and returns the contents as an Uint8Array.
- * @param {string|URL} filePath The path to the file to read.
- * @returns {Promise<Uint8Array|undefined>} A promise that resolves with the contents
- * of the file or undefined if the file doesn't exist.
- * @throws {Error} If the file cannot be read.
- * @throws {TypeError} If the file path is not a string.
- */
- bytes(filePath) {
- return this.#retrier
- .retry(() => this.#fsp.readFile(filePath))
- .then(buffer => new Uint8Array(buffer.buffer))
- .catch(error => {
- if (error.code === "ENOENT") {
- return undefined;
- }
- throw error;
- });
- }
- /**
- * Writes a value to a file. If the value is a string, UTF-8 encoding is used.
- * @param {string|URL} filePath The path to the file to write.
- * @param {Uint8Array} contents The contents to write to the
- * file.
- * @returns {Promise<void>} A promise that resolves when the file is
- * written.
- * @throws {TypeError} If the file path is not a string.
- * @throws {Error} If the file cannot be written.
- */
- async write(filePath, contents) {
- const value = Buffer.from(contents);
- return this.#retrier
- .retry(() => this.#fsp.writeFile(filePath, value))
- .catch(error => {
- // the directory may not exist, so create it
- if (error.code === "ENOENT") {
- const dirPath = path.dirname(
- filePath instanceof URL
- ? fileURLToPath(filePath)
- : filePath,
- );
- return this.#fsp
- .mkdir(dirPath, { recursive: true })
- .then(() => this.#fsp.writeFile(filePath, value));
- }
- throw error;
- });
- }
- /**
- * Appends a value to a file. If the value is a string, UTF-8 encoding is used.
- * @param {string|URL} filePath The path to the file to append to.
- * @param {Uint8Array} contents The contents to append to the
- * file.
- * @returns {Promise<void>} A promise that resolves when the file is
- * written.
- * @throws {TypeError} If the file path is not a string.
- * @throws {Error} If the file cannot be appended to.
- */
- async append(filePath, contents) {
- const value = Buffer.from(contents);
- return this.#retrier
- .retry(() => this.#fsp.appendFile(filePath, value))
- .catch(error => {
- // the directory may not exist, so create it
- if (error.code === "ENOENT") {
- const dirPath = path.dirname(
- filePath instanceof URL
- ? fileURLToPath(filePath)
- : filePath,
- );
- return this.#fsp
- .mkdir(dirPath, { recursive: true })
- .then(() => this.#fsp.appendFile(filePath, value));
- }
- throw error;
- });
- }
- /**
- * Checks if a file exists.
- * @param {string|URL} filePath The path to the file to check.
- * @returns {Promise<boolean>} A promise that resolves with true if the
- * file exists or false if it does not.
- * @throws {Error} If the operation fails with a code other than ENOENT.
- */
- isFile(filePath) {
- return this.#fsp
- .stat(filePath)
- .then(stat => stat.isFile())
- .catch(error => {
- if (error.code === "ENOENT") {
- return false;
- }
- throw error;
- });
- }
- /**
- * Checks if a directory exists.
- * @param {string|URL} dirPath The path to the directory to check.
- * @returns {Promise<boolean>} A promise that resolves with true if the
- * directory exists or false if it does not.
- * @throws {Error} If the operation fails with a code other than ENOENT.
- */
- isDirectory(dirPath) {
- return this.#fsp
- .stat(dirPath)
- .then(stat => stat.isDirectory())
- .catch(error => {
- if (error.code === "ENOENT") {
- return false;
- }
- throw error;
- });
- }
- /**
- * Creates a directory recursively.
- * @param {string|URL} dirPath The path to the directory to create.
- * @returns {Promise<void>} A promise that resolves when the directory is
- * created.
- */
- async createDirectory(dirPath) {
- await this.#fsp.mkdir(dirPath, { recursive: true });
- }
- /**
- * Deletes a file or empty directory.
- * @param {string|URL} fileOrDirPath The path to the file or directory to
- * delete.
- * @returns {Promise<boolean>} A promise that resolves when the file or
- * directory is deleted, true if the file or directory is deleted, false
- * if the file or directory does not exist.
- * @throws {TypeError} If the file or directory path is not a string.
- * @throws {Error} If the file or directory cannot be deleted.
- */
- delete(fileOrDirPath) {
- return this.#fsp
- .rm(fileOrDirPath)
- .then(() => true)
- .catch(error => {
- if (error.code === "ERR_FS_EISDIR") {
- return this.#fsp.rmdir(fileOrDirPath).then(() => true);
- }
- if (error.code === "ENOENT") {
- return false;
- }
- throw error;
- });
- }
- /**
- * Deletes a file or directory recursively.
- * @param {string|URL} fileOrDirPath The path to the file or directory to
- * delete.
- * @returns {Promise<boolean>} A promise that resolves when the file or
- * directory is deleted, true if the file or directory is deleted, false
- * if the file or directory does not exist.
- * @throws {TypeError} If the file or directory path is not a string.
- * @throws {Error} If the file or directory cannot be deleted.
- */
- deleteAll(fileOrDirPath) {
- return this.#fsp
- .rm(fileOrDirPath, { recursive: true })
- .then(() => true)
- .catch(error => {
- if (error.code === "ENOENT") {
- return false;
- }
- throw error;
- });
- }
- /**
- * Returns a list of directory entries for the given path.
- * @param {string|URL} dirPath The path to the directory to read.
- * @returns {AsyncIterable<HfsDirectoryEntry>} A promise that resolves with the
- * directory entries.
- * @throws {TypeError} If the directory path is not a string.
- * @throws {Error} If the directory cannot be read.
- */
- async *list(dirPath) {
- const entries = await this.#fsp.readdir(dirPath, {
- withFileTypes: true,
- });
- for (const entry of entries) {
- yield new NodeHfsDirectoryEntry(entry);
- }
- }
- /**
- * Returns the size of a file. This method handles ENOENT errors
- * and returns undefined in that case.
- * @param {string|URL} filePath The path to the file to read.
- * @returns {Promise<number|undefined>} A promise that resolves with the size of the
- * file in bytes or undefined if the file doesn't exist.
- */
- size(filePath) {
- return this.#fsp
- .stat(filePath)
- .then(stat => stat.size)
- .catch(error => {
- if (error.code === "ENOENT") {
- return undefined;
- }
- throw error;
- });
- }
- /**
- * Returns the last modified date of a file or directory. This method handles ENOENT errors
- * and returns undefined in that case.
- * @param {string|URL} fileOrDirPath The path to the file to read.
- * @returns {Promise<Date|undefined>} A promise that resolves with the last modified
- * date of the file or directory, or undefined if the file doesn't exist.
- */
- lastModified(fileOrDirPath) {
- return this.#fsp
- .stat(fileOrDirPath)
- .then(stat => stat.mtime)
- .catch(error => {
- if (error.code === "ENOENT") {
- return undefined;
- }
- throw error;
- });
- }
- /**
- * Copies a file from one location to another.
- * @param {string|URL} source The path to the file to copy.
- * @param {string|URL} destination The path to copy the file to.
- * @returns {Promise<void>} A promise that resolves when the file is copied.
- * @throws {Error} If the source file does not exist.
- * @throws {Error} If the source file is a directory.
- * @throws {Error} If the destination file is a directory.
- */
- copy(source, destination) {
- return this.#fsp.copyFile(source, destination);
- }
- /**
- * Copies a file or directory from one location to another.
- * @param {string|URL} source The path to the file or directory to copy.
- * @param {string|URL} destination The path to copy the file or directory to.
- * @returns {Promise<void>} A promise that resolves when the file or directory is
- * copied.
- * @throws {Error} If the source file or directory does not exist.
- * @throws {Error} If the destination file or directory is a directory.
- */
- async copyAll(source, destination) {
- // for files use copy() and exit
- if (await this.isFile(source)) {
- return this.copy(source, destination);
- }
- const sourceStr =
- source instanceof URL ? fileURLToPath(source) : source;
- const destinationStr =
- destination instanceof URL
- ? fileURLToPath(destination)
- : destination;
- // for directories, create the destination directory and copy each entry
- await this.createDirectory(destination);
- for await (const entry of this.list(source)) {
- const fromEntryPath = path.join(sourceStr, entry.name);
- const toEntryPath = path.join(destinationStr, entry.name);
- if (entry.isDirectory) {
- await this.copyAll(fromEntryPath, toEntryPath);
- } else {
- await this.copy(fromEntryPath, toEntryPath);
- }
- }
- }
- /**
- * Moves a file from the source path to the destination path.
- * @param {string|URL} source The location of the file to move.
- * @param {string|URL} destination The destination of the file to move.
- * @returns {Promise<void>} A promise that resolves when the move is complete.
- * @throws {TypeError} If the file paths are not strings.
- * @throws {Error} If the file cannot be moved.
- */
- move(source, destination) {
- return this.#fsp.stat(source).then(stat => {
- if (stat.isDirectory()) {
- throw new Error(
- `EISDIR: illegal operation on a directory, move '${source}' -> '${destination}'`,
- );
- }
- return this.#fsp.rename(source, destination);
- });
- }
- /**
- * Moves a file or directory from the source path to the destination path.
- * @param {string|URL} source The location of the file or directory to move.
- * @param {string|URL} destination The destination of the file or directory to move.
- * @returns {Promise<void>} A promise that resolves when the move is complete.
- * @throws {TypeError} If the file paths are not strings.
- * @throws {Error} If the file or directory cannot be moved.
- */
- async moveAll(source, destination) {
- return this.#fsp.rename(source, destination);
- }
- }
- /**
- * A class representing a file system utility library.
- * @implements {HfsImpl}
- */
- export class NodeHfs extends Hfs {
- /**
- * Creates a new instance.
- * @param {object} [options] The options for the instance.
- * @param {Fsp} [options.fsp] The file system module to use.
- */
- constructor({ fsp } = {}) {
- super({ impl: new NodeHfsImpl({ fsp }) });
- }
- }
- export const hfs = new NodeHfs();
|