'use strict';
const {
MathRandom,
ObjectFreeze,
StringPrototypeLastIndexOf,
StringPrototypeSlice,
StringPrototypeStartsWith,
Symbol,
SymbolDispose,
} = primordials;
const {
codes: {
ERR_INVALID_STATE,
},
} = require('internal/errors');
const { MemoryProvider } = require('internal/vfs/providers/memory');
const path = require('path');
const { posix: pathPosix, resolve: resolvePath, sep, toNamespacedPath } = path;
const { join: joinPath } = pathPosix;
const {
getLayerRoot,
getRelativePath,
getVfsRoot,
} = require('internal/vfs/router');
const {
openVirtualFd,
getVirtualFd,
closeVirtualFd,
} = require('internal/vfs/fd');
const {
createENOENT,
createEBADF,
createEBUSY,
createEISDIR,
} = require('internal/vfs/errors');
const { encodeRealpathResult, getOptions } = require('internal/fs/utils');
const { VirtualReadStream, VirtualWriteStream } = require('internal/vfs/streams');
const { VirtualDir } = require('internal/vfs/dir');
const { emitExperimentalWarning, kEmptyObject } = require('internal/util');
const { validateBoolean } = require('internal/validators');
let debug = require('internal/util/debuglog').debuglog('vfs', (fn) => {
debug = fn;
});
const kProvider = Symbol('kProvider');
const kMountPoint = Symbol('kMountPoint');
// Cached normalizeMountedPath(kMountPoint) so the hot dispatch path
// does not recompute toNamespacedPath(resolve(...)) on every fs call.
const kNormalizedMountPoint = Symbol('kNormalizedMountPoint');
const kMounted = Symbol('kMounted');
const kPromises = Symbol('kPromises');
const kLayerId = Symbol('kLayerId');
const kReservedRoot = Symbol('kReservedRoot');
const kLoadLayer = Symbol('kLoadLayer');
// Layer 0 is reserved for the file system --vfs-load mounts, so that source is
// at the same mount point in every thread whatever else a thread mounts, and a
// path into it stays valid in a worker. Everything else is numbered from 1, in
// the order it is mounted.
const kLoadLayerId = 0;
let nextLayerId = kLoadLayerId + 1;
/**
* Normalizes paths for mount-point comparisons. On Windows, `path.resolve('/x')`
* yields `C:\x`, while loader internals often pass namespaced paths like
* `\\?\C:\x`. Compare using a single canonical representation so POSIX-style
* rooted test paths, drive-absolute paths, and namespaced paths all route to the
* same VFS mount.
* @param {string} inputPath
* @returns {string}
*/
function normalizeMountedPath(inputPath) {
return toNamespacedPath(resolvePath(inputPath));
}
const kTempChars =
'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
function randomSuffix() {
let suffix = '';
for (let i = 0; i < 6; i++) {
suffix += kTempChars[(MathRandom() * kTempChars.length) | 0];
}
return suffix;
}
// The root of a file system is its mount point, and like any mount point it
// cannot be removed or renamed, nor replaced by a rename.
function checkNotRoot(providerPath, syscall, path) {
if (providerPath === '/') throw createEBUSY(syscall, path);
}
let registerVFS;
let deregisterVFS;
function loadVfsSetup() {
if (!registerVFS) {
const setup = require('internal/vfs/setup');
registerVFS = setup.registerVFS;
deregisterVFS = setup.deregisterVFS;
}
}
/**
* Virtual File System implementation using Provider architecture.
* Wraps a Provider and exposes an fs-like API operating on
* provider-relative paths.
*/
class VirtualFileSystem {
/**
* @param {VirtualProvider|object} [providerOrOptions] The provider to use, or options
* @param {object} [options] Configuration options
*/
constructor(providerOrOptions, options = kEmptyObject) {
let provider = null;
if (providerOrOptions !== undefined && providerOrOptions !== null) {
if (typeof providerOrOptions.openSync === 'function') {
provider = providerOrOptions;
} else if (typeof providerOrOptions === 'object') {
options = providerOrOptions;
provider = null;
}
}
if (options.emitExperimentalWarning !== undefined) {
validateBoolean(options.emitExperimentalWarning, 'options.emitExperimentalWarning');
}
if (options.emitExperimentalWarning !== false) {
emitExperimentalWarning('VirtualFileSystem');
}
this[kProvider] = provider ?? new MemoryProvider();
this[kPromises] = null;
if (options[kReservedRoot] === true) {
// Serves the reserved root directory itself. It is not a layer, so it
// takes no layer id and leaves the numbering of real mounts alone.
const root = getVfsRoot();
this[kMountPoint] = root;
this[kNormalizedMountPoint] = normalizeMountedPath(root);
this[kMounted] = true;
this[kLayerId] = -1;
return;
}
this[kMountPoint] = null;
this[kNormalizedMountPoint] = null;
this[kMounted] = false;
this[kLayerId] = options[kLoadLayer] === true ? kLoadLayerId : nextLayerId++;
}
/**
* Gets the underlying provider.
* @returns {VirtualProvider}
*/
get provider() {
return this[kProvider];
}
/**
* Gets the mount point path, or null if not mounted.
* @returns {string|null}
*/
get mountPoint() {
return this[kMountPoint];
}
/**
* Gets the mount point as a `file:` URL string, or null if not
* mounted.
* @returns {string|null}
*/
get mountPointURL() {
if (this[kMountPoint] === null) return null;
const { pathToFileURL } = require('internal/url');
return pathToFileURL(this[kMountPoint]).href;
}
/**
* Returns true if VFS is mounted.
* @returns {boolean}
*/
get mounted() {
return this[kMounted];
}
/**
* Returns true if the provider is read-only.
* @returns {boolean}
*/
get readonly() {
return this[kProvider].readonly;
}
/**
* Mounts the VFS at its reserved mount point and returns that
* mount point. The mount point is `${os.devNull}/vfs/<layerId>` -
* a location that cannot exist on the real file system (`os.devNull`
* is a character device on POSIX and a device-namespace path on
* Windows; neither can have child filesystem entries) - so virtual
* paths never conflate with real paths, and the owning layer of any
* path is decidable from the path alone.
* @returns {string} The absolute mount point
*/
mount() {
if (this[kMounted]) {
throw new ERR_INVALID_STATE('VFS is already mounted');
}
const mountPoint = getLayerRoot(this[kLayerId]);
this[kMountPoint] = mountPoint;
this[kNormalizedMountPoint] = normalizeMountedPath(mountPoint);
this[kMounted] = true;
debug('mount %s', mountPoint);
loadVfsSetup();
registerVFS(this);
return mountPoint;
}
/**
* Unmounts the VFS.
*/
unmount() {
debug('unmount %s', this[kMountPoint]);
loadVfsSetup();
deregisterVFS(this);
this[kMountPoint] = null;
this[kNormalizedMountPoint] = null;
this[kMounted] = false;
}
/**
* Disposes of the VFS by unmounting it.
* Supports the Explicit Resource Management proposal (using declaration).
*/
[SymbolDispose]() {
if (this[kMounted]) {
this.unmount();
}
}
/**
* Returns true when `normalizedInput` falls inside this VFS's
* reserved mount point. The caller must have already passed the
* input through `normalizeMountedPath`; the dispatch loop in
* `internal/vfs/setup.js` normalizes once and hands the same value
* to every candidate layer.
* @param {string} normalizedInput
* @returns {boolean}
*/
shouldHandleNormalized(normalizedInput) {
const mountPoint = this[kNormalizedMountPoint];
if (mountPoint === null) return false;
if (normalizedInput === mountPoint) return true;
return StringPrototypeStartsWith(normalizedInput, mountPoint + sep);
}
/**
* Converts an absolute mounted path to a provider-relative POSIX path.
* If not mounted, treats the path as already provider-relative.
* @param {string} inputPath The path to convert
* @returns {string}
*/
#toProviderPath(inputPath) {
const mountPoint = this[kNormalizedMountPoint];
if (mountPoint !== null) {
const resolved = normalizeMountedPath(inputPath);
if (!this.shouldHandleNormalized(resolved)) {
throw createENOENT('open', inputPath);
}
return getRelativePath(resolved, mountPoint);
}
return pathPosix.normalize(inputPath);
}
/**
* Converts a provider-relative path back to a mounted path.
* If not mounted, returns the path as-is.
* @param {string} providerPath The provider-relative path
* @returns {string} The mounted path
*/
#toMountedPath(providerPath) {
if (this[kMounted] && this[kMountPoint]) {
// path.join() would keep the trailing separator of the provider root.
if (providerPath === '/') return this[kMountPoint];
return path.join(this[kMountPoint], providerPath);
}
return providerPath;
}
/**
* Checks if a path exists synchronously.
* @param {string} filePath The path to check
* @returns {boolean}
*/
existsSync(filePath) {
try {
const providerPath = this.#toProviderPath(filePath);
return this[kProvider].existsSync(providerPath);
} catch {
return false;
}
}
/**
* Gets stats for a path synchronously.
* @param {string} filePath The path to stat
* @param {object} [options] Options
* @returns {Stats}
*/
statSync(filePath, options) {
const providerPath = this.#toProviderPath(filePath);
return this[kProvider].statSync(providerPath, options);
}
/**
* Gets stats for a path synchronously without following symlinks.
* @param {string} filePath The path to stat
* @param {object} [options] Options
* @returns {Stats}
*/
lstatSync(filePath, options) {
const providerPath = this.#toProviderPath(filePath);
return this[kProvider].lstatSync(providerPath, options);
}
/**
* Reads a file synchronously.
* @param {string} filePath The path to read
* @param {object|string} [options] Options or encoding
* @returns {Buffer|string}
*/
readFileSync(filePath, options) {
const providerPath = this.#toProviderPath(filePath);
return this[kProvider].readFileSync(providerPath, options);
}
/**
* Writes a file synchronously.
* @param {string} filePath The path to write
* @param {Buffer|string} data The data to write
* @param {object} [options] Options
*/
writeFileSync(filePath, data, options) {
const providerPath = this.#toProviderPath(filePath);
this[kProvider].writeFileSync(providerPath, data, options);
}
/**
* Appends to a file synchronously.
* @param {string} filePath The path to append to
* @param {Buffer|string} data The data to append
* @param {object} [options] Options
*/
appendFileSync(filePath, data, options) {
const providerPath = this.#toProviderPath(filePath);
this[kProvider].appendFileSync(providerPath, data, options);
}
/**
* Reads directory contents synchronously.
* @param {string} dirPath The directory path
* @param {object} [options] Options
* @returns {string[]|Dirent[]}
*/
readdirSync(dirPath, options) {
const providerPath = this.#toProviderPath(dirPath);
const result = this[kProvider].readdirSync(providerPath, options);
return this.#toMountedDirents(dirPath, result, options);
}
/**
* Rewrites the Dirents of a listing of `dirPath` from provider-relative to
* VFS paths, so that each `parentPath` is the directory the entry is in.
* @param {string} dirPath The listed directory, as given by the caller
* @param {string[]|Dirent[]} result The provider's listing
* @param {object} [options] The readdir options
* @returns {string[]|Dirent[]}
*/
#toMountedDirents(dirPath, result, options) {
if (options?.withFileTypes !== true) return result;
const recursive = options?.recursive === true;
// A mounted VFS is addressed by host paths, so, like fs, join with the
// host's separator; an unmounted one uses POSIX paths throughout.
const join = this[kMounted] ? path.join : joinPath;
for (let i = 0; i < result.length; i++) {
const dirent = result[i];
dirent.parentPath = dirPath;
if (recursive) {
// In recursive mode, name may contain slashes (e.g. 'a/b.txt').
const slashIdx = StringPrototypeLastIndexOf(dirent.name, '/');
if (slashIdx !== -1) {
const subdir = StringPrototypeSlice(dirent.name, 0, slashIdx);
dirent.parentPath = join(dirPath, subdir);
dirent.name = StringPrototypeSlice(dirent.name, slashIdx + 1);
}
}
}
return result;
}
/**
* Creates a directory synchronously.
* @param {string} dirPath The directory path
* @param {object} [options] Options
* @returns {string|undefined}
*/
mkdirSync(dirPath, options) {
const providerPath = this.#toProviderPath(dirPath);
const created = this[kProvider].mkdirSync(providerPath, options);
return created === undefined ? undefined : this.#toMountedPath(created);
}
/**
* Removes a directory synchronously.
* @param {string} dirPath The directory path
*/
rmdirSync(dirPath) {
const providerPath = this.#toProviderPath(dirPath);
checkNotRoot(providerPath, 'rmdir', dirPath);
this[kProvider].rmdirSync(providerPath);
}
/**
* Removes a file synchronously.
* @param {string} filePath The file path
*/
unlinkSync(filePath) {
const providerPath = this.#toProviderPath(filePath);
this[kProvider].unlinkSync(providerPath);
}
/**
* Renames a file or directory synchronously.
* @param {string} oldPath The old path
* @param {string} newPath The new path
*/
renameSync(oldPath, newPath) {
const oldProviderPath = this.#toProviderPath(oldPath);
const newProviderPath = this.#toProviderPath(newPath);
checkNotRoot(oldProviderPath, 'rename', oldPath);
checkNotRoot(newProviderPath, 'rename', newPath);
this[kProvider].renameSync(oldProviderPath, newProviderPath);
}
/**
* Copies a file synchronously.
* @param {string} src Source path
* @param {string} dest Destination path
* @param {number} [mode] Copy mode flags
*/
copyFileSync(src, dest, mode) {
const srcProviderPath = this.#toProviderPath(src);
const destProviderPath = this.#toProviderPath(dest);
this[kProvider].copyFileSync(srcProviderPath, destProviderPath, mode);
}
/**
* Gets the real path by resolving all symlinks.
* @param {string} filePath The path
* @param {object|string} [options] Options or encoding
* @returns {string|Buffer}
*/
realpathSync(filePath, options) {
options = getOptions(options);
const providerPath = this.#toProviderPath(filePath);
const realProviderPath = this[kProvider].realpathSync(providerPath);
return encodeRealpathResult(this.#toMountedPath(realProviderPath), options);
}
/**
* Reads the target of a symbolic link.
* @param {string} linkPath The symlink path
* @param {object|string} [options] Options or encoding
* @returns {string|Buffer}
*/
readlinkSync(linkPath, options) {
options = getOptions(options);
const providerPath = this.#toProviderPath(linkPath);
return encodeRealpathResult(this[kProvider].readlinkSync(providerPath), options);
}
/**
* Creates a symbolic link.
* @param {string} target The symlink target
* @param {string} path The symlink path
* @param {string} [type] The symlink type
*/
symlinkSync(target, path, type) {
const providerPath = this.#toProviderPath(path);
this[kProvider].symlinkSync(target, providerPath, type);
}
/**
* Checks file accessibility synchronously.
* @param {string} filePath The path to check
* @param {number} [mode] Access mode
*/
accessSync(filePath, mode) {
const providerPath = this.#toProviderPath(filePath);
this[kProvider].accessSync(providerPath, mode);
}
/**
* Removes a file or directory synchronously.
* @param {string} filePath The path to remove
* @param {object} [options] Options
* @param {boolean} [options.recursive] If true, remove directories recursively
* @param {boolean} [options.force] If true, ignore ENOENT errors
*/
rmSync(filePath, options) {
const recursive = options?.recursive === true;
const force = options?.force === true;
let stats;
try {
stats = this.lstatSync(filePath);
} catch (err) {
if (force && err?.code === 'ENOENT') return;
throw err;
}
// Symlinks should be unlinked directly, never recursed into
if (stats.isSymbolicLink()) {
this.unlinkSync(filePath);
return;
}
if (stats.isDirectory()) {
if (!recursive) {
throw createEISDIR('rm', filePath);
}
const entries = this.readdirSync(filePath);
for (let i = 0; i < entries.length; i++) {
this.rmSync(joinPath(filePath, entries[i]), options);
}
this.rmdirSync(filePath);
} else {
this.unlinkSync(filePath);
}
}
/**
* Truncates a file synchronously.
* @param {string} filePath The file path
* @param {number} [len] The new length
*/
truncateSync(filePath, len = 0) {
if (len < 0) len = 0;
const providerPath = this.#toProviderPath(filePath);
const handle = this[kProvider].openSync(providerPath, 'r+');
try {
handle.truncateSync(len);
} finally {
handle.closeSync();
}
}
/**
* Truncates a file descriptor synchronously.
* @param {number} fd The file descriptor
* @param {number} [len] The new length
*/
ftruncateSync(fd, len = 0) {
const vfd = getVirtualFd(fd);
if (!vfd) {
throw createEBADF('ftruncate');
}
vfd.entry.truncateSync(len);
}
/**
* Creates a hard link synchronously.
* @param {string} existingPath The existing file path
* @param {string} newPath The new link path
*/
linkSync(existingPath, newPath) {
const existingProviderPath = this.#toProviderPath(existingPath);
const newProviderPath = this.#toProviderPath(newPath);
this[kProvider].linkSync(existingProviderPath, newProviderPath);
}
chmodSync(filePath, mode) {
const providerPath = this.#toProviderPath(filePath);
this[kProvider].chmodSync(providerPath, mode);
}
chownSync(filePath, uid, gid) {
const providerPath = this.#toProviderPath(filePath);
this[kProvider].chownSync(providerPath, uid, gid);
}
lchownSync(filePath, uid, gid) {
const providerPath = this.#toProviderPath(filePath);
this[kProvider].lchownSync(providerPath, uid, gid);
}
utimesSync(filePath, atime, mtime) {
const providerPath = this.#toProviderPath(filePath);
this[kProvider].utimesSync(providerPath, atime, mtime);
}
lutimesSync(filePath, atime, mtime) {
const providerPath = this.#toProviderPath(filePath);
this[kProvider].lutimesSync(providerPath, atime, mtime);
}
/**
* Creates a unique temporary directory synchronously.
* @param {string} prefix The prefix for the temp directory
* @returns {string} The full path of the created directory
*/
mkdtempSync(prefix) {
const dirPath = this.#toProviderPrefix(prefix) + randomSuffix();
this[kProvider].mkdirSync(dirPath);
return this.#toMountedPath(dirPath);
}
/**
* Creates and opens a unique temporary file synchronously.
* @param {string} prefix The prefix for the temp file
* @returns {{ path: string, fd: number }} The full path and a file descriptor
*/
mkstempSync(prefix) {
const filePath = this.#toProviderPrefix(prefix) + randomSuffix();
const handle = this[kProvider].openSync(filePath, 'wx+', 0o600);
return { path: this.#toMountedPath(filePath), fd: openVirtualFd(handle) };
}
/**
* Converts a mkdtemp or mkstemp prefix to a provider-relative one, keeping
* a trailing separator.
* @param {string} prefix The mounted prefix
* @returns {string}
*/
#toProviderPrefix(prefix) {
const last = prefix[prefix.length - 1];
const trailing = last === '/' || last === sep;
const providerPrefix = this.#toProviderPath(prefix);
if (!trailing) return providerPrefix;
return providerPrefix === '/' ? '/' : `${providerPrefix}/`;
}
/**
* Opens a directory synchronously.
* @param {string} dirPath The directory path
* @param {object} [options] Options
* @returns {VirtualDir} A directory handle
*/
opendirSync(dirPath, options) {
const entries = this.readdirSync(dirPath, {
withFileTypes: true,
recursive: options?.recursive,
});
return new VirtualDir(dirPath, entries);
}
/**
* Opens a file as a Blob.
* @param {string} filePath The file path
* @param {object} [options] Options
* @returns {Blob} The file content as a Blob
*/
openAsBlob(filePath, options) {
const { Blob } = require('buffer');
const providerPath = this.#toProviderPath(filePath);
const content = this[kProvider].readFileSync(providerPath);
const type = options?.type || '';
return new Blob([content], { type });
}
/**
* Opens a file synchronously and returns a file descriptor.
* @param {string} filePath The path to open
* @param {string} [flags] Open flags
* @param {number} [mode] File mode
* @returns {number} The file descriptor
*/
openSync(filePath, flags = 'r', mode) {
const providerPath = this.#toProviderPath(filePath);
const handle = this[kProvider].openSync(providerPath, flags, mode);
return openVirtualFd(handle);
}
/**
* Closes a file descriptor synchronously.
* @param {number} fd The file descriptor
*/
closeSync(fd) {
const vfd = getVirtualFd(fd);
if (!vfd) {
throw createEBADF('close');
}
vfd.entry.closeSync();
closeVirtualFd(fd);
}
/**
* Reads from a file descriptor synchronously.
* @param {number} fd The file descriptor
* @param {Buffer} buffer The buffer to read into
* @param {number} offset The offset in the buffer
* @param {number} length The number of bytes to read
* @param {number|null} position The position in the file
* @returns {number} The number of bytes read
*/
readSync(fd, buffer, offset, length, position) {
const vfd = getVirtualFd(fd);
if (!vfd) {
throw createEBADF('read');
}
return vfd.entry.readSync(buffer, offset, length, position);
}
/**
* Writes to a file descriptor synchronously.
* @param {number} fd The file descriptor
* @param {Buffer} buffer The buffer to write from
* @param {number} offset The offset in the buffer
* @param {number} length The number of bytes to write
* @param {number|null} position The position in the file
* @returns {number} The number of bytes written
*/
writeSync(fd, buffer, offset, length, position) {
const vfd = getVirtualFd(fd);
if (!vfd) {
throw createEBADF('write');
}
return vfd.entry.writeSync(buffer, offset, length, position);
}
/**
* Gets file stats from a file descriptor synchronously.
* @param {number} fd The file descriptor
* @param {object} [options] Options
* @returns {Stats}
*/
fstatSync(fd, options) {
const vfd = getVirtualFd(fd);
if (!vfd) {
throw createEBADF('fstat');
}
return vfd.entry.statSync(options);
}
/**
* Reads a file asynchronously.
* @param {string} filePath The path to read
* @param {object|string|Function} [options] Options, encoding, or callback
* @param {Function} [callback] Callback (err, data)
*/
readFile(filePath, options, callback) {
if (typeof options === 'function') {
callback = options;
options = undefined;
}
this[kProvider].readFile(this.#toProviderPath(filePath), options)
.then((data) => callback(null, data), (err) => callback(err));
}
/**
* Writes a file asynchronously.
* @param {string} filePath The path to write
* @param {Buffer|string} data The data to write
* @param {object|Function} [options] Options or callback
* @param {Function} [callback] Callback (err)
*/
writeFile(filePath, data, options, callback) {
if (typeof options === 'function') {
callback = options;
options = undefined;
}
this[kProvider].writeFile(this.#toProviderPath(filePath), data, options)
.then(() => callback(null), (err) => callback(err));
}
/**
* Gets stats for a path asynchronously.
* @param {string} filePath The path to stat
* @param {object|Function} [options] Options or callback
* @param {Function} [callback] Callback (err, stats)
*/
stat(filePath, options, callback) {
if (typeof options === 'function') {
callback = options;
options = undefined;
}
this[kProvider].stat(this.#toProviderPath(filePath), options)
.then((stats) => callback(null, stats), (err) => callback(err));
}
/**
* Gets stats without following symlinks asynchronously.
* @param {string} filePath The path to stat
* @param {object|Function} [options] Options or callback
* @param {Function} [callback] Callback (err, stats)
*/
lstat(filePath, options, callback) {
if (typeof options === 'function') {
callback = options;
options = undefined;
}
this[kProvider].lstat(this.#toProviderPath(filePath), options)
.then((stats) => callback(null, stats), (err) => callback(err));
}
/**
* Reads directory contents asynchronously.
* @param {string} dirPath The directory path
* @param {object|Function} [options] Options or callback
* @param {Function} [callback] Callback (err, entries)
*/
readdir(dirPath, options, callback) {
if (typeof options === 'function') {
callback = options;
options = undefined;
}
this[kProvider].readdir(this.#toProviderPath(dirPath), options)
.then((entries) => callback(null, this.#toMountedDirents(dirPath, entries, options)),
(err) => callback(err));
}
/**
* Gets the real path asynchronously.
* @param {string} filePath The path
* @param {object|string|Function} [options] Options, encoding, or callback
* @param {Function} [callback] Callback (err, resolvedPath)
*/
realpath(filePath, options, callback) {
if (typeof options === 'function') {
callback = options;
options = undefined;
}
options = getOptions(options);
this[kProvider].realpath(this.#toProviderPath(filePath))
.then((realPath) => callback(null, encodeRealpathResult(this.#toMountedPath(realPath), options)),
(err) => callback(err));
}
/**
* Reads symlink target asynchronously.
* @param {string} linkPath The symlink path
* @param {object|string|Function} [options] Options, encoding, or callback
* @param {Function} [callback] Callback (err, target)
*/
readlink(linkPath, options, callback) {
if (typeof options === 'function') {
callback = options;
options = undefined;
}
options = getOptions(options);
this[kProvider].readlink(this.#toProviderPath(linkPath))
.then((target) => callback(null, encodeRealpathResult(target, options)), (err) => callback(err));
}
/**
* Checks file accessibility asynchronously.
* @param {string} filePath The path to check
* @param {number|Function} [mode] Access mode or callback
* @param {Function} [callback] Callback (err)
*/
access(filePath, mode, callback) {
if (typeof mode === 'function') {
callback = mode;
mode = undefined;
}
this[kProvider].access(this.#toProviderPath(filePath), mode)
.then(() => callback(null), (err) => callback(err));
}
/**
* Opens a file asynchronously.
* @param {string} filePath The path to open
* @param {string|Function} [flags] Open flags or callback
* @param {number|Function} [mode] File mode or callback
* @param {Function} [callback] Callback (err, fd)
*/
open(filePath, flags, mode, callback) {
if (typeof flags === 'function') {
callback = flags;
flags = 'r';
mode = undefined;
} else if (typeof mode === 'function') {
callback = mode;
mode = undefined;
}
const providerPath = this.#toProviderPath(filePath);
this[kProvider].open(providerPath, flags, mode)
.then((handle) => {
const fd = openVirtualFd(handle);
callback(null, fd);
}, (err) => callback(err));
}
/**
* Closes a file descriptor asynchronously.
* @param {number} fd The file descriptor
* @param {Function} callback Callback (err)
*/
close(fd, callback) {
const vfd = getVirtualFd(fd);
if (!vfd) {
process.nextTick(callback, createEBADF('close'));
return;
}
vfd.entry.close()
.then(() => {
closeVirtualFd(fd);
callback(null);
}, (err) => callback(err));
}
/**
* Reads from a file descriptor asynchronously.
* @param {number} fd The file descriptor
* @param {Buffer} buffer The buffer to read into
* @param {number} offset The offset in the buffer
* @param {number} length The number of bytes to read
* @param {number|null} position The position in the file
* @param {Function} callback Callback (err, bytesRead, buffer)
*/
read(fd, buffer, offset, length, position, callback) {
const vfd = getVirtualFd(fd);
if (!vfd) {
process.nextTick(callback, createEBADF('read'));
return;
}
vfd.entry.read(buffer, offset, length, position)
.then(({ bytesRead }) => callback(null, bytesRead, buffer), (err) => callback(err));
}
/**
* Writes to a file descriptor asynchronously.
* @param {number} fd The file descriptor
* @param {Buffer} buffer The buffer to write from
* @param {number} offset The offset in the buffer
* @param {number} length The number of bytes to write
* @param {number|null} position The position in the file
* @param {Function} callback Callback (err, bytesWritten, buffer)
*/
write(fd, buffer, offset, length, position, callback) {
const vfd = getVirtualFd(fd);
if (!vfd) {
process.nextTick(callback, createEBADF('write'));
return;
}
vfd.entry.write(buffer, offset, length, position)
.then(({ bytesWritten }) => callback(null, bytesWritten, buffer), (err) => callback(err));
}
/**
* Removes a file or directory asynchronously.
* @param {string} filePath The path to remove
* @param {object|Function} [options] Options or callback
* @param {Function} [callback] Callback (err)
*/
rm(filePath, options, callback) {
if (typeof options === 'function') {
callback = options;
options = undefined;
}
try {
this.rmSync(filePath, options);
process.nextTick(callback, null);
} catch (err) {
process.nextTick(callback, err);
}
}
/**
* Gets file stats from a file descriptor asynchronously.
* @param {number} fd The file descriptor
* @param {object|Function} [options] Options or callback
* @param {Function} [callback] Callback (err, stats)
*/
fstat(fd, options, callback) {
if (typeof options === 'function') {
callback = options;
options = undefined;
}
const vfd = getVirtualFd(fd);
if (!vfd) {
process.nextTick(callback, createEBADF('fstat'));
return;
}
vfd.entry.stat(options)
.then((stats) => callback(null, stats), (err) => callback(err));
}
/**
* Truncates a file asynchronously.
* @param {string} filePath The file path
* @param {number|Function} [len] The new length or callback
* @param {Function} [callback] Callback (err)
*/
truncate(filePath, len, callback) {
if (typeof len === 'function') {
callback = len;
len = 0;
}
try {
this.truncateSync(filePath, len);
process.nextTick(callback, null);
} catch (err) {
process.nextTick(callback, err);
}
}
/**
* Truncates a file descriptor asynchronously.
* @param {number} fd The file descriptor
* @param {number|Function} [len] The new length or callback
* @param {Function} [callback] Callback (err)
*/
ftruncate(fd, len, callback) {
if (typeof len === 'function') {
callback = len;
len = 0;
}
try {
this.ftruncateSync(fd, len);
process.nextTick(callback, null);
} catch (err) {
process.nextTick(callback, err);
}
}
/**
* Creates a hard link asynchronously.
* @param {string} existingPath The existing file path
* @param {string} newPath The new link path
* @param {Function} callback Callback (err)
*/
link(existingPath, newPath, callback) {
try {
this.linkSync(existingPath, newPath);
process.nextTick(callback, null);
} catch (err) {
process.nextTick(callback, err);
}
}
/**
* Creates a unique temporary directory asynchronously.
* @param {string} prefix The prefix for the temp directory
* @param {object|Function} [options] Options or callback
* @param {Function} [callback] Callback (err, dirPath)
*/
mkdtemp(prefix, options, callback) {
if (typeof options === 'function') {
callback = options;
options = undefined;
}
try {
const dirPath = this.mkdtempSync(prefix);
process.nextTick(callback, null, dirPath);
} catch (err) {
process.nextTick(callback, err);
}
}
/**
* Creates and opens a unique temporary file asynchronously.
* @param {string} prefix The prefix for the temp file
* @param {object|Function} [options] Options or callback
* @param {Function} [callback] Callback (err, { path, fd })
*/
mkstemp(prefix, options, callback) {
if (typeof options === 'function') {
callback = options;
options = undefined;
}
try {
const file = this.mkstempSync(prefix);
process.nextTick(callback, null, file);
} catch (err) {
process.nextTick(callback, err);
}
}
/**
* Opens a directory asynchronously.
* @param {string} dirPath The directory path
* @param {object|Function} [options] Options or callback
* @param {Function} [callback] Callback (err, dir)
*/
opendir(dirPath, options, callback) {
if (typeof options === 'function') {
callback = options;
options = undefined;
}
try {
const dir = this.opendirSync(dirPath, options);
process.nextTick(callback, null, dir);
} catch (err) {
process.nextTick(callback, err);
}
}
/**
* Creates a readable stream for a virtual file.
* @param {string} filePath The path to the file
* @param {object} [options] Stream options
* @returns {ReadStream}
*/
createReadStream(filePath, options) {
return new VirtualReadStream(this, filePath, options);
}
/**
* Creates a writable stream for a virtual file.
* @param {string} filePath The path to the file
* @param {object} [options] Stream options
* @returns {WriteStream}
*/
createWriteStream(filePath, options) {
return new VirtualWriteStream(this, filePath, options);
}
/**
* Watches a file or directory for changes.
* @param {string} filePath The path to watch
* @param {object|Function} [options] Watch options or listener
* @param {Function} [listener] Change listener
* @returns {EventEmitter} A watcher that emits 'change' events
*/
watch(filePath, options, listener) {
if (typeof options === 'function') {
listener = options;
options = {};
}
const providerPath = this.#toProviderPath(filePath);
const watcher = this[kProvider].watch(providerPath, options);
if (listener) {
watcher.on('change', listener);
}
return watcher;
}
/**
* Watches a file for changes using stat polling.
* @param {string} filePath The path to watch
* @param {object|Function} [options] Watch options or listener
* @param {Function} [listener] Change listener
* @returns {EventEmitter} A stat watcher that emits 'change' events
*/
watchFile(filePath, options, listener) {
if (typeof options === 'function') {
listener = options;
options = {};
}
const providerPath = this.#toProviderPath(filePath);
return this[kProvider].watchFile(providerPath, options, listener);
}
/**
* Stops watching a file for changes.
* @param {string} filePath The path to stop watching
* @param {Function} [listener] Optional listener to remove
*/
unwatchFile(filePath, listener) {
const providerPath = this.#toProviderPath(filePath);
this[kProvider].unwatchFile(providerPath, listener);
}
/**
* Gets the promises API for this VFS instance.
* @returns {object} Promise-based fs methods
*/
get promises() {
if (this[kPromises] === null) {
this[kPromises] = this.#createPromisesAPI();
}
return this[kPromises];
}
/**
* Creates the promises API object for this VFS instance.
* @returns {object} Promise-based fs methods
*/
#createPromisesAPI() {
const provider = this[kProvider];
// Arrow functions capture `this` for private method access.
const toProviderPath = (p) => this.#toProviderPath(p);
const toProviderPrefix = (p) => this.#toProviderPrefix(p);
const toMountedPath = (p) => this.#toMountedPath(p);
const toMountedDirents = (p, result, options) =>
this.#toMountedDirents(p, result, options);
return ObjectFreeze({
async readFile(filePath, options) {
const providerPath = toProviderPath(filePath);
return provider.readFile(providerPath, options);
},
async writeFile(filePath, data, options) {
const providerPath = toProviderPath(filePath);
return provider.writeFile(providerPath, data, options);
},
async appendFile(filePath, data, options) {
const providerPath = toProviderPath(filePath);
return provider.appendFile(providerPath, data, options);
},
async stat(filePath, options) {
const providerPath = toProviderPath(filePath);
return provider.stat(providerPath, options);
},
async lstat(filePath, options) {
const providerPath = toProviderPath(filePath);
return provider.lstat(providerPath, options);
},
async readdir(dirPath, options) {
const providerPath = toProviderPath(dirPath);
const result = await provider.readdir(providerPath, options);
return toMountedDirents(dirPath, result, options);
},
async mkdir(dirPath, options) {
const providerPath = toProviderPath(dirPath);
const created = await provider.mkdir(providerPath, options);
return created === undefined ? undefined : toMountedPath(created);
},
async rmdir(dirPath) {
const providerPath = toProviderPath(dirPath);
checkNotRoot(providerPath, 'rmdir', dirPath);
return provider.rmdir(providerPath);
},
async unlink(filePath) {
const providerPath = toProviderPath(filePath);
return provider.unlink(providerPath);
},
async rename(oldPath, newPath) {
const oldProviderPath = toProviderPath(oldPath);
const newProviderPath = toProviderPath(newPath);
checkNotRoot(oldProviderPath, 'rename', oldPath);
checkNotRoot(newProviderPath, 'rename', newPath);
return provider.rename(oldProviderPath, newProviderPath);
},
async copyFile(src, dest, mode) {
const srcProviderPath = toProviderPath(src);
const destProviderPath = toProviderPath(dest);
return provider.copyFile(srcProviderPath, destProviderPath, mode);
},
async realpath(filePath, options) {
options = getOptions(options);
const providerPath = toProviderPath(filePath);
return encodeRealpathResult(toMountedPath(await provider.realpath(providerPath)), options);
},
async readlink(linkPath, options) {
options = getOptions(options);
const providerPath = toProviderPath(linkPath);
return encodeRealpathResult(await provider.readlink(providerPath), options);
},
async symlink(target, path, type) {
const providerPath = toProviderPath(path);
return provider.symlink(target, providerPath, type);
},
async access(filePath, mode) {
const providerPath = toProviderPath(filePath);
return provider.access(providerPath, mode);
},
async rm(filePath, options) {
const recursive = options?.recursive === true;
const force = options?.force === true;
let stats;
try {
stats = await provider.lstat(toProviderPath(filePath));
} catch (err) {
if (force && err?.code === 'ENOENT') return;
throw err;
}
// Symlinks should be unlinked directly, never recursed into
if (stats.isSymbolicLink()) {
await provider.unlink(toProviderPath(filePath));
return;
}
if (stats.isDirectory()) {
if (!recursive) {
throw createEISDIR('rm', filePath);
}
const entries = await provider.readdir(toProviderPath(filePath));
for (let i = 0; i < entries.length; i++) {
await this.rm(joinPath(filePath, entries[i]), options);
}
await this.rmdir(filePath);
} else {
await provider.unlink(toProviderPath(filePath));
}
},
async truncate(filePath, len = 0) {
const providerPath = toProviderPath(filePath);
const handle = await provider.open(providerPath, 'r+');
try {
await handle.truncate(len);
} finally {
await handle.close();
}
},
async link(existingPath, newPath) {
const existingProviderPath = toProviderPath(existingPath);
const newProviderPath = toProviderPath(newPath);
return provider.link(existingProviderPath, newProviderPath);
},
async mkdtemp(prefix) {
const dirPath = toProviderPrefix(prefix) + randomSuffix();
await provider.mkdir(dirPath);
return toMountedPath(dirPath);
},
async mkstemp(prefix) {
const filePath = toProviderPrefix(prefix) + randomSuffix();
const handle = provider.openSync(filePath, 'wx+', 0o600);
return {
__proto__: null,
path: toMountedPath(filePath),
fd: openVirtualFd(handle),
};
},
async chmod(filePath, mode) {
const providerPath = toProviderPath(filePath);
provider.chmodSync(providerPath, mode);
},
async chown(filePath, uid, gid) {
const providerPath = toProviderPath(filePath);
provider.chownSync(providerPath, uid, gid);
},
async lchown(filePath, uid, gid) {
const providerPath = toProviderPath(filePath);
provider.lchownSync(providerPath, uid, gid);
},
async utimes(filePath, atime, mtime) {
const providerPath = toProviderPath(filePath);
provider.utimesSync(providerPath, atime, mtime);
},
async lutimes(filePath, atime, mtime) {
const providerPath = toProviderPath(filePath);
provider.lutimesSync(providerPath, atime, mtime);
},
async open(filePath, flags, mode) {
const providerPath = toProviderPath(filePath);
const handle = provider.openSync(providerPath, flags, mode);
return openVirtualFd(handle);
},
async lchmod(filePath, mode) {
const providerPath = toProviderPath(filePath);
provider.lchmodSync(providerPath, mode);
},
watch(filePath, options) {
const providerPath = toProviderPath(filePath);
return provider.watchAsync(providerPath, options);
},
});
}
}
module.exports = {
VirtualFileSystem,
kLayerId,
kLoadLayer,
kReservedRoot,
normalizeMountedPath,
};