@zenfs/core
Version:
A filesystem, anywhere
552 lines (551 loc) • 19 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;
}
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;