s3: return x-amz-copy-source-version-id from CopyObject and UploadPartCopy (#12181)

* s3: return x-amz-copy-source-version-id from CopyObject and UploadPartCopy

* s3: cover x-amz-copy-source-version-id on latest copies and part copies

The explicit-version case was already covered; add a .versions pointer
fixture so the unversioned request resolves the latest version and
asserts the header on CopyObject, plus the same assertion on
UploadPartCopy.

* s3: carry the resolved id on a version file that lacks it

A .versions child written before the version-id attribute resolves to an
empty source version even though the pointer or the rescan already knows
it. Stamp the id on the returned entry so copy and get readers report it;
the version copy tests now drop the attribute to cover that shape.

* s3: report the null version in x-amz-copy-source-version-id

A regular-path source in a versioning-configured bucket is the null
version; echo it like every other version id, matching the GET path and
AWS. Unversioned sources still report no header.

* s3: report the version a self-copy retry actually read

---------

Co-authored-by: user.email <chrislusf@users.noreply.github.com>
This commit is contained in:
Jay Hemnanianduser.email authored and GitHub committed 2026-10-28 15:49:37 -07:00
1 parent 48782e2b30
commit 6225e0a705
3 files changed
+223 -1

No files matched your search

+19 -1
View File
@@ -2078,7 +2078,23 @@ func (s3a *S3ApiServer) doGetLatestObjectVersion(bucket, object string, maxRetri
return nil, fmt.Errorf("failed to get latest version file %s: %v", latestVersionPath, err)
}
return latestVersionEntry, nil
return attachResolvedVersionId(latestVersionEntry, latestVersionId), nil
}
// attachResolvedVersionId stamps the resolved id on a versioned child that
// predates the version-id attribute, so readers see the id the pointer or the
// rescan already recovered without re-deriving it from the file name.
func attachResolvedVersionId(entry *filer_pb.Entry, versionId string) *filer_pb.Entry {
if entry == nil || versionId == "" {
return entry
}
if entry.Extended == nil {
entry.Extended = make(map[string][]byte)
}
if len(entry.Extended[s3_constants.ExtVersionIdKey]) == 0 {
entry.Extended[s3_constants.ExtVersionIdKey] = []byte(versionId)
}
return entry
}
// recoverLatestVersionWithoutPointer handles a .versions directory that exists
@@ -2249,6 +2265,8 @@ func (s3a *S3ApiServer) healStaleLatestVersionPointer(bucket, normalizedObject s
return nil, fmt.Errorf("%w: no remaining version in %s", filer_pb.ErrNotFound, versionsDir)
}
latestEntry = attachResolvedVersionId(latestEntry, latestVersionId)
// The persist is CAS-style (mirroring clearStaleLatestVersionPointer) so a
// stale repair cannot supersede a concurrent writer: the rescan takes time,
// during which a PUT or delete may have atomically advanced the pointer on