From 17f05fec89db5c84dcca57525c01d27f4681ea71 Mon Sep 17 00:00:00 2001 From: firu11 Date: Sat, 18 Apr 2026 12:18:46 +0200 Subject: [PATCH 1/3] 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/3] 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/3] 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 }