Skip to content

Instantly share code, notes, and snippets.

@andy0130tw
Created January 20, 2026 21:26
Show Gist options
  • Select an option

  • Save andy0130tw/9e524c97198d3c2adf36e07bac6cee96 to your computer and use it in GitHub Desktop.

Select an option

Save andy0130tw/9e524c97198d3c2adf36e07bac6cee96 to your computer and use it in GitHub Desktop.
IndexedDB as a filesystem
import { Buffer } from 'buffer'
;(globalThis as any).Buffer = Buffer
import git from 'isomorphic-git'
import http from 'isomorphic-git/http/web'
function idbRequestToPromise<T>(req: IDBRequest<T>) {
return new Promise<T>((resolve, reject) => {
req.onsuccess = evt => resolve((evt.target as IDBRequest<T>).result)
req.onerror = evt => reject((evt.target as IDBRequest<T>).error)
})
}
function idbTransactionToPromise(tx: IDBTransaction) {
return new Promise<Event>((resolve, reject) => {
tx.oncomplete = evt => resolve(evt)
tx.onabort = evt => reject({ type: 'abort', event: (evt.target! as IDBTransaction).error })
tx.onerror = evt => reject({ type: 'error', event: (evt.target! as IDBTransaction).error })
})
}
async function createDatabase() {
await indexedDB.databases().then(dbinfos => {
if (!dbinfos.some(dbinfo => dbinfo.name === 'inbrowserfs')) {
return
}
console.warn('deleting previous db')
const deleteOldReq = indexedDB.deleteDatabase('inbrowserfs')
deleteOldReq.onblocked = evt => { debugger; throw (evt.target as IDBOpenDBRequest).error }
return idbRequestToPromise(deleteOldReq)
})
console.log('open db')
const dbOpenRequest = indexedDB.open('inbrowserfs', 1)
dbOpenRequest.onupgradeneeded = evt => {
console.log('onupgradeneeded')
const db = (evt.target! as IDBOpenDBRequest).result
db.createObjectStore('meta', { keyPath: 'name' })
db.createObjectStore('inode', { keyPath: 'ino', autoIncrement: true })
const direntStore = db.createObjectStore('dirent', { keyPath: ['parent', 'name'] })
direntStore.createIndex('dir', 'parent')
const contentStore = db.createObjectStore('content', { keyPath: ['ino', 'partid'] })
contentStore.createIndex('ino', 'ino')
}
dbOpenRequest.onblocked = evt => {
console.warn('dbopen onblocked', evt)
}
dbOpenRequest.onerror = evt => {
debugger
throw (evt.target! as IDBOpenDBRequest).error
}
const db = await idbRequestToPromise(dbOpenRequest)
.catch(err => {
debugger
throw err
})
console.log('db created')
db.onversionchange = evt => {
console.log('db onversionchange')
;(evt.target as IDBDatabase).close()
}
const tx = db.transaction(['meta', 'inode', 'dirent', 'content'], 'readwrite')
const sb = tx.objectStore('meta')
await idbRequestToPromise(sb.add({ name: 'block-size', content: 512 }))
const si = tx.objectStore('inode')
await idbRequestToPromise(si.add({ ino: 2, type: 'dir' }))
await idbRequestToPromise(si.add({ ino: 3, type: 'file', stype: 'inline', content: 'asd' }))
await idbRequestToPromise(si.add({ ino: 4, type: 'file' }))
await idbRequestToPromise(si.add({ ino: 5, type: 'dir' }))
const sd = tx.objectStore('dirent')
await idbRequestToPromise(sd.add({ parent: 2, name: 'asd', ino: 3 }))
await idbRequestToPromise(sd.add({ parent: 2, name: 'fgh', ino: 5 }))
await idbRequestToPromise(sd.add({ parent: 5, name: 'foo', ino: 4 }))
await idbTransactionToPromise(tx)
return db
}
function toPathSegments(s: string): [string[], string] {
if (!s.startsWith('/')) throw new Error(`illegal abs path "${s}"!`)
if (s === '/') return [[], '/']
const segs = s.split('/').slice(1)
return [segs.slice(0, -1), segs.at(-1)!]
}
class MyFSError extends Error {
constructor(
readonly code: string, message?: string) {
super(message)
}
}
function makeMyFSErrorClass(code: string, defaultMessage: string) {
return {
[code]: class extends MyFSError {
constructor(message?: string) { super(code, message ?? defaultMessage) }
}
}[code]
}
const EEXIST = makeMyFSErrorClass('EEXIST', 'Path already exist')
const ENOENT = makeMyFSErrorClass('ENOENT', 'No such file')
const ENOTDIR = makeMyFSErrorClass('ENOTDIR', 'Not a dir')
const ENOTEMPTY = makeMyFSErrorClass('ENOTEMPTY', 'Dir not empty')
const ETIMEDOUT = makeMyFSErrorClass('ETIMEDOUT', 'Timed out')
const EISDIR = makeMyFSErrorClass('EISDIR', 'Is a dir')
class MyFileSystem {
constructor(readonly db: IDBDatabase) {}
async _resolvePath(segs: string[], si: IDBObjectStore, sd: IDBObjectStore): Promise<number> {
let ino = 2
for (const seg of segs) {
const pdir = await idbRequestToPromise(sd.get([ino, seg]))
if (!pdir) {
throw new ENOENT()
}
const isDir = await idbRequestToPromise(si.get(ino)).then(x => x?.type === 'dir')
if (!isDir) {
throw new ENOTDIR()
}
ino = pdir.ino
}
return ino
}
async readFile(path: string, options?: { encoding?: string }) {
console.log('readFile', path, options)
const tx = this.db.transaction(['inode', 'dirent'], 'readonly')
const si = tx.objectStore('inode')
const sd = tx.objectStore('dirent')
if (path == '/') {
throw new EISDIR()
}
const [_segs, fname] = toPathSegments(path)
const fino = await this._resolvePath([..._segs, fname], si, sd)
const f = await idbRequestToPromise(si.get(fino))
if (!f) {
throw new ENOENT()
}
if (f.type === 'dir') {
throw new EISDIR()
}
const tx2 = this.db.transaction(['content'], 'readonly')
const sc = tx2.objectStore('content')
const sci = sc.index('ino')
const parts = await idbRequestToPromise(sci.getAll(fino))
const content = parts[0].content
if (options?.encoding === 'utf8' || options?.encoding === 'utf-8') {
return new TextDecoder().decode(content)
}
return content
}
async writeFile(path: string, content: string | Uint8Array, _options: string | { encoding?: string }) {
const [segs, fname] = toPathSegments(path)
const tx = this.db.transaction(['inode', 'dirent', 'content'], 'readwrite')
const si = tx.objectStore('inode')
const sd = tx.objectStore('dirent')
const pdir = await this._resolvePath(segs, si, sd)
const pino = await idbRequestToPromise(si.get(pdir))
let newIno
const oldf = await idbRequestToPromise(sd.get([pino.ino, fname]))
if (oldf) {
// FIXME: overwrite old file
newIno = oldf.ino
} else {
const si2 = tx.objectStore('inode')
newIno = await idbRequestToPromise(si2.add({ type: 'file' }))
const sd2 = tx.objectStore('dirent')
await idbRequestToPromise(sd2.add({ parent: pino.ino, name: fname, ino: newIno }))
}
const tx2 = this.db.transaction(['content'], 'readwrite')
const sc = tx2.objectStore('content')
if (typeof content === 'string') {
content = new TextEncoder().encode(content)
}
await idbRequestToPromise(sc.put({ino: newIno, partid: 0, content}))
// TODO: update inode metadata
await idbTransactionToPromise(tx2)
}
async unlink(...args) {
console.log('unlink', args)
}
async readdir(path: string) {
console.log('readdir', path)
const [segs, fname] = toPathSegments(path)
const tx = this.db.transaction(['inode', 'dirent'], 'readonly')
const si = tx.objectStore('inode')
const sd = tx.objectStore('dirent')
const pdir = await this._resolvePath(segs, si, sd)
const pino = await idbRequestToPromise(si.get(pdir))
if (pino.type !== 'dir') {
throw new ENOENT()
}
const res = await idbRequestToPromise(sd.get([pino.ino, fname]))
if (!res) {
throw new ENOENT()
}
const sdi = sd.index('dir')
const ks = await idbRequestToPromise(sdi.getAllKeys(res.ino)).then(ks => {
return (ks as [number, string][]).map(([, name]) => name)
})
return ks
}
async mkdir(path: string) {
console.log('mkdir', path)
const [segs, fname] = toPathSegments(path)
const tx = this.db.transaction(['inode', 'dirent'], 'readwrite')
const si = tx.objectStore('inode')
const sd = tx.objectStore('dirent')
const pdir = await this._resolvePath(segs, si, sd)
const pino = await idbRequestToPromise(si.get(pdir))
if (pino.type !== 'dir') {
throw new ENOTDIR()
}
if (await idbRequestToPromise(sd.get([pino.ino, fname]))) {
throw new EEXIST()
}
const newIno = await idbRequestToPromise(si.add({ type: 'dir' }))
await idbRequestToPromise(sd.add({ parent: pino.ino, name: fname, ino: newIno }))
await idbTransactionToPromise(tx)
}
async rmdir(...args) {
console.log('rmdir', args)
}
async stat(path: string) {
console.log('stat', path)
const [segs, fname] = toPathSegments(path)
const tx = this.db.transaction(['inode', 'dirent'], 'readonly')
const si = tx.objectStore('inode')
const sd = tx.objectStore('dirent')
const pdir = await this._resolvePath(segs, si, sd)
const pino = await idbRequestToPromise(si.get(pdir))
if (pino.type !== 'dir') {
throw new ENOENT()
}
const res = await idbRequestToPromise(sd.get([pino.ino, fname])) as { ino: number, type: string }
if (!res) {
throw new ENOENT()
}
return {
atime: 0,
mtime: 0,
ctime: 0,
isFile() { return res.type === 'file' },
isDirectory() { return res.type === 'dir' },
}
}
async lstat(path: string) {
console.log('lstat', path)
// FIXME: this is lame
return this.stat(path)
}
async readlink(...args) {
console.log('readlink', args)
}
async symlink(...args) {
console.log('symlink', args)
throw 1
}
}
async function main() {
const db = await createDatabase()
const myfs = new MyFileSystem(db)
const fs = { promises: myfs }
await git.clone({
fs,
http,
dir: "/runtime",
corsProxy: 'https://cors.isomorphic-git.org',
url: 'https://github.com/observablehq/runtime',
ref: 'main',
// singleBranch: true,
// depth: 10
})
console.log('done!')
console.log(await git.log({fs, dir: '/runtime'}))
}
main()
export default {}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment