From c197206897e18d90f09bd37ca82e247c1faba96b Mon Sep 17 00:00:00 2001 From: Chris Lu Date: Wed, 18 Mar 2026 17:26:33 -0700 Subject: [PATCH] fix(s3): return ETag header for directory marker PutObject requests (#8688) * fix(s3): return ETag header for directory marker PutObject requests The PutObject handler has a special path for keys ending with "/" (directory markers) that creates the entry via mkdir. This path never computed or set the ETag response header, unlike the regular PutObject path. AWS S3 always returns an ETag header, even for empty-body puts. Compute the MD5 of the content (empty or otherwise), store it in the entry attributes and extended attributes, and set the ETag response header. Fixes #8682 * fix: handle io.ReadAll error and chunked encoding for directory markers Address review feedback: - Handle error from io.ReadAll instead of silently discarding it - Change condition from ContentLength > 0 to ContentLength != 0 to correctly handle chunked transfer encoding (ContentLength == -1) * fix hanging tests --- weed/glog/glog_test.go | 7 +++++-- weed/s3api/s3api_object_handlers_put.go | 28 +++++++++++++++++++++++-- 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/weed/glog/glog_test.go b/weed/glog/glog_test.go index 48d1c9ec5..a311e320c 100644 --- a/weed/glog/glog_test.go +++ b/weed/glog/glog_test.go @@ -333,10 +333,13 @@ func TestRollover(t *testing.T) { logExitFunc = func(e error) { err = e } + + Info("x") // Be sure we have a file (also triggers createLogDirs via sync.Once). + + // Set MaxSize after the first Info call so that createLogDirs (which + // overwrites MaxSize from the flag default) has already executed. defer func(previous uint64) { MaxSize = previous }(MaxSize) MaxSize = 512 - - Info("x") // Be sure we have a file. info, ok := logging.file[infoLog].(*syncBuffer) if !ok { t.Fatal("info wasn't created") diff --git a/weed/s3api/s3api_object_handlers_put.go b/weed/s3api/s3api_object_handlers_put.go index 4b4772e2e..d944f496c 100644 --- a/weed/s3api/s3api_object_handlers_put.go +++ b/weed/s3api/s3api_object_handlers_put.go @@ -139,6 +139,22 @@ func (s3a *S3ApiServer) PutObjectHandler(w http.ResponseWriter, r *http.Request) fullDirPath = fullDirPath + "/" + dirName } + // Read any content through dataReader (handles chunked encoding properly) + var dirContent []byte + if r.ContentLength != 0 { + var readErr error + dirContent, readErr = io.ReadAll(dataReader) + if readErr != nil { + glog.Errorf("PutObjectHandler: failed to read directory marker content %s/%s: %v", bucket, object, readErr) + s3err.WriteErrorResponse(w, r, s3err.ErrInternalError) + return + } + } + + // Compute MD5 for ETag (md5.Sum of nil/empty = MD5 of empty content) + dirMd5 := md5.Sum(dirContent) + dirEtag := fmt.Sprintf("%x", dirMd5) + glog.Infof("PutObjectHandler: explicit directory marker %s/%s (contentType=%q, len=%d)", bucket, object, objectContentType, r.ContentLength) if err := s3a.mkdir( @@ -147,10 +163,17 @@ func (s3a *S3ApiServer) PutObjectHandler(w http.ResponseWriter, r *http.Request) if objectContentType == "" { objectContentType = s3_constants.FolderMimeType } - if r.ContentLength > 0 { - entry.Content, _ = io.ReadAll(r.Body) + if len(dirContent) > 0 { + entry.Content = dirContent } entry.Attributes.Mime = objectContentType + entry.Attributes.Md5 = dirMd5[:] + + // Store ETag in extended attributes for consistency with regular objects + if entry.Extended == nil { + entry.Extended = make(map[string][]byte) + } + entry.Extended[s3_constants.ExtETagKey] = []byte(dirEtag) // Set object owner for directory objects (same as regular objects) s3a.setObjectOwnerFromRequest(r, bucket, entry) @@ -158,6 +181,7 @@ func (s3a *S3ApiServer) PutObjectHandler(w http.ResponseWriter, r *http.Request) s3err.WriteErrorResponse(w, r, s3err.ErrInternalError) return } + setEtag(w, dirEtag) } else { // Get detailed versioning state for the bucket versioningState, err := s3a.getVersioningState(bucket)