package handler import ( "bytes" "context" "io" "net/http" "path" "strconv" "time" "git.frostfs.info/TrueCloudLab/frostfs-http-gw/internal/logs" "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 getMultiobjectBodyParams struct { obj *Object req request strSize string } func (h *Handler) receiveFile(ctx context.Context, req request, objAddress oid.Address) { var ( shouldDownload = req.QueryArgs().GetBool("download") start = time.Now() filename string filepath string contentType string ) prm := PrmObjectGet{ PrmAuth: 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? req.setIDs(rObj.Header) payload := rObj.Payload payloadSize := rObj.Header.PayloadSize() for _, attr := range rObj.Header.Attributes() { key := attr.Key() val := attr.Value() if !isValidToken(key) || !isValidValue(val) { continue } key = utils.BackwardTransformIfSystem(key) req.Response.Header.Set(utils.UserAttributeHeaderPrefix+key, val) switch key { case object.AttributeFileName: filename = val case object.AttributeTimestamp: if err = req.setTimestamp(val); err != nil { req.log.Error(logs.CouldntParseCreationDate, zap.String("val", val), zap.Error(err)) } case object.AttributeContentType: contentType = val case object.AttributeFilePath: filepath = val case attributeMultipartObjectSize: payload, payloadSize, err = h.getPayload(getMultiobjectBodyParams{ obj: rObj, req: req, strSize: val, }) if err != nil { req.handleFrostFSErr(err, start) return } } } if filename == "" { filename = filepath } req.setDisposition(shouldDownload, filename) req.Response.Header.Set(fasthttp.HeaderAccessControlAllowOrigin, "*") req.Response.Header.Set(fasthttp.HeaderContentLength, strconv.FormatUint(payloadSize, 10)) 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 (r *request) setIDs(obj object.Object) { objID, _ := obj.ID() cnrID, _ := obj.ContainerID() r.Response.Header.Set(hdrObjectID, objID.String()) r.Response.Header.Set(hdrOwnerID, obj.OwnerID().String()) r.Response.Header.Set(hdrContainerID, cnrID.String()) } func (r *request) setDisposition(shouldDownload bool, filename string) { const ( inlineDisposition = "inline" attachmentDisposition = "attachment" ) dis := inlineDisposition if shouldDownload { dis = attachmentDisposition } r.Response.Header.Set(fasthttp.HeaderContentDisposition, dis+"; filename="+path.Base(filename)) } func (r *request) setTimestamp(timestamp string) error { value, err := strconv.ParseInt(timestamp, 10, 64) if err != nil { return err } r.Response.Header.Set(fasthttp.HeaderLastModified, time.Unix(value, 0).UTC().Format(http.TimeFormat)) return nil }