From 17f05fec89db5c84dcca57525c01d27f4681ea71 Mon Sep 17 00:00:00 2001 From: firu11 Date: Sat, 18 Apr 2026 12:18:46 +0200 Subject: [PATCH 1/7] move opfs to separate pkg --- pkg/filesystem/{fs_classic.go => classic.go} | 0 pkg/filesystem/wasm.go | 24 +++++++++++++++++++ .../fs_file_wasm.go => opfs/file.go} | 2 +- .../file_info.go} | 2 +- pkg/{filesystem/fs_wasm.go => opfs/fs.go} | 20 +++------------- .../fs_inode_wasm.go => opfs/inode.go} | 2 +- .../inode_test.go} | 2 +- 7 files changed, 31 insertions(+), 21 deletions(-) rename pkg/filesystem/{fs_classic.go => classic.go} (100%) create mode 100644 pkg/filesystem/wasm.go rename pkg/{filesystem/fs_file_wasm.go => opfs/file.go} (99%) rename pkg/{filesystem/fs_file_info_wasm.go => opfs/file_info.go} (98%) rename pkg/{filesystem/fs_wasm.go => opfs/fs.go} (96%) rename pkg/{filesystem/fs_inode_wasm.go => opfs/inode.go} (98%) rename pkg/{filesystem/fs_inode_wasm_test.go => opfs/inode_test.go} (97%) diff --git a/pkg/filesystem/fs_classic.go b/pkg/filesystem/classic.go similarity index 100% rename from pkg/filesystem/fs_classic.go rename to pkg/filesystem/classic.go diff --git a/pkg/filesystem/wasm.go b/pkg/filesystem/wasm.go new file mode 100644 index 0000000..2727d5c --- /dev/null +++ b/pkg/filesystem/wasm.go @@ -0,0 +1,24 @@ +//go:build js && wasm + +package filesystem + +import ( + "errors" + "syscall/js" + + "github.com/git-calendar/core/pkg/opfs" + "github.com/go-git/go-billy/v5" +) + +const DirName = "git-calendar-data" + +func GetFS() (billy.Filesystem, error) { + rootHandle := js.Global().Get("opfsRootHandle") + if rootHandle.IsUndefined() { + return nil, errors.New("opfsRootHandle not initialized") + } + + return &opfs.OPFS{ + RootHandle: rootHandle, + }, nil +} diff --git a/pkg/filesystem/fs_file_wasm.go b/pkg/opfs/file.go similarity index 99% rename from pkg/filesystem/fs_file_wasm.go rename to pkg/opfs/file.go index 9f28262..82a8ed1 100644 --- a/pkg/filesystem/fs_file_wasm.go +++ b/pkg/opfs/file.go @@ -1,6 +1,6 @@ //go:build js && wasm -package filesystem +package opfs import ( "fmt" diff --git a/pkg/filesystem/fs_file_info_wasm.go b/pkg/opfs/file_info.go similarity index 98% rename from pkg/filesystem/fs_file_info_wasm.go rename to pkg/opfs/file_info.go index f864fcd..f4b5e9e 100644 --- a/pkg/filesystem/fs_file_info_wasm.go +++ b/pkg/opfs/file_info.go @@ -1,6 +1,6 @@ //go:build js && wasm -package filesystem +package opfs import ( "io/fs" diff --git a/pkg/filesystem/fs_wasm.go b/pkg/opfs/fs.go similarity index 96% rename from pkg/filesystem/fs_wasm.go rename to pkg/opfs/fs.go index 046b404..1c1f0f3 100644 --- a/pkg/filesystem/fs_wasm.go +++ b/pkg/opfs/fs.go @@ -1,9 +1,8 @@ //go:build js && wasm -package filesystem +package opfs import ( - "errors" "fmt" "io" "io/fs" @@ -19,24 +18,11 @@ import ( "github.com/go-git/go-billy/v5/helper/chroot" ) -const DirName = "git-calendar-data" - -func GetFS() (billy.Filesystem, error) { - rootHandle := js.Global().Get("opfsRootHandle") - if rootHandle.IsUndefined() { - return nil, errors.New("opfsRootHandle not initialized") - } - - return &OPFS{ - root: rootHandle, - }, nil -} - // Origin private file system // // https://developer.mozilla.org/en-US/docs/Web/API/File_System_API/Origin_private_file_system type OPFS struct { - root js.Value // FileSystemDirectoryHandle + RootHandle js.Value // FileSystemDirectoryHandle } var _ billy.Filesystem = (*OPFS)(nil) // makes sure that it implements all the interface methods, it wont compile without it @@ -375,7 +361,7 @@ func (fs *OPFS) applyFlags(f *OPFSFile, flag int) error { func (fs *OPFS) getDirectoryHandle(path string, create bool) (js.Value, error) { parts := strings.Split(path, "/") - dir := fs.root + dir := fs.RootHandle for _, part := range parts { if part == "" || part == "." { continue diff --git a/pkg/filesystem/fs_inode_wasm.go b/pkg/opfs/inode.go similarity index 98% rename from pkg/filesystem/fs_inode_wasm.go rename to pkg/opfs/inode.go index 373049c..7ae8c35 100644 --- a/pkg/filesystem/fs_inode_wasm.go +++ b/pkg/opfs/inode.go @@ -1,6 +1,6 @@ //go:build js && wasm -package filesystem +package opfs import ( "path/filepath" diff --git a/pkg/filesystem/fs_inode_wasm_test.go b/pkg/opfs/inode_test.go similarity index 97% rename from pkg/filesystem/fs_inode_wasm_test.go rename to pkg/opfs/inode_test.go index 1f6926d..d7fef83 100644 --- a/pkg/filesystem/fs_inode_wasm_test.go +++ b/pkg/opfs/inode_test.go @@ -1,6 +1,6 @@ //go:build js && wasm -package filesystem +package opfs import "testing" From 1b3cf7166a313fbe25a89a22e7a8dcf699359cab Mon Sep 17 00:00:00 2001 From: firu11 Date: Sat, 18 Apr 2026 12:43:52 +0200 Subject: [PATCH 2/7] simplify fs path root was $HOME, now its $HOME/.git-calendar-data --- pkg/core/core.go | 11 ++--------- pkg/core/core_calendars.go | 18 ++++++++---------- pkg/core/core_events.go | 5 ++--- pkg/filesystem/classic.go | 15 ++++++++++++++- pkg/filesystem/wasm.go | 16 +++++++++++++--- pkg/opfs/file.go | 2 +- pkg/opfs/fs.go | 24 +++++++++++++++--------- 7 files changed, 55 insertions(+), 36 deletions(-) diff --git a/pkg/core/core.go b/pkg/core/core.go index 5ef055c..1eb0e85 100644 --- a/pkg/core/core.go +++ b/pkg/core/core.go @@ -38,11 +38,6 @@ func NewCore() *Core { panic(err) } - err = c.fs.MkdirAll(filesystem.DirName, 0o755) - if err != nil { - panic(err) - } - return &c } @@ -104,13 +99,11 @@ func (c *Core) resetCore() { // Loads, if exists, or creates new repository with the given name. func (c *Core) initCalendarRepo(name string) (*gogit.Repository, error) { - repoPath := c.fs.Join(filesystem.DirName, name) - - if err := c.fs.MkdirAll(repoPath, 0o755); err != nil { + if err := c.fs.MkdirAll(name, 0o755); err != nil { return nil, fmt.Errorf("create repo dir: %w", err) } - repoFS, err := c.fs.Chroot(repoPath) + repoFS, err := c.fs.Chroot(name) if err != nil { return nil, fmt.Errorf("chroot repo dir: %w", err) } diff --git a/pkg/core/core_calendars.go b/pkg/core/core_calendars.go index 4996108..32d57bc 100644 --- a/pkg/core/core_calendars.go +++ b/pkg/core/core_calendars.go @@ -10,7 +10,6 @@ import ( "strings" "github.com/git-calendar/core/pkg/encryption" - "github.com/git-calendar/core/pkg/filesystem" gogitutil "github.com/go-git/go-billy/v5/util" gogit "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/config" @@ -29,7 +28,7 @@ func (c *Core) CreateCalendar(name, password string) error { if len(password) != 0 { key = encryption.DeriveKey(password, []byte(name)) - keyFile, err := c.fs.Create(c.fs.Join(filesystem.DirName, fmt.Sprintf("%s.key", name))) + keyFile, err := c.fs.Create(fmt.Sprintf("%s.key", name)) if err != nil { return fmt.Errorf("failed to create key file: %w", err) } @@ -61,7 +60,7 @@ func (c *Core) LoadCalendars() error { c.resetCore() // load repositories - entries, err := c.fs.ReadDir(filesystem.DirName) + entries, err := c.fs.ReadDir(".") if err != nil { return fmt.Errorf("failed to list all directories in root: %w", err) } @@ -79,7 +78,7 @@ func (c *Core) LoadCalendars() error { } var key []byte = nil - keyFile, err := c.fs.Open(c.fs.Join(filesystem.DirName, fmt.Sprintf("%s.key", name))) + keyFile, err := c.fs.Open(fmt.Sprintf("%s.key", name)) if err == nil { key, err = io.ReadAll(keyFile) if err != nil { @@ -147,11 +146,10 @@ func (c *Core) CloneCalendar(repoUrl url.URL, password string) error { } // make sure that the repo dir is created - repoPath := c.fs.Join(filesystem.DirName, calendarName) - if err := c.fs.MkdirAll(repoPath, 0o755); err != nil { + if err := c.fs.MkdirAll(calendarName, 0o755); err != nil { return fmt.Errorf("create repo dir: %w", err) } - repoFS, err := c.fs.Chroot(repoPath) + repoFS, err := c.fs.Chroot(calendarName) if err != nil { return fmt.Errorf("chroot repo dir: %w", err) } @@ -186,7 +184,7 @@ func (c *Core) CloneCalendar(repoUrl url.URL, password string) error { if len(password) != 0 { key = encryption.DeriveKey(password, []byte(calendarName)) - keyFile, err := c.fs.Create(c.fs.Join(filesystem.DirName, fmt.Sprintf("%s.key", calendarName))) + keyFile, err := c.fs.Create(fmt.Sprintf("%s.key", calendarName)) if err != nil { return fmt.Errorf("failed to create key file: %w", err) } @@ -211,12 +209,12 @@ func (c *Core) RemoveCalendar(name string) error { delete(c.calendars, name) // remove dir from filesystem - if err := gogitutil.RemoveAll(c.fs, c.fs.Join(filesystem.DirName, name)); err != nil { + if err := gogitutil.RemoveAll(c.fs, name); err != nil { return fmt.Errorf("failed to remove repo directory: %w", err) } // try to remove encryption key - _ = c.fs.Remove(c.fs.Join(filesystem.DirName, fmt.Sprintf("%s.key", name))) + _ = c.fs.Remove(fmt.Sprintf("%s.key", name)) // TODO: This is the lazy way. // LoadCalendars does full erase and load again for events map and tree. It also deletes all the repos, and reloads them from disk. diff --git a/pkg/core/core_events.go b/pkg/core/core_events.go index 1a3b015..efc95c6 100644 --- a/pkg/core/core_events.go +++ b/pkg/core/core_events.go @@ -8,7 +8,6 @@ import ( "slices" "time" - "github.com/git-calendar/core/pkg/filesystem" gogit "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing/object" "github.com/google/uuid" @@ -381,7 +380,7 @@ func (c *Core) saveAndCommitEvent(event *Event, commitMsg string) error { } // ensure events directory exists - dirPath := c.fs.Join(filesystem.DirName, event.Calendar, EventsDirName) + dirPath := c.fs.Join(event.Calendar, EventsDirName) if err := c.fs.MkdirAll(dirPath, 0o755); err != nil { return fmt.Errorf("failed mkdir events: %w", err) } @@ -437,7 +436,7 @@ func (c *Core) deleteAndCommitEvent(eventId uuid.UUID, commitMsg string) error { filename := fmt.Sprintf("%s.json", eventId) // -------- remove from disk -------- - filePath := c.fs.Join(filesystem.DirName, event.Calendar, EventsDirName, filename) + filePath := c.fs.Join(event.Calendar, EventsDirName, filename) if err := c.fs.Remove(filePath); err != nil { // TODO maybe continue, to clean the git from this file return fmt.Errorf("failed to remove file from disk: %w", err) diff --git a/pkg/filesystem/classic.go b/pkg/filesystem/classic.go index c4c7715..38a741a 100644 --- a/pkg/filesystem/classic.go +++ b/pkg/filesystem/classic.go @@ -4,8 +4,10 @@ package filesystem import ( "os" + "path/filepath" "github.com/go-git/go-billy/v5" + "github.com/go-git/go-billy/v5/helper/chroot" "github.com/go-git/go-billy/v5/osfs" ) @@ -21,5 +23,16 @@ func GetFS() (billy.Filesystem, error) { return nil, err } - return osfs.New(home), nil + // ensure the directory exists on the real filesystem + rootPath := filepath.Join(home, DirName) + if err := os.MkdirAll(rootPath, 0o755); err != nil { + return nil, err + } + + // base filesystem rooted at home + base := osfs.New(home) + + // chroot into DirName + scoped := chroot.New(base, DirName) + return scoped, nil } diff --git a/pkg/filesystem/wasm.go b/pkg/filesystem/wasm.go index 2727d5c..645ae4d 100644 --- a/pkg/filesystem/wasm.go +++ b/pkg/filesystem/wasm.go @@ -4,6 +4,7 @@ package filesystem import ( "errors" + "fmt" "syscall/js" "github.com/git-calendar/core/pkg/opfs" @@ -13,12 +14,21 @@ import ( const DirName = "git-calendar-data" func GetFS() (billy.Filesystem, error) { + // gets the handle from js window.opfsRootHandle rootHandle := js.Global().Get("opfsRootHandle") if rootHandle.IsUndefined() { return nil, errors.New("opfsRootHandle not initialized") } - return &opfs.OPFS{ - RootHandle: rootHandle, - }, nil + // get or create subdirectory + dirHandlePromise := rootHandle.Call("getDirectoryHandle", DirName, map[string]any{ + "create": true, + }) + + dirHandle, err := opfs.Await(dirHandlePromise) + if err != nil { + return nil, fmt.Errorf("cant get the git-calendar-data folder handle: %w", err) + } + + return opfs.New(dirHandle), nil } diff --git a/pkg/opfs/file.go b/pkg/opfs/file.go index 82a8ed1..e35ec71 100644 --- a/pkg/opfs/file.go +++ b/pkg/opfs/file.go @@ -214,7 +214,7 @@ func (f *OPFSFile) openAccess() error { var err error // https://developer.mozilla.org/en-US/docs/Web/API/FileSystemFileHandle/createSyncAccessHandle - f.inode.access, err = await(f.inode.handle.Call("createSyncAccessHandle")) // returns Promise + f.inode.access, err = Await(f.inode.handle.Call("createSyncAccessHandle")) // returns Promise return err } diff --git a/pkg/opfs/fs.go b/pkg/opfs/fs.go index 1c1f0f3..32243a0 100644 --- a/pkg/opfs/fs.go +++ b/pkg/opfs/fs.go @@ -25,7 +25,13 @@ type OPFS struct { RootHandle js.Value // FileSystemDirectoryHandle } -var _ billy.Filesystem = (*OPFS)(nil) // makes sure that it implements all the interface methods, it wont compile without it +var _ billy.Filesystem = (*OPFS)(nil) // makes sure that it implements all the interface methods, it won't compile without it + +func New(baseDirHandle js.Value) *OPFS { + return &OPFS{ + RootHandle: baseDirHandle, + } +} func (fs *OPFS) MkdirAll(path string, perm fs.FileMode) error { // OPFS ignores permissions (perm) @@ -79,7 +85,7 @@ func (fs *OPFS) OpenFile(fullPath string, flag int, perm os.FileMode) (billy.Fil } // https://developer.mozilla.org/en-US/docs/Web/API/FileSystemDirectoryHandle/getFileHandle - handle, err := await(dirHandle.Call("getFileHandle", fileName, map[string]any{"create": create})) // returns Promise + handle, err := Await(dirHandle.Call("getFileHandle", fileName, map[string]any{"create": create})) // returns Promise if err != nil { if strings.Contains(err.Error(), "NotFoundError") { return nil, os.ErrNotExist @@ -135,7 +141,7 @@ func (fs *OPFS) Remove(path string) error { // OPFS FileSystemDirectoryHandle provides a native removeEntry method // https://developer.mozilla.org/en-US/docs/Web/API/FileSystemDirectoryHandle/removeEntry // a non-empty directory will not be removed - _, err = await(dirHandle.Call("removeEntry", name)) + _, err = Await(dirHandle.Call("removeEntry", name)) if err == nil { return nil // removed ok } @@ -212,7 +218,7 @@ func (fs *OPFS) ReadDir(path string) (infos []os.FileInfo, err error) { // the JS AsyncIterator has a .next() -> {done, value} for { // get one entry - result, err := await(itValue.Call("next")) // {done, value} + result, err := Await(itValue.Call("next")) // {done, value} if err != nil { return nil, err } @@ -296,10 +302,10 @@ func (fs *OPFS) Stat(path string) (os.FileInfo, error) { // try as file first // https://developer.mozilla.org/en-US/docs/Web/API/FileSystemDirectoryHandle/getFileHandle - handle, err := await(parentDirHandle.Call("getFileHandle", name)) + handle, err := Await(parentDirHandle.Call("getFileHandle", name)) if err == nil { // https://developer.mozilla.org/en-US/docs/Web/API/FileSystemFileHandle/getFile - file, err := await(handle.Call("getFile")) // returns Promise + file, err := Await(handle.Call("getFile")) // returns Promise if err != nil { return nil, err } @@ -313,7 +319,7 @@ func (fs *OPFS) Stat(path string) (os.FileInfo, error) { } // if file failed, try as directory - _, err = await(parentDirHandle.Call("getDirectoryHandle", name)) + _, err = Await(parentDirHandle.Call("getDirectoryHandle", name)) if err == nil { return &OPFSFileInfo{ name: name, @@ -367,7 +373,7 @@ func (fs *OPFS) getDirectoryHandle(path string, create bool) (js.Value, error) { continue } - d, err := await(dir.Call("getDirectoryHandle", part, map[string]any{"create": create})) + d, err := Await(dir.Call("getDirectoryHandle", part, map[string]any{"create": create})) if err != nil { return js.Undefined(), err } @@ -392,7 +398,7 @@ func (fs *OPFS) split(fullPath string) (string, string) { // }); // // But instead of "something", we pass the value/error to Go. -func await(p js.Value) (js.Value, error) { +func Await(p js.Value) (js.Value, error) { // create channel for each callback valCh := make(chan js.Value, 1) errCh := make(chan error, 1) From 1c8e81f326a1ce5b106d2aab3d73b66a7dbb10ae Mon Sep 17 00:00:00 2001 From: firu11 Date: Sat, 18 Apr 2026 21:20:19 +0200 Subject: [PATCH 3/7] use chroot not js directly --- pkg/filesystem/wasm.go | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/pkg/filesystem/wasm.go b/pkg/filesystem/wasm.go index 645ae4d..c5245af 100644 --- a/pkg/filesystem/wasm.go +++ b/pkg/filesystem/wasm.go @@ -20,15 +20,19 @@ func GetFS() (billy.Filesystem, error) { return nil, errors.New("opfsRootHandle not initialized") } - // get or create subdirectory - dirHandlePromise := rootHandle.Call("getDirectoryHandle", DirName, map[string]any{ - "create": true, - }) + // create OPFS rooted at / + fs := opfs.New(rootHandle) - dirHandle, err := opfs.Await(dirHandlePromise) + // ensure directory exists + if err := fs.MkdirAll(DirName, 0o755); err != nil { + return nil, fmt.Errorf("failed to create dir: %w", err) + } + + // chroot into the directory + chrooted, err := fs.Chroot(DirName) if err != nil { - return nil, fmt.Errorf("cant get the git-calendar-data folder handle: %w", err) + return nil, fmt.Errorf("failed to chroot: %w", err) } - return opfs.New(dirHandle), nil + return chrooted, nil } From 75b965e52e7893f44b4b774cf2f73427995cfed0 Mon Sep 17 00:00:00 2001 From: firu11 Date: Fri, 24 Apr 2026 00:14:02 +0200 Subject: [PATCH 4/7] indexed db --- pkg/filesystem/classic.go | 2 - pkg/filesystem/wasm.go | 30 +-- pkg/idb/db.go | 470 ++++++++++++++++++++++++++++++++++++++ pkg/idb/file.go | 189 +++++++++++++++ pkg/idb/file_info.go | 45 ++++ pkg/idb/helpers.go | 57 +++++ pkg/idb/transaction.go | 191 ++++++++++++++++ 7 files changed, 955 insertions(+), 29 deletions(-) create mode 100644 pkg/idb/db.go create mode 100644 pkg/idb/file.go create mode 100644 pkg/idb/file_info.go create mode 100644 pkg/idb/helpers.go create mode 100644 pkg/idb/transaction.go diff --git a/pkg/filesystem/classic.go b/pkg/filesystem/classic.go index 38a741a..0195169 100644 --- a/pkg/filesystem/classic.go +++ b/pkg/filesystem/classic.go @@ -13,8 +13,6 @@ import ( const DirName string = ".git-calendar-data" -// easy as that you bozo -// // Returns a FS starting from users home directory func GetFS() (billy.Filesystem, error) { // get user home dir diff --git a/pkg/filesystem/wasm.go b/pkg/filesystem/wasm.go index c5245af..577b6df 100644 --- a/pkg/filesystem/wasm.go +++ b/pkg/filesystem/wasm.go @@ -3,36 +3,12 @@ package filesystem import ( - "errors" - "fmt" - "syscall/js" - - "github.com/git-calendar/core/pkg/opfs" + "github.com/git-calendar/core/pkg/idb" "github.com/go-git/go-billy/v5" ) -const DirName = "git-calendar-data" +const DirName = "git-calendar-data" // the storeName for IndexedDB func GetFS() (billy.Filesystem, error) { - // gets the handle from js window.opfsRootHandle - rootHandle := js.Global().Get("opfsRootHandle") - if rootHandle.IsUndefined() { - return nil, errors.New("opfsRootHandle not initialized") - } - - // create OPFS rooted at / - fs := opfs.New(rootHandle) - - // ensure directory exists - if err := fs.MkdirAll(DirName, 0o755); err != nil { - return nil, fmt.Errorf("failed to create dir: %w", err) - } - - // chroot into the directory - chrooted, err := fs.Chroot(DirName) - if err != nil { - return nil, fmt.Errorf("failed to chroot: %w", err) - } - - return chrooted, nil + return idb.New(DirName, 1) } diff --git a/pkg/idb/db.go b/pkg/idb/db.go new file mode 100644 index 0000000..6bd0e13 --- /dev/null +++ b/pkg/idb/db.go @@ -0,0 +1,470 @@ +//go:build js && wasm + +// Package indexeddb implements the billy.Filesystem interface backed by IndexedDB store +// It is only usable in a js/wasm build targeting a browser environment. +// https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API +package idb + +import ( + "errors" + "fmt" + "os" + "path" + "strings" + "syscall/js" + "time" + + "github.com/go-git/go-billy/v5" +) + +type IndexedDB struct { + name string + jsDB js.Value + root string +} + +const ( + contentStoreName = "content" + infoStoreName = "info" +) + +func New(name string, version int) (*IndexedDB, error) { + jsIdb := js.Global().Get("indexedDB") + if jsIdb.IsUndefined() { + return nil, fmt.Errorf("indexedDB not supported") + } + req := jsIdb.Call("open", name, version) + + dbValue, err := awaitOpen(req, func(db js.Value) { + storeNames := db.Get("objectStoreNames") + if !storeNames.Call("contains", contentStoreName).Bool() { + db.Call("createObjectStore", contentStoreName) // create "content" store + } + if !storeNames.Call("contains", infoStoreName).Bool() { + db.Call("createObjectStore", infoStoreName) // create "info" store + } + }) + if err != nil { + return nil, err + } + + idb := IndexedDB{ + name: name, + jsDB: dbValue, + root: "/", + } + + // check if root exists + tx := NewTx() + rootReq := tx.Get(infoStoreName, "/") + _ = tx.Commit(idb.jsDB) + + if !rootReq.Result().Truthy() { + info := IDBFileInfo{ + name: "/", + modTime: time.Now(), + mode: os.ModeDir | 0o755, + } + + txInit := NewTx() + txInit.Put(infoStoreName, "/", info.toJS()) + if err := txInit.Commit(idb.jsDB); err != nil { + return nil, err + } + } + + return &idb, nil +} + +func (idb *IndexedDB) Create(filename string) (billy.File, error) { + key := idb.absolutePath(filename) + fileInfo := IDBFileInfo{ + name: path.Base(filename), + size: 0, + modTime: time.Now(), + mode: 0o666, + } + + tx := NewTx() + tx.Put(infoStoreName, key, fileInfo.toJS()) + if err := tx.Commit(idb.jsDB); err != nil { + return nil, fmt.Errorf("failed to create file %s in idb: %w", filename, err) + } + + return &IDBFile{ + fs: idb, + key: key, + relPath: filename, + offset: 0, + }, nil +} + +func (idb *IndexedDB) Open(filename string) (billy.File, error) { + key := idb.absolutePath(filename) + + info, err := idb.Stat(filename) + exists := err == nil + if !exists { + return nil, os.ErrNotExist + } + if info.IsDir() { + return nil, fmt.Errorf("cannot open directory: %s", filename) + } + + return &IDBFile{ + fs: idb, + key: key, + relPath: filename, + offset: 0, + }, nil +} + +func (idb *IndexedDB) OpenFile(filename string, flag int, perm os.FileMode) (billy.File, error) { + key := idb.absolutePath(filename) + create := flag&os.O_CREATE != 0 + + info, err := idb.Stat(filename) + exists := err == nil + + if exists && info.IsDir() { + return nil, fmt.Errorf("cannot open directory: %s", filename) + } + + if !exists && !create { + return nil, os.ErrNotExist + } + + if !exists && create { + // check if parent exists + if _, err := idb.Stat(path.Dir(filename)); err != nil { + return nil, err + } + + // create new file + newInfo := IDBFileInfo{ + name: path.Base(filename), + modTime: time.Now(), + mode: perm, + } + + tx := NewTx() + tx.Put(infoStoreName, key, newInfo.toJS()) + if err := tx.Commit(idb.jsDB); err != nil { + return nil, err + } + } + + file := IDBFile{ + fs: idb, + key: key, + relPath: filename, + offset: 0, + } + if err := idb.applyFlags(&file, flag); err != nil { + return nil, err + } + return &file, nil +} + +func (idb *IndexedDB) Stat(name string) (os.FileInfo, error) { + tx := NewTx() + req := tx.Get(infoStoreName, idb.absolutePath(name)) + if err := tx.Commit(idb.jsDB); err != nil { + return nil, err + } + + result := req.Result() + if !result.Truthy() { + return nil, os.ErrNotExist + } + + return FileInfoFromJS(result), nil +} + +func (idb *IndexedDB) Rename(oldpath, newpath string) error { + // check source exists + info, err := idb.Stat(oldpath) + if err != nil { + return err + } + + // fail if target exists + if _, err := idb.Stat(newpath); err == nil { + return errors.New("target already exists") + } else if !os.IsNotExist(err) { + return err + } + + // call one of the helpers + if info.IsDir() { + return idb.renameDir(oldpath, newpath) + } + return idb.renameFile(oldpath, newpath) +} + +func (idb *IndexedDB) Remove(name string) error { + info, err := idb.Stat(name) + if err != nil { + return os.ErrNotExist + } + + fullpath := idb.absolutePath(name) + + if !info.IsDir() { + tx := NewTx() + tx.Delete(infoStoreName, fullpath) + tx.Delete(contentStoreName, fullpath) + if err := tx.Commit(idb.jsDB); err != nil { + return err + } + } else { + entries, err := idb.ReadDir(name) + if err != nil { + return err + } + if len(entries) != 0 { + return errors.New("not empty") + } + + tx := NewTx() + tx.Delete(infoStoreName, fullpath) + if err := tx.Commit(idb.jsDB); err != nil { + return err + } + } + + return nil +} + +func (idb *IndexedDB) Join(elem ...string) string { + return path.Join(elem...) +} + +func (idb *IndexedDB) MkdirAll(p string, perm os.FileMode) error { + parts := strings.Split(path.Clean(p), "/") + var currentPath string = idb.root + + tx := NewTx() + + for _, part := range parts { + if part == "." || part == "" { + continue + } + + currentPath = idb.Join(currentPath, part) + info := IDBFileInfo{ + name: part, + modTime: time.Now(), + mode: os.ModeDir | perm, + } + + tx.Put(infoStoreName, currentPath, info.toJS()) + } + + return tx.Commit(idb.jsDB) +} + +func (idb *IndexedDB) ReadDir(p string) ([]os.FileInfo, error) { + fullpath := idb.absolutePath(p) + prefix := strings.TrimSuffix(fullpath, "/") + "/" + + rangeObj := js.Global().Get("IDBKeyRange").Call( + "bound", + fullpath, + fullpath+"\uffff", + ) + + txKeys := NewTx() + keysReq := txKeys.GetAllKeys(infoStoreName, rangeObj) + if err := txKeys.Commit(idb.jsDB); err != nil { + return nil, err + } + + result := keysReq.Result() + if result.IsNull() || result.IsUndefined() { + return nil, os.ErrNotExist + } + + length := result.Length() + keys := make([]string, 0, length) + for idx := range length { + key := result.Index(idx).String() + if key == fullpath { + continue + } + if !strings.HasPrefix(key, prefix) { + continue + } + rest := key[len(prefix):] + if strings.Contains(rest, "/") { + continue + } + + keys = append(keys, key) + } + + txInfos := NewTx() + infoReqs := make([]interface{ Result() js.Value }, len(keys)) + + for idx, key := range keys { + infoReqs[idx] = txInfos.Get(infoStoreName, key) + } + + if err := txInfos.Commit(idb.jsDB); err != nil { + return nil, err + } + + files := make([]os.FileInfo, 0, len(keys)) + for idx := range keys { + files = append(files, FileInfoFromJS(infoReqs[idx].Result())) + } + + return files, nil +} + +func (idb *IndexedDB) TempFile(dir, prefix string) (billy.File, error) { + filename := prefix + randString(10) + return idb.Create(idb.Join(dir, filename)) +} + +func (idb *IndexedDB) Lstat(filename string) (os.FileInfo, error) { + return idb.Stat(filename) // Lstat is same as Stat, but if the file is a symbolic link, it doesn't resolve it +} + +func (idb *IndexedDB) Symlink(target, link string) error { + return billy.ErrNotSupported +} + +func (idb *IndexedDB) Readlink(link string) (string, error) { + return "", billy.ErrNotSupported +} + +func (idb *IndexedDB) Chroot(path string) (billy.Filesystem, error) { + // make sure it exists + if err := idb.MkdirAll(path, 0o777); err != nil { + return nil, err + } + // return a whole new instance + return &IndexedDB{ + name: idb.name, + jsDB: idb.jsDB, + root: idb.absolutePath(path), + }, nil +} + +func (idb *IndexedDB) Root() string { + return idb.root +} + +// ----- HELPERS ----- + +func (idb *IndexedDB) renameFile(oldpath, newpath string) error { + txRead := NewTx() + + oldFull := idb.absolutePath(oldpath) + newFull := idb.absolutePath(newpath) + if oldFull == newFull { + return nil + } + + oldInfo := txRead.Get(infoStoreName, oldFull) + oldContent := txRead.Get(contentStoreName, oldFull) + newInfo := txRead.Get(infoStoreName, newFull) + + // execute reads + if err := txRead.Commit(idb.jsDB); err != nil { + return fmt.Errorf("rename failed during read: %w", err) + } + + if !oldInfo.Result().Truthy() { + return os.ErrNotExist + } + if newInfo.Result().Truthy() { + return os.ErrExist + } + + info := FileInfoFromJS(oldInfo.Result()) + info.name = path.Base(newpath) + info.modTime = time.Now() + + txWrite := NewTx() + + txWrite.Put(infoStoreName, newFull, info.toJS()) + txWrite.Put(contentStoreName, newFull, oldContent.Result()) + txWrite.Delete(infoStoreName, oldFull) + txWrite.Delete(contentStoreName, oldFull) + + if err := txWrite.Commit(idb.jsDB); err != nil { + return fmt.Errorf("rename failed during write: %w", err) + } + + return nil +} + +func (idb *IndexedDB) renameDir(oldpath, newpath string) error { + oldFull := idb.absolutePath(oldpath) + newFull := idb.absolutePath(newpath) + + // create new dir + txRead := NewTx() + oldInfoReq := txRead.Get(infoStoreName, oldFull) + if err := txRead.Commit(idb.jsDB); err != nil { + return err + } + + txWrite := NewTx() + txWrite.Put(infoStoreName, newFull, oldInfoReq.Result()) + if err := txWrite.Commit(idb.jsDB); err != nil { + return err + } + + // list children + children, err := idb.ReadDir(oldpath) + if err != nil { + return err + } + + for _, child := range children { + oldChild := path.Join(oldFull, child.Name()) + newChild := path.Join(newFull, child.Name()) + + if child.IsDir() { + if err := idb.renameDir(oldChild, newChild); err != nil { + return err + } + } else { + if err := idb.renameFile(oldChild, newChild); err != nil { + return err + } + } + } + + // remove old dir (now empty) + return idb.Remove(oldpath) +} + +// Applies the O_TRUNC and O_APPEND flags to a file. +func (idb *IndexedDB) applyFlags(f *IDBFile, flag int) error { + if flag&os.O_TRUNC != 0 { + // truncate the file and then return it empty + if err := f.Truncate(0); err != nil { + return fmt.Errorf("failed to truncate file: %w", err) + } + } + + if flag&os.O_APPEND != 0 { + // prepare the file for appending + info, err := idb.Stat(f.key) + if err != nil { + return fmt.Errorf("failed to stat file: %w", err) + } + f.offset = info.Size() // set the offset to the end so that future Write() calls append + } + + return nil +} + +func (idb *IndexedDB) absolutePath(p string) string { + p = path.Clean(p) + return path.Join(idb.root, p) +} diff --git a/pkg/idb/file.go b/pkg/idb/file.go new file mode 100644 index 0000000..c6a5ab4 --- /dev/null +++ b/pkg/idb/file.go @@ -0,0 +1,189 @@ +//go:build js && wasm + +package idb + +import ( + "fmt" + "io" + "syscall/js" + "time" +) + +type IDBFile struct { + fs *IndexedDB // + key string // The key in IndexedDB (absolute path). + relPath string // The path relative to fs. + offset int64 // The current offset in bytes. +} + +func (f *IDBFile) Name() string { + return f.relPath // returns the filepath RELATIVE to current fs +} + +func (f *IDBFile) Read(p []byte) (int, error) { + n, err := f.ReadAt(p, f.offset) + f.offset += int64(n) + return n, err +} + +func (f *IDBFile) Write(p []byte) (int, error) { + n, err := f.WriteAt(p, f.offset) + f.offset += int64(n) + return n, err +} + +func (f *IDBFile) ReadAt(p []byte, off int64) (x int, err error) { + tx := NewTx() + req := tx.Get(contentStoreName, f.key) + + if err := tx.Commit(f.fs.jsDB); err != nil { + return 0, err + } + + existingVal := req.Result() + if !existingVal.Truthy() { + return 0, io.EOF + } + + length := existingVal.Get("length").Int() + if int(off) >= length { + return 0, io.EOF + } + + sub := existingVal.Call("subarray", off) + n := js.CopyBytesToGo(p, sub) + + if n < len(p) { + return n, io.EOF + } + return n, nil +} + +func (f *IDBFile) WriteAt(p []byte, off int64) (s int, err error) { + // read existing data and info + txRead := NewTx() + contentReq := txRead.Get(contentStoreName, f.key) + infoReq := txRead.Get(infoStoreName, f.key) + if err := txRead.Commit(f.fs.jsDB); err != nil { + return 0, err + } + existingVal := contentReq.Result() + + // prepare buffer + var buffer []byte + if existingVal.Truthy() { + end := int(off) + len(p) + size := max(existingVal.Length(), end) + buffer = make([]byte, size) + js.CopyBytesToGo(buffer, existingVal) + } else { + end := int(off) + len(p) + buffer = make([]byte, end) + } + + // write into buffer at offset + copy(buffer[off:], p) + + // convert to JS array + jsBuf := js.Global().Get("Uint8Array").New(len(buffer)) + js.CopyBytesToJS(jsBuf, buffer) + + // update FileInfo + info := FileInfoFromJS(infoReq.Result()) + info.size = int64(len(buffer)) // update size + info.modTime = time.Now() // update modification time + + // store Content and Info in a fresh transaction + txWrite := NewTx() + txWrite.Put(contentStoreName, f.key, jsBuf) + txWrite.Put(infoStoreName, f.key, info.toJS()) + if err := txWrite.Commit(f.fs.jsDB); err != nil { + return 0, err + } + + return len(p), nil +} + +func (f *IDBFile) Seek(offset int64, whence int) (int64, error) { + var base int64 + + switch whence { + case io.SeekStart: + base = 0 + + case io.SeekCurrent: + base = f.offset + + case io.SeekEnd: + stat, err := f.fs.Stat(f.relPath) + if err != nil { + return 0, err + } + base = stat.Size() + + default: + return 0, fmt.Errorf("invalid whence") + } + + newOffset := base + offset + if newOffset < 0 { + return 0, fmt.Errorf("negative position") + } + + f.offset = newOffset + return f.offset, nil +} + +func (f *IDBFile) Close() error { + return nil +} + +func (f *IDBFile) Lock() error { + return nil +} + +func (f *IDBFile) Unlock() error { + return nil +} + +func (f *IDBFile) Truncate(size int64) error { + txRead := NewTx() + contentReq := txRead.Get(contentStoreName, f.key) + infoReq := txRead.Get(infoStoreName, f.key) + + if err := txRead.Commit(f.fs.jsDB); err != nil { + return err + } + + existingVal := contentReq.Result() + var data []byte + if existingVal.Truthy() { + data = make([]byte, existingVal.Length()) + js.CopyBytesToGo(data, existingVal) + } + + if int64(len(data)) > size { + data = data[:size] + } else if int64(len(data)) < size { + newBuf := make([]byte, size) + copy(newBuf, data) + data = newBuf + } + + jsBuf := js.Global().Get("Uint8Array").New(len(data)) + js.CopyBytesToJS(jsBuf, data) + + info := FileInfoFromJS(infoReq.Result()) + info.size = size + info.modTime = time.Now() + + txWrite := NewTx() + txWrite.Put(contentStoreName, f.key, jsBuf) + txWrite.Put(infoStoreName, f.key, info.toJS()) + + if err := txWrite.Commit(f.fs.jsDB); err != nil { + return err + } + + return nil +} diff --git a/pkg/idb/file_info.go b/pkg/idb/file_info.go new file mode 100644 index 0000000..341e68a --- /dev/null +++ b/pkg/idb/file_info.go @@ -0,0 +1,45 @@ +//go:build js && wasm + +package idb + +import ( + "io/fs" + "os" + "syscall/js" + "time" +) + +type IDBFileInfo struct { + name string + size int64 + modTime time.Time + mode os.FileMode +} + +func (fi *IDBFileInfo) Name() string { return fi.name } +func (fi *IDBFileInfo) Size() int64 { return fi.size } +func (fi *IDBFileInfo) ModTime() time.Time { return fi.modTime } +func (fi *IDBFileInfo) Sys() any { return nil } +func (fi *IDBFileInfo) Mode() os.FileMode { return fi.mode } +func (fi *IDBFileInfo) Type() fs.FileMode { return fi.Mode().Type() } +func (fi *IDBFileInfo) IsDir() bool { return fi.mode.IsDir() } + +// Converts the file info to a JS Object. +func (fi *IDBFileInfo) toJS() js.Value { + obj := js.Global().Get("Object").New() + obj.Set("name", fi.name) + obj.Set("size", fi.size) + obj.Set("mod_time", fi.modTime.UnixMilli()) + obj.Set("mode", int(fi.mode)) + return obj +} + +// Converts a JS Object into a IDBFileInfo struct. +func FileInfoFromJS(jsVal js.Value) *IDBFileInfo { + return &IDBFileInfo{ + name: jsVal.Get("name").String(), + size: int64(jsVal.Get("size").Int()), + modTime: time.UnixMilli(int64(jsVal.Get("mod_time").Int())), + mode: os.FileMode(jsVal.Get("mode").Int()), + } +} diff --git a/pkg/idb/helpers.go b/pkg/idb/helpers.go new file mode 100644 index 0000000..7fbe15c --- /dev/null +++ b/pkg/idb/helpers.go @@ -0,0 +1,57 @@ +//go:build js && wasm + +package idb + +import ( + "fmt" + "math/rand/v2" + "syscall/js" +) + +// onUpgrade is called if db version is higher or DB does not exist. It's like custom migration method. +func awaitOpen(req js.Value, onUpgrade func(db js.Value)) (js.Value, error) { + resultCh := make(chan js.Value, 1) + errCh := make(chan error, 1) + + success := js.FuncOf(func(this js.Value, args []js.Value) any { + resultCh <- req.Get("result") + return nil + }) + + fail := js.FuncOf(func(this js.Value, args []js.Value) any { + errObj := args[0].Get("error") + errCh <- fmt.Errorf("failed to open indexeddb: %s", errObj.String()) + return nil + }) + + upgrade := js.FuncOf(func(this js.Value, args []js.Value) any { + db := req.Get("result") + onUpgrade(db) + return nil + }) + + req.Set("onsuccess", success) + req.Set("onerror", fail) + req.Set("onupgradeneeded", upgrade) // version is higher or DB does not exist + + defer success.Release() + defer fail.Release() + defer upgrade.Release() + + select { + case err := <-errCh: + return js.Null(), err + case res := <-resultCh: + return res, nil + } +} + +const letters string = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + +func randString(length int) string { + b := make([]byte, length) + for i := range b { + b[i] = letters[rand.IntN(len(letters))] + } + return string(b) +} diff --git a/pkg/idb/transaction.go b/pkg/idb/transaction.go new file mode 100644 index 0000000..d464e85 --- /dev/null +++ b/pkg/idb/transaction.go @@ -0,0 +1,191 @@ +//go:build js && wasm + +package idb + +import ( + "fmt" + "syscall/js" + "time" +) + +const reqTimeout = 2 * time.Second // should be more than enough + +type Transaction struct { + pending []*Operation +} + +type opType int + +const ( + opGet opType = iota + opPut + opDelete + opGetAllKeys +) + +type Operation struct { + typee opType + store string + key string + value js.Value + + result js.Value + err error + + done chan int +} + +// Creates a transaction-like interface. The real JS IDB tx is created and commited inside Tx.Commit(). +func NewTx() *Transaction { + return &Transaction{ + pending: make([]*Operation, 0), + } +} + +// JS: store.get(key) +func (tx *Transaction) Get(store, key string) *Operation { + op := &Operation{ + typee: opGet, + store: store, + key: key, + done: make(chan int, 1), + } + tx.pending = append(tx.pending, op) + return op +} + +// JS: store.delete(key) +func (tx *Transaction) Delete(store, key string) *Operation { + op := &Operation{ + typee: opDelete, + store: store, + key: key, + done: make(chan int, 1), + } + tx.pending = append(tx.pending, op) + return op +} + +// JS: store.put(value, key) +func (tx *Transaction) Put(store, key string, value js.Value) *Operation { + op := &Operation{ + typee: opPut, + store: store, + key: key, + value: value, + done: make(chan int, 1), + } + tx.pending = append(tx.pending, op) + return op +} + +// JS: store.getAllKeys(query) +func (tx *Transaction) GetAllKeys(store string, query js.Value) *Operation { + op := &Operation{ + typee: opGetAllKeys, + store: store, + value: query, + done: make(chan int, 1), + } + tx.pending = append(tx.pending, op) + return op +} + +// It creates the real JS IDB tx, runs all the queued request/queries and waits for them to finish. +func (tx *Transaction) Commit(db js.Value) error { + if len(tx.pending) == 0 { + return nil + } + + // duplicate store names + storeSet := make(map[string]bool) + stores := js.Global().Get("Array").New() + for _, op := range tx.pending { + if !storeSet[op.store] { + stores.Call("push", op.store) + storeSet[op.store] = true + } + } + + // create the transaction + idbTx := db.Call("transaction", stores, "readwrite") // TODO: not only readwrite + + // run requests/queries + for _, op := range tx.pending { + store := idbTx.Call("objectStore", op.store) + req := tx.dispatch(store, op) + tx.bind(req, op) + } + + return tx.waitAll() +} + +// A helper to call the JS methods. +func (tx *Transaction) dispatch(store js.Value, op *Operation) js.Value { + switch op.typee { + case opGet: + return store.Call("get", op.key) + case opPut: + return store.Call("put", op.value, op.key) + case opDelete: + return store.Call("delete", op.key) + case opGetAllKeys: + if op.value.IsNull() || op.value.IsUndefined() { // jsArray.Truthy() is always true even if empty + return store.Call("getAllKeys") + } + return store.Call("getAllKeys", op.value) + default: + panic("unknown op") + } +} + +// Binds the callbacks for specified op. +func (tx *Transaction) bind(req js.Value, op *Operation) { + var success, fail js.Func + + success = js.FuncOf(func(this js.Value, args []js.Value) any { + // release memory + defer success.Release() + defer fail.Release() + + op.result = args[0].Get("target").Get("result") + op.done <- 1 + return nil + }) + + fail = js.FuncOf(func(this js.Value, args []js.Value) any { + // release memory + defer success.Release() + defer fail.Release() + + errObj := args[0].Get("target").Get("error") + op.err = fmt.Errorf("IDB error: %s", errObj.String()) + op.done <- 1 + return nil + }) + + req.Set("onsuccess", success) + req.Set("onerror", fail) +} + +// Waits for all op +func (tx *Transaction) waitAll() error { + timeout := time.After(reqTimeout) + + for _, op := range tx.pending { + select { + case <-op.done: + if op.err != nil { + return op.err + } + case <-timeout: + return fmt.Errorf("transaction timeout after %v", reqTimeout) + } + } + return nil +} + +// Result simply returns the stored value, safe to call multiple times. +func (op *Operation) Result() js.Value { + return op.result +} From f53279a9d0fcc1196a8768775eec1753f8e227f7 Mon Sep 17 00:00:00 2001 From: firu11 Date: Fri, 24 Apr 2026 22:58:48 +0200 Subject: [PATCH 5/7] small fixes --- pkg/idb/db.go | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/pkg/idb/db.go b/pkg/idb/db.go index 6bd0e13..b3f11e5 100644 --- a/pkg/idb/db.go +++ b/pkg/idb/db.go @@ -1,6 +1,6 @@ //go:build js && wasm -// Package indexeddb implements the billy.Filesystem interface backed by IndexedDB store +// Package idb implements the billy.Filesystem interface backed by IndexedDB store. // It is only usable in a js/wasm build targeting a browser environment. // https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API package idb @@ -78,15 +78,15 @@ func New(name string, version int) (*IndexedDB, error) { func (idb *IndexedDB) Create(filename string) (billy.File, error) { key := idb.absolutePath(filename) - fileInfo := IDBFileInfo{ + + tx := NewTx() + tx.Delete(contentStoreName, key) // clear old content if any + tx.Put(infoStoreName, key, (&IDBFileInfo{ name: path.Base(filename), size: 0, modTime: time.Now(), mode: 0o666, - } - - tx := NewTx() - tx.Put(infoStoreName, key, fileInfo.toJS()) + }).toJS()) if err := tx.Commit(idb.jsDB); err != nil { return nil, fmt.Errorf("failed to create file %s in idb: %w", filename, err) } @@ -208,12 +208,12 @@ func (idb *IndexedDB) Remove(name string) error { return os.ErrNotExist } - fullpath := idb.absolutePath(name) + key := idb.absolutePath(name) if !info.IsDir() { tx := NewTx() - tx.Delete(infoStoreName, fullpath) - tx.Delete(contentStoreName, fullpath) + tx.Delete(infoStoreName, key) + tx.Delete(contentStoreName, key) if err := tx.Commit(idb.jsDB); err != nil { return err } @@ -227,7 +227,7 @@ func (idb *IndexedDB) Remove(name string) error { } tx := NewTx() - tx.Delete(infoStoreName, fullpath) + tx.Delete(infoStoreName, key) if err := tx.Commit(idb.jsDB); err != nil { return err } @@ -454,7 +454,7 @@ func (idb *IndexedDB) applyFlags(f *IDBFile, flag int) error { if flag&os.O_APPEND != 0 { // prepare the file for appending - info, err := idb.Stat(f.key) + info, err := idb.Stat(f.relPath) if err != nil { return fmt.Errorf("failed to stat file: %w", err) } From 5c099b2a41f6bbfeb088c01fca1307b7a4f04e5f Mon Sep 17 00:00:00 2001 From: firu11 Date: Fri, 24 Apr 2026 23:16:38 +0200 Subject: [PATCH 6/7] comments --- pkg/idb/db.go | 6 +++--- pkg/idb/file.go | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/pkg/idb/db.go b/pkg/idb/db.go index b3f11e5..58af9d6 100644 --- a/pkg/idb/db.go +++ b/pkg/idb/db.go @@ -18,9 +18,9 @@ import ( ) type IndexedDB struct { - name string - jsDB js.Value - root string + name string // The DB name. + jsDB js.Value // JS IDBDatabase object (https://developer.mozilla.org/en-US/docs/Web/API/IDBDatabase) + root string // The current abosolute root. } const ( diff --git a/pkg/idb/file.go b/pkg/idb/file.go index c6a5ab4..f65ab13 100644 --- a/pkg/idb/file.go +++ b/pkg/idb/file.go @@ -10,14 +10,14 @@ import ( ) type IDBFile struct { - fs *IndexedDB // - key string // The key in IndexedDB (absolute path). - relPath string // The path relative to fs. - offset int64 // The current offset in bytes. + fs *IndexedDB // A reference to it's fs. + key string // The key used in IndexedDB (absolute filepath). + relPath string // Path relative to fs. + offset int64 // Current offset in bytes. } func (f *IDBFile) Name() string { - return f.relPath // returns the filepath RELATIVE to current fs + return f.relPath // returns the filepath RELATIVE to current fs root } func (f *IDBFile) Read(p []byte) (int, error) { From cd2089eef96e15e61c5d31fb5244da0cb4a87873 Mon Sep 17 00:00:00 2001 From: firu11 Date: Sun, 26 Apr 2026 12:21:44 +0200 Subject: [PATCH 7/7] fix Create --- pkg/filesystem/classic.go | 2 +- pkg/filesystem/wasm.go | 1 + pkg/idb/db.go | 4 ++-- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/pkg/filesystem/classic.go b/pkg/filesystem/classic.go index 0195169..9b2926a 100644 --- a/pkg/filesystem/classic.go +++ b/pkg/filesystem/classic.go @@ -13,7 +13,7 @@ import ( const DirName string = ".git-calendar-data" -// Returns a FS starting from users home directory +// Returns a FS starting from users home directory. func GetFS() (billy.Filesystem, error) { // get user home dir home, err := os.UserHomeDir() diff --git a/pkg/filesystem/wasm.go b/pkg/filesystem/wasm.go index 577b6df..d19f86e 100644 --- a/pkg/filesystem/wasm.go +++ b/pkg/filesystem/wasm.go @@ -9,6 +9,7 @@ import ( const DirName = "git-calendar-data" // the storeName for IndexedDB +// Returns a FS inside IndexedDB. func GetFS() (billy.Filesystem, error) { return idb.New(DirName, 1) } diff --git a/pkg/idb/db.go b/pkg/idb/db.go index 58af9d6..20253a3 100644 --- a/pkg/idb/db.go +++ b/pkg/idb/db.go @@ -135,8 +135,8 @@ func (idb *IndexedDB) OpenFile(filename string, flag int, perm os.FileMode) (bil } if !exists && create { - // check if parent exists - if _, err := idb.Stat(path.Dir(filename)); err != nil { + // create all parent dirs + if err := idb.MkdirAll(path.Dir(filename), 0o755); err != nil { return nil, err }