Thanks to visit codestin.com
Credit goes to github.com

Skip to content

s3: preserve system metadata when setting the modification time - #9857

Open
tirdesh wants to merge 2 commits into
rclone:masterfrom
tirdesh:fix-5243-s3-setmodtime-metadata
Open

s3: preserve system metadata when setting the modification time#9857
tirdesh wants to merge 2 commits into
rclone:masterfrom
tirdesh:fix-5243-s3-setmodtime-metadata

Conversation

@tirdesh

@tirdesh tirdesh commented Sep 3, 2026

Copy link
Copy Markdown

What does this change do?

SetModTime stores the modification time in the object's user metadata, so its
copy-to-self has to use the REPLACE metadata directive. REPLACE drops any system
metadata header not supplied with the copy, and the request only carried the content type.

So updating a modification time silently dropped Cache-Control,
Content-Disposition, Content-Encoding and Content-Language, and replaced the object's
storage class with --s3-storage-class or the account default rather than keeping the
class the object already had.

No extra request is needed to fix it, per the optimization @ncw noted on the issue:
SetModTime already calls readMetaData, which populates these fields, so they were in
hand and simply were not being passed to the copy. This passes them through.

New test InternalTestSetModTimeMetadata fails on master on all four headers.

Linked issue

Fixes #5243

For new or changed backends

go run ./fstest/test_all -backends s3 against Minio:

SUMMARY
PASS: All tests finished OK in 16m40.021824416s

(s3 backend, fs/operations and fs/sync each with and without -fast-list, vfs,
cmd/gitannex, cmd/bisync.) Also golangci-lint run ./backend/s3/..., 0 issues.

Not changed here

The storage class is included because a modification time update resetting the tier is the
same wipe, but happy to drop those three lines if you would rather keep this to the headers
named in the issue. The // Guess the content type comment is now misleading, since
fs.MimeType prefers the object's own value, but it predates this change so I left it.

Checklist

  • This change is trivial OR it has been discussed and agreed in the linked issue.
  • I have read the contribution guidelines.
  • (If I used AI tools to help write this code) I have read and understood the AI-assisted contributions guidance, and I have tested and take ownership of this change myself.
  • I have added tests for all changes in this PR if appropriate.
  • I have added documentation for the changes if appropriate.
  • All commit messages are in house style.
  • (Backend changes only) test_all passes for this backend and if submitting a new backend can provide a test account for the integration tester - see CONTRIBUTING.md.
  • This Pull Request is ready for review.

@tirdesh
tirdesh requested a review from ncw as a code owner September 3, 2026 07:31
@tirdesh
tirdesh force-pushed the fix-5243-s3-setmodtime-metadata branch 2 times, most recently from af347e5 to 50dbb22 Compare September 3, 2026 07:42

@ncw ncw left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for fixing this - see inline for comments.

This could also fix the similar case in Copy which is also discarding metadata in certain cases, something like this

