@@ -26,6 +26,122 @@ import (
2626 "golang.org/x/sys/unix"
2727)
2828
29+ // maxCopyChunk is the maximum size passed to copy_file_range per call,
30+ // avoiding int overflow on 32-bit architectures.
31+ const maxCopyChunk = 1 << 30 // 1 GiB
32+
33+ // copyFile copies a file from source to target preserving sparse file holes.
34+ //
35+ // If the filesystem does not support SEEK_DATA/SEEK_HOLE, it falls back
36+ // to a plain io.Copy.
37+ func copyFile (target , source string ) error {
38+ src , err := os .Open (source )
39+ if err != nil {
40+ return fmt .Errorf ("failed to open source %s: %w" , source , err )
41+ }
42+ defer src .Close ()
43+
44+ fi , err := src .Stat ()
45+ if err != nil {
46+ return fmt .Errorf ("failed to stat source %s: %w" , source , err )
47+ }
48+ size := fi .Size ()
49+
50+ tgt , err := os .Create (target )
51+ if err != nil {
52+ return fmt .Errorf ("failed to open target %s: %w" , target , err )
53+ }
54+ defer tgt .Close ()
55+
56+ if err := tgt .Truncate (size ); err != nil {
57+ return fmt .Errorf ("failed to truncate target %s: %w" , target , err )
58+ }
59+
60+ srcFd := int (src .Fd ())
61+
62+ // Try a SEEK_DATA to check if the filesystem supports it.
63+ // If not, fall back to a plain copy.
64+ if _ , err := unix .Seek (srcFd , 0 , unix .SEEK_DATA ); err != nil {
65+ // ENXIO means no data in the file at all. In other words it's entirely sparse.
66+ // The truncated target is already correct.
67+ if errors .Is (err , syscall .ENXIO ) {
68+ return nil
69+ }
70+
71+ if errors .Is (err , syscall .EOPNOTSUPP ) || errors .Is (err , syscall .ENOTSUP ) || errors .Is (err , syscall .EINVAL ) {
72+ // Filesystem doesn't support SEEK_DATA/SEEK_HOLE. Fall back to a plain copy.
73+ src .Close ()
74+ tgt .Close ()
75+ return openAndCopyFile (target , source )
76+ }
77+
78+ return fmt .Errorf ("failed to seek data in source %s: %w" , source , err )
79+ }
80+
81+ // Copy data regions from source to target, skipping holes.
82+ var offset int64
83+ tgtFd := int (tgt .Fd ())
84+
85+ for offset < size {
86+ dataStart , err := unix .Seek (srcFd , offset , unix .SEEK_DATA )
87+ if err != nil {
88+ // No more data past offset. Remainder of file is a hole.
89+ if errors .Is (err , syscall .ENXIO ) {
90+ break
91+ }
92+ return fmt .Errorf ("SEEK_DATA failed at offset %d: %w" , offset , err )
93+ }
94+
95+ // Find the end of this data region (start of next hole).
96+ holeStart , err := unix .Seek (srcFd , dataStart , unix .SEEK_HOLE )
97+ if err != nil {
98+ // ENXIO shouldn't happen after a successful SEEK_DATA, but
99+ // treat it as data extending to end of file.
100+ if errors .Is (err , syscall .ENXIO ) {
101+ holeStart = size
102+ } else {
103+ return fmt .Errorf ("SEEK_HOLE failed at offset %d: %w" , dataStart , err )
104+ }
105+ }
106+
107+ // Copy the data region [dataStart, holeStart).
108+ srcOff := dataStart
109+ tgtOff := dataStart
110+ remain := holeStart - dataStart
111+
112+ for remain > 0 {
113+ chunk := remain
114+ if chunk > maxCopyChunk {
115+ chunk = maxCopyChunk
116+ }
117+
118+ n , err := unix .CopyFileRange (srcFd , & srcOff , tgtFd , & tgtOff , int (chunk ), 0 )
119+ if err != nil {
120+ // Fall back to a plain copy if copy_file_range is not supported
121+ // across the source and target filesystems.
122+ if errors .Is (err , syscall .EXDEV ) || errors .Is (err , syscall .ENOSYS ) || errors .Is (err , syscall .EOPNOTSUPP ) {
123+ src .Close ()
124+ tgt .Close ()
125+ return openAndCopyFile (target , source )
126+ }
127+ return fmt .Errorf ("copy_file_range failed: %w" , err )
128+ }
129+ if n == 0 {
130+ return fmt .Errorf ("copy_file_range returned 0 with %d bytes remaining" , remain )
131+ }
132+ remain -= int64 (n )
133+ }
134+
135+ offset = holeStart
136+ }
137+
138+ if err := tgt .Sync (); err != nil {
139+ return fmt .Errorf ("failed to sync target %s: %w" , target , err )
140+ }
141+
142+ return nil
143+ }
144+
29145func copyFileInfo (fi os.FileInfo , src , name string ) error {
30146 st := fi .Sys ().(* syscall.Stat_t )
31147 if err := os .Lchown (name , int (st .Uid ), int (st .Gid )); err != nil {
0 commit comments