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>
109 lines
3.8 KiB
Go
109 lines
3.8 KiB
Go
package files
|
||
|
||
import (
|
||
"context"
|
||
"strings"
|
||
"unicode"
|
||
|
||
"github.com/rs/zerolog/log"
|
||
|
||
"aether/internal/storage"
|
||
)
|
||
|
||
// maxFilenameLen begrenzt den Dateinamen so, dass der zusammengesetzte Pfad
|
||
// auf gängigen Dateisystemen (255 Bytes pro Segment) sicher passt.
|
||
const maxFilenameLen = 200
|
||
|
||
// BuildStorageKey erzeugt einen menschenlesbaren Pfad, der die Struktur der
|
||
// App spiegelt:
|
||
//
|
||
// users/{username}/home/{folderPath}/{filename}
|
||
//
|
||
// Bewusst lesbar statt UUID-basiert: fällt die Datenbank aus, sollen die
|
||
// Dateien per SFTP weiterhin auffindbar sein. Versionen (.versions/) und
|
||
// Papierkorb (.trash/) sind davon ausgenommen – das sind interne Artefakte.
|
||
// BuildStorageKey setzt den lesbaren Ablagepfad zusammen. root ist das
|
||
// Wurzelsegment – `users/<name>` für persönliche Daten, `groups/<id>` für
|
||
// Gruppenordner (siehe acl.Access.StorageRoot).
|
||
func BuildStorageKey(root, folderPath, filename string) string {
|
||
if folderPath == "" {
|
||
return root + "/home/" + filename
|
||
}
|
||
return root + "/home/" + folderPath + "/" + filename
|
||
}
|
||
|
||
// SanitizeFilename macht einen vom Client gelieferten Namen als einzelnes
|
||
// Pfadsegment sicher.
|
||
//
|
||
// Der Traversal-Schutz sitzt in storage.safePath; hier geht es darum, dass
|
||
// Name in der Datenbank und Pfad im Storage nicht auseinanderlaufen: Ein Name
|
||
// wie "a/b.txt" würde sonst still eine Ordnerebene im Storage erzeugen, die
|
||
// die Datenbank nicht kennt.
|
||
//
|
||
// Gibt "" zurück, wenn nichts Brauchbares übrig bleibt – Aufrufer müssen das
|
||
// als ungültige Eingabe behandeln.
|
||
func SanitizeFilename(name string) string {
|
||
// Nur das letzte Segment übernehmen – Clients schicken teils ganze Pfade.
|
||
if i := strings.LastIndexAny(name, `/\`); i >= 0 {
|
||
name = name[i+1:]
|
||
}
|
||
|
||
// Steuerzeichen entfernen (inkl. NUL und Zeilenumbrüche in Namen).
|
||
name = strings.Map(func(r rune) rune {
|
||
if r == unicode.ReplacementChar || unicode.IsControl(r) {
|
||
return -1
|
||
}
|
||
return r
|
||
}, name)
|
||
|
||
// Führende/abschließende Punkte und Leerraum: "." und ".." wären sonst
|
||
// Verzeichnisverweise, abschließende Punkte stören unter Windows.
|
||
name = strings.Trim(strings.TrimSpace(name), ".")
|
||
name = strings.TrimSpace(name)
|
||
|
||
if len(name) > maxFilenameLen {
|
||
// Auf Rune-Grenze kürzen, damit kein kaputtes UTF-8 entsteht.
|
||
runes := []rune(name)
|
||
for len(string(runes)) > maxFilenameLen {
|
||
runes = runes[:len(runes)-1]
|
||
}
|
||
name = strings.TrimSpace(string(runes))
|
||
}
|
||
|
||
return name
|
||
}
|
||
|
||
// ResyncBlobPaths bringt die Blobs der übergebenen Dateien auf die Pfade, die
|
||
// sich aus dem aktuellen Datenbankstand ergeben.
|
||
//
|
||
// Wird ein Ordner umbenannt oder verschoben, ändert sich der Pfad aller
|
||
// Dateien darunter. Weil die Storage-Schlüssel lesbar sind (und nicht bloß
|
||
// UUIDs), müssen die Blobs mitwandern. Aufrufen, *nachdem* die Umbenennung in
|
||
// der Datenbank committet ist – erst dann liefert GetFolderPath den neuen Pfad.
|
||
//
|
||
// Die Dateiliste muss *vorher* eingesammelt werden, damit die alten
|
||
// Storage-Schlüssel noch bekannt sind.
|
||
func ResyncBlobPaths(ctx context.Context, store *Store, st storage.Storage, root string, moved []*File) {
|
||
for _, f := range moved {
|
||
newFolderPath, err := store.GetFolderPath(ctx, f.FolderID)
|
||
if err != nil {
|
||
log.Ctx(ctx).Warn().Err(err).Str("file_id", f.ID.String()).
|
||
Msg("resync: folder path lookup failed – blob keeps old path")
|
||
continue
|
||
}
|
||
newKey := BuildStorageKey(root, newFolderPath, f.Name)
|
||
if newKey == f.StorageKey {
|
||
continue
|
||
}
|
||
if err := st.Move(ctx, f.StorageKey, newKey); err != nil {
|
||
log.Ctx(ctx).Warn().Err(err).Str("file_id", f.ID.String()).
|
||
Msg("resync: blob move failed – storage key now stale")
|
||
continue
|
||
}
|
||
if err := store.UpdateFileStorageKey(ctx, f.ID, newKey); err != nil {
|
||
log.Ctx(ctx).Error().Err(err).Str("file_id", f.ID.String()).
|
||
Msg("resync: blob moved but storage key not updated")
|
||
}
|
||
}
|
||
}
|