Files
SocialPub/PrivaPub/Infrastructure/Backup/RestoreMarker.cs
T
thepraandClaude Opus 5.5 fba57318fa A backup is restored at boot, and never undoes a protective act
PrivaPub admin restore <id> (and soon the administrator's page) checks the backup (same host, a format and newest
migration this build reads, every hash) and writes restore.json; the running service sees it within seconds and stops,
and the next start restores it in MaintenanceGate, before migrations, indexes and hosted services: a pre-restore backup
taken once, every collection dropped and imported raw with its indexes, the media the live directory lacks brought
back, then the protective merge from the pre-restore backup. Followers and follows are the live ones; blocks, mutes,
domain blocks, reserved names, tombstones, reports, filters and OAuth applications are the union; deletions win;
accounts made since become tombstones and local posts made since answer 410; every session ends.

Each attempt redoes everything; one refused before any change is abandoned and recorded, one failed midway exits 1 for
systemd to retry, and after three it exits 75, which the unit no longer restarts. Commands wait (exit 75) while a
restore is pending. RestoreRecord tells what happened (admin restore --status).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw
2026-10-07 11:55:17 +02:00

59 lines
1.9 KiB
C#

using System.Text.Json;
namespace PrivaPub.Infrastructure.Backup
{
// A restore asked for (by the administrator's page or the CLI) and carried out at the next boot, before anything
// else (Program, MaintenanceGate): restore.json in the backups' directory, written whole or not at all.
public sealed class RestoreMarker
{
public const string FileName = "restore.json";
public const int MaxAttempts = 3;
public string Backup { get; set; }//the backup restored
public string PreRestore { get; set; }//the backup taken just before, what the protective merge reads
public string State { get; set; } = "pending";//pending, then running
public string RequestedBy { get; set; }//who asked: a root's name, or "cli"
public int Attempts { get; set; }
public DateTime RequestedAt { get; set; } = DateTime.UtcNow;
public string Error { get; set; }
public static string PathIn(string backupsRoot) => Path.Combine(backupsRoot, FileName);
public static RestoreMarker Read(string backupsRoot)
{
var path = PathIn(backupsRoot);
if (!File.Exists(path))
return default;
try
{
return JsonSerializer.Deserialize<RestoreMarker>(File.ReadAllText(path));
}
catch (JsonException)
{
//never written half (Write is atomic): someone's hand; the server won't guess what was meant
return new RestoreMarker { State = "unreadable", Attempts = MaxAttempts, Error = $"{FileName} can't be read" };
}
}
public void Write(string backupsRoot)
{
Directory.CreateDirectory(backupsRoot);
var path = PathIn(backupsRoot);
var part = path + ".part";
using (var file = new FileStream(part, FileMode.Create, FileAccess.Write))
{
JsonSerializer.Serialize(file, this);
file.Flush(flushToDisk: true);
}
File.Move(part, path, overwrite: true);
}
public static void Clear(string backupsRoot)
{
var path = PathIn(backupsRoot);
if (File.Exists(path))
File.Delete(path);
}
}
}