packages/kie-sandbox-fs/src/DefaultBackend.js (220 lines of code) (raw):
const { encode, decode } = require("isomorphic-textencoder");
const debounce = require("just-debounce-it");
const CacheFS = require("./CacheFS.js");
const { ENOENT, ENOTEMPTY, ETIMEDOUT } = require("./errors.js");
const IdbBackend = require("./IdbBackend.js");
const Mutex = require("./Mutex.js");
const Mutex2 = require("./Mutex2.js");
const path = require("./path.js");
module.exports = class DefaultBackend {
constructor(args) {
this._idbBackendDelegate = args ? args.idbBackendDelegate : undefined;
this.saveSuperblock = debounce(() => {
this._saveSuperblock();
}, 500);
}
async init(
name,
{
wipe,
fileDbName = name,
fileStoreName = name + "_files",
lockDbName = name + "_lock",
lockStoreName = name + "_lock",
idbBackend = this._idbBackendDelegate
? this._idbBackendDelegate(fileDbName, fileStoreName)
: new IdbBackend(fileDbName, fileStoreName),
} = {}
) {
this._name = name;
this._idb = idbBackend;
this._mutex = navigator.locks ? new Mutex2(name) : new Mutex(lockDbName, lockStoreName);
this._cache = new CacheFS(name);
this._opts = { wipe };
this._needsWipe = !!wipe;
}
async activate() {
if (this._cache.activated) return;
// Wipe IDB if requested
if (this._needsWipe) {
this._needsWipe = false;
await this._idb.wipe();
await this._mutex.release({ force: true });
}
if (!(await this._mutex.has())) await this._mutex.wait();
// Attempt to load FS from IDB backend
const root = await this._idb.loadSuperblock();
if (root) {
this._cache.activate(root);
} else {
// Start with an empty filesystem
this._cache.activate();
}
if (await this._mutex.has()) {
return;
} else {
throw new ETIMEDOUT();
}
}
async deactivate() {
if (await this._mutex.has()) {
await this._saveSuperblock();
}
this._cache.deactivate();
try {
await this._mutex.release();
} catch (e) {
console.log(e);
}
await this._idb.close();
}
async _saveSuperblock() {
if (this._cache.activated) {
this._lastSavedAt = Date.now();
await this._idb.saveSuperblock(this._cache._root);
}
}
_writeStat(filepath, size, opts) {
let dirparts = path.split(path.dirname(filepath));
let dir = dirparts.shift();
for (let dirpart of dirparts) {
dir = path.join(dir, dirpart);
try {
this._cache.mkdir(dir, { mode: 0o777 });
} catch (e) {}
}
return this._cache.writeStat(filepath, size, opts);
}
async readFile(filepath, opts) {
const { encoding } = opts;
if (encoding && encoding !== "utf8") throw new Error('Only "utf8" encoding is supported in readFile');
let data = null,
stat = null;
stat = this._cache.stat(filepath);
data = await this._idb.readFile(stat.ino);
if (data) {
if (!stat || stat.size != data.byteLength) {
stat = await this._writeStat(filepath, data.byteLength, { mode: stat ? stat.mode : 0o666 });
this.saveSuperblock(); // debounced
}
if (encoding === "utf8") {
data = decode(data);
}
}
if (!stat) throw new ENOENT(filepath);
return data;
}
async readFileBulk(filepaths, opts) {
const { encoding } = opts;
if (encoding && encoding !== "utf8") {
console.info(encoding);
throw new Error('Only "utf8" encoding is supported in readFile');
}
if (!this._idb.readFileBulk) {
throw new Error("Current IndexedDB backend doesn't support bulk-reading operations.");
}
const inoBulk = [];
const enoentBulk = [];
for (const filepath of filepaths) {
try {
const stat = this._cache.stat(filepath);
if (stat) {
inoBulk.push(stat.ino);
} else {
enoentBulk.push(filepath);
}
} catch (e) {
enoentBulk.push(filepath);
}
}
if (enoentBulk.length > 0) {
throw new ENOENT(enoentBulk.join(", "));
}
const dataBulk = await this._idb.readFileBulk(inoBulk);
if (dataBulk.length !== filepaths.length) {
throw new Error("Unexpected error during bulk-read");
}
const fileBulk = [];
for (let i = 0; i < dataBulk.length; i++) {
fileBulk[i] = [filepaths[i], encoding ? decode(dataBulk[i]) : dataBulk[i]];
}
return fileBulk;
}
async writeFile(filepath, data, opts) {
const { mode, encoding = "utf8" } = opts;
if (typeof data === "string") {
if (encoding !== "utf8") {
throw new Error('Only "utf8" encoding is supported in writeFile');
}
data = encode(data);
}
const stat = await this._cache.writeStat(filepath, data.byteLength, { mode });
await this._idb.writeFile(stat.ino, data);
}
async writeFileBulk(files, opts) {
if (!this._idb.writeFileBulk) {
throw new Error("Current IndexedDB backend doesn't support bulk-writing operations.");
}
const { mode, encoding = "utf8" } = opts;
if (encoding !== "utf8") {
throw new Error('Only "utf8" encoding is supported in writeFileBulk');
}
const inoBulk = [];
const dataBulk = [];
for (const [filepath, data] of files) {
const stat = this._cache.writeStat(filepath, data.byteLength, { mode });
inoBulk.push(stat.ino);
dataBulk.push(typeof data === "string" ? encode(data) : data);
}
await this._idb.writeFileBulk(inoBulk, dataBulk);
}
async unlink(filepath, opts) {
const stat = this._cache.lstat(filepath);
this._cache.unlink(filepath);
if (stat.type !== "symlink") {
await this._idb.unlink(stat.ino);
}
}
async unlinkBulk(filepaths, opts) {
if (!this._idb.unlinkBulk) {
throw new Error("Current IndexedDB backend doesn't support bulk-unlinking operations.");
}
const inoBulk = [];
for (const filepath of filepaths) {
const stat = this._cache.lstat(filepath);
this._cache.unlink(filepath);
if (stat.type !== "symlink") {
inoBulk.push(stat.ino);
}
}
await this._idb.unlinkBulk(inoBulk);
}
readdir(filepath, opts) {
return this._cache.readdir(filepath);
}
mkdir(filepath, opts) {
const { mode = 0o777 } = opts;
this._cache.mkdir(filepath, { mode });
}
rmdir(filepath, opts) {
// Never allow deleting the root directory.
if (filepath === "/") {
throw new ENOTEMPTY();
}
this._cache.rmdir(filepath);
}
rename(oldFilepath, newFilepath) {
this._cache.rename(oldFilepath, newFilepath);
}
stat(filepath, opts) {
return this._cache.stat(filepath);
}
lstat(filepath, opts) {
return this._cache.lstat(filepath);
}
readlink(filepath, opts) {
return this._cache.readlink(filepath);
}
symlink(target, filepath) {
this._cache.symlink(target, filepath);
}
du(filepath) {
return this._cache.du(filepath);
}
};