vfs: preserve shrinking truncate on uncached files

If a file that hadn't been read was shrunk with Truncate, the cache
file was cut to the new size but the kept part of the file still had
to be fetched on close. The downloader wrote the whole remote object
into the cache file, which grew it back to its old size, so the upload
replaced the object with the original contents.

Stop the downloader writing past the end of the item, and add tests at
the vfscache and vfs levels.

Fixes #10047
This commit is contained in:
47th authored and GitHub committed 2026-10-15 16:36:28 +01:00
1 parent 36febacabd
commit 78be7079b9
3 files changed
+65

No files matched your search

+33
View File
@@ -631,6 +631,39 @@ func testRWFileHandleOpenTest(t *testing.T, vfs *VFS, test *openTest) {
assert.Equal(t, test.contents, contents)
}
// Shrinking an uncached file with Truncate (what a mount does for
// truncate(2) with no open handle) must stick after the upload.
func TestRWFileTruncateShrinkUncached(t *testing.T) {
for _, cacheMode := range []vfscommon.CacheMode{vfscommon.CacheModeWrites, vfscommon.CacheModeFull} {
t.Run(cacheMode.String(), func(t *testing.T) {
opt := vfscommon.Opt
opt.CacheMode = cacheMode
opt.WriteBack = writeBackDelay
r, vfs := newTestVFSOpt(t, &opt)
file1 := r.WriteObject(context.Background(), "file1", "0123456789abcdef", t1)
r.CheckRemoteItems(t, file1)
node, err := vfs.Stat("file1")
require.NoError(t, err)
require.NoError(t, node.(*File).Truncate(5))
vfs.WaitForWriters(waitForWritersDelay)
obj, err := r.Fremote.NewObject(context.Background(), "file1")
require.NoError(t, err)
assert.Equal(t, int64(5), obj.Size())
rc, err := obj.Open(context.Background())
require.NoError(t, err)
contents, err := io.ReadAll(rc)
require.NoError(t, err)
require.NoError(t, rc.Close())
assert.Equal(t, "01234", string(contents))
})
}
}
func TestRWFileHandleOpenTests(t *testing.T) {
for _, cacheMode := range []vfscommon.CacheMode{vfscommon.CacheModeWrites, vfscommon.CacheModeFull} {
t.Run(cacheMode.String(), func(t *testing.T) {
+18
View File
@@ -1468,11 +1468,25 @@ func (item *Item) WriteAt(b []byte, off int64) (n int, err error) {
//
// This is used by the downloader to write bytes to the file.
//
// Bytes past the end of the item are not written; they are counted
// as processed and skipped.
//
// It returns n the total bytes processed and skipped the number of
// bytes which were processed but not actually written to the file.
func (item *Item) WriteAtNoOverwrite(b []byte, off int64) (n int, skipped int, err error) {
item.mu.Lock()
// Don't write past the end of the item. If the file was shrunk
// with Truncate while the remote object is still longer, the
// downloader would otherwise grow the cache file back to the
// size of the remote object.
beyond := 0
if end := off + int64(len(b)); end > item.info.Size {
keep := max(item.info.Size-off, 0)
beyond = len(b) - int(keep)
b = b[:keep]
}
var (
// Range we wish to write
r = ranges.Range{Pos: off, Size: int64(len(b))}
@@ -1514,6 +1528,10 @@ func (item *Item) WriteAtNoOverwrite(b []byte, off int64) (n int, skipped int, e
}
}
item.mu.Unlock()
if err == nil {
n += beyond
skipped += beyond
}
return n, skipped, err
}
+14
View File
@@ -181,6 +181,20 @@ func TestItemTruncateExisting(t *testing.T) {
checkObject(t, r, "existing", contents[:40]+zeroes[:20])
}
func TestItemTruncateShrinkUncached(t *testing.T) {
r, c := newItemTestCache(t)
contents, obj, item := newFile(t, r, c, "existing")
// Shrink a file which hasn't been read so the downloader has to
// fetch the kept part of the file on close.
require.NoError(t, item.Open(obj))
require.NoError(t, item.Truncate(40))
require.NoError(t, item.Close(nil))
checkObject(t, r, "existing", contents[:40])
}
func TestItemReadAt(t *testing.T) {
r, c := newItemTestCache(t)