Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 39 additions & 8 deletions utils/sparse_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ import (
"golang.org/x/sys/unix"
)

// SparseCopy copies src to dst preserving sparsity via SEEK_HOLE/SEEK_DATA; dst is truncated to src size and only data segments are written.
const copyBlockSize = 1 << 20

// SparseCopy copies src to dst preserving sparsity via SEEK_HOLE/SEEK_DATA, and from FUSE or NFS also leaves all-zero 1 MiB blocks as holes; dst is truncated to src size.
func SparseCopy(dst, src string, mode SyncMode) error {
return CopyWithCleanup(dst, src, func(srcFile, dstFile *os.File) error {
fi, err := srcFile.Stat()
Expand All @@ -28,14 +30,12 @@ func SparseCopy(dst, src string, mode SyncMode) error {
if err != nil {
return err
}
var zeroSkipBuf []byte
if seekBlind(srcFile) {
zeroSkipBuf = make([]byte, copyBlockSize)
}
for _, seg := range segments {
if _, err := srcFile.Seek(seg.Offset, io.SeekStart); err != nil {
return fmt.Errorf("seek src to %d: %w", seg.Offset, err)
}
if _, err := dstFile.Seek(seg.Offset, io.SeekStart); err != nil {
return fmt.Errorf("seek dst to %d: %w", seg.Offset, err)
}
if _, err := io.CopyN(dstFile, srcFile, seg.Length); err != nil {
if err := copySegment(dstFile, srcFile, seg, zeroSkipBuf); err != nil {
return fmt.Errorf("copy segment at %d len %d: %w", seg.Offset, seg.Length, err)
}
}
Expand Down Expand Up @@ -77,3 +77,34 @@ func scanDataSegments(fd int, size int64) ([]sparseSegment, error) {
}
return segments, nil
}

func copySegment(dst, src *os.File, seg sparseSegment, zeroSkipBuf []byte) error {
if zeroSkipBuf == nil {
if _, err := src.Seek(seg.Offset, io.SeekStart); err != nil {
return err
}
if _, err := dst.Seek(seg.Offset, io.SeekStart); err != nil {
return err
}
_, err := io.CopyN(dst, src, seg.Length)
return err
}
for pos, end := seg.Offset, seg.Offset+seg.Length; pos < end; {
block := zeroSkipBuf[:min(int64(len(zeroSkipBuf)), end-pos)]
if _, err := src.ReadAt(block, pos); err != nil {
return err
}
if !isAllZero(block) {
if _, err := dst.WriteAt(block, pos); err != nil {
return err
}
}
pos += int64(len(block))
}
return nil
}

func seekBlind(f *os.File) bool {
var st unix.Statfs_t
return unix.Fstatfs(int(f.Fd()), &st) == nil && (st.Type == unix.FUSE_SUPER_MAGIC || st.Type == unix.NFS_SUPER_MAGIC)
}
37 changes: 37 additions & 0 deletions utils/sparse_linux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,43 @@ func TestSparseCopy_MultiSegment(t *testing.T) {
}
}

func TestCopySegmentSkipsZeroBlocks(t *testing.T) {
dir := t.TempDir()
src := filepath.Join(dir, "src")
dst := filepath.Join(dir, "dst")

const size = 8 << 20
data := make([]byte, size)
copy(data[size-4:], "tail")
if err := os.WriteFile(src, data, 0o600); err != nil {
t.Fatal(err)
}
in, err := os.Open(src)
if err != nil {
t.Fatal(err)
}
defer in.Close()
out, err := os.Create(dst)
if err != nil {
t.Fatal(err)
}
defer out.Close()
if err := out.Truncate(size); err != nil {
t.Fatal(err)
}

if err := copySegment(out, in, sparseSegment{Offset: 0, Length: size}, make([]byte, copyBlockSize)); err != nil {
t.Fatalf("copySegment: %v", err)
}

if !bytes.Equal(readFull(t, dst), data) {
t.Error("content mismatch")
}
if blocks, fullBlocks := fileBlocks(t, dst), int64(size/512); blocks >= fullBlocks/2 {
t.Errorf("expected zero blocks left as holes: got %d blocks, full would be %d", blocks, fullBlocks)
}
}

func writeAt(t *testing.T, f *os.File, offset int64, data []byte) {
t.Helper()
if _, err := f.WriteAt(data, offset); err != nil {
Expand Down
Loading