fix(storage migration): overwrite target blob if mismatch

if target blob already exists, migration will check the blob
    if header is already with the targeted key (or no key if not ciphered)  no need write
    otherwise write the blob (that will convert any blob with no header into the correct version)
This commit is contained in:
Edouard Vanbelle
2026-08-02 14:13:55 +02:00
parent 9902a6f8fe
commit a10d3254bf
5 changed files with 169 additions and 80 deletions
@@ -237,6 +237,48 @@ impl EncryptedBlobBackend {
}
}
/// Smart-skip probe used by `storage_migration` (and potentially
/// `storage_rotate` if it ever gains a fast-path). Reads the
/// first [`HEADER_SIZE`] bytes of the on-disk blob and returns
/// `true` iff:
///
/// * the blob exists, AND
/// * its first bytes carry the v1 header (`OXCPT | 0001`), AND
/// * the header's `key_fp` matches this wrapper's head key
/// (encrypted-v1 → `head_key_fp`; plaintext-v1 → all-zero).
///
/// Any error (blob missing, short read, IO failure) returns
/// `Ok(false)` — the caller re-writes, which is always safe.
/// Only propagates errors that the caller genuinely cannot
/// distinguish from "blob absent" and needs to see.
///
/// Backend-agnostic: reads through the trait's
/// `get_blob_range_stream` on the *inner* backend (bypasses this
/// wrapper's decrypt so we see the raw on-disk header bytes).
/// Local pays one `pread` syscall; S3 pays one HEAD/GET with
/// `Range: bytes=0-14`; Azure the same.
pub async fn is_at_head_format(&self, hash: &str) -> Result<bool, DomainError> {
// Skip the probe entirely for backends that don't stream —
// `get_blob_range_stream` would fail on a legit-missing blob
// with `NotFound` which we want to translate to `Ok(false)`.
let stream = match self
.inner
.get_blob_range_stream(hash, 0, Some(HEADER_SIZE as u64))
.await
{
Ok(s) => s,
Err(_) => return Ok(false),
};
let raw = match collect_stream(stream).await {
Ok(b) => b,
Err(_) => return Ok(false),
};
if raw.len() < HEADER_SIZE {
return Ok(false);
}
Ok(BlobFormat::classify(&raw) == self.head_format())
}
/// Fetch, classify, and decrypt a blob in one round-trip. Used by
/// K3's `storage_rotate` per-blob step: it needs both the
/// plaintext (to re-encrypt under the head pair) AND the current