The file backend writes a blob by creating the destination file and streaming into it (internal/storage/file.go, PutWithProgress) with no temp-file-and-rename. If the upload is cut off mid-stream, a truncated object is left at the blob's final key.
On the next backup, uploadBlobIfNeeded (internal/snapshot/scanner.go) treats any object that Stat finds as a complete blob and skips re-upload, so the truncated object is recorded in the local index as a complete blob. The snapshot then reports success but is unrestorable, and only an explicit read-back reveals it.
Reproduction: internal/vaultik/fault_injection_test.go, TestFaultInjection_MidBlobFailureRetryStaysConsistent (skipped, points to this issue).
Scope: S3 PutObject is atomic, so this is specific to the file:// backend (and any other backend that writes non-atomically).
Fix direction (owner's call): write blobs atomically (temp + rename), and/or have uploadBlobIfNeeded verify the existing object's size before trusting it.
Model: opus-4-8
Found while adding fault-injection tests for https://git.eeqj.de/sneak/vaultik/issues/72.
The file backend writes a blob by creating the destination file and streaming into it (`internal/storage/file.go`, `PutWithProgress`) with no temp-file-and-rename. If the upload is cut off mid-stream, a truncated object is left at the blob's final key.
On the next backup, `uploadBlobIfNeeded` (`internal/snapshot/scanner.go`) treats any object that `Stat` finds as a complete blob and skips re-upload, so the truncated object is recorded in the local index as a complete blob. The snapshot then reports success but is unrestorable, and only an explicit read-back reveals it.
Reproduction: `internal/vaultik/fault_injection_test.go`, `TestFaultInjection_MidBlobFailureRetryStaysConsistent` (skipped, points to this issue).
Scope: S3 `PutObject` is atomic, so this is specific to the `file://` backend (and any other backend that writes non-atomically).
Fix direction (owner's call): write blobs atomically (temp + rename), and/or have `uploadBlobIfNeeded` verify the existing object's size before trusting it.
Model: opus-4-8
clawbot
added this to the 1.0.0 milestone 2026-09-21 09:47:34 +02:00
The file:// backend now writes each object to a temp file, fsyncs it, and renames it onto the key, so an interrupted upload leaves a stray .partial temp file (skipped by List/ListStream) rather than a truncated object the next run trusts as a complete blob. S3 is already atomic and unaffected. Includes tests that fail without the change.
model: claude-opus-4-8
Fixed in https://git.eeqj.de/sneak/vaultik/pulls/143.
The `file://` backend now writes each object to a temp file, fsyncs it, and renames it onto the key, so an interrupted upload leaves a stray `.partial` temp file (skipped by `List`/`ListStream`) rather than a truncated object the next run trusts as a complete blob. S3 is already atomic and unaffected. Includes tests that fail without the change.
model: claude-opus-4-8
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Found while adding fault-injection tests for #72.
The file backend writes a blob by creating the destination file and streaming into it (
internal/storage/file.go,PutWithProgress) with no temp-file-and-rename. If the upload is cut off mid-stream, a truncated object is left at the blob's final key.On the next backup,
uploadBlobIfNeeded(internal/snapshot/scanner.go) treats any object thatStatfinds as a complete blob and skips re-upload, so the truncated object is recorded in the local index as a complete blob. The snapshot then reports success but is unrestorable, and only an explicit read-back reveals it.Reproduction:
internal/vaultik/fault_injection_test.go,TestFaultInjection_MidBlobFailureRetryStaysConsistent(skipped, points to this issue).Scope: S3
PutObjectis atomic, so this is specific to thefile://backend (and any other backend that writes non-atomically).Fix direction (owner's call): write blobs atomically (temp + rename), and/or have
uploadBlobIfNeededverify the existing object's size before trusting it.Model: opus-4-8
Fixed in #143.
The
file://backend now writes each object to a temp file, fsyncs it, and renames it onto the key, so an interrupted upload leaves a stray.partialtemp file (skipped byList/ListStream) rather than a truncated object the next run trusts as a complete blob. S3 is already atomic and unaffected. Includes tests that fail without the change.model: claude-opus-4-8