using ClawdDotNet.Core.Backup; using Microsoft.Extensions.Logging; namespace ClawdDotNet.Services; /// /// Erstellt einmal täglich zur eingestellten Uhrzeit eine Sicherung. /// /// Bewusst ohne Zugangsdaten: Die Passphrase müsste dafür gespeichert werden, und /// neben den Sicherungen abgelegt wäre sie wirkungslos. Wer die Zugangsdaten /// mitsichern will, macht das von Hand. /// /// Der Zeitpunkt wird bei jedem Durchlauf neu gegen die Einstellungen geprüft, damit /// eine Änderung ohne Neustart greift. /// public sealed class BackupScheduler : IDisposable { private readonly string _instanceDir; private readonly string _instanceName; private readonly SettingsManager _settings; private readonly ILogger _logger; private readonly BackupService _service = new(); private readonly System.Windows.Forms.Timer _timer; /// Verhindert mehrere Sicherungen innerhalb derselben Minute. private DateTime? _lastRun; private bool _running; public event Action? OnBackupCreated; public BackupScheduler(string instanceDir, string instanceName, SettingsManager settings, ILogger logger) { _instanceDir = instanceDir; _instanceName = instanceName; _settings = settings; _logger = logger; // Minütlich prüfen reicht — die Uhrzeit ist auf Minuten genau eingestellt. _timer = new System.Windows.Forms.Timer { Interval = 60_000 }; _timer.Tick += async (_, _) => await TickAsync(); } public void Start() => _timer.Start(); public void Stop() => _timer.Stop(); private async Task TickAsync() { if (_running) return; var settings = _settings.AppSettings; if (!settings.AutoBackupEnabled) return; if (!TimeSpan.TryParse(settings.AutoBackupTime, out var scheduled)) return; var now = DateTime.Now; // Fällig, sobald die Uhrzeit erreicht ist und heute noch nichts lief. if (now.TimeOfDay < scheduled) return; if (_lastRun?.Date == now.Date) return; _running = true; try { await RunAsync(settings.BackupDirectory, settings.BackupKeepCount); _lastRun = now; } catch (Exception ex) { // Ein Fehlschlag darf die Anwendung nicht stören; er wird protokolliert // und morgen erneut versucht. _logger.LogError(ex, "Automatische Sicherung fehlgeschlagen"); _lastRun = now; } finally { _running = false; } } private async Task RunAsync(string folder, int keepCount) { var safeName = string.Concat( _instanceName.Select(c => Path.GetInvalidFileNameChars().Contains(c) ? '_' : c)); var file = Path.Combine( Path.GetFullPath(folder), $"backup_{safeName}_{DateTime.Now:yyyy-MM-dd_HHmm}.zip"); var result = await _service.CreateAsync(_instanceDir, file, new BackupOptions { Secrets = SecretMode.Exclude, IncludeChatHistory = true, IncludeLogs = false }); _logger.LogInformation("Automatische Sicherung erstellt: {Path} ({Size} Bytes)", result.ZipPath, result.SizeBytes); ApplyRotation(Path.GetFullPath(folder), safeName, keepCount); OnBackupCreated?.Invoke(result.ZipPath); } /// Behält die neuesten Sicherungen dieser Instanz und entfernt den Rest. private void ApplyRotation(string folder, string safeName, int keepCount) { if (keepCount <= 0 || !Directory.Exists(folder)) return; try { var prefix = $"backup_{safeName}_"; var obsolete = new DirectoryInfo(folder) .GetFiles("*.zip") .Where(f => f.Name.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) .OrderByDescending(f => f.LastWriteTime) .Skip(keepCount) .ToList(); foreach (var file in obsolete) { file.Delete(); _logger.LogInformation("Alte Sicherung entfernt: {Name}", file.Name); } } catch (Exception ex) { _logger.LogWarning(ex, "Rotation der Sicherungen fehlgeschlagen"); } } public void Dispose() { _timer.Stop(); _timer.Dispose(); } }