├── go.mod ├── linux.cover.dockerfile ├── go.sum ├── js.cover.sh ├── linux.cover.sh ├── js.cover.dockerfile ├── use_generic_stat.go ├── times_plan9.go ├── times_windows.go ├── times_aix.go ├── times_dragonfly.go ├── times_openbsd.go ├── times_solaris.go ├── times_nacl.go ├── times_js.go ├── times_darwin.go ├── times_netbsd.go ├── times_freebsd.go ├── times_wasip1.go ├── .github └── workflows │ └── go-test.yml ├── LICENSE ├── bench_test.go ├── times_windows_test.go ├── times.go ├── util_test.go ├── example └── main.go ├── README.md ├── times_test.go ├── times_linux_test.go ├── ctime_windows.go └── times_linux.go /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/djherbis/times 2 | 3 | go 1.16 4 | 5 | require golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c 6 | -------------------------------------------------------------------------------- /linux.cover.dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.17 2 | 3 | WORKDIR /go/src/github.com/djherbis/times 4 | COPY . . 5 | 6 | RUN GO111MODULE=auto go test -covermode=count -coverprofile=profile.cov 7 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c h1:aFV+BgZ4svzjfabn8ERpuB4JI4N6/rdy1iusx77G3oU= 2 | golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 3 | -------------------------------------------------------------------------------- /js.cover.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | docker build -f js.cover.dockerfile -t js.cover.djherbis.times . 5 | docker create --name js.cover.djherbis.times js.cover.djherbis.times 6 | docker cp js.cover.djherbis.times:/go/src/github.com/djherbis/times/profile.cov . 7 | docker rm -v js.cover.djherbis.times -------------------------------------------------------------------------------- /linux.cover.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | docker build -f linux.cover.dockerfile -t linux.cover.djherbis.times . 5 | docker create --name linux.cover.djherbis.times linux.cover.djherbis.times 6 | docker cp linux.cover.djherbis.times:/go/src/github.com/djherbis/times/profile.cov . 7 | docker rm -v linux.cover.djherbis.times -------------------------------------------------------------------------------- /js.cover.dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.17 2 | 3 | RUN curl -sL https://deb.nodesource.com/setup_17.x | bash 4 | RUN apt-get install --yes nodejs 5 | 6 | WORKDIR /go/src/github.com/djherbis/times 7 | COPY . . 8 | 9 | RUN GO111MODULE=auto GOOS=js GOARCH=wasm go test -covermode=count -coverprofile=profile.cov -exec="$(go env GOROOT)/misc/wasm/go_js_wasm_exec" 10 | -------------------------------------------------------------------------------- /use_generic_stat.go: -------------------------------------------------------------------------------- 1 | // +build !windows,!linux 2 | 3 | package times 4 | 5 | import "os" 6 | 7 | // Stat returns the Timespec for the given filename. 8 | func Stat(name string) (Timespec, error) { 9 | return stat(name, os.Stat) 10 | } 11 | 12 | // Lstat returns the Timespec for the given filename, and does not follow Symlinks. 13 | func Lstat(name string) (Timespec, error) { 14 | return stat(name, os.Lstat) 15 | } 16 | 17 | // StatFile returns the Timespec for the given *os.File. 18 | func StatFile(file *os.File) (Timespec, error) { 19 | fi, err := file.Stat() 20 | if err != nil { 21 | return nil, err 22 | } 23 | return getTimespec(fi), nil 24 | } 25 | -------------------------------------------------------------------------------- /times_plan9.go: -------------------------------------------------------------------------------- 1 | // Copyright 2009 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // http://golang.org/src/os/stat_plan9.go 6 | 7 | package times 8 | 9 | import ( 10 | "os" 11 | "syscall" 12 | "time" 13 | ) 14 | 15 | // HasChangeTime and HasBirthTime are true if and only if 16 | // the target OS supports them. 17 | const ( 18 | HasChangeTime = false 19 | HasBirthTime = false 20 | ) 21 | 22 | type timespec struct { 23 | atime 24 | mtime 25 | noctime 26 | nobtime 27 | } 28 | 29 | func getTimespec(fi os.FileInfo) (t timespec) { 30 | stat := fi.Sys().(*syscall.Dir) 31 | t.atime.v = time.Unix(int64(stat.Atime), 0) 32 | t.mtime.v = time.Unix(int64(stat.Mtime), 0) 33 | return t 34 | } 35 | -------------------------------------------------------------------------------- /times_windows.go: -------------------------------------------------------------------------------- 1 | // Copyright 2009 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // http://golang.org/src/os/stat_windows.go 6 | 7 | package times 8 | 9 | import ( 10 | "os" 11 | "syscall" 12 | "time" 13 | ) 14 | 15 | // HasChangeTime and HasBirthTime are true if and only if 16 | // the target OS supports them. 17 | const ( 18 | HasChangeTime = false 19 | HasBirthTime = true 20 | ) 21 | 22 | type timespec struct { 23 | atime 24 | mtime 25 | noctime 26 | btime 27 | } 28 | 29 | func getTimespec(fi os.FileInfo) Timespec { 30 | var t timespec 31 | stat := fi.Sys().(*syscall.Win32FileAttributeData) 32 | t.atime.v = time.Unix(0, stat.LastAccessTime.Nanoseconds()) 33 | t.mtime.v = time.Unix(0, stat.LastWriteTime.Nanoseconds()) 34 | t.btime.v = time.Unix(0, stat.CreationTime.Nanoseconds()) 35 | return t 36 | } 37 | -------------------------------------------------------------------------------- /times_aix.go: -------------------------------------------------------------------------------- 1 | // Copyright 2009 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // https://golang.org/src/os/stat_aix.go 6 | 7 | package times 8 | 9 | import ( 10 | "os" 11 | "syscall" 12 | "time" 13 | ) 14 | 15 | // HasChangeTime and HasBirthTime are true if and only if 16 | // the target OS supports them. 17 | const ( 18 | HasChangeTime = true 19 | HasBirthTime = false 20 | ) 21 | 22 | type timespec struct { 23 | atime 24 | mtime 25 | ctime 26 | nobtime 27 | } 28 | 29 | func timespecToTime(ts syscall.StTimespec_t) time.Time { 30 | return time.Unix(int64(ts.Sec), int64(ts.Nsec)) 31 | } 32 | 33 | func getTimespec(fi os.FileInfo) (t timespec) { 34 | stat := fi.Sys().(*syscall.Stat_t) 35 | t.atime.v = timespecToTime(stat.Atim) 36 | t.mtime.v = timespecToTime(stat.Mtim) 37 | t.ctime.v = timespecToTime(stat.Ctim) 38 | return t 39 | } 40 | -------------------------------------------------------------------------------- /times_dragonfly.go: -------------------------------------------------------------------------------- 1 | // Copyright 2009 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // http://golang.org/src/os/stat_dragonfly.go 6 | 7 | package times 8 | 9 | import ( 10 | "os" 11 | "syscall" 12 | "time" 13 | ) 14 | 15 | // HasChangeTime and HasBirthTime are true if and only if 16 | // the target OS supports them. 17 | const ( 18 | HasChangeTime = true 19 | HasBirthTime = false 20 | ) 21 | 22 | type timespec struct { 23 | atime 24 | mtime 25 | ctime 26 | nobtime 27 | } 28 | 29 | func timespecToTime(ts syscall.Timespec) time.Time { 30 | return time.Unix(int64(ts.Sec), int64(ts.Nsec)) 31 | } 32 | 33 | func getTimespec(fi os.FileInfo) (t timespec) { 34 | stat := fi.Sys().(*syscall.Stat_t) 35 | t.atime.v = timespecToTime(stat.Atim) 36 | t.mtime.v = timespecToTime(stat.Mtim) 37 | t.ctime.v = timespecToTime(stat.Ctim) 38 | return t 39 | } 40 | -------------------------------------------------------------------------------- /times_openbsd.go: -------------------------------------------------------------------------------- 1 | // Copyright 2009 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // http://golang.org/src/os/stat_openbsd.go 6 | 7 | package times 8 | 9 | import ( 10 | "os" 11 | "syscall" 12 | "time" 13 | ) 14 | 15 | // HasChangeTime and HasBirthTime are true if and only if 16 | // the target OS supports them. 17 | const ( 18 | HasChangeTime = true 19 | HasBirthTime = false 20 | ) 21 | 22 | type timespec struct { 23 | atime 24 | mtime 25 | ctime 26 | nobtime 27 | } 28 | 29 | func timespecToTime(ts syscall.Timespec) time.Time { 30 | return time.Unix(int64(ts.Sec), int64(ts.Nsec)) 31 | } 32 | 33 | func getTimespec(fi os.FileInfo) (t timespec) { 34 | stat := fi.Sys().(*syscall.Stat_t) 35 | t.atime.v = timespecToTime(stat.Atim) 36 | t.mtime.v = timespecToTime(stat.Mtim) 37 | t.ctime.v = timespecToTime(stat.Ctim) 38 | return t 39 | } 40 | -------------------------------------------------------------------------------- /times_solaris.go: -------------------------------------------------------------------------------- 1 | // Copyright 2009 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // http://golang.org/src/os/stat_solaris.go 6 | 7 | package times 8 | 9 | import ( 10 | "os" 11 | "syscall" 12 | "time" 13 | ) 14 | 15 | // HasChangeTime and HasBirthTime are true if and only if 16 | // the target OS supports them. 17 | const ( 18 | HasChangeTime = true 19 | HasBirthTime = false 20 | ) 21 | 22 | type timespec struct { 23 | atime 24 | mtime 25 | ctime 26 | nobtime 27 | } 28 | 29 | func timespecToTime(ts syscall.Timespec) time.Time { 30 | return time.Unix(int64(ts.Sec), int64(ts.Nsec)) 31 | } 32 | 33 | func getTimespec(fi os.FileInfo) (t timespec) { 34 | stat := fi.Sys().(*syscall.Stat_t) 35 | t.atime.v = timespecToTime(stat.Atim) 36 | t.mtime.v = timespecToTime(stat.Mtim) 37 | t.ctime.v = timespecToTime(stat.Ctim) 38 | return t 39 | } 40 | -------------------------------------------------------------------------------- /times_nacl.go: -------------------------------------------------------------------------------- 1 | // Copyright 2009 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // https://golang.org/src/os/stat_nacljs.go 6 | 7 | package times 8 | 9 | import ( 10 | "os" 11 | "syscall" 12 | "time" 13 | ) 14 | 15 | // HasChangeTime and HasBirthTime are true if and only if 16 | // the target OS supports them. 17 | const ( 18 | HasChangeTime = true 19 | HasBirthTime = false 20 | ) 21 | 22 | type timespec struct { 23 | atime 24 | mtime 25 | ctime 26 | nobtime 27 | } 28 | 29 | func timespecToTime(sec, nsec int64) time.Time { 30 | return time.Unix(sec, nsec) 31 | } 32 | 33 | func getTimespec(fi os.FileInfo) (t timespec) { 34 | stat := fi.Sys().(*syscall.Stat_t) 35 | t.atime.v = timespecToTime(stat.Atime, stat.AtimeNsec) 36 | t.mtime.v = timespecToTime(stat.Mtime, stat.MtimeNsec) 37 | t.ctime.v = timespecToTime(stat.Ctime, stat.CtimeNsec) 38 | return t 39 | } 40 | -------------------------------------------------------------------------------- /times_js.go: -------------------------------------------------------------------------------- 1 | // Copyright 2009 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // https://golang.org/src/os/stat_nacljs.go 6 | 7 | // +build js,wasm 8 | 9 | package times 10 | 11 | import ( 12 | "os" 13 | "syscall" 14 | "time" 15 | ) 16 | 17 | // HasChangeTime and HasBirthTime are true if and only if 18 | // the target OS supports them. 19 | const ( 20 | HasChangeTime = true 21 | HasBirthTime = false 22 | ) 23 | 24 | type timespec struct { 25 | atime 26 | mtime 27 | ctime 28 | nobtime 29 | } 30 | 31 | func timespecToTime(sec, nsec int64) time.Time { 32 | return time.Unix(sec, nsec) 33 | } 34 | 35 | func getTimespec(fi os.FileInfo) (t timespec) { 36 | stat := fi.Sys().(*syscall.Stat_t) 37 | t.atime.v = timespecToTime(stat.Atime, stat.AtimeNsec) 38 | t.mtime.v = timespecToTime(stat.Mtime, stat.MtimeNsec) 39 | t.ctime.v = timespecToTime(stat.Ctime, stat.CtimeNsec) 40 | return t 41 | } 42 | -------------------------------------------------------------------------------- /times_darwin.go: -------------------------------------------------------------------------------- 1 | // Copyright 2009 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // http://golang.org/src/os/stat_darwin.go 6 | 7 | package times 8 | 9 | import ( 10 | "os" 11 | "syscall" 12 | "time" 13 | ) 14 | 15 | // HasChangeTime and HasBirthTime are true if and only if 16 | // the target OS supports them. 17 | const ( 18 | HasChangeTime = true 19 | HasBirthTime = true 20 | ) 21 | 22 | type timespec struct { 23 | atime 24 | mtime 25 | ctime 26 | btime 27 | } 28 | 29 | func timespecToTime(ts syscall.Timespec) time.Time { 30 | return time.Unix(int64(ts.Sec), int64(ts.Nsec)) 31 | } 32 | 33 | func getTimespec(fi os.FileInfo) (t timespec) { 34 | stat := fi.Sys().(*syscall.Stat_t) 35 | t.atime.v = timespecToTime(stat.Atimespec) 36 | t.mtime.v = timespecToTime(stat.Mtimespec) 37 | t.ctime.v = timespecToTime(stat.Ctimespec) 38 | t.btime.v = timespecToTime(stat.Birthtimespec) 39 | return t 40 | } 41 | -------------------------------------------------------------------------------- /times_netbsd.go: -------------------------------------------------------------------------------- 1 | // Copyright 2009 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // http://golang.org/src/os/stat_netbsd.go 6 | 7 | package times 8 | 9 | import ( 10 | "os" 11 | "syscall" 12 | "time" 13 | ) 14 | 15 | // HasChangeTime and HasBirthTime are true if and only if 16 | // the target OS supports them. 17 | const ( 18 | HasChangeTime = true 19 | HasBirthTime = true 20 | ) 21 | 22 | type timespec struct { 23 | atime 24 | mtime 25 | ctime 26 | btime 27 | } 28 | 29 | func timespecToTime(ts syscall.Timespec) time.Time { 30 | return time.Unix(int64(ts.Sec), int64(ts.Nsec)) 31 | } 32 | 33 | func getTimespec(fi os.FileInfo) (t timespec) { 34 | stat := fi.Sys().(*syscall.Stat_t) 35 | t.atime.v = timespecToTime(stat.Atimespec) 36 | t.mtime.v = timespecToTime(stat.Mtimespec) 37 | t.ctime.v = timespecToTime(stat.Ctimespec) 38 | t.btime.v = timespecToTime(stat.Birthtimespec) 39 | return t 40 | } 41 | -------------------------------------------------------------------------------- /times_freebsd.go: -------------------------------------------------------------------------------- 1 | // Copyright 2009 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // http://golang.org/src/os/stat_freebsd.go 6 | 7 | package times 8 | 9 | import ( 10 | "os" 11 | "syscall" 12 | "time" 13 | ) 14 | 15 | // HasChangeTime and HasBirthTime are true if and only if 16 | // the target OS supports them. 17 | const ( 18 | HasChangeTime = true 19 | HasBirthTime = true 20 | ) 21 | 22 | type timespec struct { 23 | atime 24 | mtime 25 | ctime 26 | btime 27 | } 28 | 29 | func timespecToTime(ts syscall.Timespec) time.Time { 30 | return time.Unix(int64(ts.Sec), int64(ts.Nsec)) 31 | } 32 | 33 | func getTimespec(fi os.FileInfo) (t timespec) { 34 | stat := fi.Sys().(*syscall.Stat_t) 35 | t.atime.v = timespecToTime(stat.Atimespec) 36 | t.mtime.v = timespecToTime(stat.Mtimespec) 37 | t.ctime.v = timespecToTime(stat.Ctimespec) 38 | t.btime.v = timespecToTime(stat.Birthtimespec) 39 | return t 40 | } 41 | -------------------------------------------------------------------------------- /times_wasip1.go: -------------------------------------------------------------------------------- 1 | // Copyright 2009 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // https://github.com/golang/go/blob/master/src/os/stat_wasip1.go 6 | 7 | //go:build wasip1 8 | // +build wasip1 9 | 10 | package times 11 | 12 | import ( 13 | "os" 14 | "syscall" 15 | "time" 16 | ) 17 | 18 | // HasChangeTime and HasBirthTime are true if and only if 19 | // the target OS supports them. 20 | const ( 21 | HasChangeTime = true 22 | HasBirthTime = false 23 | ) 24 | 25 | type timespec struct { 26 | atime 27 | mtime 28 | ctime 29 | nobtime 30 | } 31 | 32 | func timespecToTime(sec, nsec int64) time.Time { 33 | return time.Unix(sec, nsec) 34 | } 35 | 36 | func getTimespec(fi os.FileInfo) (t timespec) { 37 | stat := fi.Sys().(*syscall.Stat_t) 38 | t.atime.v = timespecToTime(int64(stat.Atime), 0) 39 | t.mtime.v = timespecToTime(int64(stat.Mtime), 0) 40 | t.ctime.v = timespecToTime(int64(stat.Ctime), 0) 41 | return t 42 | } 43 | -------------------------------------------------------------------------------- /.github/workflows/go-test.yml: -------------------------------------------------------------------------------- 1 | name: go test 2 | on: 3 | workflow_dispatch: 4 | push: 5 | branches: 6 | - master 7 | schedule: 8 | - cron: '0 17 * * 1' # https://crontab.guru/#0_17_*_*_1 9 | 10 | jobs: 11 | build: 12 | runs-on: ${{ matrix.os }} 13 | strategy: 14 | matrix: 15 | os: [ubuntu-latest, windows-latest, macos-latest] 16 | js: [false] 17 | include: 18 | - os: ubuntu-latest 19 | js: true 20 | steps: 21 | - id: go-test 22 | uses: djherbis/actions/go-test@main 23 | with: 24 | github_token: ${{ secrets.GITHUB_TOKEN }} 25 | coveralls_parallel: true 26 | if: ${{ !matrix.js }} 27 | - id: go-test-js 28 | uses: djherbis/actions/go-test-js@main 29 | with: 30 | github_token: ${{ secrets.GITHUB_TOKEN }} 31 | coveralls_parallel: true 32 | if: ${{ matrix.js }} 33 | finish: 34 | needs: build 35 | runs-on: ubuntu-latest 36 | steps: 37 | - uses: shogo82148/actions-goveralls@v1 38 | with: 39 | parallel-finished: true 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Dustin H 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /bench_test.go: -------------------------------------------------------------------------------- 1 | package times 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | ) 7 | 8 | func BenchmarkGet(t *testing.B) { 9 | fileTest(t, func(f *os.File) { 10 | fi, err := os.Stat(f.Name()) 11 | if err != nil { 12 | t.Error(err) 13 | } 14 | 15 | for i := 0; i < t.N; i++ { 16 | Get(fi) 17 | } 18 | }) 19 | t.ReportAllocs() 20 | } 21 | 22 | func BenchmarkStatFile(t *testing.B) { 23 | fileTest(t, func(f *os.File) { 24 | for i := 0; i < t.N; i++ { 25 | StatFile(f) 26 | } 27 | }) 28 | t.ReportAllocs() 29 | } 30 | 31 | func BenchmarkStat(t *testing.B) { 32 | fileTest(t, func(f *os.File) { 33 | for i := 0; i < t.N; i++ { 34 | Stat(f.Name()) 35 | } 36 | }) 37 | t.ReportAllocs() 38 | } 39 | 40 | func BenchmarkLstat(t *testing.B) { 41 | fileTest(t, func(f *os.File) { 42 | for i := 0; i < t.N; i++ { 43 | Lstat(f.Name()) 44 | } 45 | }) 46 | t.ReportAllocs() 47 | } 48 | 49 | func BenchmarkOsStat(t *testing.B) { 50 | fileTest(t, func(f *os.File) { 51 | for i := 0; i < t.N; i++ { 52 | os.Stat(f.Name()) 53 | } 54 | }) 55 | t.ReportAllocs() 56 | } 57 | 58 | func BenchmarkOsLstat(t *testing.B) { 59 | fileTest(t, func(f *os.File) { 60 | for i := 0; i < t.N; i++ { 61 | os.Lstat(f.Name()) 62 | } 63 | }) 64 | t.ReportAllocs() 65 | } 66 | -------------------------------------------------------------------------------- /times_windows_test.go: -------------------------------------------------------------------------------- 1 | package times 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | "syscall" 7 | "testing" 8 | "time" 9 | ) 10 | 11 | func TestStatFileProcErr(t *testing.T) { 12 | fileTest(t, func(f *os.File) { 13 | findProcErr = errors.New("fake error") 14 | defer func() { findProcErr = nil }() 15 | 16 | _, err := StatFile(f) 17 | if err == nil { 18 | t.Error("got nil err, but err was expected!") 19 | } 20 | }) 21 | } 22 | 23 | func TestStatBadNameErr(t *testing.T) { 24 | _, err := platformSpecficStat(string([]byte{0})) 25 | if err != syscall.EINVAL { 26 | t.Error(err) 27 | } 28 | } 29 | 30 | func TestStatProcErrFallback(t *testing.T) { 31 | fileAndDirTest(t, func(name string) { 32 | findProcErr = errors.New("fake error") 33 | defer func() { findProcErr = nil }() 34 | 35 | ts, err := Stat(name) 36 | if err != nil { 37 | t.Error(err.Error()) 38 | } 39 | timespecTest(ts, newInterval(time.Now(), time.Second), t) 40 | }) 41 | } 42 | 43 | func TestLstatProcErrFallback(t *testing.T) { 44 | fileAndDirTest(t, func(name string) { 45 | findProcErr = errors.New("fake error") 46 | defer func() { findProcErr = nil }() 47 | 48 | ts, err := Lstat(name) 49 | if err != nil { 50 | t.Error(err.Error()) 51 | } 52 | timespecTest(ts, newInterval(time.Now(), time.Second), t) 53 | }) 54 | } 55 | -------------------------------------------------------------------------------- /times.go: -------------------------------------------------------------------------------- 1 | // Package times provides a platform-independent way to get atime, mtime, ctime and btime for files. 2 | package times 3 | 4 | import ( 5 | "os" 6 | "time" 7 | ) 8 | 9 | // Get returns the Timespec for the given FileInfo 10 | func Get(fi os.FileInfo) Timespec { 11 | return getTimespec(fi) 12 | } 13 | 14 | type statFunc func(string) (os.FileInfo, error) 15 | 16 | func stat(name string, sf statFunc) (Timespec, error) { 17 | fi, err := sf(name) 18 | if err != nil { 19 | return nil, err 20 | } 21 | return getTimespec(fi), nil 22 | } 23 | 24 | // Timespec provides access to file times. 25 | // ChangeTime() panics unless HasChangeTime() is true and 26 | // BirthTime() panics unless HasBirthTime() is true. 27 | type Timespec interface { 28 | ModTime() time.Time 29 | AccessTime() time.Time 30 | ChangeTime() time.Time 31 | BirthTime() time.Time 32 | HasChangeTime() bool 33 | HasBirthTime() bool 34 | } 35 | 36 | type atime struct { 37 | v time.Time 38 | } 39 | 40 | func (a atime) AccessTime() time.Time { return a.v } 41 | 42 | type ctime struct { 43 | v time.Time 44 | } 45 | 46 | func (ctime) HasChangeTime() bool { return true } 47 | 48 | func (c ctime) ChangeTime() time.Time { return c.v } 49 | 50 | type mtime struct { 51 | v time.Time 52 | } 53 | 54 | func (m mtime) ModTime() time.Time { return m.v } 55 | 56 | type btime struct { 57 | v time.Time 58 | } 59 | 60 | func (btime) HasBirthTime() bool { return true } 61 | 62 | func (b btime) BirthTime() time.Time { return b.v } 63 | 64 | type noctime struct{} 65 | 66 | func (noctime) HasChangeTime() bool { return false } 67 | 68 | func (noctime) ChangeTime() time.Time { panic("ctime not available") } 69 | 70 | type nobtime struct{} 71 | 72 | func (nobtime) HasBirthTime() bool { return false } 73 | 74 | func (nobtime) BirthTime() time.Time { panic("birthtime not available") } 75 | -------------------------------------------------------------------------------- /util_test.go: -------------------------------------------------------------------------------- 1 | package times 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "reflect" 7 | "runtime" 8 | "testing" 9 | "time" 10 | ) 11 | 12 | type timeRange struct { 13 | start time.Time 14 | end time.Time 15 | } 16 | 17 | func newInterval(t time.Time, dur time.Duration) timeRange { 18 | return timeRange{start: t.Add(-dur), end: t.Add(dur)} 19 | } 20 | 21 | func (t timeRange) Contains(findTime time.Time) bool { 22 | return !findTime.Before(t.start) && !findTime.After(t.end) 23 | } 24 | 25 | func fileAndDirTest(t testing.TB, testFunc func(name string)) { 26 | filenameTest(t, testFunc) 27 | dirTest(t, testFunc) 28 | } 29 | 30 | // creates a file and cleans it up after the test is run. 31 | func fileTest(t testing.TB, testFunc func(f *os.File)) { 32 | f, err := ioutil.TempFile("", "") 33 | if err != nil { 34 | t.Fatal(err) 35 | } 36 | defer os.Remove(f.Name()) 37 | defer f.Close() 38 | testFunc(f) 39 | } 40 | 41 | func filenameTest(t testing.TB, testFunc func(filename string)) { 42 | fileTest(t, func(f *os.File) { 43 | testFunc(f.Name()) 44 | }) 45 | } 46 | 47 | // creates a dir and cleans it up after the test is run. 48 | func dirTest(t testing.TB, testFunc func(dirname string)) { 49 | dirname, err := ioutil.TempDir("", "") 50 | if err != nil { 51 | t.Fatal(err) 52 | } 53 | defer os.Remove(dirname) 54 | testFunc(dirname) 55 | } 56 | 57 | type timeFetcher func(Timespec) time.Time 58 | 59 | func timespecTest(ts Timespec, r timeRange, t testing.TB, getTimes ...timeFetcher) { 60 | if len(getTimes) == 0 { 61 | getTimes = append(getTimes, Timespec.AccessTime, Timespec.ModTime) 62 | 63 | if ts.HasChangeTime() { 64 | getTimes = append(getTimes, Timespec.ChangeTime) 65 | } 66 | 67 | if ts.HasBirthTime() { 68 | getTimes = append(getTimes, Timespec.BirthTime) 69 | } 70 | } 71 | 72 | for _, getTime := range getTimes { 73 | if !r.Contains(getTime(ts)) { 74 | t.Errorf("expected %s=%s to be in range: \n[%s, %s]\n", GetFunctionName(getTime), getTime(ts), r.start, r.end) 75 | } 76 | } 77 | } 78 | 79 | func GetFunctionName(i interface{}) string { 80 | return runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name() 81 | } 82 | -------------------------------------------------------------------------------- /example/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "log" 7 | "os" 8 | "path/filepath" 9 | "time" 10 | 11 | "github.com/djherbis/times" 12 | ) 13 | 14 | func main() { 15 | switch len(os.Args) { 16 | case 1: 17 | tempFile() 18 | fmt.Println() 19 | tempDir() 20 | 21 | default: 22 | printTimes(os.Args[1]) 23 | } 24 | } 25 | 26 | func tempDir() { 27 | name, err := ioutil.TempDir("", "") 28 | if err != nil { 29 | log.Fatal(err) 30 | } 31 | defer os.Remove(name) 32 | fmt.Println("# DIR: " + name) 33 | 34 | symname := filepath.Join(filepath.Dir(name), "sym-"+filepath.Base(name)) 35 | if err := os.Symlink(name, symname); err != nil { 36 | log.Fatal(err) 37 | } 38 | defer os.Remove(symname) 39 | 40 | newAtime := time.Now().Add(-10 * time.Second) 41 | newMtime := time.Now().Add(10 * time.Second) 42 | if err := os.Chtimes(name, newAtime, newMtime); err != nil { 43 | log.Fatal(err) 44 | } 45 | 46 | printTimes(symname) 47 | } 48 | 49 | func tempFile() { 50 | f, err := ioutil.TempFile("", "") 51 | if err != nil { 52 | log.Fatal(err) 53 | } 54 | defer os.Remove(f.Name()) 55 | defer f.Close() 56 | fmt.Println("# FILE: " + f.Name()) 57 | 58 | symname := filepath.Join(filepath.Dir(f.Name()), "sym-"+filepath.Base(f.Name())) 59 | if err := os.Symlink(f.Name(), symname); err != nil { 60 | log.Fatal(err) 61 | } 62 | defer os.Remove(symname) 63 | 64 | newAtime := time.Now().Add(-10 * time.Second) 65 | newMtime := time.Now().Add(10 * time.Second) 66 | if err := os.Chtimes(f.Name(), newAtime, newMtime); err != nil { 67 | log.Fatal(err) 68 | } 69 | 70 | printTimes(symname) 71 | } 72 | 73 | func printTimes(name string) { 74 | fmt.Println("## Stat:", name) 75 | printTimespec(times.Stat(name)) 76 | 77 | fmt.Println("\n## Lstat:", name) 78 | printTimespec(times.Lstat(name)) 79 | } 80 | 81 | func printTimespec(ts times.Timespec, err error) { 82 | if err != nil { 83 | log.Fatal(err) 84 | } 85 | 86 | fmt.Println("AccessTime:", ts.AccessTime()) 87 | fmt.Println("ModTime:", ts.ModTime()) 88 | 89 | if ts.HasChangeTime() { 90 | fmt.Println("ChangeTime:", ts.ChangeTime()) 91 | } 92 | 93 | if ts.HasBirthTime() { 94 | fmt.Println("BirthTime:", ts.BirthTime()) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | times 2 | ========== 3 | 4 | [![GoDoc](https://godoc.org/github.com/djherbis/times?status.svg)](https://godoc.org/github.com/djherbis/times) 5 | [![Release](https://img.shields.io/github/release/djherbis/times.svg)](https://github.com/djherbis/times/releases/latest) 6 | [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg)](LICENSE.txt) 7 | [![go test](https://github.com/djherbis/times/actions/workflows/go-test.yml/badge.svg)](https://github.com/djherbis/times/actions/workflows/go-test.yml) 8 | [![Coverage Status](https://coveralls.io/repos/djherbis/times/badge.svg?branch=master)](https://coveralls.io/r/djherbis/times?branch=master) 9 | [![Go Report Card](https://goreportcard.com/badge/github.com/djherbis/times)](https://goreportcard.com/report/github.com/djherbis/times) 10 | [![Sourcegraph](https://sourcegraph.com/github.com/djherbis/times/-/badge.svg)](https://sourcegraph.com/github.com/djherbis/times?badge) 11 | 12 | Usage 13 | ------------ 14 | File Times for #golang 15 | 16 | Go has a hidden time functions for most platforms, this repo makes them accessible. 17 | 18 | ```go 19 | package main 20 | 21 | import ( 22 | "log" 23 | 24 | "github.com/djherbis/times" 25 | ) 26 | 27 | func main() { 28 | t, err := times.Stat("myfile") 29 | if err != nil { 30 | log.Fatal(err.Error()) 31 | } 32 | 33 | log.Println(t.AccessTime()) 34 | log.Println(t.ModTime()) 35 | 36 | if t.HasChangeTime() { 37 | log.Println(t.ChangeTime()) 38 | } 39 | 40 | if t.HasBirthTime() { 41 | log.Println(t.BirthTime()) 42 | } 43 | } 44 | ``` 45 | 46 | Supported Times 47 | ------------ 48 | | | windows | linux | solaris | dragonfly | nacl | freebsd | darwin | netbsd | openbsd | plan9 | js | aix | 49 | |:-----:|:-------:|:-----:|:-------:|:---------:|:------:|:-------:|:----:|:------:|:-------:|:-----:|:-----:|:-----:| 50 | | atime | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | 51 | | mtime | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | 52 | | ctime | ✓* | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | | ✓ | ✓ | 53 | | btime | ✓ | ✓* | | | | ✓ | ✓| ✓ | | | 54 | 55 | * Linux btime requires kernel 4.11 and filesystem support, so HasBirthTime = false. 56 | Use Timespec.HasBirthTime() to check if file has birth time. 57 | Get(FileInfo) never returns btime. 58 | * Windows XP does not have ChangeTime so HasChangeTime = false, 59 | however Vista onward does have ChangeTime so Timespec.HasChangeTime() will 60 | only return false on those platforms when the syscall used to obtain them fails. 61 | * Also note, Get(FileInfo) will now only return values available in FileInfo.Sys(), this means Stat() is required to get ChangeTime on Windows 62 | 63 | Installation 64 | ------------ 65 | ```sh 66 | go get -u github.com/djherbis/times 67 | ``` 68 | -------------------------------------------------------------------------------- /times_test.go: -------------------------------------------------------------------------------- 1 | package times 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | func TestStat(t *testing.T) { 11 | fileAndDirTest(t, func(name string) { 12 | ts, err := Stat(name) 13 | if err != nil { 14 | t.Error(err.Error()) 15 | } 16 | timespecTest(ts, newInterval(time.Now(), time.Second), t) 17 | }) 18 | } 19 | 20 | func TestGet(t *testing.T) { 21 | fileAndDirTest(t, func(name string) { 22 | fi, err := os.Stat(name) 23 | if err != nil { 24 | t.Error(err.Error()) 25 | } 26 | timespecTest(Get(fi), newInterval(time.Now(), time.Second), t) 27 | }) 28 | } 29 | 30 | func TestStatFile(t *testing.T) { 31 | fileTest(t, func(f *os.File) { 32 | ts, err := StatFile(f) 33 | if err != nil { 34 | t.Error(err.Error()) 35 | } 36 | timespecTest(ts, newInterval(time.Now(), time.Second), t) 37 | }) 38 | } 39 | 40 | func TestStatFileErr(t *testing.T) { 41 | fileTest(t, func(f *os.File) { 42 | f.Close() 43 | 44 | _, err := StatFile(f) 45 | if err == nil { 46 | t.Error("got nil err, but err was expected!") 47 | } 48 | }) 49 | } 50 | 51 | type tsFunc func(string) (Timespec, error) 52 | 53 | var offsetTime = -10 * time.Second 54 | 55 | func TestStatSymlink(t *testing.T) { 56 | testStatSymlink(Stat, time.Now().Add(offsetTime), t) 57 | } 58 | 59 | func TestLstatSymlink(t *testing.T) { 60 | testStatSymlink(Lstat, time.Now(), t) 61 | } 62 | 63 | func testStatSymlink(sf tsFunc, expectTime time.Time, t *testing.T) { 64 | fileAndDirTest(t, func(name string) { 65 | start := time.Now() 66 | 67 | symname := filepath.Join(filepath.Dir(name), "sym-"+filepath.Base(name)) 68 | if err := os.Symlink(name, symname); err != nil { 69 | t.Fatal(err.Error()) 70 | } 71 | defer os.Remove(symname) 72 | 73 | // modify the realFileTime so symlink and real file see diff values. 74 | realFileTime := start.Add(offsetTime) 75 | if err := os.Chtimes(name, realFileTime, realFileTime); err != nil { 76 | t.Fatal(err.Error()) 77 | } 78 | 79 | ts, err := sf(symname) 80 | if err != nil { 81 | t.Fatal(err.Error()) 82 | } 83 | timespecTest(ts, newInterval(expectTime, time.Second), t, Timespec.AccessTime, Timespec.ModTime) 84 | }) 85 | } 86 | 87 | func TestStatErr(t *testing.T) { 88 | _, err := Stat("badfile?") 89 | if err == nil { 90 | t.Error("expected an error") 91 | } 92 | } 93 | 94 | func TestLstatErr(t *testing.T) { 95 | _, err := Lstat("badfile?") 96 | if err == nil { 97 | t.Error("expected an error") 98 | } 99 | } 100 | 101 | func TestCheat(t *testing.T) { 102 | // not all times are available for all platforms 103 | // this allows us to get 100% test coverage for platforms which do not have 104 | // ChangeTime/BirthTime 105 | var c ctime 106 | if c.HasChangeTime() { 107 | c.ChangeTime() 108 | } 109 | 110 | var b btime 111 | if b.HasBirthTime() { 112 | b.BirthTime() 113 | } 114 | 115 | var paniced = false 116 | var nc noctime 117 | func() { 118 | if !nc.HasChangeTime() { 119 | defer func() { 120 | recover() 121 | paniced = true 122 | }() 123 | } 124 | nc.ChangeTime() 125 | }() 126 | 127 | if !paniced { 128 | t.Error("expected panic") 129 | } 130 | 131 | paniced = false 132 | var nb nobtime 133 | func() { 134 | if !nb.HasBirthTime() { 135 | defer func() { 136 | recover() 137 | paniced = true 138 | }() 139 | } 140 | nb.BirthTime() 141 | }() 142 | 143 | if !paniced { 144 | t.Error("expected panic") 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /times_linux_test.go: -------------------------------------------------------------------------------- 1 | package times 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | "sync/atomic" 7 | "testing" 8 | "time" 9 | 10 | "golang.org/x/sys/unix" 11 | ) 12 | 13 | func timeToStatx(t time.Time) unix.StatxTimestamp { 14 | nsec := time.Duration(t.UnixNano()) * time.Nanosecond 15 | nsec -= time.Duration(t.Unix()) * time.Second 16 | return unix.StatxTimestamp{Sec: t.Unix(), Nsec: uint32(nsec)} 17 | } 18 | 19 | func statxT(t time.Time, hasBtime bool) *unix.Statx_t { 20 | var statx unix.Statx_t 21 | 22 | statxt := timeToStatx(t) 23 | 24 | statx.Atime = statxt 25 | statx.Mtime = statxt 26 | statx.Ctime = statxt 27 | 28 | if hasBtime { 29 | statx.Mask = unix.STATX_BTIME 30 | statx.Btime = statxt 31 | } 32 | 33 | return &statx 34 | } 35 | 36 | type statxFuncTyp func(dirfd int, path string, flags int, mask int, stat *unix.Statx_t) (err error) 37 | 38 | func unsupportedStatx(dirfd int, path string, flags int, mask int, stat *unix.Statx_t) (err error) { 39 | return unix.ENOSYS 40 | } 41 | 42 | var errBadStatx = errors.New("bad") 43 | 44 | func badStatx(dirfd int, path string, flags int, mask int, stat *unix.Statx_t) (err error) { 45 | return errBadStatx 46 | } 47 | 48 | func fakeSupportedStatx(ts *unix.Statx_t) statxFuncTyp { 49 | return func(dirfd int, path string, flags int, mask int, stat *unix.Statx_t) (err error) { 50 | *stat = *ts 51 | return nil 52 | } 53 | } 54 | 55 | func setStatx(fn statxFuncTyp) func() { 56 | atomic.StoreInt32(&supportsStatx, 1) 57 | restoreStatx := statxFunc 58 | statxFunc = fn 59 | return func() { statxFunc = restoreStatx } 60 | } 61 | 62 | func TestStatx(t *testing.T) { 63 | tests := []struct { 64 | name string 65 | statx statxFuncTyp 66 | wantErr error 67 | }{ 68 | {name: "unsupported", statx: unsupportedStatx}, 69 | {name: "fake supported with btime", statx: fakeSupportedStatx(statxT(time.Now(), true))}, 70 | {name: "fake supported without btime", statx: fakeSupportedStatx(statxT(time.Now(), false))}, 71 | {name: "bad stat", statx: badStatx, wantErr: errBadStatx}, 72 | } 73 | for _, test := range tests { 74 | t.Run(test.name, func(t *testing.T) { 75 | t.Run("stat", func(t *testing.T) { 76 | restore := setStatx(test.statx) 77 | defer restore() 78 | 79 | fileAndDirTest(t, func(name string) { 80 | ts, err := Stat(name) 81 | if err != nil { 82 | if err == test.wantErr { 83 | return 84 | } 85 | t.Fatal(err.Error()) 86 | } 87 | timespecTest(ts, newInterval(time.Now(), time.Second), t) 88 | }) 89 | }) 90 | 91 | t.Run("statFile", func(t *testing.T) { 92 | restore := setStatx(test.statx) 93 | defer restore() 94 | 95 | fileAndDirTest(t, func(name string) { 96 | fi, err := os.Open(name) 97 | if err != nil { 98 | t.Fatal(err.Error()) 99 | } 100 | defer fi.Close() 101 | 102 | ts, err := StatFile(fi) 103 | if err != nil { 104 | if err == test.wantErr { 105 | return 106 | } 107 | t.Fatal(err.Error()) 108 | } 109 | timespecTest(ts, newInterval(time.Now(), time.Second), t) 110 | }) 111 | }) 112 | 113 | t.Run("lstat", func(t *testing.T) { 114 | restore := setStatx(test.statx) 115 | defer restore() 116 | 117 | fileAndDirTest(t, func(name string) { 118 | ts, err := Lstat(name) 119 | if err != nil { 120 | if err == test.wantErr { 121 | return 122 | } 123 | t.Fatal(err.Error()) 124 | } 125 | timespecTest(ts, newInterval(time.Now(), time.Second), t) 126 | }) 127 | }) 128 | }) 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /ctime_windows.go: -------------------------------------------------------------------------------- 1 | package times 2 | 3 | import ( 4 | "os" 5 | "syscall" 6 | "time" 7 | "unsafe" 8 | ) 9 | 10 | // Stat returns the Timespec for the given filename. 11 | func Stat(name string) (Timespec, error) { 12 | ts, err := platformSpecficStat(name) 13 | if err == nil { 14 | return ts, err 15 | } 16 | 17 | return stat(name, os.Stat) 18 | } 19 | 20 | // Lstat returns the Timespec for the given filename, and does not follow Symlinks. 21 | func Lstat(name string) (Timespec, error) { 22 | ts, err := platformSpecficLstat(name) 23 | if err == nil { 24 | return ts, err 25 | } 26 | 27 | return stat(name, os.Lstat) 28 | } 29 | 30 | type timespecEx struct { 31 | atime 32 | mtime 33 | ctime 34 | btime 35 | } 36 | 37 | // StatFile finds a Windows Timespec with ChangeTime. 38 | func StatFile(file *os.File) (Timespec, error) { 39 | return statFile(syscall.Handle(file.Fd())) 40 | } 41 | 42 | func statFile(h syscall.Handle) (Timespec, error) { 43 | var fileInfo fileBasicInfo 44 | if err := getFileInformationByHandleEx(h, &fileInfo); err != nil { 45 | return nil, err 46 | } 47 | 48 | var t timespecEx 49 | t.atime.v = time.Unix(0, fileInfo.LastAccessTime.Nanoseconds()) 50 | t.mtime.v = time.Unix(0, fileInfo.LastWriteTime.Nanoseconds()) 51 | t.ctime.v = time.Unix(0, fileInfo.ChangeTime.Nanoseconds()) 52 | t.btime.v = time.Unix(0, fileInfo.CreationTime.Nanoseconds()) 53 | return t, nil 54 | } 55 | 56 | func platformSpecficLstat(name string) (Timespec, error) { 57 | if findProcErr != nil { 58 | return nil, findProcErr 59 | } 60 | 61 | isSym, err := isSymlink(name) 62 | if err != nil { 63 | return nil, err 64 | } 65 | 66 | var attrs = uint32(syscall.FILE_FLAG_BACKUP_SEMANTICS) 67 | if isSym { 68 | attrs |= syscall.FILE_FLAG_OPEN_REPARSE_POINT 69 | } 70 | 71 | return openHandleAndStat(name, attrs) 72 | } 73 | 74 | func isSymlink(name string) (bool, error) { 75 | fi, err := os.Lstat(name) 76 | if err != nil { 77 | return false, err 78 | } 79 | return fi.Mode()&os.ModeSymlink != 0, nil 80 | } 81 | 82 | func platformSpecficStat(name string) (Timespec, error) { 83 | if findProcErr != nil { 84 | return nil, findProcErr 85 | } 86 | 87 | return openHandleAndStat(name, syscall.FILE_FLAG_BACKUP_SEMANTICS) 88 | } 89 | 90 | func openHandleAndStat(name string, attrs uint32) (Timespec, error) { 91 | pathp, e := syscall.UTF16PtrFromString(name) 92 | if e != nil { 93 | return nil, e 94 | } 95 | h, e := syscall.CreateFile(pathp, 96 | syscall.FILE_WRITE_ATTRIBUTES, syscall.FILE_SHARE_WRITE, nil, 97 | syscall.OPEN_EXISTING, attrs, 0) 98 | if e != nil { 99 | return nil, e 100 | } 101 | defer syscall.Close(h) 102 | 103 | return statFile(h) 104 | } 105 | 106 | var ( 107 | findProcErr error 108 | procGetFileInformationByHandleEx *syscall.Proc 109 | ) 110 | 111 | func init() { 112 | var modkernel32 *syscall.DLL 113 | if modkernel32, findProcErr = syscall.LoadDLL("kernel32.dll"); findProcErr == nil { 114 | procGetFileInformationByHandleEx, findProcErr = modkernel32.FindProc("GetFileInformationByHandleEx") 115 | } 116 | } 117 | 118 | // fileBasicInfo holds the C++ data for FileTimes. 119 | // 120 | // https://msdn.microsoft.com/en-us/library/windows/desktop/aa364217(v=vs.85).aspx 121 | type fileBasicInfo struct { 122 | CreationTime syscall.Filetime 123 | LastAccessTime syscall.Filetime 124 | LastWriteTime syscall.Filetime 125 | ChangeTime syscall.Filetime 126 | FileAttributes uint32 127 | _ uint32 // padding 128 | } 129 | 130 | type fileInformationClass int 131 | 132 | const ( 133 | fileBasicInfoClass fileInformationClass = iota 134 | ) 135 | 136 | func getFileInformationByHandleEx(handle syscall.Handle, data *fileBasicInfo) (err error) { 137 | if findProcErr != nil { 138 | return findProcErr 139 | } 140 | 141 | r1, _, e1 := syscall.Syscall6(procGetFileInformationByHandleEx.Addr(), 4, uintptr(handle), uintptr(fileBasicInfoClass), uintptr(unsafe.Pointer(data)), unsafe.Sizeof(*data), 0, 0) 142 | if r1 == 0 { 143 | err = syscall.EINVAL 144 | if e1 != 0 { 145 | err = error(e1) 146 | } 147 | } 148 | return 149 | } 150 | -------------------------------------------------------------------------------- /times_linux.go: -------------------------------------------------------------------------------- 1 | // Copyright 2009 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // http://golang.org/src/os/stat_linux.go 6 | 7 | package times 8 | 9 | import ( 10 | "errors" 11 | "os" 12 | "sync/atomic" 13 | "syscall" 14 | "time" 15 | 16 | "golang.org/x/sys/unix" 17 | ) 18 | 19 | // HasChangeTime and HasBirthTime are true if and only if 20 | // the target OS supports them. 21 | const ( 22 | HasChangeTime = true 23 | HasBirthTime = false 24 | ) 25 | 26 | type timespec struct { 27 | atime 28 | mtime 29 | ctime 30 | nobtime 31 | } 32 | 33 | type timespecBtime struct { 34 | atime 35 | mtime 36 | ctime 37 | btime 38 | } 39 | 40 | var ( 41 | supportsStatx int32 = 1 42 | statxFunc = unix.Statx 43 | ) 44 | 45 | func isStatXSupported() bool { 46 | return atomic.LoadInt32(&supportsStatx) == 1 47 | } 48 | 49 | func isStatXUnsupported(err error) bool { 50 | // linux 4.10 and earlier does not support Statx syscall 51 | if err != nil && errors.Is(err, unix.ENOSYS) { 52 | atomic.StoreInt32(&supportsStatx, 0) 53 | return true 54 | } 55 | return false 56 | } 57 | 58 | // Stat returns the Timespec for the given filename. 59 | func Stat(name string) (Timespec, error) { 60 | if isStatXSupported() { 61 | ts, err := statX(name) 62 | if err == nil { 63 | return ts, nil 64 | } 65 | if !isStatXUnsupported(err) { 66 | return nil, err 67 | } 68 | // Fallback. 69 | } 70 | return stat(name, os.Stat) 71 | } 72 | 73 | func statX(name string) (Timespec, error) { 74 | // https://man7.org/linux/man-pages/man2/statx.2.html 75 | var statx unix.Statx_t 76 | err := statxFunc(unix.AT_FDCWD, name, unix.AT_STATX_SYNC_AS_STAT, unix.STATX_ATIME|unix.STATX_MTIME|unix.STATX_CTIME|unix.STATX_BTIME, &statx) 77 | if err != nil { 78 | return nil, err 79 | } 80 | return extractTimes(&statx), nil 81 | } 82 | 83 | // Lstat returns the Timespec for the given filename, and does not follow Symlinks. 84 | func Lstat(name string) (Timespec, error) { 85 | if isStatXSupported() { 86 | ts, err := lstatx(name) 87 | if err == nil { 88 | return ts, nil 89 | } 90 | if !isStatXUnsupported(err) { 91 | return nil, err 92 | } 93 | // Fallback. 94 | } 95 | return stat(name, os.Lstat) 96 | } 97 | 98 | func lstatx(name string) (Timespec, error) { 99 | // https://man7.org/linux/man-pages/man2/statx.2.html 100 | var statX unix.Statx_t 101 | err := statxFunc(unix.AT_FDCWD, name, unix.AT_STATX_SYNC_AS_STAT|unix.AT_SYMLINK_NOFOLLOW, unix.STATX_ATIME|unix.STATX_MTIME|unix.STATX_CTIME|unix.STATX_BTIME, &statX) 102 | if err != nil { 103 | return nil, err 104 | } 105 | return extractTimes(&statX), nil 106 | } 107 | 108 | func statXFile(file *os.File) (Timespec, error) { 109 | sc, err := file.SyscallConn() 110 | if err != nil { 111 | return nil, err 112 | } 113 | 114 | var statx unix.Statx_t 115 | var statxErr error 116 | err = sc.Control(func(fd uintptr) { 117 | // https://man7.org/linux/man-pages/man2/statx.2.html 118 | statxErr = statxFunc(int(fd), "", unix.AT_EMPTY_PATH|unix.AT_STATX_SYNC_AS_STAT, unix.STATX_ATIME|unix.STATX_MTIME|unix.STATX_CTIME|unix.STATX_BTIME, &statx) 119 | }) 120 | if err != nil { 121 | return nil, err 122 | } 123 | 124 | if statxErr != nil { 125 | return nil, statxErr 126 | } 127 | 128 | return extractTimes(&statx), nil 129 | } 130 | 131 | // StatFile returns the Timespec for the given *os.File. 132 | func StatFile(file *os.File) (Timespec, error) { 133 | if isStatXSupported() { 134 | ts, err := statXFile(file) 135 | if err == nil { 136 | return ts, nil 137 | } 138 | if !isStatXUnsupported(err) { 139 | return nil, err 140 | } 141 | // Fallback. 142 | } 143 | return statFile(file) 144 | } 145 | 146 | func statFile(file *os.File) (Timespec, error) { 147 | fi, err := file.Stat() 148 | if err != nil { 149 | return nil, err 150 | } 151 | return getTimespec(fi), nil 152 | } 153 | 154 | func statxTimestampToTime(ts unix.StatxTimestamp) time.Time { 155 | return time.Unix(ts.Sec, int64(ts.Nsec)) 156 | } 157 | 158 | func extractTimes(statx *unix.Statx_t) Timespec { 159 | if statx.Mask&unix.STATX_BTIME == unix.STATX_BTIME { 160 | var t timespecBtime 161 | t.atime.v = statxTimestampToTime(statx.Atime) 162 | t.mtime.v = statxTimestampToTime(statx.Mtime) 163 | t.ctime.v = statxTimestampToTime(statx.Ctime) 164 | t.btime.v = statxTimestampToTime(statx.Btime) 165 | return t 166 | } 167 | 168 | var t timespec 169 | t.atime.v = statxTimestampToTime(statx.Atime) 170 | t.mtime.v = statxTimestampToTime(statx.Mtime) 171 | t.ctime.v = statxTimestampToTime(statx.Ctime) 172 | return t 173 | } 174 | 175 | func timespecToTime(ts syscall.Timespec) time.Time { 176 | return time.Unix(int64(ts.Sec), int64(ts.Nsec)) 177 | } 178 | 179 | func getTimespec(fi os.FileInfo) (t timespec) { 180 | stat := fi.Sys().(*syscall.Stat_t) 181 | t.atime.v = timespecToTime(stat.Atim) 182 | t.mtime.v = timespecToTime(stat.Mtim) 183 | t.ctime.v = timespecToTime(stat.Ctim) 184 | return t 185 | } 186 | --------------------------------------------------------------------------------