Go-Backend (chi, pgx, golang-migrate) + React/TS-Frontend als PWA. Enthalten: Auth inkl. OIDC, Dateien mit Versionen und Papierkorb, Freigaben (Links + intern), WebDAV, Gruppenordner, Admin/Theming, Volltextsuche mit OCR, eigener PDF-Betrachter und WOPI-Office (Collabora/OnlyOffice). Nicht im Repo: deploy/.env (Sitzungsschlüssel, DB- und Admin-Passwort) sowie alle Daten- und Build-Verzeichnisse – siehe .gitignore. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
71 lines
2.8 KiB
Go
71 lines
2.8 KiB
Go
package storage
|
||
|
||
import (
|
||
"context"
|
||
"io"
|
||
"os"
|
||
"time"
|
||
)
|
||
|
||
// TempFiler kann Uploads unbekannter Größe auf der Platte zwischenlagern,
|
||
// statt sie im Arbeitsspeicher zu halten. Bewusst eine eigene, optionale
|
||
// Schnittstelle: *os.File gehört nicht in den allgemeinen Storage-Vertrag, und
|
||
// eine Implementierung ohne lokales Dateisystem kann sie schlicht weglassen.
|
||
type TempFiler interface {
|
||
CreateTemp(pattern string) (*os.File, error)
|
||
}
|
||
|
||
// LocalPather gibt den echten Pfad eines Blobs heraus – nur lesend gedacht.
|
||
//
|
||
// Gebraucht von der Texterkennung: ocrmypdf und tesseract sind externe
|
||
// Programme und wollen einen Pfad, keinen Datenstrom. Ohne diese Schnittstelle
|
||
// müsste jede Datei erst in eine vollständige Kopie geschrieben werden, bevor
|
||
// sie erkannt werden kann – bei einem 300-MB-Scan also 300 MB zusätzlich auf
|
||
// einer Platte, die ohnehin knapp sein kann.
|
||
//
|
||
// Wie TempFiler bewusst optional: Eine Ablage ohne lokales Dateisystem (S3)
|
||
// lässt sie weg, und der Aufrufer fällt dann auf die Kopie zurück.
|
||
type LocalPather interface {
|
||
LocalPath(key string) (string, error)
|
||
}
|
||
|
||
// FileInfo holds metadata about a stored blob.
|
||
type FileInfo struct {
|
||
Size int64
|
||
Checksum string // SHA-256 hex
|
||
ModTime time.Time
|
||
}
|
||
|
||
// Storage is the blob storage abstraction.
|
||
// All implementations must guarantee atomic writes and never corrupt existing data.
|
||
type Storage interface {
|
||
// Put streams r to the given key atomically (temp → fsync → rename).
|
||
// Returns the SHA-256 checksum and byte count written.
|
||
Put(ctx context.Context, key string, r io.Reader) (checksum string, size int64, err error)
|
||
|
||
// Get returns a reader for the blob at key. Caller must close it.
|
||
Get(ctx context.Context, key string) (io.ReadCloser, error)
|
||
|
||
// Delete removes a blob. In Phase 1 this is a hard delete used only by
|
||
// cleanup/GC paths; user-facing deletes are soft-deletes in the DB.
|
||
Delete(ctx context.Context, key string) error
|
||
|
||
// Stat returns metadata for a blob.
|
||
Stat(ctx context.Context, key string) (FileInfo, error)
|
||
|
||
// Move atomically moves a blob from oldKey to newKey within the same storage root.
|
||
Move(ctx context.Context, oldKey, newKey string) error
|
||
|
||
// Walk ruft fn für jeden Blob unterhalb von prefix auf. Existiert der
|
||
// Präfix nicht, ist das kein Fehler – nur nichts zu tun. Gedacht für die
|
||
// Bereinigung verwaister Blobs, die den Bestand auf der Platte gegen die
|
||
// Datenbank abgleichen muss.
|
||
Walk(ctx context.Context, prefix string, fn func(key string, info FileInfo) error) error
|
||
|
||
// DeletePrefix entfernt alles unterhalb eines Präfixes, etwa den kompletten
|
||
// Bereich eines Nutzers ("users/alice/"). Gedacht für das endgültige Löschen
|
||
// eines Kontos: danach existiert keine Datenbankzeile mehr, die noch auf die
|
||
// einzelnen Blobs zeigen könnte.
|
||
DeletePrefix(ctx context.Context, prefix string) error
|
||
}
|