Previously, the code would set the mtime on the directories before creating files in the directory itself. This was problematic because it resulted in the mtimes on the directories being incorrectly set. This change makes it so that the mtime is set only _after_ all of the files have been created. Signed-off-by: Sargun Dhillon <sargun@sargun.me> Upstream-commit: 77a2bc3e5bbc9be3fe166ed8321b7cd04e7bd097 Component: engine
180 lines
5.1 KiB
Go
180 lines
5.1 KiB
Go
// +build linux
|
|
|
|
package copy
|
|
|
|
import (
|
|
"fmt"
|
|
"io/ioutil"
|
|
"math/rand"
|
|
"os"
|
|
"path/filepath"
|
|
"syscall"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/docker/docker/pkg/parsers/kernel"
|
|
"github.com/docker/docker/pkg/system"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"golang.org/x/sys/unix"
|
|
)
|
|
|
|
func TestIsCopyFileRangeSyscallAvailable(t *testing.T) {
|
|
// Verifies:
|
|
// 1. That copyFileRangeEnabled is being set to true when copy_file_range syscall is available
|
|
// 2. That isCopyFileRangeSyscallAvailable() works on "new" kernels
|
|
v, err := kernel.GetKernelVersion()
|
|
require.NoError(t, err)
|
|
|
|
copyWithFileRange := true
|
|
copyWithFileClone := false
|
|
doCopyTest(t, ©WithFileRange, ©WithFileClone)
|
|
|
|
if kernel.CompareKernelVersion(*v, kernel.VersionInfo{Kernel: 4, Major: 5, Minor: 0}) < 0 {
|
|
assert.False(t, copyWithFileRange)
|
|
} else {
|
|
assert.True(t, copyWithFileRange)
|
|
}
|
|
|
|
}
|
|
|
|
func TestCopy(t *testing.T) {
|
|
copyWithFileRange := true
|
|
copyWithFileClone := true
|
|
doCopyTest(t, ©WithFileRange, ©WithFileClone)
|
|
}
|
|
|
|
func TestCopyWithoutRange(t *testing.T) {
|
|
copyWithFileRange := false
|
|
copyWithFileClone := false
|
|
doCopyTest(t, ©WithFileRange, ©WithFileClone)
|
|
}
|
|
|
|
func TestCopyDir(t *testing.T) {
|
|
srcDir, err := ioutil.TempDir("", "srcDir")
|
|
require.NoError(t, err)
|
|
populateSrcDir(t, srcDir, 3)
|
|
|
|
dstDir, err := ioutil.TempDir("", "testdst")
|
|
require.NoError(t, err)
|
|
defer os.RemoveAll(dstDir)
|
|
|
|
assert.NoError(t, DirCopy(srcDir, dstDir, Content, false))
|
|
require.NoError(t, filepath.Walk(srcDir, func(srcPath string, f os.FileInfo, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Rebase path
|
|
relPath, err := filepath.Rel(srcDir, srcPath)
|
|
require.NoError(t, err)
|
|
if relPath == "." {
|
|
return nil
|
|
}
|
|
|
|
dstPath := filepath.Join(dstDir, relPath)
|
|
require.NoError(t, err)
|
|
|
|
// If we add non-regular dirs and files to the test
|
|
// then we need to add more checks here.
|
|
dstFileInfo, err := os.Lstat(dstPath)
|
|
require.NoError(t, err)
|
|
|
|
srcFileSys := f.Sys().(*syscall.Stat_t)
|
|
dstFileSys := dstFileInfo.Sys().(*syscall.Stat_t)
|
|
|
|
t.Log(relPath)
|
|
if srcFileSys.Dev == dstFileSys.Dev {
|
|
assert.NotEqual(t, srcFileSys.Ino, dstFileSys.Ino)
|
|
}
|
|
// Todo: check size, and ctim is not equal
|
|
/// on filesystems that have granular ctimes
|
|
assert.Equal(t, srcFileSys.Mode, dstFileSys.Mode)
|
|
assert.Equal(t, srcFileSys.Uid, dstFileSys.Uid)
|
|
assert.Equal(t, srcFileSys.Gid, dstFileSys.Gid)
|
|
assert.Equal(t, srcFileSys.Mtim, dstFileSys.Mtim)
|
|
|
|
return nil
|
|
}))
|
|
}
|
|
|
|
func randomMode(baseMode int) os.FileMode {
|
|
for i := 0; i < 7; i++ {
|
|
baseMode = baseMode | (1&rand.Intn(2))<<uint(i)
|
|
}
|
|
return os.FileMode(baseMode)
|
|
}
|
|
|
|
func populateSrcDir(t *testing.T, srcDir string, remainingDepth int) {
|
|
if remainingDepth == 0 {
|
|
return
|
|
}
|
|
aTime := time.Unix(rand.Int63(), 0)
|
|
mTime := time.Unix(rand.Int63(), 0)
|
|
|
|
for i := 0; i < 10; i++ {
|
|
dirName := filepath.Join(srcDir, fmt.Sprintf("srcdir-%d", i))
|
|
// Owner all bits set
|
|
require.NoError(t, os.Mkdir(dirName, randomMode(0700)))
|
|
populateSrcDir(t, dirName, remainingDepth-1)
|
|
require.NoError(t, system.Chtimes(dirName, aTime, mTime))
|
|
}
|
|
|
|
for i := 0; i < 10; i++ {
|
|
fileName := filepath.Join(srcDir, fmt.Sprintf("srcfile-%d", i))
|
|
// Owner read bit set
|
|
require.NoError(t, ioutil.WriteFile(fileName, []byte{}, randomMode(0400)))
|
|
require.NoError(t, system.Chtimes(fileName, aTime, mTime))
|
|
}
|
|
}
|
|
|
|
func doCopyTest(t *testing.T, copyWithFileRange, copyWithFileClone *bool) {
|
|
dir, err := ioutil.TempDir("", "docker-copy-check")
|
|
require.NoError(t, err)
|
|
defer os.RemoveAll(dir)
|
|
srcFilename := filepath.Join(dir, "srcFilename")
|
|
dstFilename := filepath.Join(dir, "dstilename")
|
|
|
|
r := rand.New(rand.NewSource(0))
|
|
buf := make([]byte, 1024)
|
|
_, err = r.Read(buf)
|
|
require.NoError(t, err)
|
|
require.NoError(t, ioutil.WriteFile(srcFilename, buf, 0777))
|
|
fileinfo, err := os.Stat(srcFilename)
|
|
require.NoError(t, err)
|
|
|
|
require.NoError(t, copyRegular(srcFilename, dstFilename, fileinfo, copyWithFileRange, copyWithFileClone))
|
|
readBuf, err := ioutil.ReadFile(dstFilename)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, buf, readBuf)
|
|
}
|
|
|
|
func TestCopyHardlink(t *testing.T) {
|
|
var srcFile1FileInfo, srcFile2FileInfo, dstFile1FileInfo, dstFile2FileInfo unix.Stat_t
|
|
|
|
srcDir, err := ioutil.TempDir("", "srcDir")
|
|
require.NoError(t, err)
|
|
defer os.RemoveAll(srcDir)
|
|
|
|
dstDir, err := ioutil.TempDir("", "dstDir")
|
|
require.NoError(t, err)
|
|
defer os.RemoveAll(dstDir)
|
|
|
|
srcFile1 := filepath.Join(srcDir, "file1")
|
|
srcFile2 := filepath.Join(srcDir, "file2")
|
|
dstFile1 := filepath.Join(dstDir, "file1")
|
|
dstFile2 := filepath.Join(dstDir, "file2")
|
|
require.NoError(t, ioutil.WriteFile(srcFile1, []byte{}, 0777))
|
|
require.NoError(t, os.Link(srcFile1, srcFile2))
|
|
|
|
assert.NoError(t, DirCopy(srcDir, dstDir, Content, false))
|
|
|
|
require.NoError(t, unix.Stat(srcFile1, &srcFile1FileInfo))
|
|
require.NoError(t, unix.Stat(srcFile2, &srcFile2FileInfo))
|
|
require.Equal(t, srcFile1FileInfo.Ino, srcFile2FileInfo.Ino)
|
|
|
|
require.NoError(t, unix.Stat(dstFile1, &dstFile1FileInfo))
|
|
require.NoError(t, unix.Stat(dstFile2, &dstFile2FileInfo))
|
|
assert.Equal(t, dstFile1FileInfo.Ino, dstFile2FileInfo.Ino)
|
|
}
|