Files
RouterOS/RouterOSAssistant/Features/Backup/BackupViewModel.swift
T
KayandClaude Sonnet 5 20f8d5e0bf M13: Backup-Wiederherstellung, mit Modell-Schutz und Login-Erhalt
Letzter offener Punkt aus HANDOFF.md: gespeicherte .rsc-Backups lassen
sich jetzt wieder einspielen. Weg: Backup-Datei per SFTP auf den Router
hochladen (Citadel, die bereits eingebundene SSH-Bibliothek, hat einen
SFTP-Client), dann RouterOS' offiziell dokumentierter Restore-Weg in
einem Rutsch: /system reset-configuration no-defaults=yes
run-after-reset=<datei> (kompletter Wipe + sofortiges Wiederanwenden,
sicherer als ein Re-Import auf eine bestehende, andere Config).

Vor dem Bestätigungsdialog wird das Routermodell abgeglichen
(/system routerboard prints "model"-Feld gegen die "# model = ..."-
Kopfzeile der Sicherung) und bei Mismatch komplett blockiert, um ein
Brick-Risiko durch falsches Modell zu vermeiden - der Dialog selbst warnt
zusaetzlich prominent davor.

Ein ernster Bug live gefunden und gefixt: der erste echte Restore-Test
sperrte den Router komplett aus (RouterOS exportiert nie Passwoerter,
no-defaults=yes loescht zusaetzlich den Werks-Admin-Account), nur per
Hardware-Reset behebbar. Fix: das aktuell verwendete App-Login wird jetzt
vorne ins Restore-Skript eingefuegt, noch vor dem eigentlichen
Sicherungsinhalt, da RouterOS den Import beim ersten Fehler irgendwo im
Skript komplett abbricht. Zwei Tests fuer die Escaping-Logik ergaenzt.

Nebenbei: BackupListView mit den neuen Restore-Dialogen liess sich nicht
mehr kompilieren (SwiftUI-Typpruefung timeoutete bei der langen
Modifier-Kette) - Restore-Dialoge in eine eigene @ViewBuilder-Property
ausgelagert.

HANDOFF.md/CHATLOG.md aktualisiert: M13, Bug 18+19, "Backup-
Wiederherstellung fehlt" aus den offenen Punkten entfernt, Hinweis auf
den zweiten (ungewollten) Werksreset waehrend der Session.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CTgRxJTzaQwaRkngbaE1GJ
2026-09-14 15:21:05 +02:00

89 lines
3.1 KiB
Swift

import Foundation
@MainActor
final class BackupViewModel: ObservableObject {
@Published private(set) var backups: [BackupRecord] = []
@Published private(set) var isCreatingBackup = false
@Published var errorMessage: String?
@Published private(set) var backupDirectoryPath: String = BackupService.customDirectoryURL?.path
?? BackupService.defaultDirectoryURL.path
@Published private(set) var isResettingToFactoryDefaults = false
@Published var resetError: String?
@Published private(set) var didSendFactoryReset = false
@Published private(set) var isRestoring = false
@Published var restoreError: String?
@Published private(set) var didSendRestore = false
private let backupService = BackupService()
private let factoryResetService = FactoryResetService()
func load() {
backups = backupService.listBackups()
}
func setBackupDirectory(_ url: URL) {
BackupService.customDirectoryURL = url
backupDirectoryPath = url.path
load()
}
func resetBackupDirectoryToDefault() {
BackupService.customDirectoryURL = nil
backupDirectoryPath = BackupService.defaultDirectoryURL.path
load()
}
func createBackup(for credentials: RouterOSCredentials) {
isCreatingBackup = true
errorMessage = nil
Task {
do {
_ = try await backupService.createBackup(for: credentials)
load()
} catch {
errorMessage = error.localizedDescription
}
isCreatingBackup = false
}
}
/// Backs up first (best-effort — a failed backup shouldn't block the reset itself, since
/// RouterOS makes its own backup as part of the reset anyway), then restores the router's
/// vendor-default configuration and reboots it.
func resetToFactoryDefaults(for credentials: RouterOSCredentials) {
isResettingToFactoryDefaults = true
resetError = nil
didSendFactoryReset = false
Task {
do {
_ = try? await backupService.createBackup(for: credentials)
try await factoryResetService.resetToVendorDefaults(for: credentials)
didSendFactoryReset = true
} catch {
resetError = error.localizedDescription
}
isResettingToFactoryDefaults = false
}
}
/// Wipes the router's config and replaces it with `record`'s. Model-compatibility checking
/// happens in the View before this is ever called (needs `ConnectionService.deviceInfo`,
/// which this view model doesn't hold) — this only performs the already-confirmed restore.
func restore(_ record: BackupRecord, for credentials: RouterOSCredentials) {
isRestoring = true
restoreError = nil
didSendRestore = false
Task {
do {
try await backupService.restoreBackup(record, for: credentials)
didSendRestore = true
} catch {
restoreError = error.localizedDescription
}
isRestoring = false
}
}
}