Sindbad~EG File Manager
/**
* @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();
Sindbad File Manager Version 1.0, Coded By Sindbad EG ~ The Terrorists