UNPKG

@ayonli/jsext

Version:

A JavaScript extension package for building strong and modern applications.

1,488 lines (1,485 loc) 60.6 kB
'use strict'; var async = require('./async.js'); var bytes = require('./bytes.js'); var env = require('./env.js'); var object = require('./object.js'); var error_Exception = require('./error/Exception.js'); require('./external/event-target-polyfill/index.js'); var filetype = require('./filetype.js'); var fs_util = require('./fs/util.js'); var path = require('./path.js'); var reader = require('./reader.js'); var runtime = require('./runtime.js'); var string = require('./string.js'); var _try = require('./try.js'); var path_util = require('./path/util.js'); var reader_util = require('./reader/util.js'); /** * Universal file system APIs for both server and browser applications. * * This module is guaranteed to work in the following environments: * * - Node.js * - Deno * - Bun * - Modern browsers * - Cloudflare Workers (limited support and experimental) * * We can also use the {@link runtime} function to check whether the runtime * has file system support. When `runtime().fsSupport` is `true`, this module * should work properly. * * In most browsers, this module uses the * [Origin Private File System](https://developer.mozilla.org/en-US/docs/Web/API/File_System_API/Origin_private_file_system). * In Chromium browsers, this module can also access the device's local file * system via `window.showOpenFilePicker()` and `window.showDirectoryPicker()`. * * This module also provides limited support for Cloudflare Workers, however it * requires setting the `[site].bucket` option in the `wrangler.toml` file. Only * the reading functions are supported, such as {@link readFile} and * {@link readDir}, these functions allow us reading static files in the workers, * writing functions is not implemented at the moment. More details about * serving static assets in Cloudflare Workers can be found here: * [Add static assets to an existing Workers project](https://developers.cloudflare.com/workers/configuration/sites/start-from-worker/). * * **Errors:** * * When a file system operation fails, this module throws an {@link Exception} * with one of the following names: * * - `NotFoundError`: The file or directory does not exist. * - `NotAllowedError`: The operation is not allowed, such as being blocked by * the permission system. * - `AlreadyExistsError`: The file or directory already exists. * - `IsDirectoryError`: The path is a directory, not a file. * - `NotDirectoryError`: The path is a file, not a directory. * - `InvalidOperationError`: The operation is not supported, such as trying to * copy a directory without the `recursive` option. * - `BusyError`: The file is busy, such as being locked by another program. * - `InterruptedError`: The operation is interrupted by the underlying file * system. * - `FileTooLargeError`: The file is too large, or the file system doesn't have * enough space to store the new content. * - `FilesystemLoopError`: Too many symbolic links were encountered when * resolving the filename. * * Other errors may also be thrown by the runtime, such as `TypeError`. * * @experimental * @module */ /** * Platform-specific end-of-line marker. The value is `\r\n` in Windows * server-side environments, and `\n` elsewhere. */ const EOL = (() => { if (env.isDeno) { return Deno.build.os === "windows" ? "\r\n" : "\n"; } else if (typeof process === "object" && typeof process.platform === "string") { return process.platform === "win32" ? "\r\n" : "\n"; } else { return "\n"; } })(); function getErrorName(err) { if (err.constructor === Error) { return err.constructor.name; } else { return err.name; } } /** * Wraps a raw file system error to a predefined error by this module. * * @param type Used for `FileSystemHandle` operations. */ function wrapFsError(err, type = undefined) { if (err instanceof Error && !(err instanceof error_Exception.default) && !(err instanceof TypeError)) { const errName = getErrorName(err); const errCode = err.code; if (errName === "NotFoundError" || errName === "NotFound" || errCode === "ENOENT" || errCode === "ENOTFOUND") { return new error_Exception.default(err.message, { name: "NotFoundError", code: 404, cause: err }); } else if (errName === "NotAllowedError" || errName === "PermissionDenied" || errName === "InvalidStateError" || errName === "SecurityError" || errName === "EACCES" || errCode === "EPERM" || errCode === "ERR_ACCESS_DENIED") { return new error_Exception.default(err.message, { name: "NotAllowedError", code: 403, cause: err }); } else if (errName === "AlreadyExists" || errCode === "EEXIST" || errCode === "ERR_FS_CP_EEXIST") { return new error_Exception.default(err.message, { name: "AlreadyExistsError", code: 409, cause: err }); } else if ((errName === "TypeMismatchError" && type === "file") || errName === "IsADirectory" || errCode === "EISDIR" || errCode === "ERR_FS_EISDIR") { return new error_Exception.default(err.message, { name: "IsDirectoryError", code: 415, cause: err }); } else if ((errName === "TypeMismatchError" && type === "directory") || errName === "NotADirectory" || errCode === "ENOTDIR") { return new error_Exception.default(err.message, { name: "NotDirectoryError", code: 415, cause: err }); } else if (errName === "InvalidModificationError" || errName === "NotSupported" || errCode === "ENOTEMPTY" || errCode === "ERR_FS_CP_EINVAL" || errCode === "ERR_FS_CP_FIFO_PIPE" || errCode === "ERR_FS_CP_DIR_TO_NON_DIR" || errCode === "ERR_FS_CP_NON_DIR_TO_DIR" || errCode === "ERR_FS_CP_SOCKET" || errCode === "ERR_FS_CP_SYMLINK_TO_SUBDIRECTORY" || errCode === "ERR_FS_CP_UNKNOWN" || errCode === "ERR_FS_INVALID_SYMLINK_TYPE") { return new error_Exception.default(err.message, { name: "InvalidOperationError", code: 405, cause: err }); } else if (errName === "NoModificationAllowedError" || errName === "Busy" || errName === "TimedOut" || errCode === "ERR_DIR_CONCURRENT_OPERATION") { return new error_Exception.default(errName, { name: "BusyError", code: 409, cause: err }); } else if (errName === "Interrupted" || errCode === "ERR_DIR_CLOSED") { return new error_Exception.default(err.message, { name: "InterruptedError", code: 409, cause: err }); } else if (errName === "QuotaExceededError" || errCode === "ERR_FS_FILE_TOO_LARGE") { return new error_Exception.default(err.message, { name: "FileTooLargeError", code: 413, cause: err }); } else if (errName === "FilesystemLoop") { return new error_Exception.default(err.message, { name: "FilesystemLoopError", code: 508, cause: err }); } else { return err; } } else if (err instanceof Error) { return err; } else if (typeof err === "string") { return new error_Exception.default(err, { code: 500, cause: err }); } else { return new error_Exception.default("Unknown error", { code: 500, cause: err }); } } /** * Wraps a raw file system operation so that when any error occurs, the error is * wrapped to a predefined error by this module. * * @param type Only used for `FileSystemHandle` operations. */ function rawOp(op, type = undefined) { return op.catch((err) => { throw wrapFsError(err, type); }); } /** * Obtains the directory handle of the given path. * * NOTE: This function is only available in the browser. * * NOTE: If the `path` is not provided or is empty, the root directory handle * will be returned. * * @example * ```ts * // with the default storage * import { getDirHandle } from "@ayonli/jsext/fs"; * * const dir = await getDirHandle("/path/to/dir"); * ``` * * @example * ```ts * // with a user-selected directory as root (Chromium only) * import { getDirHandle } from "@ayonli/jsext/fs"; * * const root = await window.showDirectoryPicker(); * const dir = await getDirHandle("/path/to/dir", { root }); * ``` * * @example * ```ts * // create the directory if not exist * import { getDirHandle } from "@ayonli/jsext/fs"; * * const dir = await getDirHandle("/path/to/dir", { create: true, recursive: true }); * ``` * * @example * ```ts * // return the root directory handle * import { getDirHandle } from "@ayonli/jsext/fs"; * * const root = await getDirHandle(); * ``` */ async function getDirHandle(path = "", options = {}) { var _a; if (typeof location === "object" && typeof location.origin === "string") { path = string.stripStart(path, location.origin); } const { create = false, recursive = false } = options; const paths = path_util.split(string.stripStart(path, "/")).filter(p => p !== "."); const root = (_a = options.root) !== null && _a !== void 0 ? _a : await rawOp(navigator.storage.getDirectory(), "directory"); let dir = root; for (let i = 0; i < paths.length; i++) { const _path = paths[i]; dir = await rawOp(dir.getDirectoryHandle(_path, { create: create && (recursive || (i === paths.length - 1)), }), "directory"); } return dir; } /** * Obtains the file handle of the given path. * * NOTE: This function is only available in the browser. * * @example * ```ts * // with the default storage * import { getFileHandle } from "@ayonli/jsext/fs"; * * const file = await getFileHandle("/path/to/file.txt"); * ``` * * @example * ```ts * // with a user-selected directory as root (Chromium only) * import { getFileHandle } from "@ayonli/jsext/fs"; * * const root = await window.showDirectoryPicker(); * const file = await getFileHandle("/path/to/file.txt", { root }); * ``` * * @example * ```ts * // create the file if not exist * import { getFileHandle } from "@ayonli/jsext/fs"; * * const file = await getFileHandle("/path/to/file.txt", { create: true }); * ``` */ async function getFileHandle(path$1, options = {}) { var _a; const dirPath = path.dirname(path$1); const name = path.basename(path$1); const dir = await getDirHandle(dirPath, { root: options.root }); return await rawOp(dir.getFileHandle(name, { create: (_a = options.create) !== null && _a !== void 0 ? _a : false, }), "file"); } /** * Checks if the given path exists. * * This function may throw an error if the path is invalid or the operation is * not allowed. * * NOTE: This function can also be used in Cloudflare Workers. * * @example * ```ts * // with the default storage * import { exists } from "@ayonli/jsext/fs"; * * if (await exists("/path/to/file.txt")) { * console.log("The file exists."); * } else { * console.log("The file does not exist."); * } * ``` * * @example * ```ts * // with a user-selected directory as root (Chromium only) * import { exists } from "@ayonli/jsext/fs"; * * const root = await window.showDirectoryPicker(); * * if (await exists("/path/to/file.txt", { root })) { * console.log("The file exists."); * } else { * console.log("The file does not exist."); * } * ``` */ async function exists(path, options = {}) { try { await stat(path, options); return true; } catch (err) { if (err instanceof error_Exception.default) { if (err.name === "NotFoundError") { return false; } } throw err; } } /** * Returns the information of the given file or directory. * * NOTE: This function can also be used in Cloudflare Workers. * * @example * ```ts * // with the default storage * import { stat } from "@ayonli/jsext/fs"; * * const info = await stat("/path/to/file.txt"); * console.log(`${info.name} is a ${info.kind}, its size is ${info.size} bytes, with MIME type ${info.type}.`); * ``` * * @example * ```ts * // with a user-selected directory as root (Chromium only) * import { stat } from "@ayonli/jsext/fs"; * * const root = await window.showDirectoryPicker(); * const info = await stat("/path/to/file.txt", { root }); * console.log(`${info.name} is a ${info.kind}, its size is ${info.size} bytes, with MIME type ${info.type}.`); * ``` */ async function stat(target, options = {}) { var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y; if (typeof target === "object") { if (target.kind === "file") { const info = await rawOp(target.getFile(), "file"); return { name: target.name, kind: "file", size: info.size, type: (_b = (_a = info.type) !== null && _a !== void 0 ? _a : filetype.getMIME(path.extname(target.name))) !== null && _b !== void 0 ? _b : "", mtime: new Date(info.lastModified), atime: null, birthtime: null, mode: 0, uid: 0, gid: 0, isBlockDevice: false, isCharDevice: false, isFIFO: false, isSocket: false, }; } else { return { name: target.name, kind: "directory", size: 0, type: "", mtime: null, atime: null, birthtime: null, mode: 0, uid: 0, gid: 0, isBlockDevice: false, isCharDevice: false, isFIFO: false, isSocket: false, }; } } const path$1 = target; if (env.isDeno) { const stat = await rawOp(options.followSymlink ? Deno.stat(path$1) : Deno.lstat(path$1)); const kind = stat.isDirectory ? "directory" : stat.isSymlink ? "symlink" : "file"; return { name: path.basename(path$1), kind, size: stat.size, type: kind === "file" ? ((_c = filetype.getMIME(path.extname(path$1))) !== null && _c !== void 0 ? _c : "") : "", mtime: (_d = stat.mtime) !== null && _d !== void 0 ? _d : null, atime: (_e = stat.atime) !== null && _e !== void 0 ? _e : null, birthtime: (_f = stat.birthtime) !== null && _f !== void 0 ? _f : null, mode: (_g = stat.mode) !== null && _g !== void 0 ? _g : 0, uid: (_h = stat.uid) !== null && _h !== void 0 ? _h : 0, gid: (_j = stat.gid) !== null && _j !== void 0 ? _j : 0, isBlockDevice: (_k = stat.isBlockDevice) !== null && _k !== void 0 ? _k : false, isCharDevice: (_l = stat.isCharDevice) !== null && _l !== void 0 ? _l : false, isFIFO: (_m = stat.isFifo) !== null && _m !== void 0 ? _m : false, isSocket: (_o = stat.isSocket) !== null && _o !== void 0 ? _o : false, }; } else if (env.isNodeLike) { const fs = await import('fs/promises'); const stat = await rawOp(options.followSymlink ? fs.stat(path$1) : fs.lstat(path$1)); const kind = stat.isDirectory() ? "directory" : stat.isSymbolicLink() ? "symlink" : "file"; return { name: path.basename(path$1), kind, size: stat.size, type: kind === "file" ? ((_p = filetype.getMIME(path.extname(path$1))) !== null && _p !== void 0 ? _p : "") : "", mtime: (_q = stat.mtime) !== null && _q !== void 0 ? _q : null, atime: (_r = stat.atime) !== null && _r !== void 0 ? _r : null, birthtime: (_s = stat.birthtime) !== null && _s !== void 0 ? _s : null, mode: (_t = stat.mode) !== null && _t !== void 0 ? _t : 0, uid: (_u = stat.uid) !== null && _u !== void 0 ? _u : 0, gid: (_v = stat.gid) !== null && _v !== void 0 ? _v : 0, isBlockDevice: stat.isBlockDevice(), isCharDevice: stat.isCharacterDevice(), isFIFO: stat.isFIFO(), isSocket: stat.isSocket(), }; } else { const [err, file] = await _try.default(getFileHandle(path$1, options)); if (file) { const info = await rawOp(file.getFile(), "file"); return { name: info.name, kind: "file", size: info.size, type: (_x = (_w = info.type) !== null && _w !== void 0 ? _w : filetype.getMIME(path.extname(info.name))) !== null && _x !== void 0 ? _x : "", mtime: new Date(info.lastModified), atime: null, birthtime: null, mode: 0, uid: 0, gid: 0, isBlockDevice: false, isCharDevice: false, isFIFO: false, isSocket: false, }; } else if (((_y = object.as(err, error_Exception.default)) === null || _y === void 0 ? void 0 : _y.name) === "IsDirectoryError") { return { name: path.basename(path$1), kind: "directory", size: 0, type: "", mtime: null, atime: null, birthtime: null, mode: 0, uid: 0, gid: 0, isBlockDevice: false, isCharDevice: false, isFIFO: false, isSocket: false, }; } else { throw err; } } } /** * Creates a new directory with the given path. * * @example * ```ts * // with the default storage * import { mkdir } from "@ayonli/jsext/fs"; * * await mkdir("/path/to/dir"); * ``` * * @example * ```ts * // with a user-selected directory as root (Chromium only) * import { mkdir } from "@ayonli/jsext/fs"; * * const root = await window.showDirectoryPicker(); * await mkdir("/path/to/dir", { root }); * ``` * * @example * ```ts * // create the directory and its parent directories if not exist * import { mkdir } from "@ayonli/jsext/fs"; * * await mkdir("/path/to/dir", { recursive: true }); * ``` */ async function mkdir(path, options = {}) { if (env.isDeno) { await rawOp(Deno.mkdir(path, options)); } else if (env.isNodeLike) { const fs = await import('fs/promises'); await rawOp(fs.mkdir(path, options)); } else { if (await exists(path, { root: options.root })) { throw new error_Exception.default(`File or folder already exists, mkdir '${path}'`, { name: "AlreadyExistsError", code: 409, }); } await getDirHandle(path, { ...options, create: true }); } } /** * Ensures the directory exists, creating it (and any parent directory) if not. * * @example * ```ts * // with the default storage * import { ensureDir } from "@ayonli/jsext/fs"; * * await ensureDir("/path/to/dir"); * ``` * * @example * ```ts * // with a user-selected directory as root (Chromium only) * import { ensureDir } from "@ayonli/jsext/fs"; * * const root = await window.showDirectoryPicker(); * await ensureDir("/path/to/dir", { root }); * ``` */ async function ensureDir(path, options = {}) { var _a; if (await exists(path, options)) { return; } try { await mkdir(path, { ...options, recursive: true }); } catch (err) { if (((_a = object.as(err, error_Exception.default)) === null || _a === void 0 ? void 0 : _a.name) === "AlreadyExistsError") { return; } else { throw err; } } } /** * Reads the directory of the given path and iterates its entries. * * NOTE: The order of the entries is not guaranteed. * * NOTE: This function can also be used in Cloudflare Workers. * * @example * ```ts * // with the default storage * import { readDir } from "@ayonli/jsext/fs"; * * for await (const entry of readDir("/path/to/dir")) { * console.log(`${entry.name} is a ${entry.kind}, its relative path is '${entry.relativePath}'.`); * } * ``` * * @example * ```ts * // with a user-selected directory as root (Chromium only) * import { readDir } from "@ayonli/jsext/fs"; * * const root = await window.showDirectoryPicker(); * for await (const entry of readDir("/path/to/dir", { root })) { * console.log(`${entry.name} is a ${entry.kind}, its relative path is '${entry.relativePath}'.`); * } * ``` * * @example * ```ts * // read the sub-directories recursively * import { readDir } from "@ayonli/jsext/fs"; * * for await (const entry of readDir("/path/to/dir", { recursive: true })) { * console.log(`${entry.name} is a ${entry.kind}, its relative path is '${entry.relativePath}'.`); * } * ``` */ async function* readDir(target, options = {}) { if (typeof target === "object") { yield* readDirHandle(target, options); return; } const path$1 = target; if (env.isDeno) { yield* (async function* read(path$1, base) { try { for await (const entry of Deno.readDir(path$1)) { const _entry = fs_util.fixDirEntry({ name: entry.name, kind: entry.isDirectory ? "directory" : entry.isSymlink ? "symlink" : "file", relativePath: path.join(base, entry.name), }); yield _entry; if ((options === null || options === void 0 ? void 0 : options.recursive) && entry.isDirectory) { yield* read(path.join(path$1, entry.name), _entry.relativePath); } } } catch (err) { throw wrapFsError(err); } })(path$1, ""); } else if (env.isNodeLike) { const fs = await import('fs/promises'); yield* (async function* read(path$1, base) { const entries = await rawOp(fs.readdir(path$1, { withFileTypes: true })); for (const entry of entries) { const _entry = fs_util.fixDirEntry({ name: entry.name, kind: entry.isDirectory() ? "directory" : entry.isSymbolicLink() ? "symlink" : "file", relativePath: path.join(base, entry.name), }); yield _entry; if ((options === null || options === void 0 ? void 0 : options.recursive) && entry.isDirectory()) { yield* read(path.join(path$1, entry.name), _entry.relativePath); } } })(path$1, ""); } else { const dir = await getDirHandle(path$1, { root: options.root }); yield* readDirHandle(dir, options); } } /** * Recursively reads the contents of the directory and transform them into a * tree structure. * * NOTE: Unlike {@link readDir}, the order of the entries returned by this * function is guaranteed, they are ordered first by kind (directories before * files), then by names alphabetically. * * NOTE: This function can also be used in Cloudflare Workers. * * @example * ```ts * // with the default storage * import { readTree } from "@ayonli/jsext/fs"; * * const tree = await readTree("/path/to/dir"); * console.log(tree); * ``` * * @example * ```ts * // with a user-selected directory as root (Chromium only) * import { readTree } from "@ayonli/jsext/fs"; * * const root = await window.showDirectoryPicker(); * const tree = await readTree("/path/to/dir", { root }); * console.log(tree); * ``` */ async function readTree(target, options = {}) { const entries = (await reader.readAsArray(readDir(target, { ...options, recursive: true }))); const tree = fs_util.makeTree(target, entries, true); if (!tree.handle && options.root) { tree.handle = options.root; } return tree; } async function* readDirHandle(dir, options = {}) { const { base = "", recursive = false } = options; const entries = dir.entries(); for await (const [_, entry] of entries) { const _entry = fs_util.fixDirEntry({ name: entry.name, kind: entry.kind, relativePath: path.join(base, entry.name), handle: entry, }); yield _entry; if (recursive && entry.kind === "directory") { yield* readDirHandle(entry, { base: _entry.relativePath, recursive, }); } } } async function readFileHandle(handle, options) { const file = await rawOp(handle.getFile(), "file"); const arr = new Uint8Array(file.size); let offset = 0; let reader = reader_util.toAsyncIterable(file.stream()); if (options.signal) { reader = async.abortable(reader, options.signal); } for await (const chunk of reader) { arr.set(chunk, offset); offset += chunk.length; } return arr; } /** * Reads the content of the given file in bytes. * * NOTE: This function can also be used in Cloudflare Workers. * * @example * ```ts * // with the default storage * import { readFile } from "@ayonli/jsext/fs"; * * const bytes = await readFile("/path/to/file.txt"); * ``` * * @example * ```ts * // with a user-selected directory as root (Chromium only) * import { readFile } from "@ayonli/jsext/fs"; * * const root = await window.showDirectoryPicker(); * const bytes = await readFile("/path/to/file.txt", { root }); * ``` */ async function readFile(target, options = {}) { if (typeof target === "object") { return await readFileHandle(target, options); } const filename = target; if (env.isDeno) { return await rawOp(Deno.readFile(filename, options)); } else if (env.isNodeLike) { const fs = await import('fs/promises'); const buffer = await rawOp(fs.readFile(filename, options)); return new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength); } else { const handle = await getFileHandle(filename, { root: options.root }); return await readFileHandle(handle, options); } } /** * Reads the content of the given file as text with `utf-8` encoding. * * NOTE: This function can also be used in Cloudflare Workers. * * @example * ```ts * // with the default storage * import { readFileAsText } from "@ayonli/jsext/fs"; * * const text = await readFileAsText("/path/to/file.txt"); * ``` * * @example * ```ts * // with a user-selected directory as root (Chromium only) * import { readFileAsText } from "@ayonli/jsext/fs"; * * const root = await window.showDirectoryPicker(); * const text = await readFileAsText("/path/to/file.txt", { root }); * ``` */ async function readFileAsText(target, options = {}) { if (typeof target === "object") { return bytes.text(await readFileHandle(target, options)); } const filename = target; if (env.isDeno) { return await rawOp(Deno.readTextFile(filename, options)); } else if (env.isNodeLike) { const fs = await import('fs/promises'); return await rawOp(fs.readFile(filename, { encoding: "utf-8", signal: options.signal, })); } else { return bytes.text(await readFile(filename, options)); } } /** * Reads the file as a `File` object. * * NOTE: This function can also be used in Cloudflare Workers. * * @example * ```ts * // with the default storage * import { readFileAsFile } from "@ayonli/jsext/fs"; * * const file = await readFileAsFile("/path/to/file.txt"); * ``` * * @example * ```ts * // with a user-selected directory as root (Chromium only) * import { readFileAsFile } from "@ayonli/jsext/fs"; * * const root = await window.showDirectoryPicker(); * const file = await readFileAsFile("/path/to/file.txt", { root }); * ``` */ async function readFileAsFile(target, options = {}) { var _a; if (typeof target === "object") { return await readFileHandleAsFile(target); } const filename = target; if (env.isDeno || env.isNodeLike) { const bytes = await readFile(filename, options); const type = (_a = filetype.getMIME(path.extname(filename))) !== null && _a !== void 0 ? _a : ""; const file = new File([bytes], path.basename(filename), { type }); Object.defineProperty(file, "webkitRelativePath", { configurable: true, enumerable: true, writable: false, value: "", }); return file; } else { const handle = await getFileHandle(target, { root: options.root }); return await readFileHandleAsFile(handle); } } async function readFileHandleAsFile(handle) { const file = await rawOp(handle.getFile(), "file"); return fs_util.fixFileType(file); } /** * Writes the given data to the file. * * @example * ```ts * // with the default storage * import { writeFile } from "@ayonli/jsext/fs"; * * await writeFile("/path/to/file.txt", "Hello, world!"); * ``` * * @example * ```ts * // with a user-selected directory as root (Chromium only) * import { writeFile } from "@ayonli/jsext/fs"; * * const root = await window.showDirectoryPicker(); * await writeFile("/path/to/file.txt", "Hello, world!", { root }); * ``` * * @example * ```ts * // append the data to the file * import { writeFile } from "@ayonli/jsext/fs"; * * await writeFile("/path/to/file.txt", "Hello, world!", { append: true }); * ``` * * @example * ```ts * // write binary data to the file * import { writeFile } from "@ayonli/jsext/fs"; * import bytes from "@ayonli/jsext/bytes"; * * const data = bytes("Hello, world!"); * await writeFile("/path/to/file.txt", data) * ``` * * @example * ```ts * // write a blob to the file * import { writeFile } from "@ayonli/jsext/fs"; * * const blob = new Blob(["Hello, world!"], { type: "text/plain" }); * await writeFile("/path/to/file.txt", blob); * ``` * * @example * ```ts * // write a readable stream to the file * import { writeFile } from "@ayonli/jsext/fs"; * * const res = await fetch("https://example.com/file.txt"); * await writeFile("/path/to/file.txt", res.body!); * ``` */ async function writeFile(target, data, options = {}) { if (typeof target === "object") { return await writeFileHandle(target, data, options); } const filename = target; if (env.isDeno) { if (typeof data === "string") { return await rawOp(Deno.writeTextFile(filename, data, options)); } else if (data instanceof Blob) { return await rawOp(Deno.writeFile(filename, data.stream(), options)); } else if (data instanceof ArrayBuffer) { return await rawOp(Deno.writeFile(filename, new Uint8Array(data), options)); } else if (data instanceof Uint8Array) { return await rawOp(Deno.writeFile(filename, data, options)); } else if (ArrayBuffer.isView(data)) { return await rawOp(Deno.writeFile(filename, bytes.default(data), options)); } else if (data) { return await rawOp(Deno.writeFile(filename, data, options)); } } else if (env.isNodeLike) { if (typeof Blob === "function" && data instanceof Blob) { const reader = data.stream(); const writer = createNodeWritableStream(filename, options); await reader.pipeTo(writer); } else if (typeof ReadableStream === "function" && data instanceof ReadableStream) { const writer = createNodeWritableStream(filename, options); await data.pipeTo(writer); } else { const fs = await import('fs/promises'); const { append, ...rest } = options; let _data; if (data instanceof ArrayBuffer) { _data = new Uint8Array(data); } else if (data instanceof Uint8Array) { _data = data; } else if (ArrayBuffer.isView(data)) { _data = bytes.default(data); } else if (typeof data === "string") { _data = data; } else { throw new TypeError("Unsupported data type"); } return await rawOp(fs.writeFile(filename, _data, { flag: append ? "a" : "w", ...rest, })); } } else { const handle = await getFileHandle(filename, { root: options.root, create: true }); return await writeFileHandle(handle, data, options); } } async function writeFileHandle(handle, data, options) { const writer = await createFileHandleWritableStream(handle, options); if (options.signal) { const { signal } = options; if (signal.aborted) { throw signal.reason; } else { signal.addEventListener("abort", () => { writer.abort(signal.reason); }); } } try { if (data instanceof Blob) { await data.stream().pipeTo(writer); } else if (data instanceof ReadableStream) { await data.pipeTo(writer); } else { await writer.write(data); await writer.close(); } } catch (err) { throw wrapFsError(err, "file"); } } /** * Writes multiple lines of content to the file. * * This function will automatically detect the line ending of the current * content and use it to write the new lines. If the file is empty or does not * exists (will be created automatically), it will use the system's default line * ending to separate lines. * * This function will append a new line at the end of the final content, in * appending mode, it will also prepend a line ending before the input lines if * the current content doesn't ends with one. * * @example * ```ts * // with the default storage * import { writeLines } from "@ayonli/jsext/fs"; * * await writeLines("/path/to/file.txt", ["Hello", "World"]); * ``` * * @example * ```ts * // with a user-selected directory as root (Chromium only) * import { writeLines } from "@ayonli/jsext/fs"; * * const root = await window.showDirectoryPicker(); * await writeLines("/path/to/file.txt", ["Hello", "World"], { root }); * ``` * * @example * ```ts * // append the lines to the file * import { writeLines } from "@ayonli/jsext/fs"; * * await writeLines("/path/to/file.txt", ["Hello", "World"], { append: true }); * ``` */ async function writeLines(target, lines, options = {}) { const current = await readFileAsText(target, options).catch(err => { var _a; if (((_a = object.as(err, error_Exception.default)) === null || _a === void 0 ? void 0 : _a.name) !== "NotFoundError") { throw err; } else { return ""; } }); const lineEndings = current.match(/\r?\n/g); let eol = EOL; if (lineEndings) { const crlf = lineEndings.filter(e => e === "\r\n").length; const lf = lineEndings.length - crlf; eol = crlf > lf ? "\r\n" : "\n"; } let content = lines.join(eol); if (!content.endsWith(eol)) { if (eol === "\r\n" && content.endsWith("\r")) { content += "\n"; } else { content += eol; } } if (options.append && !current.endsWith(eol) && !content.startsWith(eol)) { if (eol === "\r\n" && current.endsWith("\r")) { if (!content.startsWith("\n")) { content = "\n" + content; } } else { content = eol + content; } } await writeFile(target, content, options); } /** * Truncates (or extends) the file to reach the specified `size`. If `size` is * not specified then the entire file contents are truncated. * * @example * ```ts * // with the default storage * import { stat, truncate } from "@ayonli/jsext/fs"; * * await truncate("/path/to/file.txt", 1024); * const info = await stat("/path/to/file.txt"); * console.assert(info.size === 1024); * ``` * * @example * ```ts * // with a user-selected directory as root (Chromium only) * import { stat, truncate } from "@ayonli/jsext/fs"; * * const root = await window.showDirectoryPicker(); * await truncate("/path/to/file.txt", 1024, { root }); * const info = await stat("/path/to/file.txt", { root }); * console.assert(info.size === 1024); * ``` * * @example * ```ts * // truncate the file to zero size * import { stat, truncate } from "@ayonli/jsext/fs"; * * await truncate("/path/to/file.txt"); * const info = await stat("/path/to/file.txt"); * console.assert(info.size === 0); * ``` */ async function truncate(target, size = 0, options = {}) { if (typeof target === "object") { return await truncateFileHandle(target, size); } const filename = target; if (env.isDeno) { await rawOp(Deno.truncate(filename, size)); } else if (env.isNodeLike) { const fs = await import('fs/promises'); await rawOp(fs.truncate(filename, size)); } else { const handle = await getFileHandle(filename, { root: options.root }); await truncateFileHandle(handle, size); } } async function truncateFileHandle(handle, size = 0) { try { const writer = await handle.createWritable({ keepExistingData: true }); await writer.truncate(size); await writer.close(); } catch (err) { throw wrapFsError(err, "file"); } } /** * Removes the file or directory of the given path from the file system. * * @example * ```ts * // with the default storage * import { remove } from "@ayonli/jsext/fs"; * * await remove("/path/to/file.txt"); * ``` * * @example * ```ts * // with a user-selected directory as root (Chromium only) * import { remove } from "@ayonli/jsext/fs"; * * const root = await window.showDirectoryPicker(); * await remove("/path/to/file.txt", { root }); * ``` * * @example * ```ts * // remove the directory and its contents recursively * import { remove } from "@ayonli/jsext/fs"; * * await remove("/path/to/dir", { recursive: true }); * ``` */ async function remove(path$1, options = {}) { if (env.isDeno) { await rawOp(Deno.remove(path$1, options)); } else if (env.isNodeLike) { const fs = await import('fs/promises'); if (typeof fs.rm === "function") { await rawOp(fs.rm(path$1, options)); } else { try { const _stat = await fs.stat(path$1); if (_stat.isDirectory()) { await fs.rmdir(path$1, options); } else { await fs.unlink(path$1); } } catch (err) { throw wrapFsError(err); } } } else { const parent = path.dirname(path$1); const name = path.basename(path$1); const dir = await getDirHandle(parent, { root: options.root }); await rawOp(dir.removeEntry(name, options), "directory"); } } /** * Renames the file or directory from the old path to the new path. * * @example * ```ts * // with the default storage * import { rename } from "@ayonli/jsext/fs"; * * await rename("/path/to/old.txt", "/path/to/new.txt"); * ``` * * @example * ```ts * // with a user-selected directory as root (Chromium only) * import { rename } from "@ayonli/jsext/fs"; * * const root = await window.showDirectoryPicker(); * await rename("/path/to/old.txt", "/path/to/new.txt", { root }); * ``` */ async function rename(oldPath, newPath, options = {}) { if (env.isDeno) { await rawOp(Deno.rename(oldPath, newPath)); } else if (env.isNodeLike) { const fs = await import('fs/promises'); await rawOp(fs.rename(oldPath, newPath)); } else { return await copyInBrowser(oldPath, newPath, { root: options.root, recursive: true, move: true, }); } } async function copy(src, dest, options = {}) { var _a, _b; if (typeof src === "object" || typeof dest === "object") { return copyInBrowser(src, dest, { recursive: (_a = options === null || options === void 0 ? void 0 : options.recursive) !== null && _a !== void 0 ? _a : false }); } if (env.isDeno || env.isNodeLike) { const oldStat = await stat(src, { followSymlink: true }); const isDirSrc = oldStat.kind === "directory"; let isDirDest = false; if (isDirSrc && !options.recursive) { throw new error_Exception.default("Cannot copy a directory without the 'recursive' option", { name: "InvalidOperationError", code: 400, }); } try { const newStat = await stat(dest, { followSymlink: true }); isDirDest = newStat.kind === "directory"; if (isDirSrc && !isDirDest) { throw new error_Exception.default(`'${dest}' is not a directory`, { name: "NotDirectoryError", code: 415, }); } } catch (_c) { if (isDirSrc) { await mkdir(dest); isDirDest = true; } } if (env.isDeno) { if (isDirSrc) { const entries = readDir(src, { recursive: true }); for await (const entry of entries) { const _oldPath = path.join(src, entry.relativePath); const _newPath = path.join(dest, entry.relativePath); if (entry.kind === "directory") { await rawOp(Deno.mkdir(_newPath)); } else { await rawOp(Deno.copyFile(_oldPath, _newPath)); } } } else { const _newPath = isDirDest ? path.join(dest, path.basename(src)) : dest; await rawOp(Deno.copyFile(src, _newPath)); } } else { const fs = await import('fs/promises'); if (isDirSrc) { const entries = readDir(src, { recursive: true }); for await (const entry of entries) { const _oldPath = path.join(src, entry.relativePath); const _newPath = path.join(dest, entry.relativePath); if (entry.kind === "directory") { await rawOp(fs.mkdir(_newPath)); } else { await rawOp(fs.copyFile(_oldPath, _newPath)); } } } else { const _newPath = isDirDest ? path.join(dest, path.basename(src)) : dest; await rawOp(fs.copyFile(src, _newPath)); } } } else { return await copyInBrowser(src, dest, { root: options.root, recursive: (_b = options.recursive) !== null && _b !== void 0 ? _b : false, }); } } async function copyInBrowser(src, dest, options = {}) { var _a, _b; if (typeof src === "object" && typeof dest !== "object") { throw new TypeError("The destination must be a FileSystemHandle"); } else if (typeof dest === "object" && typeof src !== "object") { throw new TypeError("The source must be a FileSystemHandle"); } else if (typeof src === "object" && typeof dest === "object") { if (src.kind === "file") { if (dest.kind === "file") { return await copyFileHandleToFileHandle(src, dest); } else { return await copyFileHandleToDirHandle(src, dest); } } else if (dest.kind === "directory") { if (!options.recursive) { throw new error_Exception.default("Cannot copy a directory without the 'recursive' option", { name: "InvalidOperationError", code: 400, }); } return await copyDirHandleToDirHandle(src, dest); } else { throw new error_Exception.default("The destination location is not a directory", { name: "NotDirectoryError", code: 415, }); } } const oldParent = path.dirname(src); const oldName = path.basename(src); let oldDir = await getDirHandle(oldParent, { root: options.root }); const [oldErr, oldFile] = await _try.default(rawOp(oldDir.getFileHandle(oldName), "file")); if (oldFile) { const newParent = path.dirname(dest); const newName = path.basename(dest); let newDir = await getDirHandle(newParent, { root: options.root }); const [newErr, newFile] = await _try.default(rawOp(newDir.getFileHandle(newName, { create: true, }), "file")); if (newFile) { await copyFileHandleToFileHandle(oldFile, newFile); if (options.move) { await rawOp(oldDir.removeEntry(oldName), "directory"); } } else if (((_a = object.as(newErr, error_Exception.default)) === null || _a === void 0 ? void 0 : _a.name) === "IsDirectoryError" && !options.move) { // The destination is a directory, copy the file into the new path // with the old name. newDir = await rawOp(newDir.getDirectoryHandle(newName), "directory"); await copyFileHandleToDirHandle(oldFile, newDir); } else { throw newErr; } } else if (((_b = object.as(oldErr, error_Exception.default)) === null || _b === void 0 ? void 0 : _b.name) === "IsDirectoryError") { if (!options.recursive) { throw new error_Exception.default("Cannot copy a directory without the 'recursive' option", { name: "InvalidOperationError", code: 400, }); } const parent = oldDir; oldDir = await rawOp(oldDir.getDirectoryHandle(oldName), "directory"); const newDir = await getDirHandle(dest, { root: options.root, create: true }); await copyDirHandleToDirHandle(oldDir, newDir); if (options.move) { await rawOp(parent.removeEntry(oldName, { recursive: true }), "directory"); } } else { throw oldErr; } } async function copyFileHandleToFileHandle(src, dest) { try { const srcFile = await src.getFile(); const destFile = await dest.createWritable(); await srcFile.stream().pipeTo(destFile); } catch (err) { throw wrapFsError(err, "file"); } } async function copyFileHandleToDirHandle(src, dest) { try { const srcFile = await src.getFile(); const newFile = await dest.getFileHandle(src.name, { create: true }); const destFile = await newFile.createWritable(); await srcFile.stream().pipeTo(destFile); } catch (err) { throw wrapFsError(err, "file"); } } async function copyDirHandleToDirHandle(src, dest) { const entries = src.entries(); for await (const [_, entry] of entries) { if (entry.kind === "file") { try { const oldFile = await entry.getFile(); const newFile = await dest.getFileHandle(entry.name, { create: true, }); const reader = oldFile.stream(); const writer = await newFile.createWritable(); await reader.pipeTo(writer); } catch (err) { throw wrapFsError(err, "file"); } } else { const newSubDir = await rawOp(dest.getDirectoryHandle(entry.name, { create: true, }), "directory"); await copyDirHandleToDirHandle(entry, newSubDir); } } } /** * Creates a hard link (or symbolic link) from the source path to the destination * path. * * NOTE: This function is only available in Node.js, Deno and Bun. * * @example * ```ts * // create a hard link * import { link } from "@ayonli/jsext/fs"; * * await link("/path/to/file.txt", "/path/to/link.txt"); * ``` * * @example * ```ts * // create a symbolic link * import { link } from "@ayonli/jsext/fs"; * * await link("/path/to/file.txt", "/path/to/link.txt", { symbolic: true }); * ``` */ async function link(src, dest, options = {}) { if (env.isDeno) { if (options.symbolic) { if (runtime.platform() === "windows") { const _stat = await stat(src); await rawOp(Deno.symlink(src, dest, { type: _stat.kind === "directory" ? "dir" : "file", })); } else {