A file lives exactly as long as something holds it
Deleting a post, editing media out, replacing an avatar or a header, and removing a whole root deleted no file: every one stayed on disk and publicly served from /media/files with a year-long immutable cache, its row orphaned. Now every upload is a row, profile pictures too (Kind avatar or header, ProfileOfAvatarId), and each of those acts trashes what it held, as does an upload never posted for a day and a dropped scheduled post. A trashed row is marked in one conditional update (an upload attached meanwhile is left alone), its files move into media-trash, beside the media root and outside what /media/files serves, and the janitor deletes them a day later. Nothing is deleted for looking unused. Along the way: a profile picture that isn't an image, or can't be read, answers 422 instead of being silently ignored with a 200; a removed root's scheduled posts are dropped, so nothing of it publishes later; media rows get indexes (they had none), and the janitor's first pass comes five minutes after boot instead of an hour. `PrivaPub admin media audit [--fix]` compares the disk with the database. With --fix (as www-data) it gives the pictures personas show today a row, and trashes media of deleted posts or personas, rows whose files are missing, and files nothing holds: the leftovers of every deletion until now. MediaLifecycleTests covers each act, that the trash is never served, and the audit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw
This commit is contained in:
1 parent
52d201eee9
commit
bb680e8cb8
15 files changed
+462
-36
No files matched your search
@@ -22,6 +22,8 @@ namespace PrivaPub.Infrastructure.Cli
|
||||
usage: PrivaPub admin promote|demote <root>
|
||||
PrivaPub admin create-root <login> [--admin] the password is read from standard input
|
||||
PrivaPub admin smoke <persona> prints "<login> <password>" for the deploy's signed-in check
|
||||
PrivaPub admin media audit [--fix] media files against what holds them; --fix (as www-data) trashes
|
||||
what nothing holds and gives today's pictures their rows
|
||||
""";
|
||||
|
||||
public static async Task<int> Run(string[] args, IServiceProvider services, TextReader input = default, TextWriter output = default)
|
||||
@@ -36,12 +38,29 @@ namespace PrivaPub.Infrastructure.Cli
|
||||
return await CreateRoot(services, login, input.ReadLine(), flags.Contains("--admin"), output);
|
||||
case ["smoke", var persona]:
|
||||
return await Smoke(services, persona, output);
|
||||
case ["media", "audit", .. var flags] when flags.All(f => f == "--fix"):
|
||||
return await AuditMedia(services, flags.Contains("--fix"), output);
|
||||
default:
|
||||
Console.Error.WriteLine(Usage);
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
|
||||
static async Task<int> AuditMedia(IServiceProvider services, bool fix, TextWriter output)
|
||||
{
|
||||
var report = await Domain.Media.MediaAudit.Run(services.GetRequiredService<Domain.Media.IMediaService>(), fix, CancellationToken.None);
|
||||
output.WriteLine($"{report.Files} files served, {report.Held} rows holding media");
|
||||
output.WriteLine($"{report.Adopted} pictures personas show without a row{(fix ? ": given one" : string.Empty)}");
|
||||
output.WriteLine($"{report.OfDeleted} media of deleted posts or personas{(fix ? ": trashed" : string.Empty)}");
|
||||
output.WriteLine($"{report.MissingFiles} rows whose files are missing{(fix ? ": trashed" : string.Empty)}");
|
||||
output.WriteLine($"{report.Unheld} files nothing holds{(fix ? ": trashed" : string.Empty)}");
|
||||
foreach (var example in report.Examples)
|
||||
output.WriteLine($" {example}");
|
||||
if (!fix && report.Adopted + report.OfDeleted + report.MissingFiles + report.Unheld > 0)
|
||||
output.WriteLine("run again with --fix, as www-data, to apply this; trashed files are deleted after a day");
|
||||
return 0;
|
||||
}
|
||||
|
||||
static async Task<int> Promote(bool promote, string userName, TextWriter output)
|
||||
{
|
||||
userName = userName.ToLowerInvariant();
|
||||
|
||||
@@ -32,6 +32,13 @@ namespace PrivaPub.Infrastructure.Data
|
||||
await Plain<Post>(token, p => p.AnsweringToPostId);
|
||||
await DB.Default.Index<Post>().Key(p => p.Geo, KeyType.Geo2DSphere).CreateAsync(token);
|
||||
|
||||
// media: a post's, a persona's, what a scheduled post holds, the janitor's never-posted uploads and its trash
|
||||
await Plain<Models.Media.MediaAttachment>(token, m => m.PostId, m => m.ScheduledStatusId, m => m.CreatedAt);
|
||||
await Plain<Models.Media.MediaAttachment>(token, m => m.OwnerAvatarId);
|
||||
await Plain<Models.Media.MediaAttachment>(token, m => m.ScheduledStatusId);
|
||||
await Plain<Models.Media.MediaAttachment>(token, m => m.ProfileOfAvatarId);
|
||||
await Plain<Models.Media.MediaAttachment>(token, m => m.TrashedAt);
|
||||
|
||||
await Unique<DmPost>(p => p.ObjectURI, Builders<DmPost>.Filter.Gt(p => p.ObjectURI, ""), token);
|
||||
await Plain<DmPost>(token, p => p.GroupId, p => p.ID);
|
||||
|
||||
|
||||
Reference in new issue
Block a user