const isNotFound = (err: unknown) => err instanceof DOMException && err.name === "NotFoundError"; const getRoot = () => { if (!navigator.storage?.getDirectory) { throw new Error("opfs is unavailable"); } return navigator.storage.getDirectory(); }; const migratedFileName = (name: string) => { const song = name.match(/^song_(.*)$/); if (song) return `song_${encodeURIComponent(song[1])}`; const art = name.match(/^art_(.*)_(128|512)$/); return art ? `art_${encodeURIComponent(art[1])}_${art[2]}` : name; }; export class OPFSStorage { private async directory(sourceId: string, create = false) { return (await getRoot()).getDirectoryHandle(sourceId, { create }); } async migrateLegacy(sourceId: string): Promise { if (!navigator.storage?.getDirectory) return; const root = await getRoot(); const directory = await this.directory(sourceId, true); const names: string[] = []; for await (const [name, entry] of root.entries()) { if (name !== sourceId && entry.kind === "file") names.push(name); } for (const name of names) { const targetName = migratedFileName(name); const blob = await (await root.getFileHandle(name)).getFile(); let handle: FileSystemFileHandle; try { handle = await directory.getFileHandle(targetName); } catch (err) { if (!isNotFound(err)) throw err; handle = await directory.getFileHandle(targetName, { create: true }); } const writable = await handle.createWritable(); await writable.write(blob); await writable.close(); await root.removeEntry(name); } } async writeBlob(sourceId: string, key: string, blob: Blob): Promise { const handle = await (await this.directory(sourceId, true)).getFileHandle( key, { create: true }, ); const writable = await handle.createWritable(); try { await writable.write(blob); await writable.close(); } catch (err) { await writable.abort().catch(() => {}); throw err; } } async readBlob(sourceId: string, key: string): Promise { try { const handle = await (await this.directory(sourceId)).getFileHandle(key); return await handle.getFile(); } catch (err) { if (isNotFound(err)) return null; throw err; } } async deleteBlob(sourceId: string, key: string): Promise { try { await (await this.directory(sourceId)).removeEntry(key); } catch (err) { if (!isNotFound(err)) throw err; } } async clearSource(sourceId: string): Promise { if (!navigator.storage?.getDirectory) return; try { await (await getRoot()).removeEntry(sourceId, { recursive: true }); } catch (err) { if (!isNotFound(err)) throw err; } } async clear(): Promise { if (!navigator.storage?.getDirectory) return; let root: FileSystemDirectoryHandle; try { root = await getRoot(); } catch (err) { if (err instanceof DOMException && err.name === "NotSupportedError") return; throw err; } const names: string[] = []; for await (const [name] of root.entries()) names.push(name); for (const name of names) await root.removeEntry(name, { recursive: true }); } }