Files
Aether/backend/internal/files/naming.go
T
HoneyandClaude Opus 5 31a027c93a feat(aether): initial commit – self-hosted Cloud, Phasen 1–7
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>
2026-08-24 09:25:36 +02:00

109 lines
3.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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")
}
}
}