Skip to main content
Entwickler Themen
Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Back to homepage

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.

Einbinden

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

API im Detail

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.

Sentinel-Errors

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.

Files seam (Root / Folder)

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:

  1. Root beschaffen (Local oder WebDAVRoot).
  2. OpenFolder, wenn der Ordner schon existieren muss; EnsureFolder, wenn fehlende Parents angelegt werden sollen.
  3. Auf dem Folder mit 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

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 / EnsureFolder mit Relativpfad; EnsureFolder legt Verzeichnisse mit Mode 0755 an.
  • LocalFolder(dir) liefert einen Folder zu einem bereits bekannten absoluten Verzeichnis (ohne Root).
  • LocalPath(f Folder) (string, bool) liefert den absoluten Pfad, wenn f ein lokaler Folder ist.
  • OpenLocal ist der Visit-Helfer (siehe Visit).

WebDAVRoot

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

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

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).

DefaultPathStore

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 Feld defaultPath (lokaler Adapter). Fehlende Datei bei GetDefault → 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.

Visit / Working Folder

Ein Visit ist eine Öffnung der ExApp: Working Folder für diese Anfrage plus Saved Default.

  • App-Icon-Launch (visitRelative leer): Saved Default lesen; fehlt er, initialFolderName nutzen; mit EnsureFolder anlegen, falls nötig.
  • Files-View (Visit-Pfad gesetzt): nur für diesen Visit, OpenFolder ohne 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 — der Folder; WorkingRel — Relativpfad; SavedDefault — gespeicherter Default.
  • SetDefault(relative) — EnsureFolder, speichern, SavedDefault aktualisieren; schaltet FS nicht um.
  • WorkingDir() — lokaler Absolutpfad, wenn FS lokal ist, sonst WorkingRel.

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.

Beispiele

Folder über Root öffnen und schreiben

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")

Local: Ordner sicherstellen

root := goncfiles.Local{RootDir: filesRoot}
folder, err := root.EnsureFolder("myapp")
if err != nil {
	return err
}
_ = folder

WebDAV: fehlenden Ordner erkennen

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

CleanRel vor dem Öffnen

rel := goncfiles.CleanRel(r.URL.Query().Get("folder"))
if rel == "" {
	http.Error(w, "invalid folder", http.StatusBadRequest)
	return
}

RequireBasename

if err := goncfiles.RequireBasename(name); err != nil {
	return err
}

FileStore lesen

store := goncfiles.FileStore{Path: filepath.Join(dir, "settings.json")}
saved, err := store.GetDefault()
if err != nil {
	return err
}
_ = saved

Visit öffnen

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

OpenLocal (Package-Example)

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"

LocalFolder (Package-Example)

folder := goncfiles.LocalFolder(dir)
_ = folder.Write("a.txt", []byte("x"))
ok, _ := folder.Exists("a.txt")
// ok == true