461 lines
13 KiB
JavaScript
461 lines
13 KiB
JavaScript
'use strict';
|
|
|
|
Object.defineProperty(exports, '__esModule', { value: true });
|
|
|
|
function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; }
|
|
|
|
var pify = _interopDefault(require('pify'));
|
|
|
|
function compareStrings(a, b) {
|
|
// https://stackoverflow.com/a/40355107/2168416
|
|
return -(a < b) || +(a > b)
|
|
}
|
|
|
|
function dirname(path) {
|
|
const last = Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\'));
|
|
if (last === -1) return '.'
|
|
if (last === 0) return '/'
|
|
return path.slice(0, last)
|
|
}
|
|
|
|
/*!
|
|
* This code for `path.join` is directly copied from @zenfs/core/path for bundle size improvements.
|
|
* SPDX-License-Identifier: LGPL-3.0-or-later
|
|
* Copyright (c) James Prevett and other ZenFS contributors.
|
|
*/
|
|
|
|
function normalizeString(path, aar) {
|
|
let res = '';
|
|
let lastSegmentLength = 0;
|
|
let lastSlash = -1;
|
|
let dots = 0;
|
|
let char = '\x00';
|
|
for (let i = 0; i <= path.length; ++i) {
|
|
if (i < path.length) char = path[i];
|
|
else if (char === '/') break
|
|
else char = '/';
|
|
|
|
if (char === '/') {
|
|
if (lastSlash === i - 1 || dots === 1) {
|
|
// NOOP
|
|
} else if (dots === 2) {
|
|
if (
|
|
res.length < 2 ||
|
|
lastSegmentLength !== 2 ||
|
|
res.at(-1) !== '.' ||
|
|
res.at(-2) !== '.'
|
|
) {
|
|
if (res.length > 2) {
|
|
const lastSlashIndex = res.lastIndexOf('/');
|
|
if (lastSlashIndex === -1) {
|
|
res = '';
|
|
lastSegmentLength = 0;
|
|
} else {
|
|
res = res.slice(0, lastSlashIndex);
|
|
lastSegmentLength = res.length - 1 - res.lastIndexOf('/');
|
|
}
|
|
lastSlash = i;
|
|
dots = 0;
|
|
continue
|
|
} else if (res.length !== 0) {
|
|
res = '';
|
|
lastSegmentLength = 0;
|
|
lastSlash = i;
|
|
dots = 0;
|
|
continue
|
|
}
|
|
}
|
|
if (aar) {
|
|
res += res.length > 0 ? '/..' : '..';
|
|
lastSegmentLength = 2;
|
|
}
|
|
} else {
|
|
if (res.length > 0) res += '/' + path.slice(lastSlash + 1, i);
|
|
else res = path.slice(lastSlash + 1, i);
|
|
lastSegmentLength = i - lastSlash - 1;
|
|
}
|
|
lastSlash = i;
|
|
dots = 0;
|
|
} else if (char === '.' && dots !== -1) {
|
|
++dots;
|
|
} else {
|
|
dots = -1;
|
|
}
|
|
}
|
|
return res
|
|
}
|
|
|
|
function normalize(path) {
|
|
if (!path.length) return '.'
|
|
|
|
const isAbsolute = path[0] === '/';
|
|
const trailingSeparator = path.at(-1) === '/';
|
|
|
|
path = normalizeString(path, !isAbsolute);
|
|
|
|
if (!path.length) {
|
|
if (isAbsolute) return '/'
|
|
return trailingSeparator ? './' : '.'
|
|
}
|
|
if (trailingSeparator) path += '/';
|
|
|
|
return isAbsolute ? `/${path}` : path
|
|
}
|
|
|
|
function join(...args) {
|
|
if (args.length === 0) return '.'
|
|
let joined;
|
|
for (let i = 0; i < args.length; ++i) {
|
|
const arg = args[i];
|
|
if (arg.length > 0) {
|
|
if (joined === undefined) joined = arg;
|
|
else joined += '/' + arg;
|
|
}
|
|
}
|
|
if (joined === undefined) return '.'
|
|
return normalize(joined)
|
|
}
|
|
|
|
/**
|
|
* Removes the directory at the specified filepath recursively. Used internally to replicate the behavior of
|
|
* fs.promises.rm({ recursive: true, force: true }) from Node.js 14 and above when not available. If the provided
|
|
* filepath resolves to a file, it will be removed.
|
|
*
|
|
* @param {import('../models/FileSystem.js').FileSystem} fs
|
|
* @param {string} filepath - The file or directory to remove.
|
|
*/
|
|
async function rmRecursive(fs, filepath) {
|
|
const entries = await fs.readdir(filepath);
|
|
if (entries == null) {
|
|
await fs.rm(filepath);
|
|
} else if (entries.length) {
|
|
await Promise.all(
|
|
entries.map(entry => {
|
|
const subpath = join(filepath, entry);
|
|
return fs.lstat(subpath).then(stat => {
|
|
if (!stat) return
|
|
return stat.isDirectory() ? rmRecursive(fs, subpath) : fs.rm(subpath)
|
|
})
|
|
})
|
|
).then(() => fs.rmdir(filepath));
|
|
} else {
|
|
await fs.rmdir(filepath);
|
|
}
|
|
}
|
|
|
|
function isPromiseLike(obj) {
|
|
return isObject(obj) && isFunction(obj.then) && isFunction(obj.catch)
|
|
}
|
|
|
|
function isObject(obj) {
|
|
return obj && typeof obj === 'object'
|
|
}
|
|
|
|
function isFunction(obj) {
|
|
return typeof obj === 'function'
|
|
}
|
|
|
|
function isPromiseFs(fs) {
|
|
const test = targetFs => {
|
|
try {
|
|
// If readFile returns a promise then we can probably assume the other
|
|
// commands do as well
|
|
return targetFs.readFile().catch(e => e)
|
|
} catch (e) {
|
|
return e
|
|
}
|
|
};
|
|
return isPromiseLike(test(fs))
|
|
}
|
|
|
|
// List of commands all filesystems are expected to provide. `rm` is not
|
|
// included since it may not exist and must be handled as a special case
|
|
const commands = [
|
|
'readFile',
|
|
'writeFile',
|
|
'mkdir',
|
|
'rmdir',
|
|
'unlink',
|
|
'stat',
|
|
'lstat',
|
|
'readdir',
|
|
'readlink',
|
|
'symlink',
|
|
];
|
|
|
|
function bindFs(target, fs) {
|
|
if (isPromiseFs(fs)) {
|
|
for (const command of commands) {
|
|
target[`_${command}`] = fs[command].bind(fs);
|
|
}
|
|
} else {
|
|
for (const command of commands) {
|
|
target[`_${command}`] = pify(fs[command].bind(fs));
|
|
}
|
|
}
|
|
|
|
// Handle the special case of `rm`
|
|
if (isPromiseFs(fs)) {
|
|
if (fs.rm) target._rm = fs.rm.bind(fs);
|
|
else if (fs.rmdir.length > 1) target._rm = fs.rmdir.bind(fs);
|
|
else target._rm = rmRecursive.bind(null, target);
|
|
} else {
|
|
if (fs.rm) target._rm = pify(fs.rm.bind(fs));
|
|
else if (fs.rmdir.length > 2) target._rm = pify(fs.rmdir.bind(fs));
|
|
else target._rm = rmRecursive.bind(null, target);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A wrapper class for file system operations, providing a consistent API for both promise-based
|
|
* and callback-based file systems. It includes utility methods for common file system tasks.
|
|
*/
|
|
class FileSystem {
|
|
/**
|
|
* Creates an instance of FileSystem.
|
|
*
|
|
* @param {Object} fs - A file system implementation to wrap.
|
|
*/
|
|
constructor(fs) {
|
|
if (typeof fs._original_unwrapped_fs !== 'undefined') return fs
|
|
|
|
const promises = Object.getOwnPropertyDescriptor(fs, 'promises');
|
|
if (promises && promises.enumerable) {
|
|
bindFs(this, fs.promises);
|
|
} else {
|
|
bindFs(this, fs);
|
|
}
|
|
this._original_unwrapped_fs = fs;
|
|
}
|
|
|
|
/**
|
|
* Return true if a file exists, false if it doesn't exist.
|
|
* Rethrows errors that aren't related to file existence.
|
|
*
|
|
* @param {string} filepath - The path to the file.
|
|
* @param {Object} [options] - Additional options.
|
|
* @returns {Promise<boolean>} - `true` if the file exists, `false` otherwise.
|
|
*/
|
|
async exists(filepath, options = {}) {
|
|
try {
|
|
await this._stat(filepath);
|
|
return true
|
|
} catch (err) {
|
|
if (
|
|
err.code === 'ENOENT' ||
|
|
err.code === 'ENOTDIR' ||
|
|
(err.code || '').includes('ENS')
|
|
) {
|
|
return false
|
|
} else {
|
|
console.log('Unhandled error in "FileSystem.exists()" function', err);
|
|
throw err
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Return the contents of a file if it exists, otherwise returns null.
|
|
*
|
|
* @param {string} filepath - The path to the file.
|
|
* @param {Object} [options] - Options for reading the file.
|
|
* @returns {Promise<Buffer|string|null>} - The file contents, or `null` if the file doesn't exist.
|
|
*/
|
|
async read(filepath, options = {}) {
|
|
try {
|
|
let buffer = await this._readFile(filepath, options);
|
|
if (options.autocrlf === 'true') {
|
|
try {
|
|
buffer = new TextDecoder('utf8', { fatal: true }).decode(buffer);
|
|
buffer = buffer.replace(/\r\n/g, '\n');
|
|
buffer = new TextEncoder().encode(buffer);
|
|
} catch (error) {
|
|
// non utf8 file
|
|
}
|
|
}
|
|
// Convert plain ArrayBuffers to Buffers
|
|
if (typeof buffer !== 'string') {
|
|
buffer = Buffer.from(buffer);
|
|
}
|
|
return buffer
|
|
} catch (err) {
|
|
return null
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Write a file (creating missing directories if need be) without throwing errors.
|
|
*
|
|
* @param {string} filepath - The path to the file.
|
|
* @param {Buffer|Uint8Array|string} contents - The data to write.
|
|
* @param {Object|string} [options] - Options for writing the file.
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async write(filepath, contents, options = {}) {
|
|
try {
|
|
await this._writeFile(filepath, contents, options);
|
|
} catch (err) {
|
|
// Hmm. Let's try mkdirp and try again.
|
|
await this.mkdir(dirname(filepath));
|
|
await this._writeFile(filepath, contents, options);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Make a directory (or series of nested directories) without throwing an error if it already exists.
|
|
*
|
|
* @param {string} filepath - The path to the directory.
|
|
* @param {boolean} [_selfCall=false] - Internal flag to prevent infinite recursion.
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async mkdir(filepath, _selfCall = false) {
|
|
try {
|
|
await this._mkdir(filepath);
|
|
} catch (err) {
|
|
// If err is null then operation succeeded!
|
|
if (err === null) return
|
|
// If the directory already exists, that's OK!
|
|
if (err.code === 'EEXIST') return
|
|
// Avoid infinite loops of failure
|
|
if (_selfCall) throw err
|
|
// If we got a "no such file or directory error" backup and try again.
|
|
if (err.code === 'ENOENT') {
|
|
const parent = dirname(filepath);
|
|
// Check to see if we've gone too far
|
|
if (parent === '.' || parent === '/' || parent === filepath) throw err
|
|
// Infinite recursion, what could go wrong?
|
|
await this.mkdir(parent);
|
|
await this.mkdir(filepath, true);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Delete a file without throwing an error if it is already deleted.
|
|
*
|
|
* @param {string} filepath - The path to the file.
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async rm(filepath) {
|
|
try {
|
|
await this._unlink(filepath);
|
|
} catch (err) {
|
|
if (err.code !== 'ENOENT') throw err
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Delete a directory without throwing an error if it is already deleted.
|
|
*
|
|
* @param {string} filepath - The path to the directory.
|
|
* @param {Object} [opts] - Options for deleting the directory.
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async rmdir(filepath, opts) {
|
|
try {
|
|
if (opts && opts.recursive) {
|
|
await this._rm(filepath, opts);
|
|
} else {
|
|
await this._rmdir(filepath);
|
|
}
|
|
} catch (err) {
|
|
if (err.code !== 'ENOENT') throw err
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Read a directory without throwing an error is the directory doesn't exist
|
|
*
|
|
* @param {string} filepath - The path to the directory.
|
|
* @returns {Promise<string[]|null>} - An array of file names, or `null` if the path is not a directory.
|
|
*/
|
|
async readdir(filepath) {
|
|
try {
|
|
const names = await this._readdir(filepath);
|
|
// Ordering is not guaranteed, and system specific (Windows vs Unix)
|
|
// so we must sort them ourselves.
|
|
names.sort(compareStrings);
|
|
return names
|
|
} catch (err) {
|
|
if (err.code === 'ENOTDIR') return null
|
|
return []
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Return a flat list of all the files nested inside a directory
|
|
*
|
|
* Based on an elegant concurrent recursive solution from SO
|
|
* https://stackoverflow.com/a/45130990/2168416
|
|
*
|
|
* @param {string} dir - The directory to read.
|
|
* @returns {Promise<string[]>} - A flat list of all files in the directory.
|
|
*/
|
|
async readdirDeep(dir) {
|
|
const subdirs = await this._readdir(dir);
|
|
const files = await Promise.all(
|
|
subdirs.map(async subdir => {
|
|
const res = dir + '/' + subdir;
|
|
return (await this._stat(res)).isDirectory()
|
|
? this.readdirDeep(res)
|
|
: res
|
|
})
|
|
);
|
|
return files.reduce((a, f) => a.concat(f), [])
|
|
}
|
|
|
|
/**
|
|
* Return the Stats of a file/symlink if it exists, otherwise returns null.
|
|
* Rethrows errors that aren't related to file existence.
|
|
*
|
|
* @param {string} filename - The path to the file or symlink.
|
|
* @returns {Promise<Object|null>} - The stats object, or `null` if the file doesn't exist.
|
|
*/
|
|
async lstat(filename) {
|
|
try {
|
|
const stats = await this._lstat(filename);
|
|
return stats
|
|
} catch (err) {
|
|
if (err.code === 'ENOENT' || (err.code || '').includes('ENS')) {
|
|
return null
|
|
}
|
|
throw err
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Reads the contents of a symlink if it exists, otherwise returns null.
|
|
* Rethrows errors that aren't related to file existence.
|
|
*
|
|
* @param {string} filename - The path to the symlink.
|
|
* @param {Object} [opts={ encoding: 'buffer' }] - Options for reading the symlink.
|
|
* @returns {Promise<Buffer|null>} - The symlink target, or `null` if it doesn't exist.
|
|
*/
|
|
async readlink(filename, opts = { encoding: 'buffer' }) {
|
|
// Note: FileSystem.readlink returns a buffer by default
|
|
// so we can dump it into GitObject.write just like any other file.
|
|
try {
|
|
const link = await this._readlink(filename, opts);
|
|
return Buffer.isBuffer(link) ? link : Buffer.from(link)
|
|
} catch (err) {
|
|
if (err.code === 'ENOENT' || (err.code || '').includes('ENS')) {
|
|
return null
|
|
}
|
|
throw err
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Write the contents of buffer to a symlink.
|
|
*
|
|
* @param {string} filename - The path to the symlink.
|
|
* @param {Buffer} buffer - The symlink target.
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async writelink(filename, buffer) {
|
|
return this._symlink(buffer.toString('utf8'), filename)
|
|
}
|
|
}
|
|
|
|
exports.FileSystem = FileSystem;
|