forked from kay/RouterOS
.help(...)-Tooltips auf jedem Eingabefeld/Picker/Toggle in Connect- sowie allen Einrichten-Schritten (WAN/LAN/VLAN/WLAN/Firewall) -- kurze Erklärung was der Wert bedeutet und welche Auswirkung er hat, passend zum Laien-Anspruch der App. Sicherungen-Tab: Speicherort jetzt änderbar (Ordner wählen über nativen macOS-Dialog, Zurücksetzen auf Standard). BackupService hält den gewählten Pfad in UserDefaults (BackupService.customDirectoryURL), fällt ohne Auswahl weiter auf ~/Library/Application Support/... zurück. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HReLXMbmPvtQ23p1iWiJNW
88 lines
3.5 KiB
Swift
88 lines
3.5 KiB
Swift
import Foundation
|
|
|
|
struct BackupRecord: Identifiable, Equatable {
|
|
let id: String
|
|
let createdAt: Date
|
|
let host: String
|
|
let fileURL: URL
|
|
}
|
|
|
|
/// Creates and lists local, human-readable configuration backups (`/export terse`).
|
|
///
|
|
/// Backups always go over SSH, independent of whether the live wizard session is using
|
|
/// REST or SSH — RouterOS's REST API mirrors config menus but has no generic "export the
|
|
/// whole config as a script" endpoint, while `/export` over SSH is well established.
|
|
/// This means creating a backup requires SSH access on the router (enabled by default).
|
|
final class BackupService {
|
|
private let fileManager = FileManager.default
|
|
|
|
private static let customDirectoryKey = "RouterOSAssistant.BackupDirectoryPath"
|
|
|
|
/// User-chosen backup folder, persisted across launches. `nil` means "use the default".
|
|
static var customDirectoryURL: URL? {
|
|
get {
|
|
guard let path = UserDefaults.standard.string(forKey: customDirectoryKey) else { return nil }
|
|
return URL(fileURLWithPath: path, isDirectory: true)
|
|
}
|
|
set { UserDefaults.standard.set(newValue?.path, forKey: customDirectoryKey) }
|
|
}
|
|
|
|
static var defaultDirectoryURL: URL {
|
|
let base = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0]
|
|
return base.appendingPathComponent("RouterOSAssistant/Backups", isDirectory: true)
|
|
}
|
|
|
|
private var backupsDirectory: URL {
|
|
let directory = Self.customDirectoryURL ?? Self.defaultDirectoryURL
|
|
try? fileManager.createDirectory(at: directory, withIntermediateDirectories: true)
|
|
return directory
|
|
}
|
|
|
|
@discardableResult
|
|
func createBackup(for credentials: RouterOSCredentials) async throws -> BackupRecord {
|
|
let transport = SSHTransport(credentials: credentials)
|
|
try await transport.connect()
|
|
|
|
let script: String
|
|
do {
|
|
script = try await transport.exportConfiguration()
|
|
} catch {
|
|
await transport.disconnect()
|
|
throw error
|
|
}
|
|
await transport.disconnect()
|
|
|
|
let timestamp = Self.fileTimestampFormatter.string(from: Date())
|
|
let fileName = "\(credentials.host)_\(timestamp).rsc"
|
|
let fileURL = backupsDirectory.appendingPathComponent(fileName)
|
|
try script.write(to: fileURL, atomically: true, encoding: .utf8)
|
|
|
|
return BackupRecord(id: fileName, createdAt: Date(), host: credentials.host, fileURL: fileURL)
|
|
}
|
|
|
|
func listBackups() -> [BackupRecord] {
|
|
guard let files = try? fileManager.contentsOfDirectory(
|
|
at: backupsDirectory,
|
|
includingPropertiesForKeys: [.creationDateKey]
|
|
) else {
|
|
return []
|
|
}
|
|
|
|
return files
|
|
.filter { $0.pathExtension == "rsc" }
|
|
.compactMap { url -> BackupRecord? in
|
|
let attributes = try? fileManager.attributesOfItem(atPath: url.path)
|
|
let createdAt = (attributes?[.creationDate] as? Date) ?? Date()
|
|
let host = url.deletingPathExtension().lastPathComponent.components(separatedBy: "_").first ?? "unbekannt"
|
|
return BackupRecord(id: url.lastPathComponent, createdAt: createdAt, host: host, fileURL: url)
|
|
}
|
|
.sorted { $0.createdAt > $1.createdAt }
|
|
}
|
|
|
|
private static let fileTimestampFormatter: DateFormatter = {
|
|
let formatter = DateFormatter()
|
|
formatter.dateFormat = "yyyy-MM-dd_HH-mm-ss"
|
|
return formatter
|
|
}()
|
|
}
|