From c7478a100cb8cffb4c24fdcbf62d99fb1f62dc80 Mon Sep 17 00:00:00 2001 From: Darren Stahl Date: Thu, 1 Oct 2015 10:45:32 -0700 Subject: [PATCH] Fixed file modified time not changing on Windows Signed-off-by: Darren Stahl --- archive/archive.go | 12 +- archive/diff.go | 4 +- system/chtimes.go | 31 +++++ system/chtimes_test.go | 120 +++++++++++++++++ system/chtimes_unix_test.go | 121 ++++++++++++++++++ system/chtimes_windows_test.go | 114 +++++++++++++++++ system/{lstat_test.go => lstat_unix_test.go} | 2 + ...nfo_linux_test.go => meminfo_unix_test.go} | 2 + system/{stat_test.go => stat_unix_test.go} | 2 + system/utimes_darwin.go | 6 - system/utimes_freebsd.go | 6 - system/utimes_linux.go | 6 - .../{utimes_test.go => utimes_unix_test.go} | 2 + system/utimes_unsupported.go | 5 - 14 files changed, 401 insertions(+), 32 deletions(-) create mode 100644 system/chtimes.go create mode 100644 system/chtimes_test.go create mode 100644 system/chtimes_unix_test.go create mode 100644 system/chtimes_windows_test.go rename system/{lstat_test.go => lstat_unix_test.go} (95%) rename system/{meminfo_linux_test.go => meminfo_unix_test.go} (97%) rename system/{stat_test.go => stat_unix_test.go} (96%) rename system/{utimes_test.go => utimes_unix_test.go} (98%) diff --git a/archive/archive.go b/archive/archive.go index ef7e331..40bb3cb 100644 --- a/archive/archive.go +++ b/archive/archive.go @@ -375,19 +375,19 @@ func createTarFile(path, extractDir string, hdr *tar.Header, reader io.Reader, L return err } - ts := []syscall.Timespec{timeToTimespec(hdr.AccessTime), timeToTimespec(hdr.ModTime)} - // syscall.UtimesNano doesn't support a NOFOLLOW flag atm + // system.Chtimes doesn't support a NOFOLLOW flag atm if hdr.Typeflag == tar.TypeLink { if fi, err := os.Lstat(hdr.Linkname); err == nil && (fi.Mode()&os.ModeSymlink == 0) { - if err := system.UtimesNano(path, ts); err != nil && err != system.ErrNotSupportedPlatform { + if err := system.Chtimes(path, hdr.AccessTime, hdr.ModTime); err != nil { return err } } } else if hdr.Typeflag != tar.TypeSymlink { - if err := system.UtimesNano(path, ts); err != nil && err != system.ErrNotSupportedPlatform { + if err := system.Chtimes(path, hdr.AccessTime, hdr.ModTime); err != nil { return err } } else { + ts := []syscall.Timespec{timeToTimespec(hdr.AccessTime), timeToTimespec(hdr.ModTime)} if err := system.LUtimesNano(path, ts); err != nil && err != system.ErrNotSupportedPlatform { return err } @@ -644,8 +644,8 @@ loop: for _, hdr := range dirs { path := filepath.Join(dest, hdr.Name) - ts := []syscall.Timespec{timeToTimespec(hdr.AccessTime), timeToTimespec(hdr.ModTime)} - if err := syscall.UtimesNano(path, ts); err != nil { + + if err := system.Chtimes(path, hdr.AccessTime, hdr.ModTime); err != nil { return err } } diff --git a/archive/diff.go b/archive/diff.go index 50656cb..515fb1d 100644 --- a/archive/diff.go +++ b/archive/diff.go @@ -9,7 +9,6 @@ import ( "path/filepath" "runtime" "strings" - "syscall" "github.com/Sirupsen/logrus" "github.com/docker/docker/pkg/pools" @@ -167,8 +166,7 @@ func UnpackLayer(dest string, layer Reader) (size int64, err error) { for _, hdr := range dirs { path := filepath.Join(dest, hdr.Name) - ts := []syscall.Timespec{timeToTimespec(hdr.AccessTime), timeToTimespec(hdr.ModTime)} - if err := syscall.UtimesNano(path, ts); err != nil { + if err := system.Chtimes(path, hdr.AccessTime, hdr.ModTime); err != nil { return 0, err } } diff --git a/system/chtimes.go b/system/chtimes.go new file mode 100644 index 0000000..31ed9ff --- /dev/null +++ b/system/chtimes.go @@ -0,0 +1,31 @@ +package system + +import ( + "os" + "time" +) + +// Chtimes changes the access time and modified time of a file at the given path +func Chtimes(name string, atime time.Time, mtime time.Time) error { + unixMinTime := time.Unix(0, 0) + // The max Unix time is 33 bits set + unixMaxTime := unixMinTime.Add((1<<33 - 1) * time.Second) + + // If the modified time is prior to the Unix Epoch, or after the + // end of Unix Time, os.Chtimes has undefined behavior + // default to Unix Epoch in this case, just in case + + if atime.Before(unixMinTime) || atime.After(unixMaxTime) { + atime = unixMinTime + } + + if mtime.Before(unixMinTime) || mtime.After(unixMaxTime) { + mtime = unixMinTime + } + + if err := os.Chtimes(name, atime, mtime); err != nil { + return err + } + + return nil +} diff --git a/system/chtimes_test.go b/system/chtimes_test.go new file mode 100644 index 0000000..f65a4b8 --- /dev/null +++ b/system/chtimes_test.go @@ -0,0 +1,120 @@ +package system + +import ( + "io/ioutil" + "os" + "path/filepath" + "testing" + "time" +) + +// prepareTempFile creates a temporary file in a temporary directory. +func prepareTempFile(t *testing.T) (string, string) { + dir, err := ioutil.TempDir("", "docker-system-test") + if err != nil { + t.Fatal(err) + } + + file := filepath.Join(dir, "exist") + if err := ioutil.WriteFile(file, []byte("hello"), 0644); err != nil { + t.Fatal(err) + } + return file, dir +} + +// TestChtimes tests Chtimes on a tempfile. Test only mTime, because aTime is OS dependent +func TestChtimes(t *testing.T) { + file, dir := prepareTempFile(t) + defer os.RemoveAll(dir) + + beforeUnixEpochTime := time.Unix(0, 0).Add(-100 * time.Second) + unixEpochTime := time.Unix(0, 0) + afterUnixEpochTime := time.Unix(100, 0) + // The max Unix time is 33 bits set + unixMaxTime := unixEpochTime.Add((1<<33 - 1) * time.Second) + afterUnixMaxTime := unixMaxTime.Add(100 * time.Second) + + // Test both aTime and mTime set to Unix Epoch + Chtimes(file, unixEpochTime, unixEpochTime) + + f, err := os.Stat(file) + if err != nil { + t.Fatal(err) + } + + if f.ModTime() != unixEpochTime { + t.Fatalf("Expected: %s, got: %s", unixEpochTime, f.ModTime()) + } + + // Test aTime before Unix Epoch and mTime set to Unix Epoch + Chtimes(file, beforeUnixEpochTime, unixEpochTime) + + f, err = os.Stat(file) + if err != nil { + t.Fatal(err) + } + + if f.ModTime() != unixEpochTime { + t.Fatalf("Expected: %s, got: %s", unixEpochTime, f.ModTime()) + } + + // Test aTime set to Unix Epoch and mTime before Unix Epoch + Chtimes(file, unixEpochTime, beforeUnixEpochTime) + + f, err = os.Stat(file) + if err != nil { + t.Fatal(err) + } + + if f.ModTime() != unixEpochTime { + t.Fatalf("Expected: %s, got: %s", unixEpochTime, f.ModTime()) + } + + // Test both aTime and mTime set to after Unix Epoch (valid time) + Chtimes(file, afterUnixEpochTime, afterUnixEpochTime) + + f, err = os.Stat(file) + if err != nil { + t.Fatal(err) + } + + if f.ModTime() != afterUnixEpochTime { + t.Fatalf("Expected: %s, got: %s", afterUnixEpochTime, f.ModTime()) + } + + // Test both aTime and mTime set to Unix max time + Chtimes(file, unixMaxTime, unixMaxTime) + + f, err = os.Stat(file) + if err != nil { + t.Fatal(err) + } + + if f.ModTime() != unixMaxTime { + t.Fatalf("Expected: %s, got: %s", unixMaxTime, f.ModTime()) + } + + // Test aTime after Unix max time and mTime set to Unix max time + Chtimes(file, afterUnixMaxTime, unixMaxTime) + + f, err = os.Stat(file) + if err != nil { + t.Fatal(err) + } + + if f.ModTime() != unixMaxTime { + t.Fatalf("Expected: %s, got: %s", unixMaxTime, f.ModTime()) + } + + // Test aTime set to Unix Epoch and mTime before Unix Epoch + Chtimes(file, unixMaxTime, afterUnixMaxTime) + + f, err = os.Stat(file) + if err != nil { + t.Fatal(err) + } + + if f.ModTime() != unixEpochTime { + t.Fatalf("Expected: %s, got: %s", unixEpochTime, f.ModTime()) + } +} diff --git a/system/chtimes_unix_test.go b/system/chtimes_unix_test.go new file mode 100644 index 0000000..6998bbe --- /dev/null +++ b/system/chtimes_unix_test.go @@ -0,0 +1,121 @@ +// +build linux freebsd + +package system + +import ( + "os" + "syscall" + "testing" + "time" +) + +// TestChtimes tests Chtimes access time on a tempfile on Linux +func TestChtimesLinux(t *testing.T) { + file, dir := prepareTempFile(t) + defer os.RemoveAll(dir) + + beforeUnixEpochTime := time.Unix(0, 0).Add(-100 * time.Second) + unixEpochTime := time.Unix(0, 0) + afterUnixEpochTime := time.Unix(100, 0) + // The max Unix time is 33 bits set + unixMaxTime := unixEpochTime.Add((1<<33 - 1) * time.Second) + afterUnixMaxTime := unixMaxTime.Add(100 * time.Second) + + // Test both aTime and mTime set to Unix Epoch + Chtimes(file, unixEpochTime, unixEpochTime) + + f, err := os.Stat(file) + if err != nil { + t.Fatal(err) + } + + stat := f.Sys().(*syscall.Stat_t) + aTime := time.Unix(int64(stat.Atim.Sec), int64(stat.Atim.Nsec)) + if aTime != unixEpochTime { + t.Fatalf("Expected: %s, got: %s", unixEpochTime, aTime) + } + + // Test aTime before Unix Epoch and mTime set to Unix Epoch + Chtimes(file, beforeUnixEpochTime, unixEpochTime) + + f, err = os.Stat(file) + if err != nil { + t.Fatal(err) + } + + stat = f.Sys().(*syscall.Stat_t) + aTime = time.Unix(int64(stat.Atim.Sec), int64(stat.Atim.Nsec)) + if aTime != unixEpochTime { + t.Fatalf("Expected: %s, got: %s", unixEpochTime, aTime) + } + + // Test aTime set to Unix Epoch and mTime before Unix Epoch + Chtimes(file, unixEpochTime, beforeUnixEpochTime) + + f, err = os.Stat(file) + if err != nil { + t.Fatal(err) + } + + stat = f.Sys().(*syscall.Stat_t) + aTime = time.Unix(int64(stat.Atim.Sec), int64(stat.Atim.Nsec)) + if aTime != unixEpochTime { + t.Fatalf("Expected: %s, got: %s", unixEpochTime, aTime) + } + + // Test both aTime and mTime set to after Unix Epoch (valid time) + Chtimes(file, afterUnixEpochTime, afterUnixEpochTime) + + f, err = os.Stat(file) + if err != nil { + t.Fatal(err) + } + + stat = f.Sys().(*syscall.Stat_t) + aTime = time.Unix(int64(stat.Atim.Sec), int64(stat.Atim.Nsec)) + if aTime != afterUnixEpochTime { + t.Fatalf("Expected: %s, got: %s", afterUnixEpochTime, aTime) + } + + // Test both aTime and mTime set to Unix max time + Chtimes(file, unixMaxTime, unixMaxTime) + + f, err = os.Stat(file) + if err != nil { + t.Fatal(err) + } + + stat = f.Sys().(*syscall.Stat_t) + aTime = time.Unix(int64(stat.Atim.Sec), int64(stat.Atim.Nsec)) + if aTime != unixMaxTime { + t.Fatalf("Expected: %s, got: %s", unixMaxTime, aTime) + } + + // Test aTime after Unix max time and mTime set to Unix max time + Chtimes(file, afterUnixMaxTime, unixMaxTime) + + f, err = os.Stat(file) + if err != nil { + t.Fatal(err) + } + + stat = f.Sys().(*syscall.Stat_t) + aTime = time.Unix(int64(stat.Atim.Sec), int64(stat.Atim.Nsec)) + if aTime != unixEpochTime { + t.Fatalf("Expected: %s, got: %s", unixEpochTime, aTime) + } + + // Test aTime set to Unix Epoch and mTime before Unix Epoch + Chtimes(file, unixMaxTime, afterUnixMaxTime) + + f, err = os.Stat(file) + if err != nil { + t.Fatal(err) + } + + stat = f.Sys().(*syscall.Stat_t) + aTime = time.Unix(int64(stat.Atim.Sec), int64(stat.Atim.Nsec)) + if aTime != unixMaxTime { + t.Fatalf("Expected: %s, got: %s", unixMaxTime, aTime) + } +} diff --git a/system/chtimes_windows_test.go b/system/chtimes_windows_test.go new file mode 100644 index 0000000..f09c402 --- /dev/null +++ b/system/chtimes_windows_test.go @@ -0,0 +1,114 @@ +// +build windows + +package system + +import ( + "os" + "syscall" + "testing" + "time" +) + +// TestChtimes tests Chtimes access time on a tempfile on Windows +func TestChtimesWindows(t *testing.T) { + file, dir := prepareTempFile(t) + defer os.RemoveAll(dir) + + beforeUnixEpochTime := time.Unix(0, 0).Add(-100 * time.Second) + unixEpochTime := time.Unix(0, 0) + afterUnixEpochTime := time.Unix(100, 0) + // The max Unix time is 33 bits set + unixMaxTime := unixEpochTime.Add((1<<33 - 1) * time.Second) + afterUnixMaxTime := unixMaxTime.Add(100 * time.Second) + + // Test both aTime and mTime set to Unix Epoch + Chtimes(file, unixEpochTime, unixEpochTime) + + f, err := os.Stat(file) + if err != nil { + t.Fatal(err) + } + + aTime := time.Unix(0, f.Sys().(*syscall.Win32FileAttributeData).LastAccessTime.Nanoseconds()) + if aTime != unixEpochTime { + t.Fatalf("Expected: %s, got: %s", unixEpochTime, aTime) + } + + // Test aTime before Unix Epoch and mTime set to Unix Epoch + Chtimes(file, beforeUnixEpochTime, unixEpochTime) + + f, err = os.Stat(file) + if err != nil { + t.Fatal(err) + } + + aTime = time.Unix(0, f.Sys().(*syscall.Win32FileAttributeData).LastAccessTime.Nanoseconds()) + if aTime != unixEpochTime { + t.Fatalf("Expected: %s, got: %s", unixEpochTime, aTime) + } + + // Test aTime set to Unix Epoch and mTime before Unix Epoch + Chtimes(file, unixEpochTime, beforeUnixEpochTime) + + f, err = os.Stat(file) + if err != nil { + t.Fatal(err) + } + + aTime = time.Unix(0, f.Sys().(*syscall.Win32FileAttributeData).LastAccessTime.Nanoseconds()) + if aTime != unixEpochTime { + t.Fatalf("Expected: %s, got: %s", unixEpochTime, aTime) + } + + // Test both aTime and mTime set to after Unix Epoch (valid time) + Chtimes(file, afterUnixEpochTime, afterUnixEpochTime) + + f, err = os.Stat(file) + if err != nil { + t.Fatal(err) + } + + aTime = time.Unix(0, f.Sys().(*syscall.Win32FileAttributeData).LastAccessTime.Nanoseconds()) + if aTime != afterUnixEpochTime { + t.Fatalf("Expected: %s, got: %s", afterUnixEpochTime, aTime) + } + + // Test both aTime and mTime set to Unix max time + Chtimes(file, unixMaxTime, unixMaxTime) + + f, err = os.Stat(file) + if err != nil { + t.Fatal(err) + } + + aTime = time.Unix(0, f.Sys().(*syscall.Win32FileAttributeData).LastAccessTime.Nanoseconds()) + if aTime != unixMaxTime { + t.Fatalf("Expected: %s, got: %s", unixMaxTime, aTime) + } + + // Test aTime after Unix max time and mTime set to Unix max time + Chtimes(file, afterUnixMaxTime, unixMaxTime) + + f, err = os.Stat(file) + if err != nil { + t.Fatal(err) + } + + aTime = time.Unix(0, f.Sys().(*syscall.Win32FileAttributeData).LastAccessTime.Nanoseconds()) + if aTime != unixEpochTime { + t.Fatalf("Expected: %s, got: %s", unixEpochTime, aTime) + } + + // Test aTime set to Unix Epoch and mTime before Unix Epoch + Chtimes(file, unixMaxTime, afterUnixMaxTime) + + f, err = os.Stat(file) + if err != nil { + t.Fatal(err) + } + + aTime = time.Unix(0, f.Sys().(*syscall.Win32FileAttributeData).LastAccessTime.Nanoseconds()) + if aTime != unixMaxTime { + t.Fatalf("Expected: %s, got: %s", unixMaxTime, aTime) + } +} diff --git a/system/lstat_test.go b/system/lstat_unix_test.go similarity index 95% rename from system/lstat_test.go rename to system/lstat_unix_test.go index 6bac492..062cf53 100644 --- a/system/lstat_test.go +++ b/system/lstat_unix_test.go @@ -1,3 +1,5 @@ +// +build linux freebsd + package system import ( diff --git a/system/meminfo_linux_test.go b/system/meminfo_unix_test.go similarity index 97% rename from system/meminfo_linux_test.go rename to system/meminfo_unix_test.go index 10ddf79..c8fec62 100644 --- a/system/meminfo_linux_test.go +++ b/system/meminfo_unix_test.go @@ -1,3 +1,5 @@ +// +build linux freebsd + package system import ( diff --git a/system/stat_test.go b/system/stat_unix_test.go similarity index 96% rename from system/stat_test.go rename to system/stat_unix_test.go index 57121f1..8b3c42b 100644 --- a/system/stat_test.go +++ b/system/stat_unix_test.go @@ -1,3 +1,5 @@ +// +build linux freebsd + package system import ( diff --git a/system/utimes_darwin.go b/system/utimes_darwin.go index 9e3dcdd..0a16197 100644 --- a/system/utimes_darwin.go +++ b/system/utimes_darwin.go @@ -6,9 +6,3 @@ import "syscall" func LUtimesNano(path string, ts []syscall.Timespec) error { return ErrNotSupportedPlatform } - -// UtimesNano is used to change access and modification time of path. -// it can't be used for symbol link file. -func UtimesNano(path string, ts []syscall.Timespec) error { - return syscall.UtimesNano(path, ts) -} diff --git a/system/utimes_freebsd.go b/system/utimes_freebsd.go index 15ce26f..e2eac3b 100644 --- a/system/utimes_freebsd.go +++ b/system/utimes_freebsd.go @@ -20,9 +20,3 @@ func LUtimesNano(path string, ts []syscall.Timespec) error { return nil } - -// UtimesNano is used to change access and modification time of the specified path. -// It can't be used for symbol link file. -func UtimesNano(path string, ts []syscall.Timespec) error { - return syscall.UtimesNano(path, ts) -} diff --git a/system/utimes_linux.go b/system/utimes_linux.go index 7909801..007bfa8 100644 --- a/system/utimes_linux.go +++ b/system/utimes_linux.go @@ -24,9 +24,3 @@ func LUtimesNano(path string, ts []syscall.Timespec) error { return nil } - -// UtimesNano is used to change access and modification time of the specified path. -// It can't be used for symbol link file. -func UtimesNano(path string, ts []syscall.Timespec) error { - return syscall.UtimesNano(path, ts) -} diff --git a/system/utimes_test.go b/system/utimes_unix_test.go similarity index 98% rename from system/utimes_test.go rename to system/utimes_unix_test.go index 350cce1..1ee0d09 100644 --- a/system/utimes_test.go +++ b/system/utimes_unix_test.go @@ -1,3 +1,5 @@ +// +build linux freebsd + package system import ( diff --git a/system/utimes_unsupported.go b/system/utimes_unsupported.go index cb614a1..50c3a04 100644 --- a/system/utimes_unsupported.go +++ b/system/utimes_unsupported.go @@ -8,8 +8,3 @@ import "syscall" func LUtimesNano(path string, ts []syscall.Timespec) error { return ErrNotSupportedPlatform } - -// UtimesNano is not supported on platforms other than linux, freebsd and darwin. -func UtimesNano(path string, ts []syscall.Timespec) error { - return ErrNotSupportedPlatform -}