Backup und Wiederherstellung einer Instanz
Gesichert wird alles, was sich nicht wiederherstellen laesst: Identity und Soul, Konfigurationen, Arbeitsverzeichnisse, Chatverlaeufe und vor allem die Datenbank mit dem Langzeitgedaechtnis. Protokolle bleiben standardmaessig aussen vor. Zwei Punkte waren dabei nicht offensichtlich. Die Datenbank darf nicht einfach kopiert werden. Mit WAL stehen die juengsten Aenderungen in der Begleitdatei, nicht in der Hauptdatei — eine reine Kopie waere veraltet oder in sich widersprueckhlich. VACUUM INTO erzeugt dagegen im laufenden Betrieb eine geschlossene, konsistente Kopie; die WAL-Begleitdateien gehoeren dann nicht mehr ins Archiv. Zugangsdaten sind seit S7 mit DPAPI geschuetzt und damit an Benutzer und Rechner gebunden. In einer Sicherung waeren sie genau dann unbrauchbar, wenn man sie braucht — bei einem defekten Rechner. PassphraseProtector schluesselt sie deshalb beim Sichern auf eine Passphrase um (PBKDF2 mit 210.000 Runden, AES-GCM) und beim Wiederherstellen zurueck auf DPAPI des Zielrechners. Alternativ laesst sich eine Sicherung ganz ohne Zugangsdaten erstellen; sie ist dann gefahrlos ablegbar, die Wiederherstellung aber unvollstaendig. Das Umschluesseln arbeitet auf dem JSON-Baum statt ueber die typisierten Konfigurationsklassen. Beim Deserialisieren und erneuten Serialisieren gingen unbekannte Felder verloren — eine Sicherung darf aber nichts wegwerfen, nur weil eine Programmfassung ein Feld nicht kennt. Ein Test haelt das fest. Weitere Eigenschaften: Manifest mit Pruefsummen je Datei, sodass ein veraendertes Archiv auffaellt, bevor etwas ueberschrieben wird. Vorschau-Modus. Vorhandene Dateien werden ohne ausdrueckliche Zustimmung nicht ueberschrieben. Eintraege, die aus dem Zielverzeichnis herauszeigen, werden abgelehnt. Der wichtigste Test ist der vollstaendige Rundlauf: Instanz aufbauen, sichern, in ein leeres Verzeichnis wiederherstellen und pruefen, dass Persoenlichkeit, Arbeitsstand, Gedaechtnis und nutzbare Zugangsdaten zurueck sind. Ein ungepruefte Wiederherstellung ist kein Backup, sondern eine Vermutung. 385 Tests gruen (237 Core, 148 Tools). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
1157d28588
commit
b55512e734
@@ -0,0 +1,92 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace ClawdDotNet.Core.Backup;
|
||||
|
||||
/// <summary>Wie mit Zugangsdaten in der Sicherung verfahren wird.</summary>
|
||||
public enum SecretMode
|
||||
{
|
||||
/// <summary>
|
||||
/// Zugangsdaten werden entfernt. Die Sicherung ist gefahrlos ablegbar, die
|
||||
/// Wiederherstellung aber unvollständig — Schlüssel und Passwörter müssen
|
||||
/// danach neu eingetragen werden.
|
||||
/// </summary>
|
||||
Exclude,
|
||||
|
||||
/// <summary>
|
||||
/// Zugangsdaten werden mit einer Passphrase geschützt. Nur so überstehen sie
|
||||
/// einen Rechner- oder Benutzerwechsel.
|
||||
/// </summary>
|
||||
Passphrase
|
||||
}
|
||||
|
||||
public sealed record BackupOptions
|
||||
{
|
||||
public SecretMode Secrets { get; init; } = SecretMode.Exclude;
|
||||
|
||||
/// <summary>Erforderlich bei <see cref="SecretMode.Passphrase"/>.</summary>
|
||||
public string? Passphrase { get; init; }
|
||||
|
||||
/// <summary>Protokolle sind groß und selten nötig.</summary>
|
||||
public bool IncludeLogs { get; init; }
|
||||
|
||||
/// <summary>Chatverläufe gehören zum Arbeitsstand, können aber umfangreich sein.</summary>
|
||||
public bool IncludeChatHistory { get; init; } = true;
|
||||
}
|
||||
|
||||
public sealed record RestoreOptions
|
||||
{
|
||||
public string? Passphrase { get; init; }
|
||||
|
||||
/// <summary>Nur prüfen und berichten, nichts schreiben.</summary>
|
||||
public bool DryRun { get; init; }
|
||||
|
||||
/// <summary>Vorhandene Dateien im Ziel überschreiben.</summary>
|
||||
public bool Overwrite { get; init; }
|
||||
}
|
||||
|
||||
public sealed record BackupEntry(
|
||||
[property: JsonPropertyName("path")] string Path,
|
||||
[property: JsonPropertyName("size")] long Size,
|
||||
[property: JsonPropertyName("sha256")] string Sha256);
|
||||
|
||||
public sealed record BackupManifest
|
||||
{
|
||||
/// <summary>Erlaubt es späteren Fassungen, ältere Sicherungen zu erkennen.</summary>
|
||||
[JsonPropertyName("formatVersion")]
|
||||
public int FormatVersion { get; init; } = 1;
|
||||
|
||||
[JsonPropertyName("createdAt")]
|
||||
public DateTime CreatedAt { get; init; }
|
||||
|
||||
[JsonPropertyName("instanceId")]
|
||||
public string InstanceId { get; init; } = "";
|
||||
|
||||
[JsonPropertyName("instanceName")]
|
||||
public string InstanceName { get; init; } = "";
|
||||
|
||||
[JsonPropertyName("secrets")]
|
||||
public string Secrets { get; init; } = nameof(SecretMode.Exclude);
|
||||
|
||||
/// <summary>Wie viele Zugangsdaten enthalten bzw. entfernt wurden.</summary>
|
||||
[JsonPropertyName("secretCount")]
|
||||
public int SecretCount { get; init; }
|
||||
|
||||
[JsonPropertyName("files")]
|
||||
public List<BackupEntry> Files { get; init; } = new();
|
||||
|
||||
[JsonIgnore]
|
||||
public bool HasSecrets => Secrets == nameof(SecretMode.Passphrase);
|
||||
}
|
||||
|
||||
public sealed record BackupResult(string ZipPath, BackupManifest Manifest, long SizeBytes);
|
||||
|
||||
public sealed record RestoreResult(
|
||||
IReadOnlyList<string> Written,
|
||||
IReadOnlyList<string> Skipped,
|
||||
IReadOnlyList<string> WouldOverwrite)
|
||||
{
|
||||
public bool HasConflicts => WouldOverwrite.Count > 0;
|
||||
}
|
||||
|
||||
public sealed class BackupException(string message, Exception? inner = null)
|
||||
: Exception(message, inner);
|
||||
Reference in New Issue
Block a user