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
This commit is contained in:
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user