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
59 lines
1.9 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|