diff --git a/fs/copy.go b/fs/copy.go index a5c21081..a1cae0f9 100644 --- a/fs/copy.go +++ b/fs/copy.go @@ -184,6 +184,10 @@ func copyDirectory(dst, src string, inodes map[uint64]string, o *copyDirOpts) er // CopyFile copies the source file to the target. // The most efficient means of copying is used for the platform. func CopyFile(target, source string) error { + return copyFile(target, source) +} + +func openAndCopyFile(target, source string) error { src, err := os.Open(source) if err != nil { return fmt.Errorf("failed to open source %s: %w", source, err) diff --git a/fs/copy_darwin.go b/fs/copy_darwin.go new file mode 100644 index 00000000..73e5af1e --- /dev/null +++ b/fs/copy_darwin.go @@ -0,0 +1,35 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package fs + +import ( + "errors" + "fmt" + + "golang.org/x/sys/unix" +) + +func copyFile(target, source string) error { + if err := unix.Clonefile(source, target, unix.CLONE_NOFOLLOW); err != nil { + if !errors.Is(err, unix.ENOTSUP) { + return fmt.Errorf("clonefile failed: %w", err) + } + + return openAndCopyFile(target, source) + } + return nil +} diff --git a/fs/copy_nondarwin.go b/fs/copy_nondarwin.go new file mode 100644 index 00000000..275b64c0 --- /dev/null +++ b/fs/copy_nondarwin.go @@ -0,0 +1,22 @@ +//go:build !darwin +// +build !darwin + +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package fs + +var copyFile = openAndCopyFile diff --git a/fs/copy_test.go b/fs/copy_test.go index d0dafb90..70db1c14 100644 --- a/fs/copy_test.go +++ b/fs/copy_test.go @@ -19,6 +19,7 @@ package fs import ( _ "crypto/sha256" "fmt" + "os" "testing" "time" @@ -89,3 +90,37 @@ func testCopy(t testing.TB, apply fstest.Applier) error { return fstest.CheckDirectoryEqual(t1, t2) } + +func BenchmarkLargeCopy100MB(b *testing.B) { + benchmarkLargeCopyFile(b, 100*1024*1024) +} + +func BenchmarkLargeCopy1GB(b *testing.B) { + benchmarkLargeCopyFile(b, 1024*1024*1024) +} + +func benchmarkLargeCopyFile(b *testing.B, size int64) { + b.StopTimer() + base := b.TempDir() + apply := fstest.Apply( + fstest.CreateRandomFile("/large", time.Now().UnixNano(), size, 0o644), + ) + if err := apply.Apply(base); err != nil { + b.Fatal("failed to apply changes:", err) + } + + for i := 0; i < b.N; i++ { + copied := b.TempDir() + b.StartTimer() + if err := CopyDir(copied, base); err != nil { + b.Fatal("failed to copy:", err) + } + b.StopTimer() + if i == 0 { + if err := fstest.CheckDirectoryEqual(base, copied); err != nil { + b.Fatal("check failed:", err) + } + } + os.RemoveAll(copied) + } +}