Fail closed on unreadable manifests instead of losing blobs (closes #157)
Prune learned which blobs are in use by reading every snapshot's manifest, but merely logged and skipped one it could not download or decode. Blobs referenced only by that snapshot then looked unreferenced and were deleted, with a zero exit -- and snapshot create --prune runs this unattended. collectReferencedBlobs now errors, naming the remote key, so prune deletes nothing and exits non-zero. Manifest generation likewise skipped a blob whose lookup failed or was missing, yielding a manifest short of what the snapshot needs; it now fails. Deep verify only warned when the manifest omitted a database blob; it now fails on any divergence. Docs corrected. Model: opus-4-8
This commit was merged in pull request #180.
This commit is contained in:
@@ -809,6 +809,11 @@ func (sm *SnapshotManager) copyFile(src, dst string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// errBlobMissingFromDatabase means a snapshot references a blob that is
|
||||
// absent from the blobs table, so a complete manifest cannot be built.
|
||||
var errBlobMissingFromDatabase = errors.New(
|
||||
"blob referenced by snapshot is not in the database")
|
||||
|
||||
// generateBlobManifest creates a compressed JSON list of all blobs in the snapshot
|
||||
func (sm *SnapshotManager) generateBlobManifest(
|
||||
ctx context.Context, dbPath string, snapshotID string,
|
||||
@@ -839,20 +844,26 @@ func (sm *SnapshotManager) generateBlobManifest(
|
||||
totalCompressedSize := int64(0)
|
||||
|
||||
for _, hash := range blobHashes {
|
||||
// Every blob the snapshot references must appear in the manifest.
|
||||
// Prune consults only the manifest to decide what is still in use,
|
||||
// so silently dropping a blob here would let a later prune delete
|
||||
// it while this snapshot still needs it. A lookup failure or a
|
||||
// missing blob row therefore fails manifest generation.
|
||||
blob, err := repos.Blobs.GetByHash(ctx, hash)
|
||||
if err != nil {
|
||||
log.Warn("Failed to get blob details", "hash", hash, "error", err)
|
||||
|
||||
continue
|
||||
return nil, fmt.Errorf("getting blob details for %s: %w", hash, err)
|
||||
}
|
||||
|
||||
if blob != nil {
|
||||
blobs = append(blobs, BlobInfo{
|
||||
Hash: hash,
|
||||
CompressedSize: blob.CompressedSize,
|
||||
})
|
||||
totalCompressedSize += blob.CompressedSize
|
||||
if blob == nil {
|
||||
return nil, fmt.Errorf("%w: blob %s, snapshot %s",
|
||||
errBlobMissingFromDatabase, hash, snapshotID)
|
||||
}
|
||||
|
||||
blobs = append(blobs, BlobInfo{
|
||||
Hash: hash,
|
||||
CompressedSize: blob.CompressedSize,
|
||||
})
|
||||
totalCompressedSize += blob.CompressedSize
|
||||
}
|
||||
|
||||
// Create manifest. SnapshotID in the unencrypted manifest is the
|
||||
|
||||
Reference in New Issue
Block a user