frostfs-http-gw/internal/handler/reader.go
Nikita Zinkevich d14cb1f695
All checks were successful
/ DCO (pull_request) Successful in 37s
/ Vulncheck (pull_request) Successful in 1m1s
/ Builds (pull_request) Successful in 1m33s
/ Lint (pull_request) Successful in 3m13s
/ Tests (pull_request) Successful in 1m15s
[#142] Fix multipart-objects download
Signed-off-by: Nikita Zinkevich <n.zinkevich@yadro.com>
2024-09-19 08:35:01 +03:00

147 lines
4 KiB
Go

package handler
import (
"bytes"
"context"
"io"
"net/http"
"strconv"
"time"
"git.frostfs.info/TrueCloudLab/frostfs-http-gw/internal/data"
"git.frostfs.info/TrueCloudLab/frostfs-http-gw/internal/logs"
"git.frostfs.info/TrueCloudLab/frostfs-http-gw/internal/service/frostfs"
"git.frostfs.info/TrueCloudLab/frostfs-http-gw/response"
"git.frostfs.info/TrueCloudLab/frostfs-http-gw/utils"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object"
oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
"github.com/valyala/fasthttp"
"go.uber.org/zap"
)
type readCloser struct {
io.Reader
io.Closer
}
// initializes io.Reader with the limited size and detects Content-Type from it.
// Returns r's error directly. Also returns the processed data.
func readContentType(maxSize uint64, rInit func(uint64) (io.Reader, error)) (string, []byte, error) {
if maxSize > sizeToDetectType {
maxSize = sizeToDetectType
}
buf := make([]byte, maxSize) // maybe sync-pool the slice?
r, err := rInit(maxSize)
if err != nil {
return "", nil, err
}
n, err := r.Read(buf)
if err != nil && err != io.EOF {
return "", nil, err
}
buf = buf[:n]
return http.DetectContentType(buf), buf, err // to not lose io.EOF
}
type getPayloadParams struct {
obj *frostfs.Object
req request
bktinfo *data.BucketInfo
attrs map[string]string
}
func (h *Handler) receiveFile(ctx context.Context, req request, objAddress oid.Address, bktInfo *data.BucketInfo) {
var (
shouldDownload = req.QueryArgs().GetBool("download")
start = time.Now()
)
prm := frostfs.PrmObjectGet{
PrmAuth: frostfs.PrmAuth{
BearerToken: bearerToken(ctx),
},
Address: objAddress,
}
rObj, err := h.frostfs.GetObject(ctx, prm)
if err != nil {
req.handleFrostFSErr(err, start)
return
}
// we can't close reader in this function, so how to do it?
attrs := makeAttributesMap(rObj.Header.Attributes())
req.setAttributes(attrs)
req.setIDs(rObj.Header)
req.setDisposition(shouldDownload, attrs[object.AttributeFileName])
if err = req.setTimestamp(attrs[object.AttributeTimestamp]); err != nil {
req.log.Error(logs.CouldntParseCreationDate,
zap.String("val", attrs[object.AttributeTimestamp]),
zap.Error(err))
response.Error(req.RequestCtx, "failed to convert timestamp: "+err.Error(), fasthttp.StatusInternalServerError)
}
payloadParams := getPayloadParams{
obj: rObj,
req: req,
bktinfo: bktInfo,
attrs: attrs,
}
payload, payloadSize, err := h.getPayload(payloadParams)
if err != nil {
req.handleFrostFSErr(err, start)
return
}
req.Response.Header.Set(fasthttp.HeaderContentLength, strconv.FormatUint(payloadSize, 10))
contentType := attrs[object.AttributeContentType]
if len(contentType) == 0 {
// determine the Content-Type from the payload head
var payloadHead []byte
contentType, payloadHead, err = readContentType(payloadSize, func(uint64) (io.Reader, error) {
return payload, nil
})
if err != nil && err != io.EOF {
req.log.Error(logs.CouldNotDetectContentTypeFromPayload, zap.Error(err))
response.Error(req.RequestCtx, "could not detect Content-Type from payload: "+err.Error(), fasthttp.StatusBadRequest)
return
}
// reset payload reader since a part of the data has been read
var headReader io.Reader = bytes.NewReader(payloadHead)
if err != io.EOF { // otherwise, we've already read full payload
headReader = io.MultiReader(headReader, payload)
}
// note: we could do with io.Reader, but SetBodyStream below closes body stream
// if it implements io.Closer and that's useful for us.
payload = readCloser{headReader, payload}
}
req.SetContentType(contentType)
req.Response.SetBodyStream(payload, int(payloadSize))
}
func makeAttributesMap(attrs []object.Attribute) map[string]string {
attributes := make(map[string]string)
for _, attr := range attrs {
if !isValidToken(attr.Key()) || !isValidValue(attr.Value()) {
continue
}
key := utils.BackwardTransformIfSystem(attr.Key())
attributes[key] = attr.Value()
}
return attributes
}