Skip to content

Commit afba265

Browse files
committed
Fix copy_file_range usage for files > 2GB
copy_file_range (or any Linux syscall) will only ever copy 2GB at a time. In this case there is no error returned from the system call. This fix uses copy_file_range in a loop until either 0 bytes are copied, or the desired amount has been copied (st.Size()). Signed-off-by: Brian Goff <[email protected]>
1 parent c6cef34 commit afba265

3 files changed

Lines changed: 80 additions & 17 deletions

File tree

fs/copy_linux.go

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -49,20 +49,26 @@ func copyFileContent(dst, src *os.File) error {
4949
return errors.Wrap(err, "unable to stat source")
5050
}
5151

52-
n, err := unix.CopyFileRange(int(src.Fd()), nil, int(dst.Fd()), nil, int(st.Size()), 0)
53-
if err != nil {
54-
if err != unix.ENOSYS && err != unix.EXDEV {
55-
return errors.Wrap(err, "copy file range failed")
56-
}
52+
size := st.Size()
53+
first := true
54+
srcFd := int(src.Fd())
55+
dstFd := int(dst.Fd())
5756

58-
buf := bufferPool.Get().(*[]byte)
59-
_, err = io.CopyBuffer(dst, src, *buf)
60-
bufferPool.Put(buf)
61-
return err
62-
}
57+
for size > 0 {
58+
n, err := unix.CopyFileRange(srcFd, nil, dstFd, nil, int(size), 0)
59+
if err != nil {
60+
if (err != unix.ENOSYS && err != unix.EXDEV) || !first {
61+
return errors.Wrap(err, "copy file range failed")
62+
}
63+
64+
buf := bufferPool.Get().(*[]byte)
65+
_, err = io.CopyBuffer(dst, src, *buf)
66+
bufferPool.Put(buf)
67+
return errors.Wrap(err, "userspace copy failed")
68+
}
6369

64-
if int64(n) != st.Size() {
65-
return errors.Wrapf(err, "short copy: %d of %d", int64(n), st.Size())
70+
first = false
71+
size -= int64(n)
6672
}
6773

6874
return nil

fs/copy_test.go

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
package fs
22

33
import (
4+
_ "crypto/sha256"
5+
"io"
46
"io/ioutil"
57
"os"
68
"testing"
79

8-
_ "crypto/sha256"
9-
1010
"github.com/containerd/continuity/fs/fstest"
1111
"github.com/pkg/errors"
1212
)
@@ -46,6 +46,45 @@ func TestCopyDirectoryWithLocalSymlink(t *testing.T) {
4646
}
4747
}
4848

49+
// TestCopyWithLargeFile tests copying a file whose size > 2^32 bytes.
50+
func TestCopyWithLargeFile(t *testing.T) {
51+
dataR, dataW := io.Pipe()
52+
var written int64
53+
max := int64(3 * 1024 * 1024 * 1024)
54+
defer dataR.Close()
55+
56+
go func() {
57+
var (
58+
data = make([]byte, 64*1024)
59+
err error
60+
n int
61+
)
62+
defer func() {
63+
dataW.CloseWithError(err)
64+
}()
65+
66+
for written < max {
67+
n, err = dataW.Write(data)
68+
if err != nil {
69+
return
70+
}
71+
written += int64(n)
72+
}
73+
}()
74+
75+
apply := fstest.Apply(
76+
fstest.CreateDir("/banana", 0755),
77+
fstest.WriteFileStream("/banana/split", dataR, 0644),
78+
)
79+
80+
if err := testCopy(apply); err != nil {
81+
t.Fatal(err)
82+
}
83+
if written < max {
84+
t.Fatalf("wrote fewer bytes than expected: %d", written)
85+
}
86+
}
87+
4988
func testCopy(apply fstest.Applier) error {
5089
t1, err := ioutil.TempDir("", "test-copy-src-")
5190
if err != nil {

fs/fstest/file.go

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
package fstest
22

33
import (
4-
"io/ioutil"
4+
"bytes"
5+
"io"
56
"net"
67
"os"
78
"path/filepath"
@@ -22,9 +23,26 @@ func (a applyFn) Apply(root string) error {
2223
// CreateFile returns a file applier which creates a file as the
2324
// provided name with the given content and permission.
2425
func CreateFile(name string, content []byte, perm os.FileMode) Applier {
25-
return applyFn(func(root string) error {
26+
return WriteFileStream(name, bytes.NewReader(content), perm)
27+
}
28+
29+
// WriteFileStream returns a file applier which creates a file as the
30+
// provided name with the given content from the provided i/o stream and permission.
31+
func WriteFileStream(name string, stream io.Reader, perm os.FileMode) Applier {
32+
return applyFn(func(root string) (retErr error) {
2633
fullPath := filepath.Join(root, name)
27-
if err := ioutil.WriteFile(fullPath, content, perm); err != nil {
34+
f, err := os.OpenFile(fullPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, perm)
35+
if err != nil {
36+
return err
37+
}
38+
defer func() {
39+
err := f.Close()
40+
if err != nil && retErr == nil {
41+
retErr = err
42+
}
43+
}()
44+
_, err = io.Copy(f, stream)
45+
if err != nil {
2846
return err
2947
}
3048
return os.Chmod(fullPath, perm)

0 commit comments

Comments
 (0)