/
githubmirror
/
node
Обзор
Документация
Войти
/
githubmirror
/
node
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
lib/internal/vfs/provider.js
630 строк
17 KB
Trivikram Kamat
vfs: make lchown update symlink metadata
28 июл 2026, 08:46
Не верифицирован
28 июл 2026, 08:46
b2a024b
Код
Авторство
О чём код?
'use strict'; const { ERR_METHOD_NOT_IMPLEMENTED, } = require('internal/errors').codes; const { createEROFS, createEEXIST, createEACCES, } = require('internal/vfs/errors'); const { fs: { R_OK, W_OK, X_OK, COPYFILE_EXCL, }, } = internalBinding('constants'); /** * Base class for VFS providers. * Providers implement the essential primitives that the VFS delegates to. * * Implementations must override the essential primitives (open, stat, readdir, etc.) * Default implementations for derived methods (readFile, writeFile, etc.) are provided. */ class VirtualProvider { // === CAPABILITY FLAGS === /** * Returns true if this provider is read-only. * @returns {boolean} */ get readonly() { return false; } /** * Returns true if this provider supports symbolic links. * @returns {boolean} */ get supportsSymlinks() { return false; } /** * Returns true if this provider supports file watching. * @returns {boolean} */ get supportsWatch() { return false; } // === ESSENTIAL PRIMITIVES (must be implemented by subclasses) === /** * Opens a file and returns a file handle. * @param {string} path The file path (relative to provider root) * @param {string} flags The open flags ('r', 'r+', 'w', 'w+', 'a', 'a+') * @param {number} [mode] The file mode (for creating files) * @returns {Promise<VirtualFileHandle>} */ async open(path, flags, mode) { throw new ERR_METHOD_NOT_IMPLEMENTED('open'); } /** * Opens a file synchronously and returns a file handle. * @param {string} path The file path (relative to provider root) * @param {string} flags The open flags ('r', 'r+', 'w', 'w+', 'a', 'a+') * @param {number} [mode] The file mode (for creating files) * @throws {ERR_METHOD_NOT_IMPLEMENTED} When not implemented by subclass */ openSync(path, flags, mode) { throw new ERR_METHOD_NOT_IMPLEMENTED('openSync'); } /** * Gets stats for a path. * @param {string} path The path to stat * @param {object} [options] Options * @returns {Promise<Stats>} */ async stat(path, options) { throw new ERR_METHOD_NOT_IMPLEMENTED('stat'); } /** * Gets stats for a path synchronously. * @param {string} path The path to stat * @param {object} [options] Options * @throws {ERR_METHOD_NOT_IMPLEMENTED} When not implemented by subclass */ statSync(path, options) { throw new ERR_METHOD_NOT_IMPLEMENTED('statSync'); } /** * Gets stats for a path without following symlinks. * @param {string} path The path to stat * @param {object} [options] Options * @returns {Promise<Stats>} */ async lstat(path, options) { // Default: same as stat (for providers that don't support symlinks) return this.stat(path, options); } /** * Gets stats for a path synchronously without following symlinks. * @param {string} path The path to stat * @param {object} [options] Options * @returns {Stats} */ lstatSync(path, options) { // Default: same as statSync (for providers that don't support symlinks) return this.statSync(path, options); } /** * Reads directory contents. * @param {string} path The directory path * @param {object} [options] Options * @returns {Promise<string[]|Dirent[]>} */ async readdir(path, options) { throw new ERR_METHOD_NOT_IMPLEMENTED('readdir'); } /** * Reads directory contents synchronously. * @param {string} path The directory path * @param {object} [options] Options * @throws {ERR_METHOD_NOT_IMPLEMENTED} When not implemented by subclass */ readdirSync(path, options) { throw new ERR_METHOD_NOT_IMPLEMENTED('readdirSync'); } /** * Creates a directory. * @param {string} path The directory path * @param {object} [options] Options * @returns {Promise<void>} */ async mkdir(path, options) { if (this.readonly) { throw createEROFS('mkdir', path); } throw new ERR_METHOD_NOT_IMPLEMENTED('mkdir'); } /** * Creates a directory synchronously. * @param {string} path The directory path * @param {object} [options] Options */ mkdirSync(path, options) { if (this.readonly) { throw createEROFS('mkdir', path); } throw new ERR_METHOD_NOT_IMPLEMENTED('mkdirSync'); } /** * Removes a directory. * @param {string} path The directory path * @returns {Promise<void>} */ async rmdir(path) { if (this.readonly) { throw createEROFS('rmdir', path); } throw new ERR_METHOD_NOT_IMPLEMENTED('rmdir'); } /** * Removes a directory synchronously. * @param {string} path The directory path */ rmdirSync(path) { if (this.readonly) { throw createEROFS('rmdir', path); } throw new ERR_METHOD_NOT_IMPLEMENTED('rmdirSync'); } /** * Removes a file. * @param {string} path The file path * @returns {Promise<void>} */ async unlink(path) { if (this.readonly) { throw createEROFS('unlink', path); } throw new ERR_METHOD_NOT_IMPLEMENTED('unlink'); } /** * Removes a file synchronously. * @param {string} path The file path */ unlinkSync(path) { if (this.readonly) { throw createEROFS('unlink', path); } throw new ERR_METHOD_NOT_IMPLEMENTED('unlinkSync'); } /** * Renames a file or directory. * @param {string} oldPath The old path * @param {string} newPath The new path * @returns {Promise<void>} */ async rename(oldPath, newPath) { if (this.readonly) { throw createEROFS('rename', oldPath); } throw new ERR_METHOD_NOT_IMPLEMENTED('rename'); } /** * Renames a file or directory synchronously. * @param {string} oldPath The old path * @param {string} newPath The new path */ renameSync(oldPath, newPath) { if (this.readonly) { throw createEROFS('rename', oldPath); } throw new ERR_METHOD_NOT_IMPLEMENTED('renameSync'); } /** * Changes ownership of a path without following the final symbolic link. * Providers with symlink support should override this. * @param {string} path The path * @param {number} uid The user id * @param {number} gid The group id * @returns {void} */ lchownSync(path, uid, gid) { return this.chownSync(path, uid, gid); } // === DEFAULT IMPLEMENTATIONS (built on primitives) === /** * Reads a file. * @param {string} path The file path * @param {object|string} [options] Options or encoding * @returns {Promise<Buffer|string>} */ async readFile(path, options) { const flag = (typeof options === 'object' && options !== null) ? (options.flag ?? 'r') : 'r'; const handle = await this.open(path, flag); try { return await handle.readFile(options); } finally { await handle.close(); } } /** * Reads a file synchronously. * @param {string} path The file path * @param {object|string} [options] Options or encoding * @returns {Buffer|string} */ readFileSync(path, options) { const flag = (typeof options === 'object' && options !== null) ? (options.flag ?? 'r') : 'r'; const handle = this.openSync(path, flag); try { return handle.readFileSync(options); } finally { handle.closeSync(); } } /** * Writes a file. * @param {string} path The file path * @param {Buffer|string} data The data to write * @param {object} [options] Options * @returns {Promise<void>} */ async writeFile(path, data, options) { if (this.readonly) { throw createEROFS('open', path); } const flag = options?.flag ?? 'w'; const handle = await this.open(path, flag, options?.mode); try { await handle.writeFile(data, options); } finally { await handle.close(); } } /** * Writes a file synchronously. * @param {string} path The file path * @param {Buffer|string} data The data to write * @param {object} [options] Options */ writeFileSync(path, data, options) { if (this.readonly) { throw createEROFS('open', path); } const flag = options?.flag ?? 'w'; const handle = this.openSync(path, flag, options?.mode); try { handle.writeFileSync(data, options); } finally { handle.closeSync(); } } /** * Appends to a file. * @param {string} path The file path * @param {Buffer|string} data The data to append * @param {object} [options] Options * @returns {Promise<void>} */ async appendFile(path, data, options) { if (this.readonly) { throw createEROFS('open', path); } const flag = options?.flag ?? 'a'; const handle = await this.open(path, flag, options?.mode); try { await handle.writeFile(data, options); } finally { await handle.close(); } } /** * Appends to a file synchronously. * @param {string} path The file path * @param {Buffer|string} data The data to append * @param {object} [options] Options */ appendFileSync(path, data, options) { if (this.readonly) { throw createEROFS('open', path); } const flag = options?.flag ?? 'a'; const handle = this.openSync(path, flag, options?.mode); try { handle.writeFileSync(data, options); } finally { handle.closeSync(); } } /** * Checks if a path exists. * @param {string} path The path to check * @returns {Promise<boolean>} */ async exists(path) { try { await this.stat(path); return true; } catch { return false; } } /** * Checks if a path exists synchronously. * @param {string} path The path to check * @returns {boolean} */ existsSync(path) { try { this.statSync(path); return true; } catch { return false; } } /** * Copies a file. * @param {string} src Source path * @param {string} dest Destination path * @param {number} [mode] Copy mode flags * @returns {Promise<void>} */ async copyFile(src, dest, mode) { if (this.readonly) { throw createEROFS('copyfile', dest); } if ((mode & COPYFILE_EXCL) !== 0) { if (await this.exists(dest)) { throw createEEXIST('copyfile', dest); } } const content = await this.readFile(src); await this.writeFile(dest, content); } /** * Copies a file synchronously. * @param {string} src Source path * @param {string} dest Destination path * @param {number} [mode] Copy mode flags */ copyFileSync(src, dest, mode) { if (this.readonly) { throw createEROFS('copyfile', dest); } if ((mode & COPYFILE_EXCL) !== 0) { if (this.existsSync(dest)) { throw createEEXIST('copyfile', dest); } } const content = this.readFileSync(src); this.writeFileSync(dest, content); } /** * Gets the real path by resolving symlinks. * @param {string} path The path * @param {object} [options] Options * @returns {Promise<string>} */ async realpath(path, options) { // Default: return the path as-is (for providers without symlinks) // First verify the path exists await this.stat(path); return path; } /** * Gets the real path synchronously. * @param {string} path The path * @param {object} [options] Options * @returns {string} */ realpathSync(path, options) { // Default: return the path as-is (for providers without symlinks) // First verify the path exists this.statSync(path); return path; } /** * Checks file accessibility. * @param {string} path The path to check * @param {number} [mode] Access mode * @returns {Promise<void>} */ async access(path, mode) { const stats = await this.stat(path); this.#checkAccessMode(path, stats, mode); } /** * Checks file accessibility synchronously. * @param {string} path The path to check * @param {number} [mode] Access mode */ accessSync(path, mode) { const stats = this.statSync(path); this.#checkAccessMode(path, stats, mode); } /** * Checks access mode bits against file stats. * @param {string} path The path (for error messages) * @param {Stats} stats The file stats * @param {number} mode The requested access mode */ #checkAccessMode(path, stats, mode) { if (mode == null || mode === 0) return; // F_OK = 0, existence-only check const fileMode = stats.mode & 0o777; // Permission bits // Check owner permissions (simplified: treat VFS user as owner) if ((mode & R_OK) !== 0 && (fileMode & 0o400) === 0) { throw createEACCES('access', path); } if ((mode & W_OK) !== 0 && (fileMode & 0o200) === 0) { throw createEACCES('access', path); } if ((mode & X_OK) !== 0 && (fileMode & 0o100) === 0) { throw createEACCES('access', path); } } // === HARD LINK OPERATIONS (optional) === /** * Creates a hard link. * @param {string} existingPath The existing file path * @param {string} newPath The new link path * @returns {Promise<void>} */ async link(existingPath, newPath) { if (this.readonly) { throw createEROFS('link', newPath); } throw new ERR_METHOD_NOT_IMPLEMENTED('link'); } /** * Creates a hard link synchronously. * @param {string} existingPath The existing file path * @param {string} newPath The new link path */ linkSync(existingPath, newPath) { if (this.readonly) { throw createEROFS('link', newPath); } throw new ERR_METHOD_NOT_IMPLEMENTED('linkSync'); } // === SYMLINK OPERATIONS (optional, throw ENOENT by default) === /** * Reads the target of a symbolic link. * @param {string} path The symlink path * @param {object} [options] Options * @returns {Promise<string>} */ async readlink(path, options) { throw new ERR_METHOD_NOT_IMPLEMENTED('readlink'); } /** * Reads the target of a symbolic link synchronously. * @param {string} path The symlink path * @param {object} [options] Options * @throws {ERR_METHOD_NOT_IMPLEMENTED} When not implemented by subclass */ readlinkSync(path, options) { throw new ERR_METHOD_NOT_IMPLEMENTED('readlinkSync'); } /** * Creates a symbolic link. * @param {string} target The symlink target * @param {string} path The symlink path * @param {string} [type] The symlink type (file, dir, junction) * @returns {Promise<void>} */ async symlink(target, path, type) { if (this.readonly) { throw createEROFS('symlink', path); } throw new ERR_METHOD_NOT_IMPLEMENTED('symlink'); } /** * Creates a symbolic link synchronously. * @param {string} target The symlink target * @param {string} path The symlink path * @param {string} [type] The symlink type (file, dir, junction) */ symlinkSync(target, path, type) { if (this.readonly) { throw createEROFS('symlink', path); } throw new ERR_METHOD_NOT_IMPLEMENTED('symlinkSync'); } // === WATCH OPERATIONS (optional, polling-based) === /** * Watches a file or directory for changes. * Returns an EventEmitter-like object that emits 'change' and 'close' events. * @param {string} path The path to watch * @param {object} [options] Watch options * @param {number} [options.interval] Polling interval in ms (default: 100) * @param {boolean} [options.recursive] Watch subdirectories (default: false) * @throws {ERR_METHOD_NOT_IMPLEMENTED} When not overridden by subclass */ watch(path, options) { throw new ERR_METHOD_NOT_IMPLEMENTED('watch'); } /** * Watches a file or directory for changes (async iterable version). * Used by fs.promises.watch(). * @param {string} path The path to watch * @param {object} [options] Watch options * @param {number} [options.interval] Polling interval in ms (default: 100) * @param {boolean} [options.recursive] Watch subdirectories (default: false) * @param {AbortSignal} [options.signal] AbortSignal for cancellation * @throws {ERR_METHOD_NOT_IMPLEMENTED} When not overridden by subclass */ watchAsync(path, options) { throw new ERR_METHOD_NOT_IMPLEMENTED('watchAsync'); } /** * Watches a file for changes using stat polling. * Returns a StatWatcher-like object that emits 'change' events with stats. * @param {string} path The path to watch * @param {object} [options] Watch options * @param {number} [options.interval] Polling interval in ms (default: 5007) * @param {boolean} [options.persistent] Whether the watcher should prevent exit * @throws {ERR_METHOD_NOT_IMPLEMENTED} When not overridden by subclass */ watchFile(path, options) { throw new ERR_METHOD_NOT_IMPLEMENTED('watchFile'); } /** * Stops watching a file for changes. * @param {string} path The path to stop watching * @param {Function} [listener] Optional listener to remove */ unwatchFile(path, listener) { throw new ERR_METHOD_NOT_IMPLEMENTED('unwatchFile'); } } module.exports = { VirtualProvider, };