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();
}
}