From de3df211d7bb884834148b2751baff38a77d8956 Mon Sep 17 00:00:00 2001 From: Robert Schade Date: Sun, 4 Jan 2026 20:24:43 +0100 Subject: [PATCH] store S3 storage class in extended atrributes #7961 (#7962) * store S3 storage class in extended atrributes #7961 * canonical * remove issue reference --------- Co-authored-by: Robert Schade Co-authored-by: Chris Lu --- weed/s3api/s3_constants/header.go | 2 +- weed/s3api/s3api_object_handlers_put.go | 26 +++++++++++++++++++++++++ weed/s3api/s3api_object_versioning.go | 18 +++++++++++++---- weed/s3api/s3err/s3api_errors.go | 6 ++++++ 4 files changed, 47 insertions(+), 5 deletions(-) diff --git a/weed/s3api/s3_constants/header.go b/weed/s3api/s3_constants/header.go index f379c91ed..70f02e1cd 100644 --- a/weed/s3api/s3_constants/header.go +++ b/weed/s3api/s3_constants/header.go @@ -32,7 +32,7 @@ const ( // Standard S3 HTTP request constants const ( // S3 storage class - AmzStorageClass = "x-amz-storage-class" + AmzStorageClass = "X-Amz-Storage-Class" // S3 user-defined metadata AmzUserMetaPrefix = "X-Amz-Meta-" diff --git a/weed/s3api/s3api_object_handlers_put.go b/weed/s3api/s3api_object_handlers_put.go index 8c0561a89..5ac893a7c 100644 --- a/weed/s3api/s3api_object_handlers_put.go +++ b/weed/s3api/s3api_object_handlers_put.go @@ -571,6 +571,15 @@ func (s3a *S3ApiServer) putToFiler(r *http.Request, filePath string, dataReader } } + // Store the storage class from header + if sc := r.Header.Get(s3_constants.AmzStorageClass); sc != "" { + if !validateStorageClass(sc) { + glog.Warningf("putToFiler: Invalid storage class '%s' for %s", sc, filePath) + return "", s3err.ErrInvalidStorageClass, SSEResponseMetadata{} + } + entry.Extended[s3_constants.AmzStorageClass] = []byte(sc) + } + // Parse and store object tags from X-Amz-Tagging header // Fix for GitHub issue #7589: Tags sent during object upload were not being stored if tagging := r.Header.Get(s3_constants.AmzObjectTagging); tagging != "" { @@ -1881,3 +1890,20 @@ func (s3a *S3ApiServer) deleteOrphanedChunks(chunks []*filer_pb.FileChunk) { glog.V(3).Infof("deleteOrphanedChunks: successfully deleted all %d orphaned chunks", successCount) } } + +func validateStorageClass(sc string) bool { + switch StorageClass(sc) { + case "STANDARD", "REDUCED_REDUNDANCY", "STANDARD_IA", "ONEZONE_IA", "INTELLIGENT_TIERING", "GLACIER", "DEEP_ARCHIVE", "OUTPOSTS", "GLACIER_IR", "SNOW": + return true + } + return false +} + +func (s3a *S3ApiServer) getStorageClassFromExtended(extended map[string][]byte) string { + if extended != nil { + if sc, ok := extended[s3_constants.AmzStorageClass]; ok { + return string(sc) + } + } + return "STANDARD" +} diff --git a/weed/s3api/s3api_object_versioning.go b/weed/s3api/s3api_object_versioning.go index 6221eac1b..42ed7a20a 100644 --- a/weed/s3api/s3api_object_versioning.go +++ b/weed/s3api/s3api_object_versioning.go @@ -134,6 +134,7 @@ type ObjectVersion struct { ETag string Size int64 OwnerID string // Owner ID extracted from entry metadata + StorageClass string } // createDeleteMarker creates a delete marker for versioned delete operations @@ -413,7 +414,7 @@ func (vc *versionCollector) addVersion(version *ObjectVersion, objectKey string) ETag: version.ETag, Size: version.Size, Owner: vc.s3a.getObjectOwnerFromVersion(version, vc.bucket, objectKey), - StorageClass: "STANDARD", + StorageClass: StorageClass(vc.s3a.getStorageClassFromExtended(entryExtended(version))), } *vc.allVersions = append(*vc.allVersions, versionEntry) } @@ -484,7 +485,7 @@ func (vc *versionCollector) processExplicitDirectory(entryPath string, entry *fi ETag: "\"d41d8cd98f00b204e9800998ecf8427e\"", // Empty content ETag Size: 0, Owner: vc.s3a.getObjectOwnerFromEntry(entry), - StorageClass: "STANDARD", + StorageClass: StorageClass(vc.s3a.getStorageClassFromExtended(entry.Extended)), } *vc.allVersions = append(*vc.allVersions, versionEntry) } @@ -545,7 +546,7 @@ func (vc *versionCollector) processRegularFile(currentPath, entryPath string, en ETag: vc.s3a.calculateETagFromChunks(entry.Chunks), Size: int64(entry.Attributes.FileSize), Owner: vc.s3a.getObjectOwnerFromEntry(entry), - StorageClass: "STANDARD", + StorageClass: StorageClass(vc.s3a.getStorageClassFromExtended(entry.Extended)), } *vc.allVersions = append(*vc.allVersions, versionEntry) } @@ -726,6 +727,7 @@ func (s3a *S3ApiServer) getObjectVersionList(bucket, object string) ([]*ObjectVe IsDeleteMarker: isDeleteMarker, LastModified: time.Unix(entry.Attributes.Mtime, 0), OwnerID: ownerID, + StorageClass: s3a.getStorageClassFromExtended(entry.Extended), } if !isDeleteMarker { @@ -1253,10 +1255,18 @@ func (s3a *S3ApiServer) getObjectOwnerFromVersion(version *ObjectVersion, bucket } } - // Ultimate fallback: return anonymous if no owner found + // Fallback: return anonymous if no owner found return CanonicalUser{ID: s3_constants.AccountAnonymousId, DisplayName: "anonymous"} } +func entryExtended(v *ObjectVersion) map[string][]byte { + return map[string][]byte{ + s3_constants.AmzStorageClass: []byte(v.StorageClass), + s3_constants.ExtAmzOwnerKey: []byte(v.OwnerID), + s3_constants.ExtETagKey: []byte(v.ETag), + } +} + // getObjectOwnerFromEntry extracts object owner information from a file entry func (s3a *S3ApiServer) getObjectOwnerFromEntry(entry *filer_pb.Entry) CanonicalUser { if entry != nil && entry.Extended != nil { diff --git a/weed/s3api/s3err/s3api_errors.go b/weed/s3api/s3err/s3api_errors.go index a23ff2aca..b782e9356 100644 --- a/weed/s3api/s3err/s3api_errors.go +++ b/weed/s3api/s3err/s3api_errors.go @@ -141,6 +141,7 @@ const ( // Bucket encryption errors ErrNoSuchBucketEncryptionConfiguration + ErrInvalidStorageClass ) // Error message constants for checksum validation @@ -588,6 +589,11 @@ var errorCodeResponse = map[ErrorCode]APIError{ Description: "The server side encryption configuration was not found.", HTTPStatusCode: http.StatusNotFound, }, + ErrInvalidStorageClass: { + Code: "InvalidStorageClass", + Description: "The storage class you specified is not valid", + HTTPStatusCode: http.StatusBadRequest, + }, } // GetAPIError provides API Error for input API error code.