Files
SocialPub/PrivaPub/Infrastructure/Backup/RestoreRecord.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

63 lines
2.8 KiB
C#

using MongoDB.Entities;
namespace PrivaPub.Infrastructure.Backup
{
// What a restore did (or why it was abandoned), kept for the administrator's page and for the followers' grace: for
// FollowersGrace after a restore the followers' digests (FEP-8fcf) are neither sent nor acted on to undo a follow, since
// what the restore lost of either side is not the other's fault.
public class RestoreRecord : Entity
{
public static readonly TimeSpan FollowersGrace = TimeSpan.FromDays(14);
public string Backup { get; set; }
public DateTime BackupCreatedAt { get; set; }
public string PreRestore { get; set; }
public string RequestedBy { get; set; }
public DateTime RequestedAt { get; set; }
public DateTime RestoredAt { get; set; } = DateTime.UtcNow;
public int Attempts { get; set; }
public bool Abandoned { get; set; }//nothing was changed: the server booted as it was
public string Error { get; set; }
public RestoreReport Report { get; set; } = new();
static (DateTime Until, DateTime Read) _grace;
/// <summary>Whether a restore ended less than FollowersGrace ago (read at most once a minute).</summary>
public static async Task<bool> InFollowersGrace(CancellationToken token)
{
var now = DateTime.UtcNow;
var cached = _grace;
if (now - cached.Read > TimeSpan.FromMinutes(1))
{
var last = await DB.Default.Find<RestoreRecord>().Match(r => !r.Abandoned).Sort(r => r.RestoredAt, Order.Descending).ExecuteFirstAsync(token);
cached = (last == default ? DateTime.MinValue : last.RestoredAt + FollowersGrace, now);
_grace = cached;
}
return now < cached.Until;
}
/// <summary>Forgets what was read, so a restore made in this process counts at once.</summary>
public static void Forget() => _grace = default;
}
public class RestoreReport
{
public int Collections { get; set; }
public long Documents { get; set; }
public int CollectionsDropped { get; set; }//live collections the backup had no documents in
public int MediaRestored { get; set; }//files the live directory lacked, brought back
public int MediaMissing { get; set; }
public int RootsDeleted { get; set; }//deleted since the backup, deleted again
public int PersonasDeleted { get; set; }
public int GroupsDeleted { get; set; }
public int PostsDeleted { get; set; }
public List<string> RootsTombstoned { get; set; } = [];//made after the backup: deleted, their names kept
public List<string> PersonasTombstoned { get; set; } = [];
public List<string> GroupsTombstoned { get; set; } = [];
public int PostsGone { get; set; }//local posts made after the backup: 410 from now on
public int MediaTrashed { get; set; }
public int ProtectiveKept { get; set; }//blocks, mutes, domain blocks, reserved names, reports, filters, deletions kept from since
public int SessionsEnded { get; set; }
}
}