|
|
@ -25,8 +25,8 @@ import ( |
|
|
|
|
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants" |
|
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants" |
|
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3err" |
|
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3err" |
|
|
"github.com/seaweedfs/seaweedfs/weed/util/mem" |
|
|
|
|
|
util_http "github.com/seaweedfs/seaweedfs/weed/util/http" |
|
|
util_http "github.com/seaweedfs/seaweedfs/weed/util/http" |
|
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/util/mem" |
|
|
|
|
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/glog" |
|
|
"github.com/seaweedfs/seaweedfs/weed/glog" |
|
|
) |
|
|
) |
|
|
@ -46,6 +46,81 @@ var corsHeaders = []string{ |
|
|
// Package-level to avoid per-call allocations in writeZeroBytes
|
|
|
// Package-level to avoid per-call allocations in writeZeroBytes
|
|
|
var zeroBuf = make([]byte, 32*1024) |
|
|
var zeroBuf = make([]byte, 32*1024) |
|
|
|
|
|
|
|
|
|
|
|
// adjustRangeForPart adjusts a client's Range header to absolute offsets within a part.
|
|
|
|
|
|
// Parameters:
|
|
|
|
|
|
// - partStartOffset: the absolute start offset of the part in the object
|
|
|
|
|
|
// - partEndOffset: the absolute end offset of the part in the object
|
|
|
|
|
|
// - clientRangeHeader: the Range header value from the client (e.g., "bytes=0-99")
|
|
|
|
|
|
//
|
|
|
|
|
|
// Returns:
|
|
|
|
|
|
// - adjustedStart: the adjusted absolute start offset
|
|
|
|
|
|
// - adjustedEnd: the adjusted absolute end offset
|
|
|
|
|
|
// - error: nil on success, error if the range is invalid
|
|
|
|
|
|
func adjustRangeForPart(partStartOffset, partEndOffset int64, clientRangeHeader string) (adjustedStart, adjustedEnd int64, err error) { |
|
|
|
|
|
// If no range header, return the full part
|
|
|
|
|
|
if clientRangeHeader == "" || !strings.HasPrefix(clientRangeHeader, "bytes=") { |
|
|
|
|
|
return partStartOffset, partEndOffset, nil |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Parse client's range request (relative to the part)
|
|
|
|
|
|
rangeSpec := clientRangeHeader[6:] // Remove "bytes=" prefix
|
|
|
|
|
|
parts := strings.Split(rangeSpec, "-") |
|
|
|
|
|
|
|
|
|
|
|
if len(parts) != 2 { |
|
|
|
|
|
return 0, 0, fmt.Errorf("invalid range format") |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
partSize := partEndOffset - partStartOffset + 1 |
|
|
|
|
|
var clientStart, clientEnd int64 |
|
|
|
|
|
|
|
|
|
|
|
// Parse start offset
|
|
|
|
|
|
if parts[0] != "" { |
|
|
|
|
|
clientStart, err = strconv.ParseInt(parts[0], 10, 64) |
|
|
|
|
|
if err != nil { |
|
|
|
|
|
return 0, 0, fmt.Errorf("invalid range start: %w", err) |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Parse end offset
|
|
|
|
|
|
if parts[1] != "" { |
|
|
|
|
|
clientEnd, err = strconv.ParseInt(parts[1], 10, 64) |
|
|
|
|
|
if err != nil { |
|
|
|
|
|
return 0, 0, fmt.Errorf("invalid range end: %w", err) |
|
|
|
|
|
} |
|
|
|
|
|
} else { |
|
|
|
|
|
// No end specified, read to end of part
|
|
|
|
|
|
clientEnd = partSize - 1 |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Handle suffix-range (e.g., "bytes=-100" means last 100 bytes)
|
|
|
|
|
|
if parts[0] == "" { |
|
|
|
|
|
// suffix-range: clientEnd is actually the suffix length
|
|
|
|
|
|
suffixLength := clientEnd |
|
|
|
|
|
if suffixLength > partSize { |
|
|
|
|
|
suffixLength = partSize |
|
|
|
|
|
} |
|
|
|
|
|
clientStart = partSize - suffixLength |
|
|
|
|
|
clientEnd = partSize - 1 |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Validate range is within part boundaries
|
|
|
|
|
|
if clientStart < 0 || clientStart >= partSize { |
|
|
|
|
|
return 0, 0, fmt.Errorf("range start %d out of bounds for part size %d", clientStart, partSize) |
|
|
|
|
|
} |
|
|
|
|
|
if clientEnd >= partSize { |
|
|
|
|
|
clientEnd = partSize - 1 |
|
|
|
|
|
} |
|
|
|
|
|
if clientStart > clientEnd { |
|
|
|
|
|
return 0, 0, fmt.Errorf("range start %d > end %d", clientStart, clientEnd) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Adjust to absolute offsets in the object
|
|
|
|
|
|
adjustedStart = partStartOffset + clientStart |
|
|
|
|
|
adjustedEnd = partStartOffset + clientEnd |
|
|
|
|
|
|
|
|
|
|
|
return adjustedStart, adjustedEnd, nil |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
// StreamError is returned when streaming functions encounter errors.
|
|
|
// StreamError is returned when streaming functions encounter errors.
|
|
|
// It tracks whether an HTTP response has already been written to prevent
|
|
|
// It tracks whether an HTTP response has already been written to prevent
|
|
|
// double WriteHeader calls that would create malformed S3 error responses.
|
|
|
// double WriteHeader calls that would create malformed S3 error responses.
|
|
|
@ -620,73 +695,17 @@ func (s3a *S3ApiServer) GetObjectHandler(w http.ResponseWriter, r *http.Request) |
|
|
// Check if client supplied a Range header - if so, apply it within the part's boundaries
|
|
|
// Check if client supplied a Range header - if so, apply it within the part's boundaries
|
|
|
// S3 allows both partNumber and Range together, where Range applies within the selected part
|
|
|
// S3 allows both partNumber and Range together, where Range applies within the selected part
|
|
|
clientRangeHeader := r.Header.Get("Range") |
|
|
clientRangeHeader := r.Header.Get("Range") |
|
|
if clientRangeHeader != "" && strings.HasPrefix(clientRangeHeader, "bytes=") { |
|
|
|
|
|
// Parse client's range request (relative to the part)
|
|
|
|
|
|
rangeSpec := clientRangeHeader[6:] // Remove "bytes=" prefix
|
|
|
|
|
|
parts := strings.Split(rangeSpec, "-") |
|
|
|
|
|
|
|
|
|
|
|
if len(parts) == 2 { |
|
|
|
|
|
partSize := endOffset - startOffset + 1 |
|
|
|
|
|
var clientStart, clientEnd int64 |
|
|
|
|
|
var parseErr error |
|
|
|
|
|
|
|
|
|
|
|
// Parse start offset
|
|
|
|
|
|
if parts[0] != "" { |
|
|
|
|
|
clientStart, parseErr = strconv.ParseInt(parts[0], 10, 64) |
|
|
|
|
|
if parseErr != nil { |
|
|
|
|
|
glog.Warningf("GetObject: Invalid Range start for part %d: %s", partNumber, parts[0]) |
|
|
|
|
|
s3err.WriteErrorResponse(w, r, s3err.ErrInvalidRange) |
|
|
|
|
|
return |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Parse end offset
|
|
|
|
|
|
if parts[1] != "" { |
|
|
|
|
|
clientEnd, parseErr = strconv.ParseInt(parts[1], 10, 64) |
|
|
|
|
|
if parseErr != nil { |
|
|
|
|
|
glog.Warningf("GetObject: Invalid Range end for part %d: %s", partNumber, parts[1]) |
|
|
|
|
|
s3err.WriteErrorResponse(w, r, s3err.ErrInvalidRange) |
|
|
|
|
|
return |
|
|
|
|
|
} |
|
|
|
|
|
} else { |
|
|
|
|
|
// No end specified, read to end of part
|
|
|
|
|
|
clientEnd = partSize - 1 |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Handle suffix-range (e.g., "bytes=-100" means last 100 bytes)
|
|
|
|
|
|
if parts[0] == "" { |
|
|
|
|
|
// suffix-range: clientEnd is actually the suffix length
|
|
|
|
|
|
suffixLength := clientEnd |
|
|
|
|
|
if suffixLength > partSize { |
|
|
|
|
|
suffixLength = partSize |
|
|
|
|
|
} |
|
|
|
|
|
clientStart = partSize - suffixLength |
|
|
|
|
|
clientEnd = partSize - 1 |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Validate range is within part boundaries
|
|
|
|
|
|
if clientStart < 0 || clientStart >= partSize { |
|
|
|
|
|
glog.Warningf("GetObject: Range start %d out of bounds for part %d (size: %d)", clientStart, partNumber, partSize) |
|
|
|
|
|
s3err.WriteErrorResponse(w, r, s3err.ErrInvalidRange) |
|
|
|
|
|
return |
|
|
|
|
|
} |
|
|
|
|
|
if clientEnd >= partSize { |
|
|
|
|
|
clientEnd = partSize - 1 |
|
|
|
|
|
} |
|
|
|
|
|
if clientStart > clientEnd { |
|
|
|
|
|
glog.Warningf("GetObject: Invalid Range: start %d > end %d for part %d", clientStart, clientEnd, partNumber) |
|
|
|
|
|
|
|
|
if clientRangeHeader != "" { |
|
|
|
|
|
adjustedStart, adjustedEnd, rangeErr := adjustRangeForPart(startOffset, endOffset, clientRangeHeader) |
|
|
|
|
|
if rangeErr != nil { |
|
|
|
|
|
glog.Warningf("GetObject: Invalid Range for part %d: %v", partNumber, rangeErr) |
|
|
s3err.WriteErrorResponse(w, r, s3err.ErrInvalidRange) |
|
|
s3err.WriteErrorResponse(w, r, s3err.ErrInvalidRange) |
|
|
return |
|
|
return |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
// Adjust to absolute offsets in the object
|
|
|
|
|
|
partStartOffset := startOffset |
|
|
|
|
|
startOffset = partStartOffset + clientStart |
|
|
|
|
|
endOffset = partStartOffset + clientEnd |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
startOffset = adjustedStart |
|
|
|
|
|
endOffset = adjustedEnd |
|
|
glog.V(3).Infof("GetObject: Client Range %s applied to part %d, adjusted to bytes=%d-%d", clientRangeHeader, partNumber, startOffset, endOffset) |
|
|
glog.V(3).Infof("GetObject: Client Range %s applied to part %d, adjusted to bytes=%d-%d", clientRangeHeader, partNumber, startOffset, endOffset) |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Set Range header to read the requested bytes (full part or client-specified range within part)
|
|
|
// Set Range header to read the requested bytes (full part or client-specified range within part)
|
|
|
rangeHeader := fmt.Sprintf("bytes=%d-%d", startOffset, endOffset) |
|
|
rangeHeader := fmt.Sprintf("bytes=%d-%d", startOffset, endOffset) |
|
|
|