Browse Source

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
pull/8691/head
Chris Lu 2 days ago
committed by GitHub
parent
commit
c197206897
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 7
      weed/glog/glog_test.go
  2. 28
      weed/s3api/s3api_object_handlers_put.go

7
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")

28
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)

Loading…
Cancel
Save