Skip to content

Commit 5fc5768

Browse files
committed
idbfs: add working indexeddb filesystem. almost 50% faster than opfs and works in safari
1 parent ea93dec commit 5fc5768

File tree

3 files changed

+1045
-0
lines changed

3 files changed

+1045
-0
lines changed

hack/idbfs/DESIGN

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
an indexeddb with a single object store called "fs" with the following schema:
2+
3+
{
4+
path: string,
5+
mode: number,
6+
mtime: number,
7+
atime: number,
8+
size: number,
9+
data: Uint8Array,
10+
}
11+
12+
the path is the full path to the file or directory.
13+
paths do not start with a slash and root is just "."
14+
mtime is the last modification time of the file or directory in unix time
15+
atime is the last access time in unix time
16+
size is the size of the file in bytes
17+
data is the content of the file as a Uint8Array
18+
19+
all operations are atomic. if a file is being written, the read will return the old content.
20+
there is a unique index on the path column.
21+
22+
the api is a class called IDBFS. the constructor takes a single argument, the name of the database.
23+
it will create the database if it doesn't exist. the IDBFS interface looks like this:
24+
25+
interface IDBFS {
26+
open(path: string): Promise<File>
27+
create(path: string): Promise<File>
28+
openfile(path: string, flags: number): Promise<File>
29+
mkdir(path: string, perm: number): Promise<void>
30+
symlink(oldpath: string, newpath: string): Promise<void>
31+
chtimes(path: string, atime: number, mtime: number): Promise<void>
32+
chmod(path: string, mode: number): Promise<void>
33+
stat(path: string): Promise<FileInfo>
34+
truncate(path: string, size: number): Promise<void>
35+
remove(path: string): Promise<void>
36+
rename(oldpath: string, newpath: string): Promise<void>
37+
readlink(path: string): Promise<string>
38+
readdir(path: string): Promise<FileInfo[]>
39+
}
40+
41+
symlink creates a file with the mode 0777 | unix symlink mode and the content is the target path.
42+
readlink returns the target path of the symlink if it is a symlink. otherwise it returns an error.
43+
44+
interface FileInfo {
45+
name: string // the basename of the file path
46+
mode: number // the mode of the file
47+
mtime: number // the last modification time in unix time
48+
atime: number // the last access time in unix time
49+
size: number // the size of the file in bytes
50+
}
51+
52+
interface File {
53+
close(): Promise<void>
54+
stat(): Promise<FileInfo>
55+
read(buf: Uint8Array): Promise<number>
56+
write(data: Uint8Array): Promise<number>
57+
seek(offset: number, whence: number): Promise<number>
58+
}

