diff --git a/README.md b/README.md index bcf29b2..8495ffa 100644 --- a/README.md +++ b/README.md @@ -30,11 +30,11 @@ func main() { log.Println(t.AccessTime()) log.Println(t.ModTime()) - if times.HasChangeTime { + if t.HasChangeTime() { log.Println(t.ChangeTime()) } - if times.HasBirthTime { + if t.HasBirthTime() { log.Println(t.BirthTime()) } } @@ -46,9 +46,13 @@ Supported Times |:-----:|:-------:|:-----:|:-------:|:---------:|:------:|:-------:|:----:|:------:|:-------:|:-----:| | atime | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | | mtime | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | -| ctime | | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | | +| ctime | ✓* | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | | | btime | ✓ | | | | | ✓ | ✓| ✓ | +* Windows XP does not have ChangeTime so HasChangeTime = false, +however Vista onward does have ChangeTime so Timespec.HasChangeTime() will +only return false on those platforms when the syscall used to obtain them fails. + Installation ------------ ```sh diff --git a/ctime_windows.go b/ctime_windows.go new file mode 100644 index 0000000..c9a5bee --- /dev/null +++ b/ctime_windows.go @@ -0,0 +1,90 @@ +package times + +import ( + "os" + "syscall" + "time" + "unsafe" +) + +type timespecEx struct { + atime + mtime + ctime + btime +} + +// StatFile finds a Windows Timespec with ChangeTime. +func StatFile(file *os.File) (Timespec, error) { + var fileInfo fileBasicInfo + if err := getFileInformationByHandleEx(syscall.Handle(file.Fd()), &fileInfo); err != nil { + return nil, err + } + + var t timespecEx + t.atime.v = time.Unix(0, fileInfo.LastAccessTime.Nanoseconds()) + t.mtime.v = time.Unix(0, fileInfo.LastWriteTime.Nanoseconds()) + t.ctime.v = time.Unix(0, fileInfo.ChangeTime.Nanoseconds()) + t.btime.v = time.Unix(0, fileInfo.CreationTime.Nanoseconds()) + return t, nil +} + +func statEx(file string) (Timespec, error) { + if findProcErr != nil { + // fail fast, don't bother opening the file + return nil, findProcErr + } + + f, err := os.Open(file) + if err != nil { + return nil, err + } + defer f.Close() + return StatFile(f) +} + +var ( + findProcErr error + procGetFileInformationByHandleEx *syscall.Proc +) + +func init() { + var modkernel32 *syscall.DLL + if modkernel32, findProcErr = syscall.LoadDLL("kernel32.dll"); findProcErr == nil { + procGetFileInformationByHandleEx, findProcErr = modkernel32.FindProc("GetFileInformationByHandleEx") + } +} + +// fileBasicInfo holds the C++ data for FileTimes. +// +// https://msdn.microsoft.com/en-us/library/windows/desktop/aa364217(v=vs.85).aspx +type fileBasicInfo struct { + CreationTime syscall.Filetime + LastAccessTime syscall.Filetime + LastWriteTime syscall.Filetime + ChangeTime syscall.Filetime + FileAttributes uint32 + _ uint32 // padding +} + +type fileInformationClass int + +const ( + fileBasicInfoClass fileInformationClass = iota +) + +func getFileInformationByHandleEx(handle syscall.Handle, data *fileBasicInfo) (err error) { + if findProcErr != nil { + return findProcErr + } + + r1, _, e1 := syscall.Syscall6(procGetFileInformationByHandleEx.Addr(), 4, uintptr(handle), uintptr(fileBasicInfoClass), uintptr(unsafe.Pointer(data)), unsafe.Sizeof(*data), 0, 0) + if r1 == 0 { + if e1 != 0 { + err = error(e1) + } else { + err = syscall.EINVAL + } + } + return +} diff --git a/stat.go b/stat.go index 11f1fd6..ab579a3 100644 --- a/stat.go +++ b/stat.go @@ -21,13 +21,15 @@ func Stat(name string) (Timespec, error) { } // Timespec provides access to file times. -// ChangeTime() panics unless times.HasChangeTime is true and -// BirthTime() panics unless times.HasBirthTime is true +// ChangeTime() panics unless HasChangeTime() is true and +// BirthTime() panics unless HasBirthTime() is true. type Timespec interface { ModTime() time.Time AccessTime() time.Time ChangeTime() time.Time BirthTime() time.Time + HasChangeTime() bool + HasBirthTime() bool } type atime struct { @@ -40,6 +42,8 @@ type ctime struct { v time.Time } +func (ctime) HasChangeTime() bool { return true } + func (c ctime) ChangeTime() time.Time { return c.v } type mtime struct { @@ -52,12 +56,18 @@ type btime struct { v time.Time } +func (btime) HasBirthTime() bool { return true } + func (b btime) BirthTime() time.Time { return b.v } type noctime struct{} +func (noctime) HasChangeTime() bool { return false } + func (noctime) ChangeTime() time.Time { panic("ctime not available") } type nobtime struct{} +func (nobtime) HasBirthTime() bool { return false } + func (nobtime) BirthTime() time.Time { panic("birthtime not available") } diff --git a/times_test.go b/times_test.go index 1c9fc25..4f6abe5 100644 --- a/times_test.go +++ b/times_test.go @@ -54,10 +54,10 @@ func TestGet(t *testing.T) { if at.ModTime().Before(et) { t.Errorf("expected mtime to be recent: got %v instead of ~%v", at.ModTime(), et) } - if HasChangeTime && at.ChangeTime().Before(et) { + if at.HasChangeTime() && at.ChangeTime().Before(et) { t.Errorf("expected ctime to be recent: got %v instead of ~%v", at.ChangeTime(), et) } - if HasBirthTime && at.BirthTime().Before(et) { + if at.HasBirthTime() && at.BirthTime().Before(et) { t.Errorf("expected btime to be recent: got %v instead of ~%v", at.BirthTime(), et) } } diff --git a/times_windows.go b/times_windows.go index b64c23b..d419c23 100644 --- a/times_windows.go +++ b/times_windows.go @@ -26,7 +26,12 @@ type timespec struct { btime } -func getTimespec(fi os.FileInfo) (t timespec) { +func getTimespec(fi os.FileInfo) Timespec { + if ts, err := statEx(fi.Name()); err == nil { + return ts + } + + var t timespec stat := fi.Sys().(*syscall.Win32FileAttributeData) t.atime.v = time.Unix(0, stat.LastAccessTime.Nanoseconds()) t.mtime.v = time.Unix(0, stat.LastWriteTime.Nanoseconds())