UNPKG

isomorphic-git

Version:

A pure JavaScript reimplementation of git for node and browsers

366 lines (338 loc) 11 kB
'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')); var pathBrowserify = require('path-browserify'); 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) } /** * 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 = pathBrowserify.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); return } 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); return } 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;