hack/idbfs/idbfs.go

Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
//go:build js && wasm
2+
3+
package idbfs
4+
5+
import (
6+
"context"
7+
_ "embed"
8+
"log"
9+
"syscall/js"
10+
"time"
11+
12+
"tractor.dev/wanix/fs"
13+
"tractor.dev/wanix/fs/pstat"
14+
"tractor.dev/wanix/web/jsutil"
15+
)
16+
17+
//go:embed idbfs.js
18+
var idbfsJS []byte
19+
20+
func BlobURL() string {
21+
jsBuf := js.Global().Get("Uint8Array").New(len(idbfsJS))
22+
js.CopyBytesToJS(jsBuf, idbfsJS)
23+
blob := js.Global().Get("Blob").New([]any{jsBuf}, js.ValueOf(map[string]any{"type": "text/javascript"}))
24+
url := js.Global().Get("URL").Call("createObjectURL", blob)
25+
return url.String()
26+
}
27+
28+
func Load() {
29+
v := js.Global().Get("IDBFS")
30+
if !v.IsUndefined() {
31+
return
32+
}
33+
p := jsutil.LoadScript(BlobURL(), true)
34+
_, err := jsutil.AwaitErr(p)
35+
if err != nil {
36+
panic(err)
37+
}
38+
}
39+
40+
func New(name string) FS {
41+
Load()
42+
return FS{Value: js.Global().Get("IDBFS").New(name)}
43+
}
44+
45+
func convertErr(e error) error {
46+
if e == nil {
47+
return nil
48+
}
49+
err, ok := e.(js.Error)
50+
if !ok {
51+
return e
52+
}
53+
code := err.Value.Get("code").String()
54+
switch code {
55+
case "ENOENT":
56+
return fs.ErrNotExist
57+
case "EEXIST":
58+
return fs.ErrExist
59+
case "EIO":
60+
return fs.ErrInvalid
61+
}
62+
log.Println("idbfs: error", code, e)
63+
return e
64+
}
65+
66+
type FS struct {
67+
js.Value
68+
}
69+
70+
var _ fs.FS = FS{}
71+
var _ fs.ReadDirFS = FS{}
72+
73+
func (fsys FS) Open(name string) (fs.File, error) {
74+
v, err := jsutil.AwaitErr(fsys.Call("open", name))
75+
if err != nil {
76+
return nil, convertErr(err)
77+
}
78+
return File{Value: v}, nil
79+
}
80+
81+
func (fsys FS) Create(name string) (fs.File, error) {
82+
v, err := jsutil.AwaitErr(fsys.Call("create", name))
83+
if err != nil {
84+
return nil, convertErr(err)
85+
}
86+
return File{Value: v}, nil
87+
}
88+
89+
func (fsys FS) OpenFile(name string, flag int, _ fs.FileMode) (fs.File, error) {
90+
v, err := jsutil.AwaitErr(fsys.Call("openfile", name, flag))
91+
if err != nil {
92+
return nil, convertErr(err)
93+
}
94+
return File{Value: v}, nil
95+
}
96+
97+
func (fsys FS) Mkdir(name string, perm fs.FileMode) error {
98+
_, err := jsutil.AwaitErr(fsys.Call("mkdir", name, uint32(perm)))
99+
return convertErr(err)
100+
}
101+
102+
func (fsys FS) Symlink(oldpath, newpath string) error {
103+
_, err := jsutil.AwaitErr(fsys.Call("symlink", oldpath, newpath))
104+
return convertErr(err)
105+
}
106+
107+
func (fsys FS) Chtimes(name string, atime time.Time, mtime time.Time) error {
108+
_, err := jsutil.AwaitErr(fsys.Call("chtimes", name, atime.Unix(), mtime.Unix()))
109+
return convertErr(err)
110+
}
111+
112+
func (fsys FS) Chmod(name string, mode fs.FileMode) error {
113+
_, err := jsutil.AwaitErr(fsys.Call("chmod", name, uint32(mode)))
114+
return convertErr(err)
115+
}
116+
117+
func (fsys FS) StatContext(_ context.Context, name string) (fs.FileInfo, error) {
118+
return fsys.Stat(name)
119+
}
120+
121+
func (fsys FS) Stat(name string) (fs.FileInfo, error) {
122+
v, err := jsutil.AwaitErr(fsys.Call("stat", name))
123+
if err != nil {
124+
return nil, convertErr(err)
125+
}
126+
return FileInfo{Value: v}, nil
127+
}
128+
129+
func (fsys FS) Truncate(name string, size int64) error {
130+
_, err := jsutil.AwaitErr(fsys.Call("truncate", name, size))
131+
return convertErr(err)
132+
}
133+
134+
func (fsys FS) Remove(name string) error {
135+
_, err := jsutil.AwaitErr(fsys.Call("remove", name))
136+
return convertErr(err)
137+
}
138+
139+
func (fsys FS) Rename(oldpath, newpath string) error {
140+
_, err := jsutil.AwaitErr(fsys.Call("rename", oldpath, newpath))
141+
return convertErr(err)
142+
}
143+
144+
func (fsys FS) ReadDir(name string) ([]fs.DirEntry, error) {
145+
v, err := jsutil.AwaitErr(fsys.Call("readdir", name))
146+
if err != nil {
147+
return nil, convertErr(err)
148+
}
149+
var entries []fs.DirEntry
150+
for i := 0; i < v.Length(); i++ {
151+
entries = append(entries, FileInfo{Value: v.Index(i)})
152+
}
153+
return entries, nil
154+
}
155+
156+
func (fsys FS) Readlink(name string) (string, error) {
157+
v, err := jsutil.AwaitErr(fsys.Call("readlink", name))
158+
if err != nil {
159+
return "", convertErr(err)
160+
}
161+
return v.String(), nil
162+
}
163+
164+
type File struct {
165+
js.Value
166+
}
167+
168+
var _ fs.File = File{}
169+
var _ fs.ReadDirFile = File{}
170+
171+
func (f File) Close() error {
172+
_, err := jsutil.AwaitErr(f.Call("close"))
173+
return convertErr(err)
174+
}
175+
176+
func (f File) Stat() (fs.FileInfo, error) {
177+
v, err := jsutil.AwaitErr(f.Call("stat"))
178+
if err != nil {
179+
return nil, convertErr(err)
180+
}
181+
return FileInfo{Value: v}, nil
182+
}
183+
184+
func (f File) Read(p []byte) (int, error) {
185+
r := &jsutil.Reader{Value: f.Value}
186+
return r.Read(p)
187+
}
188+
189+
func (f File) Write(p []byte) (int, error) {
190+
w := &jsutil.Writer{Value: f.Value}
191+
return w.Write(p)
192+
}
193+
194+
func (f File) Seek(offset int64, whence int) (int64, error) {
195+
v, err := jsutil.AwaitErr(f.Call("seek", offset, whence))
196+
if err != nil {
197+
return 0, convertErr(err)
198+
}
199+
return int64(v.Int()), nil
200+
}
201+
202+
func (f File) ReadDir(count int) ([]fs.DirEntry, error) {
203+
v, err := jsutil.AwaitErr(f.Call("readdir", count))
204+
if err != nil {
205+
return nil, convertErr(err)
206+
}
207+
var entries []fs.DirEntry
208+
for i := 0; i < v.Length(); i++ {
209+
entries = append(entries, FileInfo{Value: v.Index(i)})
210+
}
211+
return entries, nil
212+
}
213+
214+
type FileInfo struct {
215+
js.Value
216+
}
217+
218+
var _ fs.FileInfo = FileInfo{}
219+
var _ fs.DirEntry = FileInfo{}
220+
221+
func (fi FileInfo) Name() string {
222+
return fi.Get("name").String()
223+
}
224+
225+
func (fi FileInfo) Mode() fs.FileMode {
226+
return pstat.UnixModeToFileMode(uint32(fi.Get("mode").Int()))
227+
}
228+
229+
func (fi FileInfo) ModTime() time.Time {
230+
return time.Unix(int64(fi.Get("mtime").Int()), 0)
231+
}
232+
233+
func (fi FileInfo) Size() int64 {
234+
return int64(fi.Get("size").Int())
235+
}
236+
237+
func (fi FileInfo) IsDir() bool {
238+
return fi.Mode()&fs.ModeDir != 0
239+
}
240+
241+
func (fi FileInfo) Sys() any {
242+
return nil
243+
}
244+
245+
func (fi FileInfo) Type() fs.FileMode {
246+
return fi.Mode().Type()
247+
}
248+
249+
func (fi FileInfo) Info() (fs.FileInfo, error) {
250+
return fi, nil
251+
}

0 commit comments

Comments
 (0)