Replace a damaged catalog on the server instead of pinning it there

The catalog sync refuses to upload when it cannot read the server's copy,
because the upload is a read-modify-write and writing blind would discard
another device's collections. That is the right rule for a timeout, a
dropped connection or a newer schema — the remote is fine, only our view
of it failed.

A file SQLite calls malformed is not that. No device will ever read it
again, so refusing to write over it preserves nothing — and every client
declines in turn, pinning the damaged file in place for good. Collections
and people then stop crossing between devices on all of them at once,
each logging "catalog not pushed" on every pass. This library did exactly
that from 2026-09-07, on the desktop and on a freshly installed phone
alike, while 32 collections sat undelivered.

Now a copy that arrived whole and still will not open is set aside under
a dated name and replaced by ours. Whole is checked against the size the
server advertises: a truncated download will not open either, and on a
phone that is the far likelier story, so anything short — or any size the
listing cannot confirm — is treated as the transport failure it is and
the server's copy is left alone. A placeholder's size is not trusted for
the comparison, since it means nothing.

The report says when this happened, and the log line calls it "pushed
over a damaged copy" rather than folding it into an ordinary push: it is
the one push that discarded something.
This commit is contained in:
2026-09-13 19:01:00 +02:00
parent 43f70c4765
commit 693195fa96
3 changed files with 247 additions and 26 deletions
+8 -4
View File
@@ -4286,10 +4286,14 @@ fn start_derived_sync(window: &AppWindow, ctl: &Rc<LibraryController>) {
report.shards_uploaded,
report.shards_downloaded,
report.thumbnails_adopted,
if report.catalog_uploaded {
"pushed"
} else {
"not pushed"
// Replacing a damaged copy is said apart from an
// ordinary push: it is the one push that discarded
// something, and a log line that called it "pushed"
// would hide the only moment worth going back to.
match (report.catalog_uploaded, report.catalog_replaced) {
(true, true) => "pushed over a damaged copy",
(true, false) => "pushed",
_ => "not pushed",
},
if report.collections_gained > 0 {
format!(", {} collection(s) gained", report.collections_gained)