UNPKG

@zenfs/core

Version:

A filesystem, anywhere

552 lines (551 loc) 19 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; } function next() { while (env.stack.length) { var rec = env.stack.pop(); try { var result = rec.dispose && rec.dispose.call(rec.value); if (rec.async) return Promise.resolve(result).then(next, function(e) { fail(e); return next(); }); } catch (e) { fail(e); } } 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 { dirname } from '../emulation/path.js'; import { Errno, ErrnoError } from '../error.js'; import { PreloadFile, parseFlag } from '../file.js'; import { FileSystem } from '../filesystem.js'; import { Mutexed } from '../mixins/mutexed.js'; import { Stats } from '../stats.js'; import { decodeUTF8, encodeUTF8 } from '../utils.js'; /** @internal */ const deletionLogPath = '/.deleted'; /** * OverlayFS makes a read-only filesystem writable by storing writes on a second, writable file system. * Deletes are persisted via metadata stored on the writable file system. * * This class contains no locking whatsoever. It is mutexed to prevent races. * * @internal */ export class UnmutexedOverlayFS extends FileSystem { async ready() { await this.readable.ready(); await this.writable.ready(); await this._ready; } constructor({ writable, readable }) { super(); this._isInitialized = false; this._deletedFiles = new Set(); this._deleteLog = ''; // If 'true', we have scheduled a delete log update. this._deleteLogUpdatePending = false; // If 'true', a delete log update is needed after the scheduled delete log // update finishes. this._deleteLogUpdateNeeded = false; this.writable = writable; this.readable = readable; if (this.writable.metadata().readonly) { throw new ErrnoError(Errno.EINVAL, 'Writable file system must be writable.'); } this._ready = this._initialize(); } metadata() { return { ...super.metadata(), name: OverlayFS.name, }; } async sync(path, data, stats) { await this.copyForWrite(path); if (!(await this.writable.exists(path))) { await this.writable.createFile(path, 'w', 0o644); } await this.writable.sync(path, data, stats); } syncSync(path, data, stats) { this.copyForWriteSync(path); this.writable.syncSync(path, data, stats); } /** * Called once to load up metadata stored on the writable file system. * @internal */ async _initialize() { if (this._isInitialized) { return; } // Read deletion log, process into metadata. try { const file = await this.writable.openFile(deletionLogPath, parseFlag('r')); const { size } = await file.stat(); const { buffer } = await file.read(new Uint8Array(size)); this._deleteLog = decodeUTF8(buffer); } catch (err) { if (err.errno !== Errno.ENOENT) { throw err; } } this._isInitialized = true; this._reparseDeletionLog(); } getDeletionLog() { return this._deleteLog; } async restoreDeletionLog(log) { this._deleteLog = log; this._reparseDeletionLog(); await this.updateLog(''); } async rename(oldPath, newPath) { this.checkInitialized(); this.checkPath(oldPath); this.checkPath(newPath); await this.copyForWrite(oldPath); try { await this.writable.rename(oldPath, newPath); } catch { if (this._deletedFiles.has(oldPath)) { throw ErrnoError.With('ENOENT', oldPath, 'rename'); } } } renameSync(oldPath, newPath) { this.checkInitialized(); this.checkPath(oldPath); this.checkPath(newPath); this.copyForWriteSync(oldPath); try { this.writable.renameSync(oldPath, newPath); } catch { if (this._deletedFiles.has(oldPath)) { throw ErrnoError.With('ENOENT', oldPath, 'rename'); } } } async stat(path) { this.checkInitialized(); try { return await this.writable.stat(path); } catch { if (this._deletedFiles.has(path)) { throw ErrnoError.With('ENOENT', path, 'stat'); } const oldStat = new Stats(await this.readable.stat(path)); // Make the oldStat's mode writable. oldStat.mode |= 0o222; return oldStat; } } statSync(path) { this.checkInitialized(); try { return this.writable.statSync(path); } catch { if (this._deletedFiles.has(path)) { throw ErrnoError.With('ENOENT', path, 'stat'); } const oldStat = new Stats(this.readable.statSync(path)); // Make the oldStat's mode writable. oldStat.mode |= 0o222; return oldStat; } } async openFile(path, flag) { if (await this.writable.exists(path)) { return this.writable.openFile(path, flag); } // Create an OverlayFile. const file = await this.readable.openFile(path, parseFlag('r')); const stats = await file.stat(); const { buffer } = await file.read(new Uint8Array(stats.size)); return new PreloadFile(this, path, flag, stats, buffer); } openFileSync(path, flag) { if (this.writable.existsSync(path)) { return this.writable.openFileSync(path, flag); } // Create an OverlayFile. const file = this.readable.openFileSync(path, parseFlag('r')); const stats = file.statSync(); const data = new Uint8Array(stats.size); file.readSync(data); return new PreloadFile(this, path, flag, stats, data); } async createFile(path, flag, mode) { this.checkInitialized(); await this.writable.createFile(path, flag, mode); return this.openFile(path, flag); } createFileSync(path, flag, mode) { this.checkInitialized(); this.writable.createFileSync(path, flag, mode); return this.openFileSync(path, flag); } async link(srcpath, dstpath) { this.checkInitialized(); await this.copyForWrite(srcpath); await this.writable.link(srcpath, dstpath); } linkSync(srcpath, dstpath) { this.checkInitialized(); this.copyForWriteSync(srcpath); this.writable.linkSync(srcpath, dstpath); } async unlink(path) { this.checkInitialized(); this.checkPath(path); if (!(await this.exists(path))) { throw ErrnoError.With('ENOENT', path, 'unlink'); } if (await this.writable.exists(path)) { await this.writable.unlink(path); } // if it still exists add to the delete log if (await this.exists(path)) { await this.deletePath(path); } } unlinkSync(path) { this.checkInitialized(); this.checkPath(path); if (!this.existsSync(path)) { throw ErrnoError.With('ENOENT', path, 'unlink'); } if (this.writable.existsSync(path)) { this.writable.unlinkSync(path); } // if it still exists add to the delete log if (this.existsSync(path)) { void this.deletePath(path); } } async rmdir(path) { this.checkInitialized(); if (!(await this.exists(path))) { throw ErrnoError.With('ENOENT', path, 'rmdir'); } if (await this.writable.exists(path)) { await this.writable.rmdir(path); } if (!(await this.exists(path))) { return; } // Check if directory is empty. if ((await this.readdir(path)).length) { throw ErrnoError.With('ENOTEMPTY', path, 'rmdir'); } await this.deletePath(path); } rmdirSync(path) { this.checkInitialized(); if (!this.existsSync(path)) { throw ErrnoError.With('ENOENT', path, 'rmdir'); } if (this.writable.existsSync(path)) { this.writable.rmdirSync(path); } if (!this.existsSync(path)) { return; } // Check if directory is empty. if (this.readdirSync(path).length) { throw ErrnoError.With('ENOTEMPTY', path, 'rmdir'); } void this.deletePath(path); } async mkdir(path, mode) { this.checkInitialized(); if (await this.exists(path)) { throw ErrnoError.With('EEXIST', path, 'mkdir'); } // The below will throw should any of the parent directories fail to exist on _writable. await this.createParentDirectories(path); await this.writable.mkdir(path, mode); } mkdirSync(path, mode) { this.checkInitialized(); if (this.existsSync(path)) { throw ErrnoError.With('EEXIST', path, 'mkdir'); } // The below will throw should any of the parent directories fail to exist on _writable. this.createParentDirectoriesSync(path); this.writable.mkdirSync(path, mode); } async readdir(path) { this.checkInitialized(); // Readdir in both, check delete log on RO file system's listing, merge, return. const contents = []; try { contents.push(...(await this.writable.readdir(path))); } catch { // NOP. } try { contents.push(...(await this.readable.readdir(path)).filter((fPath) => !this._deletedFiles.has(`${path}/${fPath}`))); } catch { // NOP. } const seenMap = {}; return contents.filter((path) => { const result = !seenMap[path]; seenMap[path] = true; return result; }); } readdirSync(path) { this.checkInitialized(); // Readdir in both, check delete log on RO file system's listing, merge, return. let contents = []; try { contents = contents.concat(this.writable.readdirSync(path)); } catch { // NOP. } try { contents = contents.concat(this.readable.readdirSync(path).filter((fPath) => !this._deletedFiles.has(`${path}/${fPath}`))); } catch { // NOP. } const seenMap = {}; return contents.filter((path) => { const result = !seenMap[path]; seenMap[path] = true; return result; }); } async deletePath(path) { this._deletedFiles.add(path); await this.updateLog(`d${path}\n`); } async updateLog(addition) { this._deleteLog += addition; if (this._deleteLogUpdatePending) { this._deleteLogUpdateNeeded = true; return; } this._deleteLogUpdatePending = true; const log = await this.writable.openFile(deletionLogPath, parseFlag('w')); try { await log.write(encodeUTF8(this._deleteLog)); if (this._deleteLogUpdateNeeded) { this._deleteLogUpdateNeeded = false; await this.updateLog(''); } } catch (e) { this._deleteLogError = e; } finally { this._deleteLogUpdatePending = false; } } _reparseDeletionLog() { this._deletedFiles.clear(); for (const entry of this._deleteLog.split('\n')) { if (!entry.startsWith('d')) { continue; } // If the log entry begins w/ 'd', it's a deletion. this._deletedFiles.add(entry.slice(1)); } } checkInitialized() { if (!this._isInitialized) { throw new ErrnoError(Errno.EPERM, 'OverlayFS is not initialized. Please initialize OverlayFS using its initialize() method before using it.'); } if (!this._deleteLogError) { return; } const error = this._deleteLogError; delete this._deleteLogError; throw error; } checkPath(path) { if (path == deletionLogPath) { throw ErrnoError.With('EPERM', path, 'checkPath'); } } /** * Create the needed parent directories on the writable storage should they not exist. * Use modes from the read-only storage. */ createParentDirectoriesSync(path) { let parent = dirname(path); const toCreate = []; while (!this.writable.existsSync(parent)) { toCreate.push(parent); parent = dirname(parent); } for (const path of toCreate.reverse()) { this.writable.mkdirSync(path, this.statSync(path).mode); } } /** * Create the needed parent directories on the writable storage should they not exist. * Use modes from the read-only storage. */ async createParentDirectories(path) { let parent = dirname(path); const toCreate = []; while (!(await this.writable.exists(parent))) { toCreate.push(parent); parent = dirname(parent); } for (const path of toCreate.reverse()) { const stats = await this.stat(path); await this.writable.mkdir(path, stats.mode); } } /** * Helper function: * - Ensures p is on writable before proceeding. Throws an error if it doesn't exist. * - Calls f to perform operation on writable. */ copyForWriteSync(path) { if (!this.existsSync(path)) { throw ErrnoError.With('ENOENT', path, 'copyForWrite'); } if (!this.writable.existsSync(dirname(path))) { this.createParentDirectoriesSync(path); } if (!this.writable.existsSync(path)) { this.copyToWritableSync(path); } } async copyForWrite(path) { if (!(await this.exists(path))) { throw ErrnoError.With('ENOENT', path, 'copyForWrite'); } if (!(await this.writable.exists(dirname(path)))) { await this.createParentDirectories(path); } if (!(await this.writable.exists(path))) { return this.copyToWritable(path); } } /** * Copy from readable to writable storage. * PRECONDITION: File does not exist on writable storage. */ copyToWritableSync(path) { const env_1 = { stack: [], error: void 0, hasError: false }; try { const stats = this.statSync(path); if (stats.isDirectory()) { this.writable.mkdirSync(path, stats.mode); return; } const data = new Uint8Array(stats.size); const readable = __addDisposableResource(env_1, this.readable.openFileSync(path, 'r'), false); readable.readSync(data); const writable = __addDisposableResource(env_1, this.writable.createFileSync(path, 'w', stats.mode | 0o222), false); writable.writeSync(data); } catch (e_1) { env_1.error = e_1; env_1.hasError = true; } finally { __disposeResources(env_1); } } async copyToWritable(path) { const env_2 = { stack: [], error: void 0, hasError: false }; try { const stats = await this.stat(path); if (stats.isDirectory()) { await this.writable.mkdir(path, stats.mode); return; } const data = new Uint8Array(stats.size); const readable = __addDisposableResource(env_2, await this.readable.openFile(path, 'r'), true); await readable.read(data); const writable = __addDisposableResource(env_2, await this.writable.createFile(path, 'w', stats.mode | 0o222), true); await writable.write(data); } catch (e_2) { env_2.error = e_2; env_2.hasError = true; } finally { const result_1 = __disposeResources(env_2); if (result_1) await result_1; } } } /** * OverlayFS makes a read-only filesystem writable by storing writes on a second, * writable file system. Deletes are persisted via metadata stored on the writable * file system. * @internal */ export class OverlayFS extends Mutexed(UnmutexedOverlayFS) { } const _Overlay = { name: 'Overlay', options: { writable: { type: 'object', required: true, description: 'The file system to write modified files to.', }, readable: { type: 'object', required: true, description: 'The file system that initially populates this file system.', }, }, isAvailable() { return true; }, create(options) { return new OverlayFS(options); }, }; export const Overlay = _Overlay;