'use strict'; var FsErrorCode = /* @__PURE__ */ ((FsErrorCode2) => { FsErrorCode2[FsErrorCode2["NOT_FOUND"] = 1] = "NOT_FOUND"; FsErrorCode2[FsErrorCode2["INVALID_PATH"] = 2] = "INVALID_PATH"; FsErrorCode2[FsErrorCode2["ALREADY_EXISTS"] = 3] = "ALREADY_EXISTS"; FsErrorCode2[FsErrorCode2["NOT_EXIST_PARENT"] = 4] = "NOT_EXIST_PARENT"; FsErrorCode2[FsErrorCode2["FATAL"] = 5] = "FATAL"; return FsErrorCode2; })(FsErrorCode || {}); class FsError extends Error { constructor(message, code) { super(message); this.code = code; } } class FsNotFoundError extends FsError { constructor(message) { super(message, 1 /* NOT_FOUND */); } } class FsInvalidPathError extends FsError { constructor(message) { super(message, 2 /* INVALID_PATH */); } } class FsAlreadyExistsError extends FsError { constructor(message) { super(message, 3 /* ALREADY_EXISTS */); } } class FsNotExistsParentError extends FsError { constructor(message) { super(message, 4 /* NOT_EXIST_PARENT */); } } class FsFatalError extends FsError { constructor(message) { super(message, 5 /* FATAL */); } } var StoreNames = /* @__PURE__ */ ((StoreNames2) => { StoreNames2["FILES"] = "Files"; StoreNames2["DIRECTORYS"] = "Directorys"; StoreNames2["DATA"] = "Data"; return StoreNames2; })(StoreNames || {}); async function* streamToAsyncIterator(stream) { const reader = stream.getReader(); try { while (true) { const { done, value } = await reader.read(); if (done) break; yield value; } } finally { reader.releaseLock(); } } function checkFindResult(result) { for (let idx = 0; idx < result.length; idx++) { if (result[idx].type === "file") { return idx === result.length - 1; } } } function pathMatchResult(path, result) { const valid = checkFindResult(result); if (valid === false) { throw new FsFatalError( "The path cannot be composed of files:" + path.origin + ",please rm it first directly" ); } if (result.length !== path.size) { return false; } for (let idx = 0; idx < result.length; idx++) { if (result[idx].name !== path.slicePathStr[idx]) { return false; } } return true; } const RootDirId = -1; const RootDirName = "_SystemRoot_"; const RootDir = { type: "directory", name: RootDirName, id: RootDirId, parentId: null, createAt: 0, updateAt: 0 }; const KB = 1024; const MB = 1024 * KB; const DIRECT_STORE_SIZE = 256 * KB; const MEDIUM_BLOCK_SIZE = 256 * KB; const LARGE_BLOCK_SIZE = 512 * KB; const EXTRA_LARGE_BLOCK_SIZE = MB; const MAX_BLOCK_SIZE = 2 * KB; function calculateBlockSize(fileSize) { if (fileSize <= DIRECT_STORE_SIZE) { return fileSize; } else if (fileSize <= 2 * MB) { return MEDIUM_BLOCK_SIZE; } else if (fileSize <= 50 * MB) { return LARGE_BLOCK_SIZE; } else if (fileSize <= 100 * MB) { return EXTRA_LARGE_BLOCK_SIZE; } else { return MAX_BLOCK_SIZE; } } function getOneByKey(db, storeName, key) { return new Promise((res, rej) => { const transaction = db.transaction(storeName, "readonly"); const store = transaction.objectStore(storeName); const request = store.get(key); request.onsuccess = (event) => { res(request.result); }; request.onerror = (event) => { rej(request.error); }; }); } function getManyByIndex(db, storeName, indexName, query) { return new Promise((res, rej) => { const transaction = db.transaction(storeName, "readonly"); const store = transaction.objectStore(storeName); const index = store.index(indexName); const request = index.getAll(query); request.onsuccess = (event) => { res(request.result); }; request.onerror = (event) => { rej(request.error); }; }); } async function findPath(db, path) { const result = [RootDir]; if (path.size === 0) { return result; } const transaction = db.transaction( [StoreNames.DIRECTORYS, StoreNames.FILES], "readonly" ); const directory = transaction.objectStore(StoreNames.DIRECTORYS); const file = transaction.objectStore(StoreNames.FILES); let _tempParentId = void 0; try { for (let idx = 0; idx < path.slicePathStr.length; idx++) { const name = path.slicePathStr[idx]; let dirRes = void 0; if (_tempParentId === void 0) { dirRes = RootDir; } else { const p2 = new Promise((res, rej) => { const cursor = directory.index("parentId").openCursor(IDBKeyRange.only(_tempParentId)); cursor.onsuccess = (event) => { const cursor2 = event.target.result; if (cursor2) { if (cursor2.value.name === name) { res(cursor2.value); } else { cursor2.continue(); } } else { res(void 0); } }; cursor.onerror = (event) => { rej(event); }; }); dirRes = await p2; } if (dirRes) { _tempParentId = dirRes.id; } if (dirRes && _tempParentId !== RootDirId) { result.push({ type: "directory", ...dirRes }); } } } catch (error) { console.error(`IndexedDBFileSystem.exists(${path})`, error); return []; } if (result.length === path.size) { return result; } const fileName = path.slicePathStr[result.length]; const p = new Promise((res, rej) => { const query = file.index("name").getAll(fileName); query.onsuccess = (event) => { const files = event.target.result; for (let idx = 0; idx < files.length; idx++) { const el = files[idx]; if (el.directoryId === result[result.length - 1].id) { return res(el); } } res(void 0); }; query.onerror = (event) => { rej(event); }; }); const fileRes = await p; if (fileRes) { result.push({ type: "file", ...fileRes }); } return result; } async function mkdir(db, path) { const result = await findPath(db, path); if (pathMatchResult(path, result)) { throw new FsAlreadyExistsError("Directory already exists:" + path.origin); } if (pathMatchResult(path.parent(), result) === false) { throw new FsNotExistsParentError( "Parent directory does not exist:" + path.origin ); } const transaction = db.transaction(StoreNames.DIRECTORYS, "readwrite"); const store = transaction.objectStore(StoreNames.DIRECTORYS); const parent = result[result.length - 1]; const request = store.add({ name: path.slicePathStr[path.size - 1], parentId: parent ? parent.id : RootDirId, createAt: /* @__PURE__ */ new Date(), updateAt: /* @__PURE__ */ new Date() }); return new Promise((res, rej) => { request.onsuccess = () => { res(); }; request.onerror = () => { rej(request.error); }; }); } async function getFile(db, path) { const result = await findPath(db, path); if (pathMatchResult(path, result) === false) { throw new FsNotFoundError("Not found:" + path.origin); } const file = result[result.length - 1]; if (file.type !== "file") { throw new FsNotFoundError("Not a file:" + path.origin); } return file; } async function readFile(db, path) { const file = await getFile(db, path); const dataIds = file.dataIds; if (dataIds.length === 0) { return new ArrayBuffer(0); } const data = await Promise.all( dataIds.map((id) => getOneByKey(db, StoreNames.DATA, id)) ); return data.reduce((prev, cur) => { const buffer = new ArrayBuffer(prev.byteLength + cur.byteLength); const view = new Uint8Array(buffer); view.set(new Uint8Array(prev), 0); view.set(new Uint8Array(cur), prev.byteLength); return buffer; }); } async function rm(db, path, recursive = false) { const result = await findPath(db, path); if (pathMatchResult(path, result) === false) { throw new FsNotFoundError("Not found:" + path.origin); } const last = result[result.length - 1]; if (last.type === "directory") { const transaction = db.transaction( [StoreNames.DIRECTORYS, StoreNames.DATA, StoreNames.FILES], "readwrite" ); const directory = transaction.objectStore(StoreNames.DIRECTORYS); const file = transaction.objectStore(StoreNames.FILES); const data = transaction.objectStore(StoreNames.DATA); const p = new Promise((res, rej) => { const cursor = file.index("directoryId").openCursor(IDBKeyRange.only(last.id)); cursor.onsuccess = (event) => { const cursor2 = event.target.result; if (cursor2) { if (recursive === false) { throw new Error("Directory is not empty:" + path.origin); } cursor2.value.dataIds.forEach((id) => { data.delete(id); }); cursor2.delete(); cursor2.continue(); } else { res(); } }; cursor.onerror = (event) => { rej(event); }; }); await p; directory.delete(last.id); } else { const transaction = db.transaction( [StoreNames.FILES, StoreNames.DATA], "readwrite" ); const file = transaction.objectStore(StoreNames.FILES); const data = transaction.objectStore(StoreNames.DATA); const p = new Promise((res, rej) => { const cursor = file.index("name").openCursor(IDBKeyRange.only(last.name)); cursor.onsuccess = (event) => { const cursor2 = event.target.result; if (cursor2) { cursor2.value.dataIds.forEach((id) => { data.delete(id); }); cursor2.delete(); res(); } }; cursor.onerror = (event) => { rej(event); }; }); await p; } } async function createFile(db, path, desc) { const { mimeType } = desc; const findRes = await findPath(db, path); if (pathMatchResult(path, findRes)) { throw new FsAlreadyExistsError("File already exists:" + path.origin); } if (pathMatchResult(path.parent(), findRes) === false) { throw new FsNotExistsParentError( "Parent directory does not exist:" + path.origin ); } const parentDir = findRes[findRes.length - 1]; const transaction = db.transaction([StoreNames.FILES], "readwrite"); const file = transaction.objectStore(StoreNames.FILES); const idbFile = { name: path.slicePathStr[path.size - 1], directoryId: parentDir ? parentDir.id : RootDirId, dataIds: [], size: 0, createAt: /* @__PURE__ */ new Date(), updateAt: /* @__PURE__ */ new Date(), mimeType }; const request = file.add(idbFile); return new Promise((res, rej) => { request.onsuccess = (event) => { const id = event.target.result; res({ id, ...idbFile }); }; request.onerror = () => { rej(request.error); }; }); } async function appendFile(db, file, data) { return new Promise(async (resolve, reject) => { const preChunkIds = file.dataIds; const transaction = db.transaction( [StoreNames.DATA, StoreNames.FILES], "readwrite" ); const dataStore = transaction.objectStore(StoreNames.DATA); const fileStore = transaction.objectStore(StoreNames.FILES); transaction.oncomplete = () => { resolve(true); }; transaction.onerror = (event) => { reject(event); }; const blockSize = calculateBlockSize(data.byteLength); const blockCount = Math.ceil(data.byteLength / blockSize); const blocks = Array.from({ length: blockCount }, (_, idx) => { return data.slice(idx * blockSize, (idx + 1) * blockSize); }); const p = blocks.map((block) => { return new Promise((res, rej) => { const request = dataStore.add(block); request.onsuccess = (event) => { res(event.target.result); }; request.onerror = (event) => { rej(event); }; }); }); const ids = await Promise.all(p); file.dataIds = preChunkIds.concat(ids); file.size = data.byteLength + file.size; file.updateAt = /* @__PURE__ */ new Date(); fileStore.put(file); }); } async function writeFile(db, path, data, desc) { let file = await createFile(db, path, desc); return new Promise((resolve, reject) => { const fileSize = data.byteLength; const blockSize = calculateBlockSize(data.byteLength); const blockCount = Math.ceil(data.byteLength / blockSize); const blocks = Array.from({ length: blockCount }, (_, idx) => { return data.slice(idx * blockSize, (idx + 1) * blockSize); }); const transaction = db.transaction( [StoreNames.DATA, StoreNames.FILES], "readwrite" ); const fileStore = transaction.objectStore(StoreNames.FILES); const dataStore = transaction.objectStore(StoreNames.DATA); const fileRequest = fileStore.get(file.id); fileRequest.onsuccess = async (event) => { file = event.target.result; if (!file) { throw new FsNotFoundError("Not found:" + path.origin); } const p = blocks.map((block) => { return new Promise((res, rej) => { const request = dataStore.add(block); request.onsuccess = (event2) => { res(event2.target.result); }; request.onerror = (event2) => { rej(event2); }; }); }); const ids = await Promise.all(p); file.dataIds = ids; file.size = fileSize; file.updateAt = /* @__PURE__ */ new Date(); fileStore.put(file); }; transaction.oncomplete = () => { resolve(file); }; transaction.onerror = (event) => { reject(event); }; }); } async function saveFile(db, fileHandle, path) { const writable = await fileHandle.createWritable(); const file = await getFile(db, path); const stream = createReadStreamByIDBFile(db, file); for await (const chunk of streamToAsyncIterator(stream)) { writable.write(chunk); } await writable.close(); } function createReadStream(db, path) { let idx = 0; let dataIds = []; const rStream = new ReadableStream( { async start(controller) { const file = await getFile(db, path); dataIds = file.dataIds; if (dataIds.length === 0) { controller.close(); return; } }, async pull(controller) { if (idx < dataIds.length) { const dataId = dataIds[idx]; const data = await getOneByKey(db, StoreNames.DATA, dataId); controller.enqueue(data); idx++; } else { controller.close(); } } }, { highWaterMark: 3 } ); return rStream; } function createReadStreamByIDBFile(db, file) { let idx = 0; let dataIds = file.dataIds; const rStream = new ReadableStream( { async start(controller) { if (dataIds.length === 0) { controller.close(); return; } }, async pull(controller) { if (idx < dataIds.length) { const dataId = dataIds[idx]; const data = await getOneByKey(db, StoreNames.DATA, dataId); controller.enqueue(data); idx++; } else { controller.close(); } } }, { highWaterMark: 3 } ); return rStream; } function createWritableStreamByIDBFile(db, file) { const wStream = new WritableStream( { async write(chunk, controller) { let chunkOffset = 0; const tp = new Promise(async (res, rej) => { const transaction = db.transaction( [StoreNames.DATA, StoreNames.FILES], "readwrite" ); transaction.oncomplete = () => { res(file); }; transaction.onerror = (event) => { rej(event); }; const dataStore = transaction.objectStore(StoreNames.DATA); const fileStore = transaction.objectStore(StoreNames.FILES); const newFile = { ...file, updateAt: /* @__PURE__ */ new Date() }; const chunkSize = chunk.byteLength; if (chunkSize > MB) { while (chunkSize - chunkOffset > MB) { const data = chunk.slice(chunkOffset, MB); const p = new Promise((resolve, reject) => { const req = dataStore.add(data); req.onsuccess = (event) => ( //@ts-ignore resolve(event.target.result) ); }); const id = await p; newFile.dataIds.push(id); newFile.size += data.byteLength; chunkOffset = chunkOffset + MB; } if (chunkSize - chunkOffset > 0) { const data = chunk.slice(chunkOffset); const p = new Promise((resolve, reject) => { const req = dataStore.add(data); req.onsuccess = (event) => ( //@ts-ignore resolve(event.target.result) ); }); const id = await p; newFile.dataIds.push(id); newFile.size += data.byteLength; } } else { const p = new Promise((resolve, reject) => { const req = dataStore.add(chunk); req.onsuccess = (event) => resolve(event.target.result); }); const id = await p; newFile.dataIds.push(id); newFile.size += chunk.byteLength; } fileStore.put(newFile); }); await tp; }, close() { } }, { highWaterMark: 2 } ); return wStream; } var __defProp$1 = Object.defineProperty; var __defNormalProp$1 = (obj, key, value) => key in obj ? __defProp$1(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __publicField$1 = (obj, key, value) => { __defNormalProp$1(obj, typeof key !== "symbol" ? key + "" : key, value); return value; }; class Path { constructor(path) { __publicField$1(this, "slicePath"); __publicField$1(this, "path"); __publicField$1(this, "trimedBothSlash"); this.path = path.trim(); if (!Path.validCheck(path)) { throw new Error("Invalid path:" + path); } this.trimedBothSlash = path.replace(/^\/|\/$/g, ""); this.slicePath = this.trimedBothSlash.split("/").filter((item) => item !== ""); if (this.slicePath[0] !== RootDirName) { this.slicePath.unshift(RootDirName); } } static validCheck(path) { if (path.includes("//")) { return false; } return true; } parent() { if (this.size === 0) { return new Path("/"); } return new Path("/" + this.slicePath.slice(0, -1).join("/")); } get slicePathStr() { return this.slicePath; } get origin() { return this.path; } get size() { return this.slicePath.length; } *[Symbol.iterator]() { for (let item of this.slicePath) { yield item; } } } var __defProp = Object.defineProperty; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __publicField = (obj, key, value) => { __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); return value; }; const IDBSysbol = Symbol("IDBFileSystem"); const _IDBFileSystem = class _IDBFileSystem { constructor(mountRes) { this.mountRes = mountRes; __publicField(this, "dbMethod"); if (mountRes.flag !== IDBSysbol) { throw new Error("Invalid mountRes"); } this.dbMethod = { getOneByKey: getOneByKey.bind(null, this.mountRes.db), getManyByIndex: getManyByIndex.bind(null, this.mountRes.db), findPath: findPath.bind(null, this.mountRes.db), mkdir: mkdir.bind(null, this.mountRes.db), readFile: readFile.bind(null, this.mountRes.db), rm: rm.bind(null, this.mountRes.db), writeFile: writeFile.bind(null, this.mountRes.db), appendFile: appendFile.bind(null, this.mountRes.db), createReadStream: createReadStream.bind(null, this.mountRes.db), saveFile: saveFile.bind(null, this.mountRes.db), getFile: getFile.bind(null, this.mountRes.db), createWritableStreamByIDBFile: createWritableStreamByIDBFile.bind( null, this.mountRes.db ), createFile: createFile.bind(null, this.mountRes.db) }; } static getInstance(mountRes) { if (!_IDBFileSystem.instance) { _IDBFileSystem.instance = new _IDBFileSystem(mountRes); } return _IDBFileSystem.instance; } static mount() { return new Promise((res, rej) => { const request = indexedDB.open("WebFileSystem", 1); request.onupgradeneeded = (event) => { const db = request.result; if (!db.objectStoreNames.contains("Files")) { const files = db.createObjectStore("Files", { keyPath: "id", autoIncrement: true }); files.createIndex("directoryId", "directoryId", { unique: false }); files.createIndex("name", "name", { unique: false }); } if (!db.objectStoreNames.contains("Directorys")) { const directorys = db.createObjectStore("Directorys", { keyPath: "id", autoIncrement: true }); directorys.createIndex("parentId", "parentId", { unique: false }); directorys.createIndex("name", "name", { unique: false }); } if (!db.objectStoreNames.contains("Data")) { db.createObjectStore("Data", { keyPath: "id", autoIncrement: true }); } }; request.onsuccess = (event) => { res({ flag: IDBSysbol, db: request.result }); }; request.onerror = (event) => { rej(request.error); }; }); } /** * @description Internal and `stat` operations are similar. But no error, only whether it exists * @param path * @returns */ async exists(path) { const pathObj = new Path(path); const result = await this.dbMethod.findPath(pathObj); return pathMatchResult(pathObj, result); } /** * @description Get the file or directory information * @param path */ async stat(path) { const pathObj = new Path(path); const result = await this.dbMethod.findPath(pathObj); if (pathMatchResult(pathObj, result) === false) { throw new FsNotFoundError("Not found:" + path); } const last = result[result.length - 1]; const stat = { isDirectory: last.type === "directory", size: last?.size, createAt: last?.createAt, updateAt: last?.updateAt, mimeType: last?.mimeType, name: last.name }; return stat; } async mkdir(path) { return this.dbMethod.mkdir(new Path(path)); } async readFile(path) { const pathObj = new Path(path); const result = await this.dbMethod.readFile(pathObj); return result; } async writeFile(path, data, desc) { const pathObj = new Path(path); return await this.dbMethod.writeFile(pathObj, data, desc); } async writeFileByWebFile(path, file) { const data = await file.arrayBuffer(); return await this.writeFile(path, data, { mimeType: file.type }); } async readdir(path) { const pathRes = await this.dbMethod.findPath(new Path(path)); const last = pathRes[pathRes.length - 1]; if (last.type !== "directory") { throw new FsNotFoundError("Not found:" + path); } const dirId = last.id; const dirs = await this.dbMethod.getManyByIndex( StoreNames.DIRECTORYS, "parentId", dirId ); const files = await this.dbMethod.getManyByIndex( StoreNames.FILES, "directoryId", dirId ); return { dirs, files }; } async rm(path, recursive) { return this.dbMethod.rm(new Path(path), recursive); } createReadStream(path) { return this.dbMethod.createReadStream(new Path(path)); } createWriteStream(file) { return this.dbMethod.createWritableStreamByIDBFile(file); } /** * * @example * const fileHandle = await window.showSaveFilePicker(); * save(fileHandle, new ArrayBuffer(0)); */ async save(dbHandle, path) { return this.dbMethod.saveFile(dbHandle, new Path(path)); } async getFile(path) { return this.dbMethod.getFile(new Path(path)); } async appendFile(arg1, data) { let file; if (typeof arg1 === "string") { file = await this.getFile(arg1); } else { file = arg1; } return this.dbMethod.appendFile(file, data); } async createFile(path, desc) { return this.dbMethod.createFile(new Path(path), desc); } }; __publicField(_IDBFileSystem, "instance"); let IDBFileSystem = _IDBFileSystem; async function mount() { const mountRes = await IDBFileSystem.mount(); const idbFs = IDBFileSystem.getInstance(mountRes); return idbFs; } exports.FsAlreadyExistsError = FsAlreadyExistsError; exports.FsError = FsError; exports.FsErrorCode = FsErrorCode; exports.FsFatalError = FsFatalError; exports.FsInvalidPathError = FsInvalidPathError; exports.FsNotExistsParentError = FsNotExistsParentError; exports.FsNotFoundError = FsNotFoundError; exports.IDBFileSystem = IDBFileSystem; exports.StoreNames = StoreNames; exports.mount = mount; exports.streamToAsyncIterator = streamToAsyncIterator;