Files
SocialPub/PrivaPub/Infrastructure/Backup/ArchiveManifest.cs
T
thepraandClaude Opus 5.5 12bb75809f The server backs itself up: every collection byte for byte, the media linked
A backup is a directory <stamp>-<kind> under Backups:Root (/var/lib/privapub/backups, 2770, files 0640), written as
.partial and renamed once whole: a manifest (host, build, newest migration, each collection's count, size, sha256 and
indexes, what was left out and why, the media list), each collection as gzipped canonical Extended JSON read raw, and
hard links to the files of untrashed media rows (copies where a link can't be made). On a replica set every collection
is read in one snapshot session. Never in a backup: the statistics salt, jobs, recovery codes, sessions, the
maintenance lock and the configuration's copy with its SMTP password.

One backup or restore at a time (MaintenanceLock, a heartbeat document), and the janitor purges nothing meanwhile.
BackupScheduler backs up nightly at 03:30 UTC (or at once after missing a night); rotation keeps 7 daily, 4 weekly,
3 pre-deploy and 3 pre-restore backups. CLI: admin backup [--kind] [--db-only], admin backups, admin backup verify;
these run before migrations, so the deploy's own pre-deploy backup, which replaces mongodump, is of the database as the
live build left it. EntityMaps.Warm runs once under a lock, since test hosts now boot side by side.

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

66 lines
2.5 KiB
C#

using System.Text.Json;
using System.Text.Json.Serialization;
namespace PrivaPub.Infrastructure.Backup
{
// What a backup holds (manifest.json at its root): enough to check it is whole, to restore it on this host only, and to
// rebuild what Mongo had (each collection's indexes).
public sealed class ArchiveManifest
{
public const int CurrentFormat = 1;
public const string FileName = "manifest.json";
public int Format { get; set; } = CurrentFormat;
public string Kind { get; set; }//nightly, manual, pre-deploy, pre-restore, uploaded
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public string Host { get; set; }//BackendBaseAddress: URIs and media URLs are stored whole, so only this host can restore it
public string AppCommit { get; set; }
public string AppRef { get; set; }
public string NewestMigration { get; set; }
public int MigrationNumber { get; set; }
public bool Consistent { get; set; }//read at one instant (a snapshot session on a replica set)
public bool DbOnly { get; set; }//no media files, only their list (the deploy's, which can't link www-data's files)
public List<CollectionEntry> Collections { get; set; } = [];
public List<ExcludedEntry> Excluded { get; set; } = [];
public MediaEntry Media { get; set; } = new();
public sealed class CollectionEntry
{
public string Name { get; set; }
public long Count { get; set; }
public long Bytes { get; set; }
public string Sha256 { get; set; }
public List<string> Indexes { get; set; } = [];//each as canonical Extended JSON
}
public sealed class ExcludedEntry
{
public string Name { get; set; }
public string Why { get; set; }
}
public sealed class MediaEntry
{
public int Files { get; set; }
public long Bytes { get; set; }
public int Missing { get; set; }
public List<string> List { get; set; } = [];
}
static readonly JsonSerializerOptions Json = new() { WriteIndented = true, PropertyNamingPolicy = JsonNamingPolicy.CamelCase, DefaultIgnoreCondition = JsonIgnoreCondition.Never };
public static ArchiveManifest Read(string directory)
{
var path = Path.Combine(directory, FileName);
return File.Exists(path) ? JsonSerializer.Deserialize<ArchiveManifest>(File.ReadAllText(path), Json) : default;
}
public void Write(string directory)
{
using var file = new FileStream(Path.Combine(directory, FileName), FileMode.Create, FileAccess.Write);
JsonSerializer.Serialize(file, this, Json);
file.Flush(flushToDisk: true);
}
}
}