Verify the catalog snapshot before it is sent, and after it lands
Two checks around the upload, both cheap next to what they prevent. Before: the snapshot is quick_checked before it leaves. It is the copy every other device merges from, and a damaged one costs each of them a download, a failed merge and a refusal to push. After: the staged upload's size on the server is compared to the bytes sent before it is rotated into place. A chunked upload is assembled server-side, and an assembly that goes wrong is a file of plausible size no device can open — caught here, on the device that caused it, for one listing; otherwise on every other device, after the fact. A mismatch, or a size the server will not confirm, discards the upload and leaves the current copy and its generations untouched.
This commit is contained in:
@@ -54,9 +54,31 @@ pub fn checkpoint(conn: &Connection) -> Result<(), CatalogError> {
|
||||
pub fn snapshot_for_upload(conn: &Connection, dest: &Path) -> Result<(), CatalogError> {
|
||||
let out = copy_to(conn, dest)?;
|
||||
strip_face_crops(&out)?;
|
||||
verify_snapshot(&out)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// TRACES: NFR-R2
|
||||
/// Refuse to hand over a snapshot that will not pass `quick_check`.
|
||||
///
|
||||
/// The upload is the copy every other device merges from, and a damaged one
|
||||
/// costs far more than the check: each device downloads it, fails, and — for
|
||||
/// a week, once — declines to push over it. `quick_check` reads every page
|
||||
/// but skips index verification, which is the affordable version of "is this
|
||||
/// a database" on a 40 MB file that has just been written and is still in the
|
||||
/// page cache. A failure here is [`CatalogError::Corrupt`], the same thing a
|
||||
/// receiving device would have said, so the sync reports it the same way.
|
||||
fn verify_snapshot(snapshot: &Connection) -> Result<(), CatalogError> {
|
||||
let verdict: String = snapshot.query_row("PRAGMA quick_check", [], |r| r.get(0))?;
|
||||
if verdict == "ok" {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(CatalogError::Corrupt {
|
||||
detail: format!("the snapshot for upload failed quick_check: {verdict}"),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Checkpoint, then copy the whole database to `dest`, and hand back the
|
||||
/// connection to the copy.
|
||||
///
|
||||
|
||||
Reference in New Issue
Block a user