diff --git a/fs/diff_test.go b/fs/diff_test.go index f6b4590d7..506e7d867 100644 --- a/fs/diff_test.go +++ b/fs/diff_test.go @@ -150,27 +150,27 @@ func TestUpdateWithSameTime(t *testing.T) { t2 := tt.Add(6 * time.Nanosecond) l1 := fstest.Apply( fstest.CreateFile("/file-modified-time", []byte("1"), 0644), - fstest.Chtime("/file-modified-time", t1), + fstest.Chtimes("/file-modified-time", t1, t1), fstest.CreateFile("/file-no-change", []byte("1"), 0644), - fstest.Chtime("/file-no-change", t1), + fstest.Chtimes("/file-no-change", t1, t1), fstest.CreateFile("/file-same-time", []byte("1"), 0644), - fstest.Chtime("/file-same-time", t1), + fstest.Chtimes("/file-same-time", t1, t1), fstest.CreateFile("/file-truncated-time-1", []byte("1"), 0644), - fstest.Chtime("/file-truncated-time-1", t1), + fstest.Chtimes("/file-truncated-time-1", t1, t1), fstest.CreateFile("/file-truncated-time-2", []byte("1"), 0644), - fstest.Chtime("/file-truncated-time-2", tt), + fstest.Chtimes("/file-truncated-time-2", tt, tt), ) l2 := fstest.Apply( fstest.CreateFile("/file-modified-time", []byte("2"), 0644), - fstest.Chtime("/file-modified-time", t2), + fstest.Chtimes("/file-modified-time", t2, t2), fstest.CreateFile("/file-no-change", []byte("1"), 0644), - fstest.Chtime("/file-no-change", tt), // use truncated time, should be regarded as no change + fstest.Chtimes("/file-no-change", tt, tt), // use truncated time, should be regarded as no change fstest.CreateFile("/file-same-time", []byte("2"), 0644), - fstest.Chtime("/file-same-time", t1), + fstest.Chtimes("/file-same-time", t1, t1), fstest.CreateFile("/file-truncated-time-1", []byte("2"), 0644), - fstest.Chtime("/file-truncated-time-1", tt), + fstest.Chtimes("/file-truncated-time-1", tt, tt), fstest.CreateFile("/file-truncated-time-2", []byte("2"), 0644), - fstest.Chtime("/file-truncated-time-2", tt), + fstest.Chtimes("/file-truncated-time-2", tt, tt), ) diff := []TestChange{ // "/file-same-time" excluded because matching non-zero nanosecond values @@ -184,6 +184,28 @@ func TestUpdateWithSameTime(t *testing.T) { } } +// buildkit#172 +func TestLchtimes(t *testing.T) { + skipDiffTestOnWindows(t) + mtimes := []time.Time{ + time.Unix(0, 0), // nsec is 0 + time.Unix(0, 42), // nsec > 0 + } + for _, mtime := range mtimes { + atime := time.Unix(424242, 42) + l1 := fstest.Apply( + fstest.CreateFile("/foo", []byte("foo"), 0644), + fstest.Symlink("/foo", "/lnk0"), + fstest.Lchtimes("/lnk0", atime, mtime), + ) + l2 := fstest.Apply() // empty + diff := []TestChange{} + if err := testDiffWithBase(l1, l2, diff); err != nil { + t.Fatalf("Failed diff with base: %+v", err) + } + } +} + func testDiffWithBase(base, diff fstest.Applier, expected []TestChange) error { t1, err := ioutil.TempDir("", "diff-with-base-lower-") if err != nil { diff --git a/fs/fstest/file.go b/fs/fstest/file.go index a5ec2f762..9d614ee98 100644 --- a/fs/fstest/file.go +++ b/fs/fstest/file.go @@ -71,10 +71,11 @@ func Chown(name string, uid, gid int) Applier { }) } -// Chtime changes access and mod time of file -func Chtime(name string, t time.Time) Applier { +// Chtimes changes access and mod time of file. +// Use Lchtimes for symbolic links. +func Chtimes(name string, atime, mtime time.Time) Applier { return applyFn(func(root string) error { - return os.Chtimes(filepath.Join(root, name), t, t) + return os.Chtimes(filepath.Join(root, name), atime, mtime) }) } diff --git a/fs/fstest/file_unix.go b/fs/fstest/file_unix.go index 2df4a1b6d..af223b941 100644 --- a/fs/fstest/file_unix.go +++ b/fs/fstest/file_unix.go @@ -2,7 +2,13 @@ package fstest -import "github.com/containerd/continuity/sysx" +import ( + "path/filepath" + "time" + + "github.com/containerd/continuity/sysx" + "golang.org/x/sys/unix" +) // SetXAttr sets the xatter for the file func SetXAttr(name, key, value string) Applier { @@ -10,3 +16,14 @@ func SetXAttr(name, key, value string) Applier { return sysx.LSetxattr(name, key, []byte(value), 0) }) } + +// Lchtimes changes access and mod time of file without following symlink +func Lchtimes(name string, atime, mtime time.Time) Applier { + return applyFn(func(root string) error { + path := filepath.Join(root, name) + at := unix.NsecToTimespec(atime.UnixNano()) + mt := unix.NsecToTimespec(mtime.UnixNano()) + utimes := [2]unix.Timespec{at, mt} + return unix.UtimesNanoAt(unix.AT_FDCWD, path, utimes[0:], unix.AT_SYMLINK_NOFOLLOW) + }) +} diff --git a/fs/fstest/file_windows.go b/fs/fstest/file_windows.go new file mode 100644 index 000000000..022a09f15 --- /dev/null +++ b/fs/fstest/file_windows.go @@ -0,0 +1,14 @@ +package fstest + +import ( + "time" + + "github.com/containerd/containerd/errdefs" +) + +// Lchtimes changes access and mod time of file without following symlink +func Lchtimes(name string, atime, mtime time.Time) Applier { + return applyFn(func(root string) error { + return errdefs.ErrNotImplemented + }) +} diff --git a/fs/path.go b/fs/path.go index 644b1ee2e..412da6711 100644 --- a/fs/path.go +++ b/fs/path.go @@ -74,11 +74,14 @@ func sameFile(f1, f2 *currentPath) (bool, error) { // If the timestamp may have been truncated in one of the // files, check content of file to determine difference if t1.Nanosecond() == 0 || t2.Nanosecond() == 0 { - if f1.f.Size() > 0 { - eq, err := compareFileContent(f1.fullPath, f2.fullPath) - if err != nil || !eq { - return eq, err - } + var eq bool + if (f1.f.Mode() & os.ModeSymlink) == os.ModeSymlink { + eq, err = compareSymlinkTarget(f1.fullPath, f2.fullPath) + } else if f1.f.Size() > 0 { + eq, err = compareFileContent(f1.fullPath, f2.fullPath) + } + if err != nil || !eq { + return eq, err } } else if t1.Nanosecond() != t2.Nanosecond() { return false, nil @@ -88,6 +91,18 @@ func sameFile(f1, f2 *currentPath) (bool, error) { return true, nil } +func compareSymlinkTarget(p1, p2 string) (bool, error) { + t1, err := os.Readlink(p1) + if err != nil { + return false, err + } + t2, err := os.Readlink(p2) + if err != nil { + return false, err + } + return t1 == t2, nil +} + const compareChuckSize = 32 * 1024 // compareFileContent compares the content of 2 same sized files