UNPKG

@zenfs/core

Version:

A filesystem, anywhere

676 lines (675 loc) 25.4 kB
var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) { if (value !== null && value !== void 0) { if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected."); var dispose, inner; if (async) { if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined."); dispose = value[Symbol.asyncDispose]; } if (dispose === void 0) { if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined."); dispose = value[Symbol.dispose]; if (async) inner = dispose; } if (typeof dispose !== "function") throw new TypeError("Object not disposable."); if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } }; env.stack.push({ value: value, dispose: dispose, async: async }); } else if (async) { env.stack.push({ async: true }); } return value; }; var __disposeResources = (this && this.__disposeResources) || (function (SuppressedError) { return function (env) { function fail(e) { env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e; env.hasError = true; } var r, s = 0; function next() { while (r = env.stack.pop()) { try { if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next); if (r.dispose) { var result = r.dispose.call(r.value); if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); }); } else s |= 1; } catch (e) { fail(e); } } if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve(); if (env.hasError) throw env.error; } return next(); }; })(typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) { var e = new Error(message); return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e; }); import { Buffer } from 'buffer'; import { Errno, Exception, setUVMessage, UV } from 'kerium'; import { encodeUTF8 } from 'utilium'; import * as constants from '../constants.js'; import { wrap } from '../internal/error.js'; import { hasAccess, isDirectory } from '../internal/inode.js'; import { join, matchesGlob } from '../path.js'; import { _tempDirName, globToRegex, normalizeMode, normalizeOptions, normalizePath, normalizeTime } from '../utils.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 * as _sync from '../vfs/sync.js'; import { emitChange } from '../vfs/watchers.js'; import { Dir, Dirent } from './dir.js'; import { BigIntStats, Stats } from './stats.js'; export function renameSync(oldPath, newPath) { return _sync.rename.call(this, oldPath, newPath); } renameSync; /** * Test whether or not `path` exists by checking with the file system. */ export function existsSync(path) { path = normalizePath(path); try { const { fs, path: resolvedPath } = _sync.resolve(this, path); return fs.existsSync(resolvedPath); } catch (e) { if (e.errno == Errno.ENOENT) return false; throw e; } } existsSync; export function statSync(path, options) { const stats = _sync.stat.call(this, path, false); return options?.bigint ? new BigIntStats(stats) : new Stats(stats); } statSync; export function lstatSync(path, options) { const stats = _sync.stat.call(this, path, true); return options?.bigint ? new BigIntStats(stats) : new Stats(stats); } lstatSync; export function truncateSync(path, len = 0) { const env_1 = { stack: [], error: void 0, hasError: false }; try { const file = __addDisposableResource(env_1, _sync.open.call(this, path, { flag: 'r+' }), false); len ||= 0; if (len < 0) throw UV('EINVAL', 'truncate', path.toString()); file.truncateSync(len); } catch (e_1) { env_1.error = e_1; env_1.hasError = true; } finally { __disposeResources(env_1); } } truncateSync; export function unlinkSync(path) { path = normalizePath(path); const { fs, path: resolved } = resolveMount(path, this); try { if (checkAccess && !hasAccess(this, fs.statSync(resolved), constants.W_OK)) { throw UV('EACCES', 'unlink'); } fs.unlinkSync(resolved); } catch (e) { throw setUVMessage(Object.assign(e, { path })); } emitChange(this, 'rename', path.toString()); } unlinkSync; /** * Synchronous file open. * @see https://nodejs.org/api/fs.html#fsopensyncpath-flags-mode * @param flag {@link https://nodejs.org/api/fs.html#file-system-flags} */ export function openSync(path, flag, mode = constants.F_OK) { return toFD(_sync.open.call(this, path, { flag, mode })); } openSync; /** * Opens a file or symlink * @internal */ export function lopenSync(path, flag, mode) { return toFD(_sync.open.call(this, path, { flag, mode, preserveSymlinks: true })); } export function readFileSync(path, _options = {}) { const env_2 = { stack: [], error: void 0, hasError: false }; try { const options = normalizeOptions(_options, null, 'r', 0o644); const flag = flags.parse(options.flag); if (flag & constants.O_WRONLY) throw UV('EBADF', 'read', path.toString()); const file = __addDisposableResource(env_2, typeof path == 'number' ? fromFD(this, path) : _sync.open.call(this, path.toString(), { flag: options.flag, mode: 0o644, preserveSymlinks: false }), false); const { size } = file.stat(); const data = Buffer.alloc(size); file.readSync(data, 0, size, 0); return options.encoding ? data.toString(options.encoding) : data; } catch (e_2) { env_2.error = e_2; env_2.hasError = true; } finally { __disposeResources(env_2); } } readFileSync; export function writeFileSync(path, data, _options = {}) { const env_3 = { stack: [], error: void 0, hasError: false }; try { const options = normalizeOptions(_options, 'utf8', 'w+', 0o644); const flag = flags.parse(options.flag); if (!(flag & constants.O_WRONLY || flag & constants.O_RDWR)) { throw new Exception(Errno.EINVAL, 'Flag passed to writeFile must allow for writing'); } if (typeof data != 'string' && !options.encoding) { throw new Exception(Errno.EINVAL, 'Encoding not specified'); } const encodedData = typeof data == 'string' ? Buffer.from(data, options.encoding) : new Uint8Array(data.buffer, data.byteOffset, data.byteLength); if (!encodedData) { throw new Exception(Errno.EINVAL, 'Data not specified'); } const file = __addDisposableResource(env_3, typeof path == 'number' ? fromFD(this, path) : _sync.open.call(this, path.toString(), { flag, mode: options.mode, preserveSymlinks: true, }), false); file.writeSync(encodedData, 0, encodedData.byteLength, 0); emitChange(this, 'change', path.toString()); } catch (e_3) { env_3.error = e_3; env_3.hasError = true; } finally { __disposeResources(env_3); } } writeFileSync; /** * 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 function appendFileSync(filename, data, _options = {}) { const options = normalizeOptions(_options, 'utf8', 'a+', 0o644); const flag = flags.parse(options.flag); if (!(flag & constants.O_APPEND)) { throw new Exception(Errno.EINVAL, 'Flag passed to appendFile must allow for appending'); } if (typeof data != 'string' && !options.encoding) { throw new Exception(Errno.EINVAL, 'Encoding not specified'); } const encodedData = typeof data == 'string' ? Buffer.from(data, options.encoding) : new Uint8Array(data.buffer, data.byteOffset, data.byteLength); const file = typeof filename == 'number' ? fromFD(this, filename) : _sync.open.call(this, normalizePath(filename), { flag, mode: options.mode, preserveSymlinks: true, }); file.writeSync(encodedData, 0, encodedData.byteLength); if (typeof file != 'number') file.closeSync(); } appendFileSync; export function fstatSync(fd, options) { const stats = fromFD(this, fd).stat(); return options?.bigint ? new BigIntStats(stats) : new Stats(stats); } fstatSync; export function closeSync(fd) { fromFD(this, fd).closeSync(); deleteFD(this, fd); } closeSync; export function ftruncateSync(fd, len = 0) { len ||= 0; if (len < 0) { throw new Exception(Errno.EINVAL); } fromFD(this, fd).truncateSync(len); } ftruncateSync; export function fsyncSync(fd) { fromFD(this, fd).syncSync(); } fsyncSync; export function fdatasyncSync(fd) { fromFD(this, fd).datasyncSync(); } fdatasyncSync; export function writeSync(fd, data, posOrOff, lenOrEnc, pos) { let buffer, offset, length, position; if (typeof data === 'string') { // Signature 1: (fd, string, [position?, [encoding?]]) position = typeof posOrOff === 'number' ? posOrOff : null; const encoding = typeof lenOrEnc === 'string' ? lenOrEnc : 'utf8'; offset = 0; buffer = Buffer.from(data, encoding); length = buffer.byteLength; } else { // Signature 2: (fd, buffer, offset, length, position?) buffer = new Uint8Array(data.buffer, data.byteOffset, data.byteLength); offset = posOrOff; length = lenOrEnc; position = typeof pos === 'number' ? pos : null; } const file = fromFD(this, fd); position ??= file.position; const bytesWritten = file.writeSync(buffer, offset, length, position); emitChange(this, 'change', file.path); return bytesWritten; } writeSync; /** * Read data from the file specified by `fd`. * @param buffer The buffer that the data will be written to. * @param offset The offset within the buffer where writing will start. * @param length An integer specifying the number of bytes to read. * @param position An integer specifying where to begin reading from in the file. * If position is null, data will be read from the current file position. */ export function readSync(fd, buffer, options, length, position) { const file = fromFD(this, fd); const offset = typeof options == 'object' ? options.offset : options; if (typeof options == 'object') { length = options.length; position = options.position; } if (position && position > Number.MAX_SAFE_INTEGER) throw UV('EINVAL'); if (typeof position == 'bigint') position = Number(position); position = Number.isSafeInteger(position) ? position : file.position; return file.readSync(buffer, offset, length, position); } readSync; export function fchownSync(fd, uid, gid) { fromFD(this, fd).chownSync(uid, gid); } fchownSync; export function fchmodSync(fd, mode) { const numMode = normalizeMode(mode, -1); if (numMode < 0) { throw new Exception(Errno.EINVAL, `Invalid mode.`); } fromFD(this, fd).chmodSync(numMode); } fchmodSync; /** * Change the file timestamps of a file referenced by the supplied file descriptor. */ export function futimesSync(fd, atime, mtime) { fromFD(this, fd).utimesSync(normalizeTime(atime), normalizeTime(mtime)); } futimesSync; export function rmdirSync(path) { path = normalizePath(path); const { fs, path: resolved } = _sync.resolve(this, path); const stats = wrap(fs, 'statSync', path)(resolved); if (!isDirectory(stats)) throw UV('ENOTDIR', 'rmdir', path); if (checkAccess && !hasAccess(this, stats, constants.W_OK)) throw UV('EACCES', 'rmdir', path); wrap(fs, 'rmdirSync', path)(resolved); emitChange(this, 'rename', path.toString()); } rmdirSync; export function mkdirSync(path, options) { options = typeof options === 'object' ? options : { mode: options }; const mode = normalizeMode(options?.mode, 0o777); return _sync.mkdir.call(this, path, { ...options, mode }); } mkdirSync; export function readdirSync(path, options) { options = typeof options === 'object' ? options : { encoding: options }; path = normalizePath(path); const entries = []; const rawEntries = _sync.readdir.call(this, path, options ?? undefined); for (const raw of rawEntries) { if (options?.withFileTypes) { entries.push(Dirent.from(raw, options.encoding)); } else if (options?.encoding == 'buffer') { entries.push(Buffer.from(raw.path)); } else { entries.push(raw.path); } } return entries; } readdirSync; export function linkSync(targetPath, linkPath) { return _sync.link.call(this, targetPath, linkPath); } linkSync; /** * Synchronous `symlink`. * @param target target path * @param path link path * @param type can be either `'dir'` or `'file'` (default is `'file'`) */ export function symlinkSync(target, path, type = 'file') { const env_4 = { stack: [], error: void 0, hasError: false }; try { if (!['file', 'dir', 'junction'].includes(type)) throw new TypeError('Invalid symlink type: ' + type); path = normalizePath(path); const file = __addDisposableResource(env_4, _sync.open.call(this, path, { flag: 'wx', mode: 0o644 }), false); file.writeSync(encodeUTF8(normalizePath(target, true))); file.chmodSync(constants.S_IFLNK); } catch (e_4) { env_4.error = e_4; env_4.hasError = true; } finally { __disposeResources(env_4); } } symlinkSync; export function readlinkSync(path, options) { const buf = Buffer.from(_sync.readlink.call(this, path)); const encoding = typeof options == 'object' ? options?.encoding : options; if (encoding == 'buffer') { return buf; } // always defaults to utf-8 to avoid wrangler (cloudflare) worker "unknown encoding" exception return buf.toString(encoding ?? 'utf-8'); } readlinkSync; export function chownSync(path, uid, gid) { const env_5 = { stack: [], error: void 0, hasError: false }; try { const handle = __addDisposableResource(env_5, _sync.open.call(this, path, { flag: 'r+', mode: constants.F_OK }), false); handle.chownSync(uid, gid); } catch (e_5) { env_5.error = e_5; env_5.hasError = true; } finally { __disposeResources(env_5); } } chownSync; export function lchownSync(path, uid, gid) { const fd = lopenSync.call(this, path, 'r+'); fchownSync.call(this, fd, uid, gid); closeSync.call(this, fd); } lchownSync; export function chmodSync(path, mode) { const fd = openSync.call(this, path, 'r+'); fchmodSync.call(this, fd, mode); closeSync.call(this, fd); } chmodSync; export function lchmodSync(path, mode) { const fd = lopenSync.call(this, path, 'r+'); fchmodSync.call(this, fd, mode); closeSync.call(this, fd); } lchmodSync; /** * Change file timestamps of the file referenced by the supplied path. */ export function utimesSync(path, atime, mtime) { const fd = openSync.call(this, path, 'r+'); futimesSync.call(this, fd, atime, mtime); closeSync.call(this, fd); } utimesSync; /** * Change file timestamps of the file referenced by the supplied path. */ export function lutimesSync(path, atime, mtime) { const fd = lopenSync.call(this, path, 'r+'); futimesSync.call(this, fd, atime, mtime); closeSync.call(this, fd); } lutimesSync; export function realpathSync(path, options) { const encoding = typeof options == 'string' ? options : (options?.encoding ?? 'utf8'); path = normalizePath(path, true); const { fullPath } = _sync.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); } realpathSync; export function accessSync(path, mode = 0o600) { if (!checkAccess) return; if (!hasAccess(this, statSync.call(this, path), mode)) { throw new Exception(Errno.EACCES); } } accessSync; /** * Synchronous `rm`. Removes files or directories (recursively). * @param path The path to the file or directory to remove. */ export function rmSync(path, options) { path = normalizePath(path); let stats; try { stats = lstatSync.bind(this)(path); } catch (error) { if (error.code != 'ENOENT' || !options?.force) throw error; } if (!stats) return; switch (stats.mode & constants.S_IFMT) { case constants.S_IFDIR: if (options?.recursive) { for (const entry of readdirSync.call(this, path)) { rmSync.call(this, join(path, entry), options); } } rmdirSync.call(this, path); break; case constants.S_IFREG: case constants.S_IFLNK: case constants.S_IFBLK: case constants.S_IFCHR: unlinkSync.call(this, path); break; case constants.S_IFIFO: case constants.S_IFSOCK: default: throw UV('ENOSYS', 'rm', path); } } rmSync; export function mkdtempSync(prefix, options) { const encoding = typeof options === 'object' ? options?.encoding : options || 'utf8'; const path = _tempDirName(prefix); mkdirSync.call(this, path); return encoding == 'buffer' ? Buffer.from(path) : path; } mkdtempSync; /** * Returns a disposable object whose `path` property holds the created directory path. * When the object is disposed, the directory and its contents will be removed if it still exists. * If the directory cannot be deleted, disposal will throw an error. * The object has a `remove()` method which will perform the same task. * @todo Add `satisfies` and maybe change return type once @types/node adds this. */ export function mkdtempDisposableSync(prefix, options) { const path = _tempDirName(prefix); mkdirSync.call(this, path); const remove = () => rmSync(path, { recursive: true, force: true }); return { path, remove, [Symbol.dispose]: remove }; } /** * Synchronous `copyFile`. Copies a file. * @param flags Optional flags for the copy operation. Currently supports these flags: * - `fs.constants.COPYFILE_EXCL`: If the destination file already exists, the operation fails. */ export function copyFileSync(source, destination, flags) { source = normalizePath(source); destination = normalizePath(destination); if (flags && flags & constants.COPYFILE_EXCL && existsSync(destination)) throw UV('EEXIST', 'copyFile', destination); writeFileSync.call(this, destination, readFileSync(source)); emitChange(this, 'rename', destination.toString()); } copyFileSync; /** * Synchronous `readv`. Reads from a file descriptor into multiple buffers. * @param fd The file descriptor. * @param buffers An array of Uint8Array buffers. * @param position The position in the file where to begin reading. * @returns The number of bytes read. */ export function readvSync(fd, buffers, position) { const file = fromFD(this, fd); let bytesRead = 0; for (const buffer of buffers) { bytesRead += file.readSync(buffer, 0, buffer.byteLength, position + bytesRead); } return bytesRead; } readvSync; /** * Synchronous `writev`. Writes from multiple buffers into a file descriptor. * @param fd The file descriptor. * @param buffers An array of Uint8Array buffers. * @param position The position in the file where to begin writing. * @returns The number of bytes written. */ export function writevSync(fd, buffers, position) { const file = fromFD(this, fd); let bytesWritten = 0; for (const buffer of buffers) { bytesWritten += file.writeSync(new Uint8Array(buffer.buffer), 0, buffer.byteLength, position + bytesWritten); } return bytesWritten; } writevSync; /** * Synchronous `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 Handle options */ export function opendirSync(path, options) { path = normalizePath(path); return new Dir(path, this); } opendirSync; /** * Synchronous `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.cpSync': * - `dereference`: Dereference symbolic links. *(unconfirmed)* * - `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. *(unconfirmed)* * - `preserveTimestamps`: Preserve file timestamps. * - `recursive`: If `true`, copies directories recursively. */ export function cpSync(source, destination, opts) { source = normalizePath(source); destination = normalizePath(destination); const srcStats = lstatSync.call(this, source); // Use lstat to follow symlinks if not dereferencing if (opts?.errorOnExist && existsSync.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); mkdirSync.call(this, destination, { recursive: true }); // Ensure the destination directory exists for (const dirent of readdirSync.call(this, source, { withFileTypes: true })) { if (opts.filter && !opts.filter(join(source, dirent.name), join(destination, dirent.name))) { continue; // Skip if the filter returns false } cpSync.call(this, join(source, dirent.name), join(destination, dirent.name), opts); } break; case constants.S_IFREG: case constants.S_IFLNK: copyFileSync.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) { utimesSync.call(this, destination, srcStats.atime, srcStats.mtime); } } cpSync; export function statfsSync(path, options) { path = normalizePath(path); const { fs } = resolveMount(path, this); return _statfs(fs, options?.bigint); } export function globSync(pattern, options = {}) { pattern = Array.isArray(pattern) ? pattern : [pattern]; const { cwd = '/', withFileTypes = false, exclude = () => false } = options; 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); const results = []; function recursiveList(dir) { const entries = readdirSync(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 (statSync(fullPath).isDirectory()) { if (hasGlobStar || patternBases.some(base => relativePath === base || base.startsWith(relativePath + '/'))) { recursiveList(fullPath); } } if (regexPatterns.some(rx => rx.test(relativePath))) { results.push(withFileTypes ? entry : relativePath); } } } recursiveList(cwd instanceof URL ? cwd.pathname : cwd); return results; } globSync;