@zenfs/core
Version:
A filesystem, anywhere
676 lines (675 loc) • 25.4 kB
JavaScript
var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) {
if (value !== null && value !== void 0) {
if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
var dispose, inner;
if (async) {
if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
dispose = value[Symbol.asyncDispose];
}
if (dispose === void 0) {
if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
dispose = value[Symbol.dispose];
if (async) inner = dispose;
}
if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };
env.stack.push({ value: value, dispose: dispose, async: async });
}
else if (async) {
env.stack.push({ async: true });
}
return value;
};
var __disposeResources = (this && this.__disposeResources) || (function (SuppressedError) {
return function (env) {
function fail(e) {
env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
env.hasError = true;
}
var r, s = 0;
function next() {
while (r = env.stack.pop()) {
try {
if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
if (r.dispose) {
var result = r.dispose.call(r.value);
if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
}
else s |= 1;
}
catch (e) {
fail(e);
}
}
if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
if (env.hasError) throw env.error;
}
return next();
};
})(typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
var e = new Error(message);
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
});
import { Buffer } from 'buffer';
import { 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;