import Foundation /// Eine abgelegte Datei. public struct ShelfItem: Identifiable, Equatable, Sendable { public let url: URL public let name: String public let size: Int64 public let addedAt: Date public var id: String { url.path } public init(url: URL, name: String, size: Int64, addedAt: Date) { self.url = url self.name = name self.size = size self.addedAt = addedAt } /// Neuestes zuerst. /// /// Der Name als Zweitschlüssel ist kein Detail: das Dateisystem sagt keine /// Reihenfolge zu, und ohne festen zweiten Vergleich springen gleich alte /// Dateien bei jeder Aktualisierung durcheinander. public static func sorted(_ items: [ShelfItem]) -> [ShelfItem] { items.sorted { $0.addedAt == $1.addedAt ? $0.name < $1.name : $0.addedAt > $1.addedAt } } } /// Findet freie Namen, ohne je etwas zu überschreiben. public enum ShelfNaming { /// Der erste freie Name in der Reihe `Vertrag.pdf`, `Vertrag 2.pdf`, … /// /// Gleichnamige Dateien sind der Normalfall — `IMG_0001.jpg` aus zwei /// verschiedenen Chats. Überschreiben wäre stiller Datenverlust. public static func uniqueName(for name: String, taken: Set) -> String { let name = name.isEmpty ? "Datei" : name guard taken.contains(name) else { return name } let (base, ext) = split(name) var counter = 2 while true { let candidate = ext.isEmpty ? "\(base) \(counter)" : "\(base) \(counter).\(ext)" if !taken.contains(candidate) { return candidate } counter += 1 } } /// Zerlegt in Namensteil und Endung. /// /// Ein führender Punkt zählt nicht als Endung — sonst würde aus /// `.gitignore` ein ` 2.gitignore` mit führendem Leerzeichen. Und bei /// `archiv.tar.gz` zählt nur die letzte Endung: `archiv 2.tar.gz` wäre /// zwar hübscher, aber `archiv.tar 2.gz` ist die Regel, die auch bei /// unbekannten Doppelendungen nichts kaputt macht. private static func split(_ name: String) -> (base: String, ext: String) { guard let dot = name.lastIndex(of: "."), dot != name.startIndex else { return (name, "") } return (String(name[name.startIndex.. [ShelfItem] { guard let days = afterDays, days > 0 else { return [] } let limit = now.addingTimeInterval(-Double(days) * 86_400) // Streng älter: genau auf der Frist bleibt es liegen. Löschen ist // unwiderruflich, einen Tag länger warten ist es nicht. return items.filter { $0.addedAt < limit } } }