Files
ClawdDotNet/Services/BackupScheduler.cs
T
RichardandClaude Opus 4.8 b51cc29667 Backup-Oberflaeche im Settings-Tab
Der BackupService bekommt eine Bedienoberflaeche im neuen Unterreiter "Backup".

Aufbau als eigenes UserControl (UI/BackupPanel) statt direkt in frm_main.Designer.cs.
Diese Datei ist ueber 1250 Zeilen gewachsene Handarbeit; ein Fehler beim Bearbeiten
waere teuer. Eingehaengt wird das Panel mit vier Zeilen.

Drei Bereiche: Sicherung erstellen (Zielordner, Umgang mit Zugangsdaten, Umfang),
automatische Sicherung (Uhrzeit, Aufbewahrungszahl) und die Liste vorhandener
Sicherungen mit Wiederherstellen, Anzeigen und Loeschen. Die Liste liest das
Manifest jeder Datei und zeigt Instanz und ob Zugangsdaten enthalten sind;
unlesbare oder fremde Archive werden ausgegraut mitgezeigt, statt sie zu
verschweigen.

Zwei Entscheidungen praegen das Verhalten.

Die Passphrase wird nirgends gespeichert. Laege sie neben den Sicherungen, waere
die Verschluesselung wirkungslos. Sie muss deshalb bei jeder geschuetzten
Sicherung neu eingegeben und wiederholt werden, und vor dem Erstellen wird
ausdruecklich bestaetigt, dass sie notiert ist — ohne sie sind die Zugangsdaten
unwiederbringlich. Die Automatik sichert aus demselben Grund ohne Zugangsdaten.

Wiederhergestellt wird standardmaessig in einen NEUEN Ordner neben der Instanz,
nicht ueber die laufende. Diese haelt Chatverlaeufe im Speicher und die Datenbank
geoeffnet — ein Ueberschreiben im Betrieb wuerde teils sofort wieder ueberschrieben
und teils scheitern. Der Dialog benennt das.

Vor dem Schreiben laeuft immer ein Probelauf: Er prueft die Pruefsummen und zaehlt
die Konflikte, sodass ein beschaedigtes Archiv auffaellt und Ueberschreiben
bestaetigt wird, bevor etwas passiert.

BackupScheduler sichert taeglich zur eingestellten Uhrzeit und wendet die Rotation
an. Der Zeitpunkt wird bei jedem Durchlauf neu aus den Einstellungen gelesen,
damit eine Aenderung ohne Neustart greift.

Die Oberflaeche ist noch nicht optisch geprueft — die Dev-Instanz haelt einen
echten API-Key und wuerde beim Start Agenten ausfuehren.

385 Tests gruen (237 Core, 148 Tools).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-30 10:37:16 +02:00

148 lines
4.5 KiB
C#

using ClawdDotNet.Core.Backup;
using Microsoft.Extensions.Logging;
namespace ClawdDotNet.Services;
/// <summary>
/// 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.
/// </summary>
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;
/// <summary>Verhindert mehrere Sicherungen innerhalb derselben Minute.</summary>
private DateTime? _lastRun;
private bool _running;
public event Action<string>? 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);
}
/// <summary>Behält die neuesten Sicherungen dieser Instanz und entfernt den Rest.</summary>
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();
}
}