mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-22 06:22:38 +00:00
s3: apply Range to HeadObject (#11856)
* s3: apply Range to HeadObject * s3: apply Range to a HEAD of a stored directory key Directory-key entries answer through serveDirectoryContent before HeadObjectHandler's range branch, so a ranged HEAD returned 200 and the full length. * s3: keep the missing-directory check for plain directories only A stored directory-key marker is an object, so a range on its empty body answers 416 like on any other zero-byte object. * s3: let partNumber keep its priority over Range on a directory key HEAD * s3: serve ranged GETs of a stored directory key The same branch now applies the validated range to the inline content on GET, so a directory-key object answers 206 with the selected bytes like any other object. --------- Co-authored-by: Chris Lu <chrislusf@users.noreply.github.com>
This commit is contained in:
@@ -181,8 +181,10 @@ func (s3a *S3ApiServer) parseAndValidateRange(w http.ResponseWriter, r *http.Req
|
||||
return 0, totalSize, false, nil
|
||||
}
|
||||
|
||||
// Empty directory: 404. A file promoted to a directory keeps its data and stays retrievable.
|
||||
if entry.IsDirectory && totalSize == 0 {
|
||||
// Empty directory: 404. A file promoted to a directory keeps its data and
|
||||
// stays retrievable, and a stored directory-key marker is an object, not a
|
||||
// missing key — an unsatisfiable range answers 416.
|
||||
if entry.IsDirectory && totalSize == 0 && !entry.IsDirectoryKeyObject() {
|
||||
s3err.WriteErrorResponse(w, r, s3err.ErrNoSuchKey)
|
||||
return 0, 0, false, newStreamErrorWithResponse(fmt.Errorf("directory object %s/%s cannot be retrieved", bucket, object))
|
||||
}
|
||||
@@ -533,6 +535,24 @@ func (s3a *S3ApiServer) serveDirectoryContent(w http.ResponseWriter, r *http.Req
|
||||
if r.Method == http.MethodHead && len(entry.Chunks) > 0 {
|
||||
contentLength = int64(entry.Attributes.FileSize)
|
||||
}
|
||||
statusCode := http.StatusOK
|
||||
if r.Header.Get("Range") != "" && requestedPartNumber(r) == 0 {
|
||||
// A ranged request answers like the object path does: 206 with its
|
||||
// Content-Range and the range's length, served on GET, reported on HEAD.
|
||||
totalSize := contentLength
|
||||
offset, size, isRangeRequest, rangeErr := s3a.parseAndValidateRange(w, r, entry, totalSize, bucket, object)
|
||||
if rangeErr != nil {
|
||||
return
|
||||
}
|
||||
if isRangeRequest {
|
||||
contentLength = size
|
||||
w.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", offset, offset+size-1, totalSize))
|
||||
statusCode = http.StatusPartialContent
|
||||
if r.Method != http.MethodHead {
|
||||
content = content[offset : offset+size]
|
||||
}
|
||||
}
|
||||
}
|
||||
w.Header().Set("Content-Length", strconv.FormatInt(contentLength, 10))
|
||||
|
||||
// Set last modified
|
||||
@@ -545,12 +565,12 @@ func (s3a *S3ApiServer) serveDirectoryContent(w http.ResponseWriter, r *http.Req
|
||||
|
||||
// For HEAD requests, don't write body
|
||||
if r.Method == http.MethodHead {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.WriteHeader(statusCode)
|
||||
return
|
||||
}
|
||||
|
||||
// Write content
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.WriteHeader(statusCode)
|
||||
if len(content) > 0 {
|
||||
if _, err := w.Write(content); err != nil {
|
||||
glog.Errorf("serveDirectoryContent: failed to write response: %v", err)
|
||||
@@ -2745,6 +2765,17 @@ func (s3a *S3ApiServer) HeadObjectHandler(w http.ResponseWriter, r *http.Request
|
||||
w.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", startOffset, endOffset, totalSize))
|
||||
statusCode = http.StatusPartialContent
|
||||
glog.V(3).Infof("HeadObject: part %d is bytes %d-%d/%d", partNumber, startOffset, endOffset, totalSize)
|
||||
} else if r.Header.Get("Range") != "" {
|
||||
// A ranged HEAD reports the range like GET does: 206 with its Content-Range and length
|
||||
offset, size, isRangeRequest, rangeErr := s3a.parseAndValidateRange(w, r, objectEntryForSSE, totalSize, bucket, object)
|
||||
if rangeErr != nil {
|
||||
return
|
||||
}
|
||||
if isRangeRequest {
|
||||
responseSize = size
|
||||
w.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", offset, offset+size-1, totalSize))
|
||||
statusCode = http.StatusPartialContent
|
||||
}
|
||||
}
|
||||
|
||||
s3a.setResponseHeaders(w, r, objectEntryForSSE, responseSize)
|
||||
|
||||
@@ -40,3 +40,33 @@ func TestParseAndValidateRangeRejectsMalformedNumericOffsets(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHeadObjectRange(t *testing.T) {
|
||||
s3a, f := newAnonymousReadTestServer(t)
|
||||
f.entries["/buckets/b/o"] = anonymousReadEntry(anonymousReadGrant("READ"))
|
||||
for _, tc := range []struct {
|
||||
rangeHeader, contentRange, contentLength string
|
||||
code int
|
||||
}{
|
||||
{"", "", "11", http.StatusOK},
|
||||
{"bytes=1-4", "bytes 1-4/11", "4", http.StatusPartialContent},
|
||||
{"bytes=-3", "bytes 8-10/11", "3", http.StatusPartialContent},
|
||||
{"bytes=5-", "bytes 5-10/11", "6", http.StatusPartialContent},
|
||||
{"bytes=20-", "bytes */11", "", http.StatusRequestedRangeNotSatisfiable},
|
||||
} {
|
||||
headers := map[string]string{}
|
||||
if tc.rangeHeader != "" {
|
||||
headers["Range"] = tc.rangeHeader
|
||||
}
|
||||
rr := serveAnonymousRead(s3a, http.MethodHead, "o", "", headers)
|
||||
if rr.Code != tc.code {
|
||||
t.Errorf("Range %q: status %d, want %d", tc.rangeHeader, rr.Code, tc.code)
|
||||
}
|
||||
if got := rr.Header().Get("Content-Range"); got != tc.contentRange {
|
||||
t.Errorf("Range %q: Content-Range %q, want %q", tc.rangeHeader, got, tc.contentRange)
|
||||
}
|
||||
if got := rr.Header().Get("Content-Length"); tc.contentLength != "" && got != tc.contentLength {
|
||||
t.Errorf("Range %q: Content-Length %q, want %q", tc.rangeHeader, got, tc.contentLength)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user