diff --git a/README.md b/README.md index 8767544..af80691 100644 --- a/README.md +++ b/README.md @@ -2,11 +2,11 @@ Shared Go library for Nextcloud ExApp Services: a User Files seam (local disk and WebDAV), Saved Default path storage, and Visit-scoped Working Folder resolution. -Import: `gitea.neitzel.de/konrad/go-nc-files` +Import: `gitea.neitzel.de/konrad/go-nc-files` (package `goncfiles`). Depends on [go-nc-exapp](https://gitea.neitzel.de/konrad/go-nc-exapp) for AppAPI credentials on WebDAV. -## v1 scope +## Scope **Included** @@ -16,10 +16,10 @@ Depends on [go-nc-exapp](https://gitea.neitzel.de/konrad/go-nc-exapp) for AppAPI - **CleanRel** — rejects `..`, URLs, and numeric file IDs - **RequireBasename** — shared basename rules for child file names - **DefaultPathStore** — `GetDefault` / `SetDefault` for Saved Default paths; `FileStore` (JSON) and `Memory` implementations -- **Visit** — `Open` resolves Working Folder from Saved Default (app-icon) or visit-only path (Files view); fail-closed on missing/forbidden visit paths +- **Visit** — `Open` / `OpenLocal` resolve Working Folder from Saved Default (app-icon) or visit-only path (Files view); fail-closed on missing/forbidden visit paths - Sentinel errors: `ErrNotExist`, `ErrForbidden`, `ErrNotDir` -**Excluded from v1** +**Excluded** - ExApp lifecycle HTTP routes and HaRP bootstrap (see **go-nc-exapp**) - AppAPI preference OCS wiring (ExApp adapts `gonexapp.AppAPIPreferences` to `DefaultPathStore`) @@ -47,10 +47,16 @@ visit, err := goncfiles.Open(root, store, "myapp", visitRelative) Each ExApp chooses its Saved Default preference key and initial folder name; this library stays key-agnostic. +Runnable package examples: `go test -run Example`. + ## Domain language See [CONTEXT.md](./CONTEXT.md) for User Files Root, Working Folder, Saved Default, Visit, and Files seam terminology. +## Testing + +Unit tests use a temporary local root (and httptest for WebDAV). No live Nextcloud is required for Library CI. + ## Related - **go-nc-exapp** — AppAPI credentials and OCS preferences diff --git a/defaultpath.go b/defaultpath.go index 81e48f0..75daccd 100644 --- a/defaultpath.go +++ b/defaultpath.go @@ -9,7 +9,9 @@ import ( // DefaultPathStore persists the Saved Default Working Folder path (User Files Root–relative). type DefaultPathStore interface { + // GetDefault returns the stored path, or empty when none is set yet. GetDefault() (string, error) + // SetDefault writes a User Files Root–relative Working Folder path. SetDefault(rel string) error } diff --git a/example_test.go b/example_test.go new file mode 100644 index 0000000..2352ef4 --- /dev/null +++ b/example_test.go @@ -0,0 +1,51 @@ +package goncfiles_test + +import ( + "fmt" + "os" + "path/filepath" + + goncfiles "gitea.neitzel.de/konrad/go-nc-files" +) + +func ExampleOpenLocal() { + root := tTempDir() + settings := filepath.Join(root, "settings.json") + files := filepath.Join(root, "files") + _ = os.MkdirAll(files, 0o755) + + visit, err := goncfiles.OpenLocal(files, settings, "myapp", "") + if err != nil { + fmt.Println("err:", err) + return + } + if err := visit.FS.Write("note.txt", []byte("hi")); err != nil { + fmt.Println("write:", err) + return + } + data, err := visit.FS.Read("note.txt") + if err != nil { + fmt.Println("read:", err) + return + } + fmt.Println(visit.WorkingRel, string(data)) + // Output: myapp hi +} + +func ExampleLocalFolder() { + dir := tTempDir() + folder := goncfiles.LocalFolder(dir) + _ = folder.Write("a.txt", []byte("x")) + ok, _ := folder.Exists("a.txt") + fmt.Println(ok) + // Output: true +} + +// tTempDir is a tiny helper so examples stay self-contained without testing.T. +func tTempDir() string { + dir, err := os.MkdirTemp("", "goncfiles-example-*") + if err != nil { + panic(err) + } + return dir +} diff --git a/files.go b/files.go index dd4d9ee..3a80abc 100644 --- a/files.go +++ b/files.go @@ -24,9 +24,13 @@ type Entry struct { // Folder is flat storage for one Working Folder (basenames only). type Folder interface { + // List returns first-level names in the Working Folder. List() ([]Entry, error) + // Read returns the contents of a basename in the Working Folder. Read(name string) ([]byte, error) + // Write creates or replaces a basename in the Working Folder. Write(name string, data []byte) error + // Exists reports whether name is a non-directory file in the Working Folder. Exists(name string) (bool, error) } @@ -133,6 +137,7 @@ func LocalFolder(dir string) Folder { return localFolder{dir: dir} } +// List implements Folder for a local directory. func (f localFolder) List() ([]Entry, error) { entries, err := os.ReadDir(f.dir) if err != nil { @@ -145,6 +150,7 @@ func (f localFolder) List() ([]Entry, error) { return out, nil } +// Read implements Folder for a local directory. func (f localFolder) Read(name string) ([]byte, error) { if err := RequireBasename(name); err != nil { return nil, err @@ -152,6 +158,7 @@ func (f localFolder) Read(name string) ([]byte, error) { return os.ReadFile(filepath.Join(f.dir, name)) } +// Write implements Folder for a local directory. func (f localFolder) Write(name string, data []byte) error { if err := RequireBasename(name); err != nil { return err @@ -159,6 +166,7 @@ func (f localFolder) Write(name string, data []byte) error { return os.WriteFile(filepath.Join(f.dir, name), data, 0o644) } +// Exists implements Folder for a local directory. func (f localFolder) Exists(name string) (bool, error) { if err := RequireBasename(name); err != nil { return false, err diff --git a/webdav.go b/webdav.go index 466ff85..d8339e9 100644 --- a/webdav.go +++ b/webdav.go @@ -175,6 +175,7 @@ func (f webdavFolder) childURL(name string) (string, error) { return f.root.urlFor(path.Join(f.rel, name)) } +// List implements Folder via WebDAV PROPFIND Depth 1. func (f webdavFolder) List() ([]Entry, error) { u, err := f.root.urlFor(f.rel) if err != nil { @@ -267,6 +268,7 @@ func selfHref(href, folderRel string) bool { return strings.HasSuffix(h, "/"+folderRel) || strings.HasSuffix(h, folderRel) } +// Read implements Folder via WebDAV GET. func (f webdavFolder) Read(name string) ([]byte, error) { u, err := f.childURL(name) if err != nil { @@ -302,6 +304,7 @@ func (f webdavFolder) Read(name string) ([]byte, error) { } } +// Write implements Folder via WebDAV PUT. func (f webdavFolder) Write(name string, data []byte) error { u, err := f.childURL(name) if err != nil { @@ -332,6 +335,7 @@ func (f webdavFolder) Write(name string, data []byte) error { return fmt.Errorf("webdav PUT %s: %s", name, res.Status) } +// Exists implements Folder via WebDAV HEAD (falls back to GET when needed). func (f webdavFolder) Exists(name string) (bool, error) { u, err := f.childURL(name) if err != nil {