using MongoDB.Entities; using PrivaPub.ClientModels.User; namespace PrivaPub.Domain.Portability { // A persona's archive, made and restored in jobs (owner decision 2026-10-07): where each stands, one row per persona // (its ID is the persona's). The files live in /.personas//: the export, kept a week, and the upload // being imported. public class PersonaArchive : Entity { public string ExportState { get; set; } = "none"; public DateTime? ExportAskedAt { get; set; } public DateTime? ExportReadyAt { get; set; } public string ExportFile { get; set; } public long ExportBytes { get; set; } public string ExportError { get; set; } public string ImportState { get; set; } = "none"; public DateTime? ImportAskedAt { get; set; } public DateTime? ImportEndedAt { get; set; } public string ImportFile { get; set; } public List ImportParts { get; set; } = []; public long ImportDone { get; set; } public long ImportTotal { get; set; } public Dictionary ImportCounts { get; set; } = []; public string ImportError { get; set; } public string ImportFrom { get; set; } public bool ImportSameActor { get; set; } public bool StopAsked { get; set; } } public static class PersonaArchives { public static readonly TimeSpan Kept = TimeSpan.FromDays(7); /// Every part an import can take; the last three only when asked (they act, or tell others). public static readonly string[] Parts = ["posts", "profile", "follows", "blocks", "mutes", "domainblocks", "lists", "bookmarks", "filters", "tags", "notificationpolicy", "pins", "located", "scheduled", "likes"]; public static string Folder(string backupsRoot, string avatarId) => Path.Combine(backupsRoot, ".personas", avatarId); public static async Task Of(string avatarId, CancellationToken token) => await DB.Default.Find().MatchID(avatarId).ExecuteFirstAsync(token) ?? new PersonaArchive { ID = avatarId }; public static ViewPersonaArchive View(PersonaArchive archive) => new() { ExportState = archive.ExportState, ExportAskedAt = archive.ExportAskedAt, ExportReadyAt = archive.ExportReadyAt, ExportExpiresAt = archive.ExportReadyAt + Kept, ExportBytes = archive.ExportBytes, ExportError = archive.ExportError, ImportState = archive.ImportState, ImportAskedAt = archive.ImportAskedAt, ImportEndedAt = archive.ImportEndedAt, ImportParts = archive.ImportParts, ImportDone = archive.ImportDone, ImportTotal = archive.ImportTotal, ImportCounts = archive.ImportCounts, ImportError = archive.ImportError, ImportFrom = archive.ImportFrom, ImportSameActor = archive.ImportSameActor }; /// Exports older than a week go, as do imports' files left behind. public static async Task Forget(string backupsRoot, CancellationToken token) { var folder = Path.Combine(backupsRoot, ".personas"); if (!Directory.Exists(folder)) return; var before = DateTime.UtcNow - Kept; foreach (var file in Directory.EnumerateFiles(folder, "*", SearchOption.AllDirectories).Where(f => File.GetLastWriteTimeUtc(f) < before)) File.Delete(file); await DB.Default.Update().Match(a => a.ExportState == "ready" && a.ExportReadyAt < before) .Modify(a => a.ExportState, "none").Modify(a => a.ExportFile, null).ExecuteAsync(token); } } }