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/` für persönliche Daten, `groups/` 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") } } }