diff --git a/command/cp.go b/command/cp.go index c5d62072f..488f079c5 100644 --- a/command/cp.go +++ b/command/cp.go @@ -554,7 +554,7 @@ func (c Copy) doDownload(ctx context.Context, srcurl *url.URL, dsturl *url.URL) if err != nil { return err } - err = storage.SetFileTime(dsturl.Absolute(), *obj.CreateTime, *obj.ModTime) + err = storage.SetFileTime(dsturl.Absolute(), *obj.AccessTime, *obj.ModTime, *obj.CreateTime) if err != nil { return err } @@ -609,11 +609,11 @@ func (c Copy) doUpload(ctx context.Context, srcurl *url.URL, dsturl *url.URL) er SetExpires(c.expires) if c.preserveTimestamp { - cTime, mTime, err := storage.GetFileTime(srcurl.Absolute()) + aTime, mTime, cTime, err := storage.GetFileTime(srcurl.Absolute()) if err != nil { return err } - metadata.SetPreserveTimestamp(cTime.String(), mTime.String()) + metadata.SetPreserveTimestamp(aTime, mTime, cTime) } if c.contentType != "" { diff --git a/storage/fs_darwin.go b/storage/fs_darwin.go index 75c9e7455..b781e24a9 100644 --- a/storage/fs_darwin.go +++ b/storage/fs_darwin.go @@ -8,22 +8,38 @@ import ( "time" ) -func GetFileTime(filename string) (time.Time, time.Time, error) { +func GetFileTime(filename string) (time.Time, time.Time, time.Time, error) { fi, err := os.Stat(filename) if err != nil { - return time.Time{}, time.Time{}, err + return time.Time{}, time.Time{}, time.Time{}, err } stat := fi.Sys().(*syscall.Stat_t) cTime := time.Unix(int64(stat.Ctimespec.Sec), int64(stat.Ctimespec.Nsec)) + aTime := time.Unix(int64(stat.Atimespec.Sec), int64(stat.Atimespec.Nsec)) mTime := fi.ModTime() - return cTime, mTime, nil + return aTime, mTime, cTime, nil } -func SetFileTime(filename string, creationTime time.Time, modTime time.Time) error { - err := os.Chtimes(filename, modTime, modTime) +func SetFileTime(filename string, accessTime, modificationTime, creationTime time.Time) error { + var err error + if accessTime.IsZero() && modificationTime.IsZero() { + // Nothing recorded in s3. Return fast. + return nil + } else if accessTime.IsZero() { + accessTime, _, _, err = GetFileTime(filename) + if err != nil { + return err + } + } else if modificationTime.IsZero() { + _, modificationTime, _, err = GetFileTime(filename) + if err != nil { + return err + } + } + err = os.Chtimes(filename, accessTime, modificationTime) if err != nil { return err } diff --git a/storage/fs_linux.go b/storage/fs_linux.go index 4674474b0..c23e3d8ff 100644 --- a/storage/fs_linux.go +++ b/storage/fs_linux.go @@ -8,22 +8,40 @@ import ( "time" ) -func GetFileTime(filename string) (time.Time, time.Time, error) { +func GetFileTime(filename string) (time.Time, time.Time, time.Time, error) { fi, err := os.Stat(filename) if err != nil { - return time.Time{}, time.Time{}, err + return time.Time{}, time.Time{}, time.Time{}, err } stat := fi.Sys().(*syscall.Stat_t) cTime := time.Unix(int64(stat.Ctim.Sec), int64(stat.Ctim.Nsec)) + aTime := time.Unix(int64(stat.Atim.Sec), int64(stat.Atim.Nsec)) mTime := fi.ModTime() - return cTime, mTime, nil + return aTime, mTime, cTime, nil } -func SetFileTime(filename string, creationTime time.Time, modTime time.Time) error { - err := os.Chtimes(filename, modTime, modTime) +func SetFileTime(filename string, accessTime, modificationTime, creationTime time.Time) error { + if accessTime.IsZero() && modificationTime.IsZero() { + // Nothing recorded in s3. Return fast. + return nil + } + var err error + if accessTime.IsZero() { + accessTime, _, _, err = GetFileTime(filename) + if err != nil { + return err + } + } + if modificationTime.IsZero() { + _, modificationTime, _, err = GetFileTime(filename) + if err != nil { + return err + } + } + err = os.Chtimes(filename, accessTime, modificationTime) if err != nil { return err } diff --git a/storage/fs_windows.go b/storage/fs_windows.go index 3dcbca0a2..376a86223 100644 --- a/storage/fs_windows.go +++ b/storage/fs_windows.go @@ -8,29 +8,55 @@ import ( "time" ) -func GetFileTime(filename string) (time.Time, time.Time, error) { +func GetFileTime(filename string) (time.Time, time.Time, time.Time, error) { fi, err := os.Stat(filename) if err != nil { - return time.Time{}, time.Time{}, err + return time.Time{}, time.Time{}, time.Time{}, err } d := fi.Sys().(*syscall.Win32FileAttributeData) cTime := time.Unix(0, d.CreationTime.Nanoseconds()) + aTime := time.Unix(0, d.LastAccessTime.Nanoseconds()) mTime := fi.ModTime() - return cTime, mTime, nil + return aTime, mTime, cTime, nil } -func SetFileTime(filename string, creationTime time.Time, modTime time.Time) error { +func SetFileTime(filename string, accessTime, modificationTime, creationTime time.Time) error { + var err error + if accessTime.IsZero() && modificationTime.IsZero() && creationTime.IsZero() { + // Nothing recorded in s3. Return fast. + return nil + } else if accessTime.IsZero() { + accessTime, _, _, err = GetFileTime(filename) + if err != nil { + return err + } + } else if modificationTime.IsZero() { + _, modificationTime, _, err = GetFileTime(filename) + if err != nil { + return err + } + } else if creationTime.IsZero() { + _, _, creationTime, err = GetFileTime(filename) + if err != nil { + return err + } + } + + aft := syscall.NsecToFiletime(accessTime.UnixNano()) + mft := syscall.NsecToFiletime(modificationTime.UnixNano()) + cft := syscall.NsecToFiletime(creationTime.UnixNano()) + fd, err := syscall.Open(filename, os.O_RDWR, 0775) if err != nil { return err } + err = syscall.SetFileTime(fd, &cft, &aft, &mft) + + defer syscall.Close(fd) - cft := syscall.NsecToFiletime(int64(creationTime.Nanosecond())) - mft := syscall.NsecToFiletime(int64(modTime.Nanosecond())) - err = syscall.SetFileTime(fd, &cft, &mft, &mft) if err != nil { return err } diff --git a/storage/s3.go b/storage/s3.go index 58745a8b6..829fc9878 100644 --- a/storage/s3.go +++ b/storage/s3.go @@ -131,10 +131,42 @@ func (s *S3) Stat(ctx context.Context, url *url.URL) (*Object, error) { mod := aws.TimeValue(output.LastModified) obj := &Object{ - URL: url, - Etag: strings.Trim(etag, `"`), - ModTime: &mod, - Size: aws.Int64Value(output.ContentLength), + URL: url, + Etag: strings.Trim(etag, `"`), + ModTime: &mod, + Size: aws.Int64Value(output.ContentLength), + CreateTime: &time.Time{}, + AccessTime: &time.Time{}, + } + + cTimeS := aws.StringValue(output.Metadata["file-ctime"]) + if cTimeS != "" { + ctime, err := strconv.ParseInt(cTimeS, 10, 64) + if err != nil { + return nil, err + } + creationTime := time.Unix(0, ctime) + obj.CreateTime = &creationTime + } + + mTimeS := aws.StringValue(output.Metadata["file-mtime"]) + if mTimeS != "" { + mtime, err := strconv.ParseInt(mTimeS, 10, 64) + if err != nil { + return nil, err + } + modificationTime := time.Unix(0, mtime) + obj.ModTime = &modificationTime + } + + aTimeS := aws.StringValue(output.Metadata["file-atime"]) + if aTimeS != "" { + atime, err := strconv.ParseInt(aTimeS, 10, 64) + if err != nil { + return nil, err + } + accessTime := time.Unix(0, atime) + obj.AccessTime = &accessTime } ctime := output.Metadata["x-amz-meta-file-ctime"] @@ -411,12 +443,17 @@ func (s *S3) Copy(ctx context.Context, from, to *url.URL, metadata Metadata) err ctime := metadata.cTime() if ctime != "" { - input.Metadata["x-amz-meta-file-ctime"] = aws.String(ctime) + input.Metadata["file-ctime"] = aws.String(ctime) } mtime := metadata.mTime() if ctime != "" { - input.Metadata["x-amz-meta-file-mtime"] = aws.String(mtime) + input.Metadata["file-mtime"] = aws.String(mtime) + } + + atime := metadata.aTime() + if ctime != "" { + input.Metadata["file-atime"] = aws.String(atime) } _, err := s.api.CopyObject(input) return err @@ -583,6 +620,21 @@ func (s *S3) Put( input.Expires = aws.Time(t) } + ctime := metadata.cTime() + if ctime != "" { + input.Metadata["file-ctime"] = aws.String(ctime) + } + + mtime := metadata.mTime() + if ctime != "" { + input.Metadata["file-mtime"] = aws.String(mtime) + } + + atime := metadata.aTime() + if ctime != "" { + input.Metadata["file-atime"] = aws.String(atime) + } + sseEncryption := metadata.SSE() if sseEncryption != "" { input.ServerSideEncryption = aws.String(sseEncryption) diff --git a/storage/storage.go b/storage/storage.go index a4968c1ed..b6c220b7b 100644 --- a/storage/storage.go +++ b/storage/storage.go @@ -6,6 +6,7 @@ import ( "encoding/json" "fmt" "os" + "strconv" "time" "github.com/peak/s5cmd/log" @@ -105,6 +106,7 @@ func (o *Options) SetRegion(region string) { type Object struct { URL *url.URL `json:"key,omitempty"` Etag string `json:"etag,omitempty"` + AccessTime *time.Time `json:"accessed,omitempty"` ModTime *time.Time `json:"last_modified,omitempty"` CreateTime *time.Time `json:"created,omitempty"` Type ObjectType `json:"type,omitempty"` @@ -238,16 +240,21 @@ func (m Metadata) SetExpires(expires string) Metadata { } func (m Metadata) cTime() string { - return m["x-amz-meta-file-ctime"] + return m["file-ctime"] } func (m Metadata) mTime() string { - return m["x-amz-meta-file-mtime"] + return m["file-mtime"] } -func (m Metadata) SetPreserveTimestamp(cTime, mTime string) Metadata { - m["x-amz-meta-file-ctime"] = cTime - m["x-amz-meta-file-mtime"] = mTime +func (m Metadata) aTime() string { + return m["file-atime"] +} + +func (m Metadata) SetPreserveTimestamp(aTime, mTime, cTime time.Time) Metadata { + m["file-ctime"] = strconv.Itoa(int(cTime.UnixNano())) + m["file-mtime"] = strconv.Itoa(int(mTime.UnixNano())) + m["file-atime"] = strconv.Itoa(int(aTime.UnixNano())) return m }