From ac6ba11d22d51d5d5b6419ea18197edee97f6a96 Mon Sep 17 00:00:00 2001 From: Nick Craig-Wood Date: Fri, 8 Dec 2023 15:26:53 +0000 Subject: [PATCH] local: add --local-time-type to use mtime/atime/btime/ctime as the time Fixes #7484 --- backend/local/local.go | 62 ++++++++++++++++++++++++++++++- backend/local/metadata_bsd.go | 19 ++++++++++ backend/local/metadata_linux.go | 18 +++++++++ backend/local/metadata_other.go | 7 ++++ backend/local/metadata_unix.go | 17 +++++++++ backend/local/metadata_windows.go | 17 +++++++++ 6 files changed, 138 insertions(+), 2 deletions(-) diff --git a/backend/local/local.go b/backend/local/local.go index 848375901..3284a6e51 100644 --- a/backend/local/local.go +++ b/backend/local/local.go @@ -36,6 +36,27 @@ const devUnset = 0xdeadbeefcafebabe // a d const linkSuffix = ".rclonelink" // The suffix added to a translated symbolic link const useReadDir = (runtime.GOOS == "windows" || runtime.GOOS == "plan9") // these OSes read FileInfos directly +// timeType allows the user to choose what exactly ModTime() returns +type timeType = fs.Enum[timeTypeChoices] + +const ( + mTime timeType = iota + aTime + bTime + cTime +) + +type timeTypeChoices struct{} + +func (timeTypeChoices) Choices() []string { + return []string{ + mTime: "mtime", + aTime: "atime", + bTime: "btime", + cTime: "ctime", + } +} + // Register with Fs func init() { fsi := &fs.RegInfo{ @@ -213,6 +234,42 @@ when copying to a CIFS mount owned by another user. If this option is enabled, rclone will no longer update the modtime after copying a file.`, Default: false, Advanced: true, + }, { + Name: "time_type", + Help: `Set what kind of time is returned. + +Normally rclone does all operations on the mtime or Modification time. + +If you set this flag then rclone will return the Modified time as whatever +you set here. So if you use "rclone lsl --local-time-type ctime" then +you will see ctimes in the listing. + +If the OS doesn't support returning the time_type specified then rclone +will silently replace it with the modification time which all OSes support. + +- mtime is supported by all OSes +- atime is supported on all OSes except: plan9, js +- btime is only supported on: Windows, macOS, freebsd, netbsd +- ctime is supported on all Oses except: Windows, plan9, js + +Note that setting the time will still set the modified time so this is +only useful for reading. +`, + Default: mTime, + Advanced: true, + Examples: []fs.OptionExample{{ + Value: mTime.String(), + Help: "The last modification time.", + }, { + Value: aTime.String(), + Help: "The last access time.", + }, { + Value: bTime.String(), + Help: "The creation time.", + }, { + Value: cTime.String(), + Help: "The last status change time.", + }}, }, { Name: config.ConfigEncoding, Help: config.ConfigEncodingHelp, @@ -237,6 +294,7 @@ type Options struct { NoPreAllocate bool `config:"no_preallocate"` NoSparse bool `config:"no_sparse"` NoSetModTime bool `config:"no_set_modtime"` + TimeType timeType `config:"time_type"` Enc encoder.MultiEncoder `config:"encoding"` } @@ -1132,7 +1190,7 @@ func (file *localOpenFile) Read(p []byte) (n int, err error) { if oldsize != fi.Size() { return 0, fserrors.NoLowLevelRetryError(fmt.Errorf("can't copy - source file is being updated (size changed from %d to %d)", oldsize, fi.Size())) } - if !oldtime.Equal(fi.ModTime()) { + if !oldtime.Equal(readTime(file.o.fs.opt.TimeType, fi)) { return 0, fserrors.NoLowLevelRetryError(fmt.Errorf("can't copy - source file is being updated (mod time changed from %v to %v)", oldtime, fi.ModTime())) } } @@ -1428,7 +1486,7 @@ func (o *Object) setMetadata(info os.FileInfo) { } o.fs.objectMetaMu.Lock() o.size = info.Size() - o.modTime = info.ModTime() + o.modTime = readTime(o.fs.opt.TimeType, info) o.mode = info.Mode() o.fs.objectMetaMu.Unlock() // Read the size of the link. diff --git a/backend/local/metadata_bsd.go b/backend/local/metadata_bsd.go index c4c90d226..87c18fc3e 100644 --- a/backend/local/metadata_bsd.go +++ b/backend/local/metadata_bsd.go @@ -5,12 +5,31 @@ package local import ( "fmt" + "os" "syscall" "time" "github.com/rclone/rclone/fs" ) +// Read the time specified from the os.FileInfo +func readTime(t timeType, fi os.FileInfo) time.Time { + stat, ok := fi.Sys().(*syscall.Stat_t) + if !ok { + fs.Debugf(nil, "didn't return Stat_t as expected") + return fi.ModTime() + } + switch t { + case aTime: + return time.Unix(stat.Atimespec.Unix()) + case bTime: + return time.Unix(stat.Birthtimespec.Unix()) + case cTime: + return time.Unix(stat.Ctimespec.Unix()) + } + return fi.ModTime() +} + // Read the metadata from the file into metadata where possible func (o *Object) readMetadataFromFile(m *fs.Metadata) (err error) { info, err := o.fs.lstat(o.path) diff --git a/backend/local/metadata_linux.go b/backend/local/metadata_linux.go index 47294adba..261fcdbd0 100644 --- a/backend/local/metadata_linux.go +++ b/backend/local/metadata_linux.go @@ -5,8 +5,10 @@ package local import ( "fmt" + "os" "runtime" "sync" + "syscall" "time" "github.com/rclone/rclone/fs" @@ -18,6 +20,22 @@ var ( readMetadataFromFileFn func(o *Object, m *fs.Metadata) (err error) ) +// Read the time specified from the os.FileInfo +func readTime(t timeType, fi os.FileInfo) time.Time { + stat, ok := fi.Sys().(*syscall.Stat_t) + if !ok { + fs.Debugf(nil, "didn't return Stat_t as expected") + return fi.ModTime() + } + switch t { + case aTime: + return time.Unix(stat.Atim.Unix()) + case cTime: + return time.Unix(stat.Ctim.Unix()) + } + return fi.ModTime() +} + // Read the metadata from the file into metadata where possible func (o *Object) readMetadataFromFile(m *fs.Metadata) (err error) { statxCheckOnce.Do(func() { diff --git a/backend/local/metadata_other.go b/backend/local/metadata_other.go index f2ce80956..154ecd8b5 100644 --- a/backend/local/metadata_other.go +++ b/backend/local/metadata_other.go @@ -5,10 +5,17 @@ package local import ( "fmt" + "os" + "time" "github.com/rclone/rclone/fs" ) +// Read the time specified from the os.FileInfo +func readTime(t timeType, fi os.FileInfo) time.Time { + return fi.ModTime() +} + // Read the metadata from the file into metadata where possible func (o *Object) readMetadataFromFile(m *fs.Metadata) (err error) { info, err := o.fs.lstat(o.path) diff --git a/backend/local/metadata_unix.go b/backend/local/metadata_unix.go index 48bf7faa3..dd7e26ecf 100644 --- a/backend/local/metadata_unix.go +++ b/backend/local/metadata_unix.go @@ -5,12 +5,29 @@ package local import ( "fmt" + "os" "syscall" "time" "github.com/rclone/rclone/fs" ) +// Read the time specified from the os.FileInfo +func readTime(t timeType, fi os.FileInfo) time.Time { + stat, ok := fi.Sys().(*syscall.Stat_t) + if !ok { + fs.Debugf(nil, "didn't return Stat_t as expected") + return fi.ModTime() + } + switch t { + case aTime: + return time.Unix(stat.Atim.Unix()) + case cTime: + return time.Unix(stat.Ctim.Unix()) + } + return fi.ModTime() +} + // Read the metadata from the file into metadata where possible func (o *Object) readMetadataFromFile(m *fs.Metadata) (err error) { info, err := o.fs.lstat(o.path) diff --git a/backend/local/metadata_windows.go b/backend/local/metadata_windows.go index b2588953e..d74c3f51e 100644 --- a/backend/local/metadata_windows.go +++ b/backend/local/metadata_windows.go @@ -5,12 +5,29 @@ package local import ( "fmt" + "os" "syscall" "time" "github.com/rclone/rclone/fs" ) +// Read the time specified from the os.FileInfo +func readTime(t timeType, fi os.FileInfo) time.Time { + stat, ok := fi.Sys().(*syscall.Win32FileAttributeData) + if !ok { + fs.Debugf(nil, "didn't return Win32FileAttributeData as expected") + return fi.ModTime() + } + switch t { + case aTime: + return time.Unix(0, stat.LastAccessTime.Nanoseconds()) + case bTime: + return time.Unix(0, stat.CreationTime.Nanoseconds()) + } + return fi.ModTime() +} + // Read the metadata from the file into metadata where possible func (o *Object) readMetadataFromFile(m *fs.Metadata) (err error) { info, err := o.fs.lstat(o.path)