Merge branch 'feature/go-library-docs'
This commit is contained in:
@@ -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.
|
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.
|
Depends on [go-nc-exapp](https://gitea.neitzel.de/konrad/go-nc-exapp) for AppAPI credentials on WebDAV.
|
||||||
|
|
||||||
## v1 scope
|
## Scope
|
||||||
|
|
||||||
**Included**
|
**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
|
- **CleanRel** — rejects `..`, URLs, and numeric file IDs
|
||||||
- **RequireBasename** — shared basename rules for child file names
|
- **RequireBasename** — shared basename rules for child file names
|
||||||
- **DefaultPathStore** — `GetDefault` / `SetDefault` for Saved Default paths; `FileStore` (JSON) and `Memory` implementations
|
- **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`
|
- Sentinel errors: `ErrNotExist`, `ErrForbidden`, `ErrNotDir`
|
||||||
|
|
||||||
**Excluded from v1**
|
**Excluded**
|
||||||
|
|
||||||
- ExApp lifecycle HTTP routes and HaRP bootstrap (see **go-nc-exapp**)
|
- ExApp lifecycle HTTP routes and HaRP bootstrap (see **go-nc-exapp**)
|
||||||
- AppAPI preference OCS wiring (ExApp adapts `gonexapp.AppAPIPreferences` to `DefaultPathStore`)
|
- 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.
|
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
|
## Domain language
|
||||||
|
|
||||||
See [CONTEXT.md](./CONTEXT.md) for User Files Root, Working Folder, Saved Default, Visit, and Files seam terminology.
|
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
|
## Related
|
||||||
|
|
||||||
- **go-nc-exapp** — AppAPI credentials and OCS preferences
|
- **go-nc-exapp** — AppAPI credentials and OCS preferences
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ import (
|
|||||||
|
|
||||||
// DefaultPathStore persists the Saved Default Working Folder path (User Files Root–relative).
|
// DefaultPathStore persists the Saved Default Working Folder path (User Files Root–relative).
|
||||||
type DefaultPathStore interface {
|
type DefaultPathStore interface {
|
||||||
|
// GetDefault returns the stored path, or empty when none is set yet.
|
||||||
GetDefault() (string, error)
|
GetDefault() (string, error)
|
||||||
|
// SetDefault writes a User Files Root–relative Working Folder path.
|
||||||
SetDefault(rel string) error
|
SetDefault(rel string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -24,9 +24,13 @@ type Entry struct {
|
|||||||
|
|
||||||
// Folder is flat storage for one Working Folder (basenames only).
|
// Folder is flat storage for one Working Folder (basenames only).
|
||||||
type Folder interface {
|
type Folder interface {
|
||||||
|
// List returns first-level names in the Working Folder.
|
||||||
List() ([]Entry, error)
|
List() ([]Entry, error)
|
||||||
|
// Read returns the contents of a basename in the Working Folder.
|
||||||
Read(name string) ([]byte, error)
|
Read(name string) ([]byte, error)
|
||||||
|
// Write creates or replaces a basename in the Working Folder.
|
||||||
Write(name string, data []byte) error
|
Write(name string, data []byte) error
|
||||||
|
// Exists reports whether name is a non-directory file in the Working Folder.
|
||||||
Exists(name string) (bool, error)
|
Exists(name string) (bool, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,6 +137,7 @@ func LocalFolder(dir string) Folder {
|
|||||||
return localFolder{dir: dir}
|
return localFolder{dir: dir}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// List implements Folder for a local directory.
|
||||||
func (f localFolder) List() ([]Entry, error) {
|
func (f localFolder) List() ([]Entry, error) {
|
||||||
entries, err := os.ReadDir(f.dir)
|
entries, err := os.ReadDir(f.dir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -145,6 +150,7 @@ func (f localFolder) List() ([]Entry, error) {
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Read implements Folder for a local directory.
|
||||||
func (f localFolder) Read(name string) ([]byte, error) {
|
func (f localFolder) Read(name string) ([]byte, error) {
|
||||||
if err := RequireBasename(name); err != nil {
|
if err := RequireBasename(name); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -152,6 +158,7 @@ func (f localFolder) Read(name string) ([]byte, error) {
|
|||||||
return os.ReadFile(filepath.Join(f.dir, name))
|
return os.ReadFile(filepath.Join(f.dir, name))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Write implements Folder for a local directory.
|
||||||
func (f localFolder) Write(name string, data []byte) error {
|
func (f localFolder) Write(name string, data []byte) error {
|
||||||
if err := RequireBasename(name); err != nil {
|
if err := RequireBasename(name); err != nil {
|
||||||
return err
|
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)
|
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) {
|
func (f localFolder) Exists(name string) (bool, error) {
|
||||||
if err := RequireBasename(name); err != nil {
|
if err := RequireBasename(name); err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
|
|||||||
@@ -175,6 +175,7 @@ func (f webdavFolder) childURL(name string) (string, error) {
|
|||||||
return f.root.urlFor(path.Join(f.rel, name))
|
return f.root.urlFor(path.Join(f.rel, name))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// List implements Folder via WebDAV PROPFIND Depth 1.
|
||||||
func (f webdavFolder) List() ([]Entry, error) {
|
func (f webdavFolder) List() ([]Entry, error) {
|
||||||
u, err := f.root.urlFor(f.rel)
|
u, err := f.root.urlFor(f.rel)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -267,6 +268,7 @@ func selfHref(href, folderRel string) bool {
|
|||||||
return strings.HasSuffix(h, "/"+folderRel) || strings.HasSuffix(h, folderRel)
|
return strings.HasSuffix(h, "/"+folderRel) || strings.HasSuffix(h, folderRel)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Read implements Folder via WebDAV GET.
|
||||||
func (f webdavFolder) Read(name string) ([]byte, error) {
|
func (f webdavFolder) Read(name string) ([]byte, error) {
|
||||||
u, err := f.childURL(name)
|
u, err := f.childURL(name)
|
||||||
if err != nil {
|
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 {
|
func (f webdavFolder) Write(name string, data []byte) error {
|
||||||
u, err := f.childURL(name)
|
u, err := f.childURL(name)
|
||||||
if err != nil {
|
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)
|
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) {
|
func (f webdavFolder) Exists(name string) (bool, error) {
|
||||||
u, err := f.childURL(name)
|
u, err := f.childURL(name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
Reference in New Issue
Block a user