package downloader import ( "context" "io" "net/http" "strconv" "time" "git.frostfs.info/TrueCloudLab/frostfs-http-gw/response" "git.frostfs.info/TrueCloudLab/frostfs-http-gw/tokens" "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" "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/pool" "github.com/valyala/fasthttp" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" "go.uber.org/zap" ) // max bytes needed to detect content type according to http.DetectContentType docs. const sizeToDetectType = 512 const ( hdrObjectID = "X-Object-Id" hdrOwnerID = "X-Owner-Id" hdrContainerID = "X-Container-Id" ) func headObject(ctx context.Context, req request, clnt *pool.Pool, objectAddress oid.Address) { ctx, span := utils.StartHTTPServerSpan(ctx, req.RequestCtx, "HEAD Object", trace.WithAttributes( attribute.String("cid", objectAddress.Container().EncodeToString()), attribute.String("oid", objectAddress.Object().EncodeToString()), )) defer func() { utils.SetHTTPTraceInfo(ctx, span, req.RequestCtx) span.End() }() var start = time.Now() if err := tokens.StoreBearerToken(req.RequestCtx); err != nil { req.log.Error("could not fetch and store bearer token", zap.Error(err)) response.Error(req.RequestCtx, "could not fetch and store bearer token", fasthttp.StatusBadRequest) return } btoken := bearerToken(req.RequestCtx) var prm pool.PrmObjectHead prm.SetAddress(objectAddress) if btoken != nil { prm.UseBearer(*btoken) } obj, err := clnt.HeadObject(ctx, prm) if err != nil { req.handleFrostFSErr(err, start) return } req.Response.Header.Set(fasthttp.HeaderContentLength, strconv.FormatUint(obj.PayloadSize(), 10)) var contentType string for _, attr := range obj.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.AttributeTimestamp: value, err := strconv.ParseInt(val, 10, 64) if err != nil { req.log.Info("couldn't parse creation date", zap.String("key", key), zap.String("val", val), zap.Error(err)) continue } req.Response.Header.Set(fasthttp.HeaderLastModified, time.Unix(value, 0).UTC().Format(http.TimeFormat)) case object.AttributeContentType: contentType = val } } idsToResponse(&req.Response, &obj) if len(contentType) == 0 { contentType, _, err = readContentType(obj.PayloadSize(), func(sz uint64) (io.Reader, error) { var prmRange pool.PrmObjectRange prmRange.SetAddress(objectAddress) prmRange.SetLength(sz) if btoken != nil { prmRange.UseBearer(*btoken) } resObj, err := clnt.ObjectRange(ctx, prmRange) if err != nil { return nil, err } return &resObj, nil }) if err != nil && err != io.EOF { req.handleFrostFSErr(err, start) return } } req.SetContentType(contentType) } func idsToResponse(resp *fasthttp.Response, obj *object.Object) { objID, _ := obj.ID() cnrID, _ := obj.ContainerID() resp.Header.Set(hdrObjectID, objID.String()) resp.Header.Set(hdrOwnerID, obj.OwnerID().String()) resp.Header.Set(hdrContainerID, cnrID.String()) } // HeadByAddressOrBucketName handles head requests using simple cid/oid or bucketname/key format. func (d *Downloader) HeadByAddressOrBucketName(c *fasthttp.RequestCtx) { test, _ := c.UserValue("oid").(string) var id oid.ID err := id.DecodeString(test) if err != nil { d.byBucketname(c, headObject) } else { d.byAddress(c, headObject) } } // HeadByAttribute handles attribute-based head requests. func (d *Downloader) HeadByAttribute(c *fasthttp.RequestCtx) { d.byAttribute(c, headObject) }