@zenfs/core
Version:
A filesystem, anywhere
920 lines (919 loc) • 35.7 kB
JavaScript
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;