Problem
The Local FS storage backend opens existing objects with os.O_RDWR|os.O_CREATE but without os.O_TRUNC:
handle, err := os.OpenFile(o.path, os.O_RDWR|os.O_CREATE, 0o644)
fsObject.Put writes from offset zero, so overwriting an existing object with shorter content leaves the old tail in the file. The same behavior affects the uncompressed StoreFile path that uses getHandle(false).
Example:
existing object: abcdefgh
new object: xy
read result: xycdefgh
This makes the Local FS backend inconsistent with object-storage backends, where a put replaces the complete object. It can corrupt local snapshots, cache files, or metadata when an object is rewritten with a smaller payload.
Proposed fix
Use truncation for complete-object writes, either by adding os.O_TRUNC to the write handle or by explicitly truncating and resetting the offset before copying:
handle, err := os.OpenFile(o.path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o644)
if err != nil {
return nil, err
}
The truncating mode should only be used for complete replacement writes. Read, range-read, append, and multipart/random-offset paths should retain their existing semantics.
Tests
Please add two separate regression tests:
-
Put path:
- Write a longer payload to an object.
- Overwrite it with a shorter payload using
Put.
- Verify
Size equals the new payload length.
- Verify reading returns exactly the new payload, with no stale suffix.
-
Direct, uncompressed StoreFile path:
- Store a longer source file into an object.
- Overwrite it with a shorter source file using uncompressed
StoreFile.
- Verify
Size equals the new source file length.
- Verify reading returns exactly the new source content, with no stale suffix.
The compressed StoreFile path is a separate implementation and should not be conflated with this regression.
Relevant code:
packages/shared/pkg/storage/storage_fs.go
fsObject.Put
fsObject.StoreFile
fsObject.getHandle
Problem
The Local FS storage backend opens existing objects with
os.O_RDWR|os.O_CREATEbut withoutos.O_TRUNC:fsObject.Putwrites from offset zero, so overwriting an existing object with shorter content leaves the old tail in the file. The same behavior affects the uncompressedStoreFilepath that usesgetHandle(false).Example:
This makes the Local FS backend inconsistent with object-storage backends, where a put replaces the complete object. It can corrupt local snapshots, cache files, or metadata when an object is rewritten with a smaller payload.
Proposed fix
Use truncation for complete-object writes, either by adding
os.O_TRUNCto the write handle or by explicitly truncating and resetting the offset before copying:The truncating mode should only be used for complete replacement writes. Read, range-read, append, and multipart/random-offset paths should retain their existing semantics.
Tests
Please add two separate regression tests:
Putpath:Put.Sizeequals the new payload length.Direct, uncompressed
StoreFilepath:StoreFile.Sizeequals the new source file length.The compressed
StoreFilepath is a separate implementation and should not be conflated with this regression.Relevant code:
packages/shared/pkg/storage/storage_fs.gofsObject.PutfsObject.StoreFilefsObject.getHandle