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
63 lines
2.8 KiB
C#
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; }
|
|
}
|
|
}
|