7.3.1.2. go-nc-files
go-nc-files ist die gemeinsame Go-Library für den Dateizugriff von Nextcloud-ExApp, Glossar, öffnet in neuem Tab
-Services: ein Files Seam, Glossar, öffnet in neuem Tab
für lokale Festplatte und WebDAV, Speicherung des Saved Default, Glossar, öffnet in neuem Tab
und Visit-scoped Auflösung des Working Folder, Glossar, öffnet in neuem Tab
. Import-Pfad: gitea.neitzel.de/konrad/go-nc-files, Package-Name: goncfiles. Repo: gitea.neitzel.de/konrad/go-nc-files. Für AppAPI Credentials, Glossar, öffnet in neuem Tab
am WebDAV-Adapter hängt die Library von go-nc-exapp ab. Überblick über die ExApp-Libraries: 7.3.1. Nextcloud ExApps.
Englische Fachbegriffe sind im Glossar erklärt.
go get gitea.neitzel.de/konrad/go-nc-files
import goncfiles "gitea.neitzel.de/konrad/go-nc-files"
Private Module: GOPRIVATE=gitea.neitzel.de. Klonen und Fetch über SSH-Port 2222: ssh://git@gitea.neitzel.de:2222/konrad/go-nc-files.git.
Nicht Teil der Library: ExApp-Lifecycle-HTTP-Routen und HaRP, Glossar, öffnet in neuem Tab
-Bootstrap (→ go-nc-exapp); OCS, Glossar, öffnet in neuem Tab
-Preferences-Verdrahtung (die ExApp adaptiert gonexapp.AppAPIPreferences auf DefaultPathStore); produkt-spezifische Dateisemantik (Catalog, DNS, …); rekursive Tree-Walks und numerische Nextcloud-File-IDs.
flowchart TD AppIcon["App-Icon-Launch<br/>visitRelative leer"] --> SavedDefault SavedDefault -->|"fehlt → initialFolderName"| EnsureFolder EnsureFolder --> WorkingFolder["Working Folder"] FilesView["Files-View-Visit<br/>visitRelative gesetzt"] --> OpenFolder OpenFolder -->|"fehlt / verboten → fail-closed"| WorkingFolder
Domain-Begriffe (User Files Root, Glossar, öffnet in neuem Tab
, Working Folder, Saved Default, Visit, Glossar, öffnet in neuem Tab
, Files Seam) sind in der Library neutral gehalten. Produktcode schreibt gegen Root/Folder; Tests nutzen Local, Produktion gegen Nextcloud nutzt WebDAVRoot.
| Symbol | Bedeutung |
|---|---|
ErrNotExist |
Ordner oder Datei fehlt |
ErrForbidden |
kein Lese-/Schreibrecht |
ErrNotDir |
Pfad existiert, ist aber kein Verzeichnis |
IsNotExist(err) und IsForbidden(err) prüfen per errors.Is, ob der Fehler den jeweiligen Sentinel wrappen. Nicht den Fehlertext vergleichen.
Root öffnet einen Ordner relativ zum User Files Root. Folder ist flacher Speicher darin: List, Read, Write, Exists, Remove – nur per Basename.
type Root interface {
OpenFolder(rel string) (Folder, error)
EnsureFolder(rel string) (Folder, error)
}
type Folder interface {
List() ([]Entry, error)
Read(name string) ([]byte, error)
Write(name string, data []byte) error
Exists(name string) (bool, error)
Remove(name string) error
}
type Entry struct {
Name string
IsDir bool
}
Ablauf:
Rootbeschaffen (LocaloderWebDAVRoot).OpenFolder, wenn der Ordner schon existieren muss;EnsureFolder, wenn fehlende Parents angelegt werden sollen.- Auf dem
Foldermit Basename arbeiten.
List liefert Namen der ersten Ebene. Exists meldet eine Nicht-Verzeichnis-Datei. Entry.IsDir ist true für ein Kindverzeichnis.
Fehler: Ungültiger Relativpfad (CleanRel leer) → plain error. Fehlender Ordner bei OpenFolder → wrappt ErrNotExist. Existierender Nicht-Ordner → ErrNotDir. Rechtefehler → ErrForbidden. Read/Write/Exists/Remove mit ungültigem Namen → Fehler von RequireBasename. Remove und WebDAV-Read wrappen ErrNotExist, wenn das Kind fehlt. Exists liefert bei fehlendem Kind false, nil.
Local ist ein User Files Root auf dem lokalen Dateisystem. RootDir ist das absolute Verzeichnis, das der Wurzel der User-Dateien entspricht. Für Tests und go run ohne Nextcloud.
type Local struct {
RootDir string
}
OpenFolder/EnsureFoldermit Relativpfad;EnsureFolderlegt Verzeichnisse mit Mode0755an.LocalFolder(dir)liefert einenFolderzu einem bereits bekannten absoluten Verzeichnis (ohne Root).LocalPath(f Folder) (string, bool)liefert den absoluten Pfad, wennfein lokaler Folder ist.OpenLocalist der Visit-Helfer (siehe Visit).
WebDAVRoot ist ein User Files Root unter /remote.php/dav/files/{user}/. Cred liefert User-ID und AppAPI-Auth-Header aus go-nc-exapp. Client == nil → http.DefaultClient.
type WebDAVRoot struct {
Cred gonexapp.Credentials
Client *http.Client
}
Methoden wie bei Local. HTTP-Mapping:
| Methode | WebDAV |
|---|---|
OpenFolder |
PROPFIND (prüft, legt nicht an) |
EnsureFolder |
MKCOL für fehlende Parents, dann öffnen |
List |
PROPFIND Depth 1 |
Read |
GET |
Write |
PUT |
Remove |
DELETE |
Exists |
HEAD, Fallback auf GET bei Status weder Erfolg noch 404/Forbidden |
Ungültiger Relativpfad → plain error, ohne Server-Aufruf. OpenFolder: 404 → ErrNotExist, 401/403 → ErrForbidden, keine Collection → ErrNotDir. EnsureFolder: 401/403 → ErrForbidden. Andere Statuscodes → plain error mit HTTP-Status.
CleanRel(rel string) string normalisiert einen User-Files-Root-relativen Pfad: Vorwärtsslash, ohne führenden/trailing Slash. Leeres Ergebnis = ungültig (nicht öffnen). Backslashes werden wie Slashes behandelt. Abgelehnt: .., URLs, bloße numerische Nextcloud-File-ID.
Kein Error-Return; Aufrufer wie OpenFolder machen daraus z. B. invalid Working Folder path.
RequireBasename(name string) error akzeptiert nur einen einzelnen Dateinamen im Working Folder, keinen Pfad. Folder-Methoden rufen das vor dem Kindzugriff. Abgelehnt: leerer Name, / oder \, .., Name der nicht path.Base von sich selbst ist. Fehlertext: name must be a basename in the Working Folder (kein Wrap von ErrNotExist/ErrForbidden).
Persistiert den Saved Default Working Folder (relativ zum User Files Root).
type DefaultPathStore interface {
GetDefault() (string, error)
SetDefault(rel string) error
}
GetDefault→ gespeicherter Pfad oder leer, wenn noch nichts gesetzt.SetDefault→ neuen Pfad schreiben.
Implementierungen in der Library:
FileStore{Path}— JSON-Datei mit FelddefaultPath(lokaler Adapter). Fehlende Datei beiGetDefault→ leer,nil.Memory{Value}— In-Memory für Tests; kein Error.
OCS ruft die Library nicht auf. Eine ExApp, die den Pfad in Nextcloud-Preferences speichert, implementiert dieselben zwei Methoden um gonexapp.AppAPIPreferences. Visit liest und schreibt den Saved Default über dieses Interface; Visit.SetDefault stellt zusätzlich sicher, dass der Ordner existiert.
Ein Visit ist eine Öffnung der ExApp: Working Folder für diese Anfrage plus Saved Default.
- App-Icon-Launch (
visitRelativeleer): Saved Default lesen; fehlt er,initialFolderNamenutzen; mitEnsureFolderanlegen, falls nötig. - Files-View (Visit-Pfad gesetzt): nur für diesen Visit,
OpenFolderohne Anlegen und ohne Fallback auf den Saved Default. Fehlender oder verbotener Pfad → fail-closed.
type Visit struct {
SavedDefault string
WorkingRel string
VisitRelative string
FS Folder
// ...
}
func Open(root Root, defaults DefaultPathStore, initialFolderName, visitRelative string) (*Visit, error)
func OpenLocal(filesRoot, settingsPath, initialFolderName, visitRelative string) (*Visit, error)
Visit.FS— derFolder;WorkingRel— Relativpfad;SavedDefault— gespeicherter Default.SetDefault(relative)—EnsureFolder, speichern,SavedDefaultaktualisieren; schaltetFSnicht um.WorkingDir()— lokaler Absolutpfad, wennFSlokal ist, sonstWorkingRel.
Fehler: Fehler von GetDefault; ungültiger Saved-/Visit-Pfad → plain error (invalid Working Folder path / invalid saved Working Folder path); fehlender/verbotener Visit-Pfad → Fehler von OpenFolder mit Pfad gewrappt, kein Fallback.
folder, err := root.OpenFolder("Zones")
if err != nil {
return err
}
if err := folder.Write("note.txt", []byte("hi")); err != nil {
return err
}
data, err := folder.Read("note.txt")
root := goncfiles.Local{RootDir: filesRoot}
folder, err := root.EnsureFolder("myapp")
if err != nil {
return err
}
_ = folder
root := goncfiles.WebDAVRoot{Cred: cred}
folder, err := root.OpenFolder("Zones")
if err != nil {
if goncfiles.IsNotExist(err) {
return fmt.Errorf("folder missing")
}
return err
}
_ = folder
rel := goncfiles.CleanRel(r.URL.Query().Get("folder"))
if rel == "" {
http.Error(w, "invalid folder", http.StatusBadRequest)
return
}
if err := goncfiles.RequireBasename(name); err != nil {
return err
}
store := goncfiles.FileStore{Path: filepath.Join(dir, "settings.json")}
saved, err := store.GetDefault()
if err != nil {
return err
}
_ = saved
visit, err := goncfiles.Open(root, store, "myapp", visitRelative)
if err != nil {
if goncfiles.IsForbidden(err) {
http.Error(w, "forbidden", http.StatusForbidden)
return err
}
return err
}
data, err := visit.FS.Read("note.txt")
_ = visit.WorkingRel
_ = data
App-Icon-Launch lokal: Dateiwurzel + Settings-JSON, initialer Ordner myapp, leerer Visit-Pfad. Schreibt und liest note.txt; Ausgabe myapp hi.
visit, err := goncfiles.OpenLocal(files, settings, "myapp", "")
if err != nil {
return
}
if err := visit.FS.Write("note.txt", []byte("hi")); err != nil {
return
}
data, err := visit.FS.Read("note.txt")
// visit.WorkingRel == "myapp", string(data) == "hi"
folder := goncfiles.LocalFolder(dir)
_ = folder.Write("a.txt", []byte("x"))
ok, _ := folder.Exists("a.txt")
// ok == true