Add the media store: content-addressed blobs, SQLite index, showcase

First half of replacing the stub text domain with a media collector. Nothing
references this yet - the store is standalone and fully tested before anything
depends on it.

Blobs are addressed by SHA-256 and sharded two levels deep, so the same picture
re-uploaded at a dozen addresses costs one file. Downloads stage in a sibling
temp directory on the same volume and are promoted by rename, which is what
keeps blobs/ free of truncated files: a crash leaves a stray .part that the next
startup sweeps, never a half-image indistinguishable from a real one.

The SQLite index holds provenance separately from content, so purging one source
leaves blobs another source still references - that is what ref_count buys, and
it is recomputed rather than incremented because the item upsert can replace a
row pointing at a different blob. The seen_url journal deliberately outlives a
purge: without that, the next run downloads again exactly what the user just
deleted. Terminal outcomes are split from retryable ones so a flaky network does
not permanently lose content.

The showcase gives every item a dated, named path via hard links - a second name
for one file, not a second file. Hard links are a filesystem privilege rather
than a guarantee, so it degrades to copying and records which it achieved; the
UI has to be able to admit that. Names suggested by the origin are treated as
hostile: only the last path segment survives, Windows device names are pushed
aside, and the extension comes from the sniffed kind, never from the remote.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Leonid Pershin
2026-08-13 20:46:21 +03:00
co-authored by Claude Opus 5
parent 44fb0d3a5f
commit 181f974a37
17 changed files with 2556 additions and 1 deletions
+9
View File
@@ -10,6 +10,9 @@
<ReactiveUIVersion>24.1.0</ReactiveUIVersion>
<ReactiveUIPrimitivesVersion>7.1.1</ReactiveUIPrimitivesVersion>
<MicrosoftExtensionsVersion>10.0.11</MicrosoftExtensionsVersion>
<!-- Ships on the EF Core release line, not the Extensions one. They happen to match today;
they will not always, so this is deliberately its own knob. -->
<MicrosoftDataSqliteVersion>10.0.11</MicrosoftDataSqliteVersion>
</PropertyGroup>
<ItemGroup Label="Avalonia">
@@ -46,6 +49,12 @@
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="$(MicrosoftExtensionsVersion)" />
</ItemGroup>
<ItemGroup Label="Data">
<!-- Not Microsoft.Data.Sqlite.Core: we want the bundled e_sqlite3 native, so nothing extra
has to be shipped per runtime identifier. -->
<PackageVersion Include="Microsoft.Data.Sqlite" Version="$(MicrosoftDataSqliteVersion)" />
</ItemGroup>
<ItemGroup Label="Logging">
<PackageVersion Include="Serilog" Version="4.4.0" />
<PackageVersion Include="Serilog.Extensions.Logging" Version="10.0.0" />