From a3489456de86e2a60a3cdeb73a9326e07c724533 Mon Sep 17 00:00:00 2001 From: Nick Craig-Wood Date: Mon, 3 Aug 2026 12:46:43 +0100 Subject: [PATCH] serve s3: fix modtime not being set when only mtime metadata is supplied on PUT The mtime metadata fallback was nested inside the X-Amz-Meta-Mtime branch, so it only ran when X-Amz-Meta-Mtime was present but invalid - and then set the modtime from the invalid value's failed parse rather than parsing mtime. An object PUT with only mtime metadata kept the upload time as its modtime. Now the two keys are checked independently, as TouchObject already does. --- cmd/serve/s3/backend.go | 5 ++++- cmd/serve/s3/put_test.go | 23 +++++++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/cmd/serve/s3/backend.go b/cmd/serve/s3/backend.go index 913f0dc39..1ac2d2cba 100644 --- a/cmd/serve/s3/backend.go +++ b/cmd/serve/s3/backend.go @@ -424,8 +424,11 @@ func (b *s3Backend) PutObject( return result, _vfs.Chtimes(fp, ti, ti) } // ignore error since the file is successfully created + } - if val, ok := meta["mtime"]; ok { + if val, ok := meta["mtime"]; ok { + ti, err := swift.FloatStringToTime(val) + if err == nil { b.storeModtime(fp, meta, val) return result, _vfs.Chtimes(fp, ti, ti) } diff --git a/cmd/serve/s3/put_test.go b/cmd/serve/s3/put_test.go index 5cb266b63..88998b1b3 100644 --- a/cmd/serve/s3/put_test.go +++ b/cmd/serve/s3/put_test.go @@ -15,6 +15,7 @@ import ( "testing" "time" + "github.com/ncw/swift/v2" "github.com/rclone/gofakes3" "github.com/rclone/rclone/cmd/serve/proxy" "github.com/rclone/rclone/fs" @@ -152,6 +153,28 @@ func TestPutObjectFailureNewKey(t *testing.T) { } } +// TestPutObjectMtime checks that the object's modtime is set from the +// "X-Amz-Meta-Mtime" or "mtime" metadata supplied with the PUT. +func TestPutObjectMtime(t *testing.T) { + want := fstest.Time("2011-12-25T12:59:59.123456789Z") + for _, metaKey := range []string{"X-Amz-Meta-Mtime", "mtime"} { + t.Run(metaKey, func(t *testing.T) { + b, f, bucket := newPutTestBackend(t, "", nil) + ctx := context.Background() + const object = "mtime.txt" + + contents := []byte(random.String(50)) + meta := map[string]string{metaKey: swift.TimeToFloatString(want)} + _, err := b.PutObject(ctx, bucket, object, meta, bytes.NewReader(contents), int64(len(contents))) + require.NoError(t, err) + + o, err := f.NewObject(ctx, path.Join(bucket, object)) + require.NoError(t, err) + fstest.AssertTimeEqualWithPrecision(t, object, want, o.ModTime(ctx), f.Precision()) + }) + } +} + // waitForObject waits for bucket/object to appear on the backing Fs (e.g. // after the VFS write-back delay). func waitForObject(t *testing.T, f fs.Fs, bucket, object string) {