/
githubmirror
/
node
Обзор
Документация
Войти
/
githubmirror
/
node
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
lib/internal/vfs/file_system.js
1 276 строк
37 KB
Trivikram Kamat
vfs: make lchown update symlink metadata
28 июл 2026, 08:46
Не верифицирован
28 июл 2026, 08:46
b2a024b
Код
Авторство
О чём код?
'use strict'; const { MathRandom, ObjectFreeze, Symbol, SymbolDispose, } = primordials; const { codes: { ERR_INVALID_STATE, }, } = require('internal/errors'); const { validateBoolean } = require('internal/validators'); const { MemoryProvider } = require('internal/vfs/providers/memory'); const path = require('path'); const { posix: pathPosix, isAbsolute, resolve: resolvePath } = path; const { join: joinPath } = pathPosix; const { isUnderMountPoint, getRelativePath, } = require('internal/vfs/router'); const { openVirtualFd, getVirtualFd, closeVirtualFd, } = require('internal/vfs/fd'); const { createENOENT, createEBADF, createEISDIR, } = require('internal/vfs/errors'); const { VirtualReadStream, VirtualWriteStream } = require('internal/vfs/streams'); const { VirtualDir } = require('internal/vfs/dir'); const { emitExperimentalWarning, kEmptyObject } = require('internal/util'); let debug = require('internal/util/debuglog').debuglog('vfs', (fn) => { debug = fn; }); // Private symbols const kProvider = Symbol('kProvider'); const kMountPoint = Symbol('kMountPoint'); const kMounted = Symbol('kMounted'); const kPromises = Symbol('kPromises'); // Lazy-loaded VFS setup 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 * @param {boolean} [options.emitExperimentalWarning] Emit the experimental warning (default: true) */ constructor(providerOrOptions, options = kEmptyObject) { // Handle case where first arg is options object (no provider) let provider = null; if (providerOrOptions !== undefined && providerOrOptions !== null) { if (typeof providerOrOptions.openSync === 'function') { // It's a provider provider = providerOrOptions; } else if (typeof providerOrOptions === 'object') { // It's options (no provider specified) 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[kMountPoint] = null; this[kMounted] = false; this[kPromises] = null; // Lazy-initialized } /** * 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]; } /** * 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; } // ==================== Mount ==================== /** * Mounts the VFS at a specific path prefix. * @param {string} prefix The mount point path * @returns {VirtualFileSystem} The VFS instance for chaining */ mount(prefix) { if (this[kMounted]) { throw new ERR_INVALID_STATE('VFS is already mounted'); } this[kMountPoint] = resolvePath(prefix); this[kMounted] = true; debug('mount %s', this[kMountPoint]); loadVfsSetup(); registerVFS(this); return this; } /** * Unmounts the VFS. */ unmount() { debug('unmount %s', this[kMountPoint]); loadVfsSetup(); deregisterVFS(this); this[kMountPoint] = 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(); } } /** * Checks if a path should be handled by this VFS. * @param {string} inputPath The path to check (must be absolute & normalized) * @returns {boolean} */ shouldHandle(inputPath) { if (!this[kMounted] || !this[kMountPoint]) { return false; } const normalized = isAbsolute(inputPath) ? inputPath : resolvePath(inputPath); return isUnderMountPoint(normalized, this[kMountPoint]); } // ==================== Path Resolution ==================== /** * 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) { if (this[kMounted] && this[kMountPoint]) { const resolved = isAbsolute(inputPath) ? inputPath : resolvePath(inputPath); if (!isUnderMountPoint(resolved, this[kMountPoint])) { throw createENOENT('open', inputPath); } return getRelativePath(resolved, this[kMountPoint]); } 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]) { return path.join(this[kMountPoint], providerPath); } return providerPath; } // ==================== FS Operations (Sync) ==================== /** * 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); // Fix Dirent parentPath from provider-relative to actual VFS path if (options?.withFileTypes === true) { const recursive = options?.recursive === true; for (let i = 0; i < result.length; i++) { const dirent = result[i]; if (recursive) { // In recursive mode, name may contain slashes (e.g. 'a/b.txt'). // Fix to basename only and set correct parentPath. const slashIdx = dirent.name.lastIndexOf('/'); if (slashIdx !== -1) { const subdir = dirent.name.slice(0, slashIdx); dirent.parentPath = joinPath(dirPath, subdir); dirent.name = dirent.name.slice(slashIdx + 1); } else { dirent.parentPath = dirPath; } } else { dirent.parentPath = dirPath; } } } 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); return this[kProvider].mkdirSync(providerPath, options); } /** * Removes a directory synchronously. * @param {string} dirPath The directory path */ rmdirSync(dirPath) { const providerPath = this.#toProviderPath(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); 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} [options] Options * @returns {string} */ realpathSync(filePath, options) { const providerPath = this.#toProviderPath(filePath); const realProviderPath = this[kProvider].realpathSync(providerPath, options); return this.#toMountedPath(realProviderPath); } /** * Reads the target of a symbolic link. * @param {string} linkPath The symlink path * @param {object} [options] Options * @returns {string} */ readlinkSync(linkPath, options) { const providerPath = this.#toProviderPath(linkPath); return 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); } } // ==================== Additional Sync Operations ==================== /** * 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 providerPrefix = this.#toProviderPath(prefix); // Generate random 6-character suffix like Node does const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; let suffix = ''; for (let i = 0; i < 6; i++) { suffix += chars[(MathRandom() * chars.length) | 0]; } const dirPath = providerPrefix + suffix; this[kProvider].mkdirSync(dirPath); return this.#toMountedPath(dirPath); } /** * 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 }); } // ==================== File Descriptor Operations ==================== /** * 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); } // ==================== FS Operations (Async with Callbacks) ==================== /** * 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, entries), (err) => callback(err)); } /** * Gets the real path asynchronously. * @param {string} filePath The path * @param {object|Function} [options] Options or callback * @param {Function} [callback] Callback (err, resolvedPath) */ realpath(filePath, options, callback) { if (typeof options === 'function') { callback = options; options = undefined; } this[kProvider].realpath(this.#toProviderPath(filePath), options) .then((realPath) => callback(null, this.#toMountedPath(realPath)), (err) => callback(err)); } /** * Reads symlink target asynchronously. * @param {string} linkPath The symlink path * @param {object|Function} [options] Options or callback * @param {Function} [callback] Callback (err, target) */ readlink(linkPath, options, callback) { if (typeof options === 'function') { callback = options; options = undefined; } this[kProvider].readlink(this.#toProviderPath(linkPath), options) .then((target) => callback(null, target), (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); } } /** * 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); } } // ==================== Stream Operations ==================== /** * 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); } // ==================== Watch Operations ==================== /** * 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); } // ==================== Promise API ==================== /** * 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]; // Use arrow function to capture `this` for private method access const toProviderPath = (p) => this.#toProviderPath(p); const toMountedPath = (p) => this.#toMountedPath(p); 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); return provider.readdir(providerPath, options); }, async mkdir(dirPath, options) { const providerPath = toProviderPath(dirPath); return provider.mkdir(providerPath, options); }, async rmdir(dirPath) { const providerPath = toProviderPath(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); 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) { const providerPath = toProviderPath(filePath); return toMountedPath(await provider.realpath(providerPath, options)); }, async readlink(linkPath, options) { const providerPath = toProviderPath(linkPath); return 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 provider.rmdir(toProviderPath(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 providerPrefix = toProviderPath(prefix); const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; let suffix = ''; for (let i = 0; i < 6; i++) { suffix += chars[(MathRandom() * chars.length) | 0]; } const dirPath = providerPrefix + suffix; await provider.mkdir(dirPath); return toMountedPath(dirPath); }, 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, };