UNPKG

@zenfs/core

Version:

A filesystem, anywhere

920 lines (919 loc) 35.7 kB
var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) { if (value !== null && value !== void 0) { if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected."); var dispose, inner; if (async) { if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined."); dispose = value[Symbol.asyncDispose]; } if (dispose === void 0) { if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined."); dispose = value[Symbol.dispose]; if (async) inner = dispose; } if (typeof dispose !== "function") throw new TypeError("Object not disposable."); if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } }; env.stack.push({ value: value, dispose: dispose, async: async }); } else if (async) { env.stack.push({ async: true }); } return value; }; var __disposeResources = (this && this.__disposeResources) || (function (SuppressedError) { return function (env) { function fail(e) { env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e; env.hasError = true; } var r, s = 0; function next() { while (r = env.stack.pop()) { try { if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next); if (r.dispose) { var result = r.dispose.call(r.value); if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); }); } else s |= 1; } catch (e) { fail(e); } } if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve(); if (env.hasError) throw env.error; } return next(); }; })(typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) { var e = new Error(message); return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e; }); import { Buffer } from 'buffer'; import { Exception, rethrow, UV } from 'kerium'; import { encodeUTF8 } from 'utilium'; import * as constants from '../constants.js'; import { hasAccess, InodeFlags, isDirectory } from '../internal/inode.js'; import { join, matchesGlob } from '../path.js'; import '../polyfills.js'; import { _tempDirName, globToRegex, normalizeMode, normalizeOptions, normalizePath, normalizeTime } from '../utils.js'; import * as _async from '../vfs/async.js'; import { checkAccess } from '../vfs/config.js'; import { deleteFD, fromFD, toFD } from '../vfs/file.js'; import * as flags from '../vfs/flags.js'; import { _statfs, resolveMount } from '../vfs/shared.js'; import { emitChange, FSWatcher } from '../vfs/watchers.js'; import { Dir, Dirent } from './dir.js'; import { createInterface } from './readline.js'; import { BigIntStats, Stats } from './stats.js'; import { ReadStream, WriteStream } from './streams.js'; export * as constants from '../constants.js'; export class FileHandle { context; fd; vfs; constructor(context, fd) { this.context = context; this.fd = fd; this.vfs = fromFD(context, fd); } _emitChange() { emitChange(this.context, 'change', this.vfs.path); } /** * Asynchronous fchown(2) - Change ownership of a file. */ async chown(uid, gid) { await this.vfs.chown(uid, gid); this._emitChange(); } /** * Asynchronous fchmod(2) - Change permissions of a file. * @param mode A file mode. If a string is passed, it is parsed as an octal integer. */ async chmod(mode) { const numMode = normalizeMode(mode, -1); if (numMode < 0) throw UV('EINVAL', 'chmod', this.vfs.path); await this.vfs.chmod(numMode); this._emitChange(); } /** * Asynchronous fdatasync(2) - synchronize a file's in-core state with storage device. */ datasync() { return this.sync(); } /** * Asynchronous fsync(2) - synchronize a file's in-core state with the underlying storage device. */ async sync() { await this.vfs.sync(); } /** * Asynchronous ftruncate(2) - Truncate a file to a specified length. * @param length If not specified, defaults to `0`. */ async truncate(length = 0) { await this.vfs.truncate(length); this._emitChange(); } /** * Asynchronously change file timestamps of the file. * @param atime The last access time. If a string is provided, it will be coerced to number. * @param mtime The last modified time. If a string is provided, it will be coerced to number. */ async utimes(atime, mtime) { atime = normalizeTime(atime); mtime = normalizeTime(mtime); await this.vfs.utimes(atime, mtime); this._emitChange(); } /** * Asynchronously append data to a file, creating the file if it does not exist. The underlying file will _not_ be closed automatically. * The `FileHandle` must have been opened for appending. * @param data The data to write. If something other than a `Buffer` or `Uint8Array` is provided, the value is coerced to a string. * @param _options Either the encoding for the file, or an object optionally specifying the encoding, file mode, and flag. * - `encoding` defaults to `'utf8'`. * - `mode` defaults to `0o666`. * - `flag` defaults to `'a'`. */ async appendFile(data, _options = {}) { const options = normalizeOptions(_options, 'utf8', 'a', 0o644); const flag = flags.parse(options.flag); if (!(flag & constants.O_APPEND)) throw UV('EBADF', 'write', this.vfs.path); const encodedData = typeof data == 'string' ? Buffer.from(data, options.encoding) : data; await this.vfs.write(encodedData, 0, encodedData.length); this._emitChange(); } async read(buffer, offset, length, position) { if (typeof offset == 'object' && offset != null) { position = offset.position; length = offset.length; offset = offset.offset; } if (!ArrayBuffer.isView(buffer) && typeof buffer == 'object') { position = buffer.position; length = buffer.length; offset = buffer.offset; buffer = buffer.buffer; } if (position && position > Number.MAX_SAFE_INTEGER) throw UV('EINVAL'); if (typeof position == 'bigint') position = Number(position); position = Number.isSafeInteger(position) ? position : this.vfs.position; buffer ||= new Uint8Array(this.vfs.inode.size); offset ??= 0; const bytesRead = await this.vfs.read(buffer, offset, length ?? buffer.byteLength - offset, position); return { bytesRead, buffer }; } async readFile(_options) { const options = normalizeOptions(_options, null, 'r', 0o444); const flag = flags.parse(options.flag); if (flag & constants.O_WRONLY) throw UV('EBADF', 'read', this.vfs.path); const { size } = await this.stat(); const data = new Uint8Array(size); await this.vfs.read(data, 0, size, 0); const buffer = Buffer.from(data); return options.encoding ? buffer.toString(options.encoding) : buffer; } /** * Read file data using a `ReadableStream`. * The handle will not be closed automatically. */ readableWebStream(options = {}) { if (this.vfs.isClosed) throw UV('EBADF', 'readableWebStream', this.vfs.path); return this.vfs.fs.streamRead(this.vfs.internalPath, options); } /** * Not part of the Node.js API! * * Write file data using a `WritableStream`. * The handle will not be closed automatically. * @internal */ writableWebStream(options = {}) { if (this.vfs.isClosed) throw UV('EBADF', 'writableWebStream', this.vfs.path); if (this.vfs.inode.flags & InodeFlags.Immutable) throw UV('EPERM', 'writableWebStream', this.vfs.path); return this.vfs.fs.streamWrite(this.vfs.internalPath, options); } /** * Creates a readline Interface object that allows reading the file line by line * @param options Options for creating a read stream * @returns A readline interface for reading the file line by line */ readLines(options) { if (this.vfs.isClosed || this.vfs.flag & constants.O_WRONLY) throw UV('EBADF', 'read', this.vfs.path); return createInterface({ input: this.createReadStream(options), crlfDelay: Infinity }); } [Symbol.asyncDispose]() { return this.close(); } async stat(opts) { if (this.vfs.isClosed) throw UV('EBADF', 'stat', this.vfs.path); if (checkAccess && !hasAccess(this.context, this.vfs.inode, constants.R_OK)) throw UV('EACCES', 'stat', this.vfs.path); return opts?.bigint ? new BigIntStats(this.vfs.inode) : new Stats(this.vfs.inode); } /** * Asynchronously writes `string` to the file. * The `FileHandle` must have been opened for writing. * It is unsafe to call `write()` multiple times on the same file without waiting for the `Promise` * to be resolved (or rejected). For this scenario, `createWriteStream` is strongly recommended. */ async write(data, options, lenOrEnc, position) { let buffer, offset, length; if (typeof options == 'object' && options != null) { lenOrEnc = options.length; position = options.position; options = options.offset; } if (typeof data === 'string') { position = typeof options === 'number' ? options : null; offset = 0; buffer = Buffer.from(data, typeof lenOrEnc === 'string' ? lenOrEnc : 'utf8'); length = buffer.length; } else { buffer = new Uint8Array(data.buffer, data.byteOffset, data.byteLength); offset = options ?? 0; length = typeof lenOrEnc == 'number' ? lenOrEnc : buffer.byteLength; position = typeof position === 'number' ? position : null; } position ??= this.vfs.position; const bytesWritten = await this.vfs.write(buffer, offset, length, position); this._emitChange(); return { buffer: data, bytesWritten }; } /** * Asynchronously writes data to a file, replacing the file if it already exists. The underlying file will _not_ be closed automatically. * The `FileHandle` must have been opened for writing. * It is unsafe to call `writeFile()` multiple times on the same file without waiting for the `Promise` to be resolved (or rejected). * @param data The data to write. If something other than a `Buffer` or `Uint8Array` is provided, the value is coerced to a string. * @param _options Either the encoding for the file, or an object optionally specifying the encoding, file mode, and flag. * - `encoding` defaults to `'utf8'`. * - `mode` defaults to `0o666`. * - `flag` defaults to `'w'`. */ async writeFile(data, _options = {}) { const options = normalizeOptions(_options, 'utf8', 'w', 0o644); const flag = flags.parse(options.flag); if (!(flag & constants.O_WRONLY || flag & constants.O_RDWR)) throw UV('EBADF', 'writeFile', this.vfs.path); const encodedData = typeof data == 'string' ? Buffer.from(data, options.encoding) : data; await this.vfs.write(encodedData, 0, encodedData.length, 0); this._emitChange(); } /** * Asynchronous close(2) - close a `FileHandle`. */ async close() { await this.vfs.close(); deleteFD(this.context, this.fd); } /** * Asynchronous `writev`. Writes from multiple buffers. * @param buffers An array of Uint8Array buffers. * @param position The position in the file where to begin writing. * @returns The number of bytes written. */ async writev(buffers, position) { if (typeof position == 'number') this.vfs.position = position; let bytesWritten = 0; for (const buffer of buffers) { bytesWritten += (await this.write(buffer)).bytesWritten; } return { bytesWritten, buffers }; } /** * Asynchronous `readv`. Reads into multiple buffers. * @param buffers An array of Uint8Array buffers. * @param position The position in the file where to begin reading. * @returns The number of bytes read. */ async readv(buffers, position) { if (typeof position == 'number') this.vfs.position = position; let bytesRead = 0; for (const buffer of buffers) { bytesRead += (await this.read(buffer)).bytesRead; } return { bytesRead, buffers }; } /** * Creates a stream for reading from the file. * @param options Options for the readable stream */ createReadStream(options = {}) { if (this.vfs.isClosed || this.vfs.flag & constants.O_WRONLY) throw UV('EBADF', 'createReadStream', this.vfs.path); return new ReadStream(options, this); } /** * Creates a stream for writing to the file. * @param options Options for the writeable stream. */ createWriteStream(options = {}) { if (this.vfs.isClosed) throw UV('EBADF', 'createWriteStream', this.vfs.path); if (this.vfs.inode.flags & InodeFlags.Immutable) throw UV('EPERM', 'createWriteStream', this.vfs.path); if (this.vfs.fs.attributes.has('readonly')) throw UV('EROFS', 'createWriteStream', this.vfs.path); return new WriteStream(options, this); } } export async function rename(oldPath, newPath) { await _async.rename.call(this, oldPath, newPath); } rename; /** * Test whether or not `path` exists by checking with the file system. */ export async function exists(path) { path = normalizePath(path); try { const { fs, path: resolved } = await _async.resolve(this, path); return await fs.exists(resolved); } catch (e) { if (e instanceof Exception && e.code == 'ENOENT') { return false; } throw e; } } export async function stat(path, options) { const stats = await _async.stat.call(this, path, false); return options?.bigint ? new BigIntStats(stats) : new Stats(stats); } stat; export async function lstat(path, options) { const stats = await _async.stat.call(this, path, true); return options?.bigint ? new BigIntStats(stats) : new Stats(stats); } lstat; export async function truncate(path, len = 0) { const env_1 = { stack: [], error: void 0, hasError: false }; try { const handle = __addDisposableResource(env_1, await open.call(this, path, 'r+'), true); await handle.truncate(len); } catch (e_1) { env_1.error = e_1; env_1.hasError = true; } finally { const result_1 = __disposeResources(env_1); if (result_1) await result_1; } } truncate; export async function unlink(path) { path = normalizePath(path); const { fs, path: resolved } = resolveMount(path, this); const $ex = { syscall: 'unlink', path }; const stats = await fs.stat(resolved).catch(rethrow($ex)); if (checkAccess && !hasAccess(this, stats, constants.W_OK)) throw UV('EACCES', $ex); await fs.unlink(resolved).catch(rethrow($ex)); emitChange(this, 'rename', path.toString()); } unlink; /** * Asynchronous file open. * @see https://nodejs.org/api/fs.html#fspromisesopenpath-flags-mode * @param flag {@link https://nodejs.org/api/fs.html#file-system-flags} * @param mode Mode to use to open the file. Can be ignored if the filesystem doesn't support permissions. */ export async function open(path, flag = 'r', mode = 0o644) { const handle = await _async.open(this, path, { flag, mode }); return new FileHandle(this, toFD(handle)); } open; export async function readFile(path, _options) { const env_2 = { stack: [], error: void 0, hasError: false }; try { const options = normalizeOptions(_options, null, 'r', 0o444); const handle = __addDisposableResource(env_2, typeof path == 'object' && 'fd' in path ? path : await open.call(this, path, options.flag, options.mode), true); return await handle.readFile(options); } catch (e_2) { env_2.error = e_2; env_2.hasError = true; } finally { const result_2 = __disposeResources(env_2); if (result_2) await result_2; } } readFile; /** * Asynchronously writes data to a file, replacing the file if it already exists. * * The encoding option is ignored if data is a buffer. * @option encoding Defaults to `'utf8'`. * @option mode Defaults to `0644`. * @option flag Defaults to `'w'`. */ export async function writeFile(path, data, _options) { const env_3 = { stack: [], error: void 0, hasError: false }; try { const options = normalizeOptions(_options, 'utf8', 'w+', 0o644); const handle = __addDisposableResource(env_3, path instanceof FileHandle ? path : await open.call(this, path.toString(), options.flag, options.mode), true); const _data = typeof data == 'string' ? data : data instanceof DataView ? new Uint8Array(data.buffer, data.byteOffset, data.byteLength) : data; if (typeof _data != 'string' && !(_data instanceof Uint8Array)) throw new TypeError('The "data" argument must be of type string or an instance of Buffer, TypedArray, or DataView. Received ' + typeof data); await handle.writeFile(_data, options); } catch (e_3) { env_3.error = e_3; env_3.hasError = true; } finally { const result_3 = __disposeResources(env_3); if (result_3) await result_3; } } writeFile; /** * Asynchronously append data to a file, creating the file if it not yet exists. * @option encoding Defaults to `'utf8'`. * @option mode Defaults to `0644`. * @option flag Defaults to `'a'`. */ export async function appendFile(path, data, _options) { const env_4 = { stack: [], error: void 0, hasError: false }; try { const options = normalizeOptions(_options, 'utf8', 'a', 0o644); const flag = flags.parse(options.flag); const $ex = { syscall: 'write', path: path instanceof FileHandle ? path['vfs'].path : path.toString() }; if (!(flag & constants.O_APPEND)) throw UV('EBADF', $ex); const encodedData = typeof data == 'string' ? Buffer.from(data, options.encoding) : new Uint8Array(data.buffer, data.byteOffset, data.byteLength); const handle = __addDisposableResource(env_4, typeof path == 'object' && 'fd' in path ? path : await open.call(this, path, options.flag, options.mode), true); await handle.appendFile(encodedData, options); } catch (e_4) { env_4.error = e_4; env_4.hasError = true; } finally { const result_4 = __disposeResources(env_4); if (result_4) await result_4; } } appendFile; export async function rmdir(path) { path = normalizePath(path); const { fs, path: resolved } = await _async.resolve(this, path); const $ex = { syscall: 'rmdir', path }; const stats = await fs.stat(resolved).catch(rethrow($ex)); if (!stats) throw UV('ENOENT', $ex); if (!isDirectory(stats)) throw UV('ENOTDIR', $ex); if (checkAccess && !hasAccess(this, stats, constants.W_OK)) throw UV('EACCES', $ex); await fs.rmdir(resolved).catch(rethrow($ex)); emitChange(this, 'rename', path.toString()); } rmdir; export async function mkdir(path, options) { options = typeof options === 'object' ? options : { mode: options }; const mode = normalizeMode(options?.mode, 0o777); return await _async.mkdir.call(this, path, { ...options, mode }); } mkdir; export async function readdir(path, options) { path = normalizePath(path); const opt = typeof options === 'object' && options != null ? options : { encoding: options, withFileTypes: false, recursive: false }; const rawEntries = await _async.readdir.call(this, path, opt); const values = []; for (const entry of rawEntries) { if (opt.withFileTypes) { values.push(Dirent.from(entry, opt.encoding)); } else if (opt.encoding == 'buffer') { values.push(Buffer.from(entry.path)); } else { values.push(entry.path); } } return values; } readdir; export async function link(path, dest) { return await _async.link.call(this, path, dest); } link; /** * `symlink`. * @param dest target path * @param path link path * @param type can be either `'dir'` or `'file'` (default is `'file'`) */ export async function symlink(dest, path, type = 'file') { const env_5 = { stack: [], error: void 0, hasError: false }; try { if (!['file', 'dir', 'junction'].includes(type)) throw new TypeError('Invalid symlink type: ' + type); path = normalizePath(path); if (await exists.call(this, path)) throw UV('EEXIST', 'symlink', path); const handle = __addDisposableResource(env_5, await _async.open(this, path, { flag: 'w+', mode: 0o644, preserveSymlinks: true }), true); const encoded = encodeUTF8(normalizePath(dest, true)); await handle.write(encoded, 0, encoded.length, 0); await handle.chmod(constants.S_IFLNK); } catch (e_5) { env_5.error = e_5; env_5.hasError = true; } finally { const result_5 = __disposeResources(env_5); if (result_5) await result_5; } } symlink; export async function readlink(path, options) { path = normalizePath(path); const buf = Buffer.from(await _async.readlink.call(this, path), 'utf-8'); const encoding = typeof options == 'object' ? options?.encoding : options; // always defaults to utf-8 to avoid wrangler (cloudflare) worker "unknown encoding" exception return encoding == 'buffer' ? buf : buf.toString((encoding ?? 'utf-8')); } readlink; export async function chown(path, uid, gid) { const env_6 = { stack: [], error: void 0, hasError: false }; try { const handle = __addDisposableResource(env_6, await open.call(this, path, 'r+'), true); await handle.chown(uid, gid); } catch (e_6) { env_6.error = e_6; env_6.hasError = true; } finally { const result_6 = __disposeResources(env_6); if (result_6) await result_6; } } chown; export async function lchown(path, uid, gid) { const env_7 = { stack: [], error: void 0, hasError: false }; try { const handle = __addDisposableResource(env_7, await _async.open(this, path, { flag: 'r+', mode: 0o644, preserveSymlinks: true, allowDirectory: true, }), true); await handle.chown(uid, gid); } catch (e_7) { env_7.error = e_7; env_7.hasError = true; } finally { const result_7 = __disposeResources(env_7); if (result_7) await result_7; } } lchown; export async function chmod(path, mode) { const env_8 = { stack: [], error: void 0, hasError: false }; try { const handle = __addDisposableResource(env_8, await open.call(this, path, 'r+'), true); await handle.chmod(mode); } catch (e_8) { env_8.error = e_8; env_8.hasError = true; } finally { const result_8 = __disposeResources(env_8); if (result_8) await result_8; } } chmod; export async function lchmod(path, mode) { const env_9 = { stack: [], error: void 0, hasError: false }; try { mode = normalizeMode(mode); const handle = __addDisposableResource(env_9, await _async.open(this, path, { flag: 'r+', mode: 0o644, preserveSymlinks: true, allowDirectory: true, }), true); await handle.chmod(mode); } catch (e_9) { env_9.error = e_9; env_9.hasError = true; } finally { const result_9 = __disposeResources(env_9); if (result_9) await result_9; } } lchmod; /** * Change file timestamps of the file referenced by the supplied path. */ export async function utimes(path, atime, mtime) { const env_10 = { stack: [], error: void 0, hasError: false }; try { const handle = __addDisposableResource(env_10, await _async.open(this, path, { flag: 'r+', allowDirectory: true, }), true); await handle.utimes(normalizeTime(atime), normalizeTime(mtime)); } catch (e_10) { env_10.error = e_10; env_10.hasError = true; } finally { const result_10 = __disposeResources(env_10); if (result_10) await result_10; } } utimes; /** * Change file timestamps of the file referenced by the supplied path. */ export async function lutimes(path, atime, mtime) { const env_11 = { stack: [], error: void 0, hasError: false }; try { const handle = __addDisposableResource(env_11, await _async.open(this, path, { flag: 'r+', mode: 0o644, preserveSymlinks: true, allowDirectory: true, }), true); await handle.utimes(normalizeTime(atime), normalizeTime(mtime)); } catch (e_11) { env_11.error = e_11; env_11.hasError = true; } finally { const result_11 = __disposeResources(env_11); if (result_11) await result_11; } } lutimes; export async function realpath(path, options) { const encoding = typeof options == 'string' ? options : (options?.encoding ?? 'utf8'); path = normalizePath(path); const { fullPath } = await _async.resolve(this, path); if (encoding == 'utf8' || encoding == 'utf-8') return fullPath; const buf = Buffer.from(fullPath, 'utf-8'); if (encoding == 'buffer') return buf; return buf.toString(encoding); } realpath; export function watch(filename, options = {}) { const watcher = new FSWatcher(this, filename.toString(), typeof options !== 'string' ? options : { encoding: options }); // A queue to hold change events, since we need to resolve them in the async iterator const eventQueue = []; let done = false; watcher.on('change', (eventType, filename) => { eventQueue.shift()?.({ value: { eventType, filename }, done: false }); }); function cleanup() { done = true; watcher.close(); for (const resolve of eventQueue) { resolve({ value: null, done }); } eventQueue.length = 0; // Clear the queue return Promise.resolve({ value: undefined, done: true }); } return { async next() { if (done) return Promise.resolve({ value: undefined, done }); const { promise, resolve } = Promise.withResolvers(); eventQueue.push(resolve); return promise; }, return: cleanup, throw: cleanup, async [Symbol.asyncDispose]() { await cleanup(); }, [Symbol.asyncIterator]() { return this; }, }; } watch; export async function access(path, mode = constants.F_OK) { if (!checkAccess) return; const stats = await stat.call(this, path); if (!stats.hasAccess(mode, this)) throw UV('EACCES', 'access', path.toString()); } access; /** * Asynchronous `rm`. Removes files or directories (recursively). * @param path The path to the file or directory to remove. */ export async function rm(path, options) { path = normalizePath(path); const stats = await lstat.call(this, path).catch((error) => { if (error.code == 'ENOENT' && options?.force) return undefined; throw error; }); if (!stats) return; switch (stats.mode & constants.S_IFMT) { case constants.S_IFDIR: if (options?.recursive) { for (const entry of await readdir.call(this, path)) { await rm.call(this, join(path, entry), options); } } await rmdir.call(this, path); break; case constants.S_IFREG: case constants.S_IFLNK: case constants.S_IFBLK: case constants.S_IFCHR: await unlink.call(this, path); break; case constants.S_IFIFO: case constants.S_IFSOCK: default: throw UV('ENOSYS', 'rm', path); } } rm; export async function mkdtemp(prefix, options) { const encoding = typeof options === 'object' ? options?.encoding : options || 'utf8'; const path = _tempDirName(prefix); await mkdir.call(this, path); return encoding == 'buffer' ? Buffer.from(path) : path; } mkdtemp; /** * The resulting Promise holds an async-disposable object whose `path` property holds the created directory path. * When the object is disposed, the directory and its contents will be removed asynchronously if it still exists. * If the directory cannot be deleted, disposal will throw an error. * The object has an async `remove()` method which will perform the same task. * @todo Add `satisfies` and maybe change return type once @types/node adds this. */ export async function mkdtempDisposable(prefix, options) { const path = _tempDirName(prefix); await mkdir.call(this, path); const remove = () => rm(path, { recursive: true, force: true }); return { path, remove, [Symbol.asyncDispose]: remove }; } /** * Asynchronous `copyFile`. Copies a file. * @param src The source file. * @param dest The destination file. * @param mode Optional flags for the copy operation. Currently supports these flags: * * `fs.constants.COPYFILE_EXCL`: If the destination file already exists, the operation fails. */ export async function copyFile(src, dest, mode) { src = normalizePath(src); dest = normalizePath(dest); if (mode && mode & constants.COPYFILE_EXCL && (await exists.call(this, dest))) throw UV('EEXIST', 'copyFile', dest); await writeFile.call(this, dest, await readFile.call(this, src)); emitChange(this, 'rename', dest.toString()); } copyFile; /** * Asynchronous `opendir`. Opens a directory. * @param path The path to the directory. * @param options Options for opening the directory. * @returns A `Dir` object representing the opened directory. * @todo Use options */ export function opendir(path, options) { path = normalizePath(path); return Promise.resolve(new Dir(path, this)); } opendir; /** * Asynchronous `cp`. Recursively copies a file or directory. * @param source The source file or directory. * @param destination The destination file or directory. * @param opts Options for the copy operation. Currently supports these options from Node.js 'fs.await cp': * * `dereference`: Dereference symbolic links. * * `errorOnExist`: Throw an error if the destination file or directory already exists. * * `filter`: A function that takes a source and destination path and returns a boolean, indicating whether to copy `source` element. * * `force`: Overwrite the destination if it exists, and overwrite existing readonly destination files. * * `preserveTimestamps`: Preserve file timestamps. * * `recursive`: If `true`, copies directories recursively. */ export async function cp(source, destination, opts) { source = normalizePath(source); destination = normalizePath(destination); const srcStats = await lstat.call(this, source); // Use lstat to follow symlinks if not dereferencing if (opts?.errorOnExist && (await exists.call(this, destination))) throw UV('EEXIST', 'cp', destination); switch (srcStats.mode & constants.S_IFMT) { case constants.S_IFDIR: { if (!opts?.recursive) throw UV('EISDIR', 'cp', source); const [entries] = await Promise.all([ readdir.call(this, source, { withFileTypes: true }), mkdir.call(this, destination, { recursive: true }), ] // Ensure the destination directory exists ); const _cp = async (dirent) => { if (opts.filter && !opts.filter(join(source, dirent.name), join(destination, dirent.name))) { return; // Skip if the filter returns false } await cp.call(this, join(source, dirent.name), join(destination, dirent.name), opts); }; await Promise.all(entries.map(_cp)); break; } case constants.S_IFREG: case constants.S_IFLNK: await copyFile.call(this, source, destination); break; case constants.S_IFBLK: case constants.S_IFCHR: case constants.S_IFIFO: case constants.S_IFSOCK: default: throw UV('ENOSYS', 'cp', source); } // Optionally preserve timestamps if (opts?.preserveTimestamps) { await utimes.call(this, destination, srcStats.atime, srcStats.mtime); } } cp; export async function statfs(path, opts) { path = normalizePath(path); const { fs } = resolveMount(path, this); return Promise.resolve(_statfs(fs, opts?.bigint)); } export function glob(pattern, opt) { pattern = Array.isArray(pattern) ? pattern : [pattern]; const { cwd = '/', withFileTypes = false, exclude = () => false } = opt || {}; const normalizedPatterns = pattern.map(p => p.replace(/^\/+/g, '')); const hasGlobStar = normalizedPatterns.some(p => p.includes('**')); const patternBases = normalizedPatterns.map(p => { const firstGlob = p.search(/[*?[\]{]/); if (firstGlob === -1) return p; const lastSlash = p.lastIndexOf('/', firstGlob); return lastSlash === -1 ? '' : p.slice(0, lastSlash); }); const regexPatterns = normalizedPatterns.map(globToRegex); async function* recursiveList(dir) { const entries = await readdir(dir, { withFileTypes, encoding: 'utf8' }); for (const entry of entries) { const fullPath = join(dir, withFileTypes ? entry.name : entry); if (typeof exclude != 'function' ? exclude.some(p => matchesGlob(p, fullPath)) : exclude((withFileTypes ? entry : fullPath))) continue; const relativePath = fullPath.replace(/^\/+/g, ''); if ((await stat(fullPath)).isDirectory()) { if (hasGlobStar || patternBases.some(base => relativePath === base || base.startsWith(relativePath + '/'))) { yield* recursiveList(fullPath); } } if (regexPatterns.some(rx => rx.test(relativePath))) { yield withFileTypes ? entry : relativePath; } } } return recursiveList(cwd instanceof URL ? cwd.pathname : cwd); } glob;