--- a/backend/s3/s3.go
+++ b/backend/s3/s3.go
@@ -3263,6 +3263,27 @@ func (f *Fs) Copy(ctx context.Context, src fs.Object, remote string) (fs.Object,
      if needsReplace {
              req.MetadataDirective = types.MetadataDirectiveReplace
+             // Read and apply metadata if required
+             if !ci.Metadata {
+                     err = srcObj.readMetaData(ctx)
+                     if err != nil {
+                             return nil, fmt.Errorf("failed to read source metadata: %w", err)
+                     }
+                     req.CacheControl = srcObj.cacheControl
+                     req.ContentDisposition = srcObj.contentDisposition
+                     req.ContentEncoding = srcObj.contentEncoding
+                     req.ContentLanguage = srcObj.contentLanguage
+                     for k, v := range srcObj.meta {
+                             if _, found := req.Metadata[k]; !found {
+                                     req.Metadata[k] = v
+                             }
+                     }
+             }
      }

Comment thread backend/s3/s3.go Outdated
ContentEncoding: o.contentEncoding,
ContentLanguage: o.contentLanguage,
}
if o.storageClass != nil && *o.storageClass != "" {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't preserving the storage class correctly. AWS omits
x-amz-storage-class from HEAD responses for STANDARD objects, so an
unknown class has to be sent as STANDARD

if o.storageClass != nil || o.fs.opt.StorageClass != "" {
         req.StorageClass = types.StorageClass(o.GetTier())
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed with your snippet. Mine re-tiered STANDARD objects whenever --s3-storage-class was set, since the empty class let f.copy apply the configured one.

ctx := context.Background()
contents := gz(t, random.String(1000))

item := fstest.NewItem("test-setmodtime-metadata", contents, fstest.Time("2001-05-06T04:05:06.499999999Z"))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test should skip if f.features.Copy == nil as these providers can't set mod time.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added, skipping on f.features.Copy == nil.

@tirdesh
tirdesh force-pushed the fix-5243-s3-setmodtime-metadata branch from 50dbb22 to 9f96a68 Compare September 3, 2026 14:38
@tirdesh

tirdesh commented Sep 3, 2026

Copy link
Copy Markdown
Author

Thanks for the review, all three done.

Storage class. You were right, and my version was worse than not touching it: with
--s3-storage-class REDUCED_REDUNDANCY set, a touch on a STANDARD object moved it to
REDUCED_REDUNDANCY, because the empty class let f.copy apply the configured one. Verified
against Minio, before and after your suggestion:

mine:  tier STANDARD -> REDUCED_REDUNDANCY   after touch --s3-storage-class REDUCED_REDUNDANCY
now:   tier STANDARD -> STANDARD

Test skip. Added f.features.Copy == nil.

Copy. Added as you sketched. The test is a CopyMetadata sub-test of
InternalTestObjectLock so it reuses that test's Object Lock enabled bucket instead of
standing up another one. Without the fix it fails on all four headers:

cache-control not preserved by copy:       expected "no-cache", actual ""
content-disposition not preserved by copy: expected "inline",   actual ""
content-encoding not preserved by copy:    expected "gzip",     actual ""
content-language not preserved by copy:    expected "en-US",    actual ""

Two related places I checked and left as they are:

  • Copy sets the storage class from srcObj.storageClass with the same != nil test, so a
    STANDARD source falls through to the configured class there too. That is right for Copy,
    which is creating a new object that --s3-storage-class should apply to. Only the in-place
    SetModTime needs the tier held constant.
  • Those two are the only MetadataDirectiveReplace sites in the backend. The remaining
    MetadataDirectiveCopy uses, SetTier and the multipart path that carries info.Metadata
    over, preserve metadata by construction.

go run ./fstest/test_all -backends s3 against Minio passes.

SetModTime copies the object to itself to rewrite the modification time,
which is stored in the user metadata. S3 ignores user metadata supplied
with a copy unless the REPLACE metadata directive is used, so the copy
has to use REPLACE. REPLACE also discards every system metadata header
that is not supplied with the copy, and the request only carried the
content type.

Before this change, setting the modification time on an object silently
dropped its Cache-Control, Content-Disposition, Content-Encoding and
Content-Language headers, and moved it to the configured storage class.
After it, those are read from the object and passed through, so only the
modification time changes.

The storage class is sent as the object's current tier rather than left
empty, because AWS omits x-amz-storage-class from HEAD responses for
STANDARD objects and an empty value would let the configured storage
class be applied instead.

No extra request is needed to do this: SetModTime already calls
readMetaData on its first line, which is what populates these fields.

Fixes rclone#5243
Copy uses the REPLACE metadata directive whenever an Object Lock option
is in use, not only when --metadata is set. REPLACE discards every
system metadata header that is not supplied with the copy, so copying an
object while any Object Lock option was set dropped its Cache-Control,
Content-Disposition, Content-Encoding and Content-Language, along with
any user metadata the upload itself did not set.

Read the source metadata and carry it over when it was not --metadata
that forced the directive, leaving anything the upload has already set
alone.

Tested by a new CopyMetadata sub-test of InternalTestObjectLock, which
reuses that test's Object Lock enabled bucket.
@tirdesh
tirdesh force-pushed the fix-5243-s3-setmodtime-metadata branch from 9f96a68 to 64b7efc Compare September 11, 2026 21:52
@tirdesh

tirdesh commented Sep 11, 2026

Copy link
Copy Markdown
Author

Rebased onto master, so this is no longer out of date.

The three review points were addressed in the 3 September push, detailed in the comment
above. This push adds only the rebase, plus dropping an issue link from a doc comment to
match the AGENTS.md rule against referencing bug numbers in source.

go run ./fstest/test_all -backends s3 against Minio passes on the rebased branch.

Ready for another look when you have time.

@tirdesh
tirdesh requested a review from ncw September 12, 2026 00:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

S3: Updating mod-time of file wipes other headers on original file

2 participants