From fb30c5f8dd7a0fc45b6aeac14cd76ecdebdcf26e Mon Sep 17 00:00:00 2001 From: Nick Craig-Wood Date: Wed, 29 Oct 2025 21:33:01 +0000 Subject: [PATCH] operations: add ReadAt method to ReOpen --- fs/operations/reopen.go | 30 +++++++++++++++++++ fs/operations/reopen_test.go | 56 ++++++++++++++++++++++++++++-------- 2 files changed, 74 insertions(+), 12 deletions(-) diff --git a/fs/operations/reopen.go b/fs/operations/reopen.go index 9b86c6da3..fcc2a6860 100644 --- a/fs/operations/reopen.go +++ b/fs/operations/reopen.go @@ -20,6 +20,7 @@ type AccountFn func(n int) error type ReOpen struct { ctx context.Context mu sync.Mutex // mutex to protect the below + readAtMu sync.Mutex // mutex to serialize the ReadAt calls src fs.Object // object to open baseOptions []fs.OpenOption // options to pass to initial open and where offset == 0 options []fs.OpenOption // option to pass on subsequent opens where offset != 0 @@ -232,6 +233,35 @@ func (h *ReOpen) Read(p []byte) (n int, err error) { return n, err } +// ReadAt reads len(p) bytes at absolute offset off without changing +// the read position. +// +// Note: operations are serialized; it won't behave like a truly +// concurrent ReaderAt. +func (h *ReOpen) ReadAt(p []byte, off int64) (n int, err error) { + h.readAtMu.Lock() + defer h.readAtMu.Unlock() + + // Save current position + cur, err := h.Seek(0, io.SeekCurrent) + if err != nil { + return 0, err + } + // Seek to requested offset + if _, err = h.Seek(off, io.SeekStart); err != nil { + return 0, err + } + // Restore position on exit + defer func() { + if _, seekErr := h.Seek(cur, io.SeekStart); seekErr != nil && err == nil { + err = seekErr + } + }() + + // Fill p fully unless EOF + return h.Read(p) +} + // Seek sets the offset for the next Read or Write to offset, interpreted // according to whence: SeekStart means relative to the start of the file, // SeekCurrent means relative to the current offset, and SeekEnd means relative diff --git a/fs/operations/reopen_test.go b/fs/operations/reopen_test.go index 891888c50..19f6ca38e 100644 --- a/fs/operations/reopen_test.go +++ b/fs/operations/reopen_test.go @@ -18,6 +18,7 @@ import ( // check interfaces var ( _ io.ReadSeekCloser = (*ReOpen)(nil) + _ io.ReaderAt = (*ReOpen)(nil) _ pool.DelayAccountinger = (*ReOpen)(nil) ) @@ -137,6 +138,16 @@ func TestReOpen(t *testing.T) { return rc, src, err } + // Reset the start after a seek, taking into account the offset + setWantStart := func(src *reOpenTestObject, x int64) { + src.wantStart = x + if rangeOption != nil { + src.wantStart += rangeOption.Start + } else if seekOption != nil { + src.wantStart += seekOption.Offset + } + } + t.Run("Basics", func(t *testing.T) { // open h, _, err := testReOpen(nil, 10) @@ -215,6 +226,37 @@ func TestReOpen(t *testing.T) { assert.Equal(t, errFileClosed, h.Close()) }) + t.Run("ReadAt", func(t *testing.T) { + // open + h, src, err := testReOpen([]int64{2, 1, 3}, 10) + assert.NoError(t, err) + + buf := make([]byte, 5) + + // Read at 0 + n, err := h.ReadAt(buf, 0) + require.NoError(t, err) + assert.Equal(t, 5, n) + assert.Equal(t, expectedRead[:n], buf[:n]) + + // Read at 1 + setWantStart(src, 1) + n, err = h.ReadAt(buf[:3], 1) + require.NoError(t, err) + assert.Equal(t, 3, n) + assert.Equal(t, expectedRead[1:n+1], buf[:n]) + + // check position unchanged + pos, err := h.Seek(0, io.SeekCurrent) + require.NoError(t, err) + assert.Equal(t, int64(0), pos) + + // check close + assert.NoError(t, h.Close()) + _, err = h.Seek(0, io.SeekCurrent) + assert.Equal(t, errFileClosed, err) + }) + t.Run("Seek", func(t *testing.T) { // open h, src, err := testReOpen([]int64{2, 1, 3}, 10) @@ -267,18 +309,8 @@ func TestReOpen(t *testing.T) { assert.Nil(t, err) assert.Equal(t, 2, int(pos)) - // Reset the start after a seek, taking into account the offset - setWantStart := func(x int64) { - src.wantStart = x - if rangeOption != nil { - src.wantStart += rangeOption.Start - } else if seekOption != nil { - src.wantStart += seekOption.Offset - } - } - // check read - setWantStart(2) + setWantStart(src, 2) n, err = h.Read(dst) assert.Nil(t, err) assert.Equal(t, 5, n) @@ -305,7 +337,7 @@ func TestReOpen(t *testing.T) { // check read dst = make([]byte, 3) - setWantStart(int64(len(expectedRead) - 3)) + setWantStart(src, int64(len(expectedRead)-3)) n, err = h.Read(dst) assert.Nil(t, err) assert.Equal(t, 3, n)