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 }