diff --git a/api/handler/get.go b/api/handler/get.go index 7572423e5..029f5e524 100644 --- a/api/handler/get.go +++ b/api/handler/get.go @@ -65,7 +65,7 @@ func fetchRangeHeader(headers http.Header, fullSize uint64) (*layer.RangeParams, return &layer.RangeParams{Start: start, End: end}, nil } -func writeHeaders(h http.Header, info *layer.ObjectInfo) { +func writeHeaders(h http.Header, info *layer.ObjectInfo, tagSetLength int) { if len(info.ContentType) > 0 { h.Set(api.ContentType, info.ContentType) } @@ -73,6 +73,7 @@ func writeHeaders(h http.Header, info *layer.ObjectInfo) { h.Set(api.ContentLength, strconv.FormatInt(info.Size, 10)) h.Set(api.ETag, info.HashSum) h.Set(api.AmzVersionID, info.ID().String()) + h.Set(api.AmzTaggingCount, strconv.Itoa(tagSetLength)) for key, val := range info.Headers { h[api.MetadataPrefix+key] = []string{val} @@ -119,7 +120,14 @@ func (h *handler) GetObjectHandler(w http.ResponseWriter, r *http.Request) { h.logAndSendError(w, "could not parse range header", reqInfo, err) return } - writeHeaders(w.Header(), info) + + tagSet, err := h.obj.GetObjectTagging(r.Context(), info) + if err != nil && !errors.IsS3Error(err, errors.ErrNoSuchKey) { + h.logAndSendError(w, "could not get object tag set", reqInfo, err) + return + } + + writeHeaders(w.Header(), info, len(tagSet)) if params != nil { writeRangeHeaders(w, params, info.Size) } diff --git a/api/handler/head.go b/api/handler/head.go index a03c33ca8..4e494cb80 100644 --- a/api/handler/head.go +++ b/api/handler/head.go @@ -5,6 +5,7 @@ import ( "net/http" "github.com/nspcc-dev/neofs-s3-gw/api" + "github.com/nspcc-dev/neofs-s3-gw/api/errors" "github.com/nspcc-dev/neofs-s3-gw/api/layer" "go.uber.org/zap" ) @@ -46,6 +47,11 @@ func (h *handler) HeadObjectHandler(w http.ResponseWriter, r *http.Request) { h.logAndSendError(w, "could not fetch object info", reqInfo, err) return } + tagSet, err := h.obj.GetObjectTagging(r.Context(), inf) + if err != nil && !errors.IsS3Error(err, errors.ErrNoSuchKey) { + h.logAndSendError(w, "could not get object tag set", reqInfo, err) + return + } if len(info.ContentType) == 0 { buffer := bytes.NewBuffer(make([]byte, 0, sizeToDetectType)) @@ -62,7 +68,7 @@ func (h *handler) HeadObjectHandler(w http.ResponseWriter, r *http.Request) { info.ContentType = http.DetectContentType(buffer.Bytes()) } - writeHeaders(w.Header(), info) + writeHeaders(w.Header(), info, len(tagSet)) w.WriteHeader(http.StatusOK) } diff --git a/api/handler/tagging.go b/api/handler/tagging.go index 19bb42915..bb1134638 100644 --- a/api/handler/tagging.go +++ b/api/handler/tagging.go @@ -60,6 +60,38 @@ func (h *handler) PutObjectTaggingHandler(w http.ResponseWriter, r *http.Request } } +func (h *handler) GetObjectTaggingHandler(w http.ResponseWriter, r *http.Request) { + reqInfo := api.GetReqInfo(r.Context()) + + p := &layer.HeadObjectParams{ + Bucket: reqInfo.BucketName, + Object: reqInfo.ObjectName, + VersionID: reqInfo.URL.Query().Get("versionId"), + } + + objInfo, err := h.obj.GetObjectInfo(r.Context(), p) + if err != nil { + h.logAndSendError(w, "could not get object info", reqInfo, err) + return + } + + tagSet, err := h.obj.GetObjectTagging(r.Context(), objInfo) + if err != nil { + h.logAndSendError(w, "could not get object tagging", reqInfo, err) + return + } + + tagging := &Tagging{} + for k, v := range tagSet { + tagging.TagSet = append(tagging.TagSet, Tag{Key: k, Value: v}) + } + + w.Header().Set(api.AmzVersionID, objInfo.Version()) + if err = api.EncodeToResponse(w, tagging); err != nil { + h.logAndSendError(w, "something went wrong", reqInfo, err) + } +} + func checkTagSet(tagSet []Tag) error { if len(tagSet) > 10 { return errors.GetAPIError(errors.ErrInvalidTag) diff --git a/api/handler/unimplemented.go b/api/handler/unimplemented.go index 7d531ea62..a10583b66 100644 --- a/api/handler/unimplemented.go +++ b/api/handler/unimplemented.go @@ -31,10 +31,6 @@ func (h *handler) AbortMultipartUploadHandler(w http.ResponseWriter, r *http.Req h.logAndSendError(w, "not implemented", api.GetReqInfo(r.Context()), errors.GetAPIError(errors.ErrNotImplemented)) } -func (h *handler) GetObjectTaggingHandler(w http.ResponseWriter, r *http.Request) { - h.logAndSendError(w, "not implemented", api.GetReqInfo(r.Context()), errors.GetAPIError(errors.ErrNotImplemented)) -} - func (h *handler) DeleteObjectTaggingHandler(w http.ResponseWriter, r *http.Request) { h.logAndSendError(w, "not implemented", api.GetReqInfo(r.Context()), errors.GetAPIError(errors.ErrNotImplemented)) } diff --git a/api/headers.go b/api/headers.go index dfdefbdc4..a5b9e9f8f 100644 --- a/api/headers.go +++ b/api/headers.go @@ -5,6 +5,7 @@ const ( MetadataPrefix = "X-Amz-Meta-" AmzMetadataDirective = "X-Amz-Metadata-Directive" AmzVersionID = "X-Amz-Version-Id" + AmzTaggingCount = "X-Amz-Tagging-Count" LastModified = "Last-Modified" Date = "Date" diff --git a/api/layer/layer.go b/api/layer/layer.go index 763c0d007..dcee78625 100644 --- a/api/layer/layer.go +++ b/api/layer/layer.go @@ -162,6 +162,7 @@ type ( GetObject(ctx context.Context, p *GetObjectParams) error GetObjectInfo(ctx context.Context, p *HeadObjectParams) (*ObjectInfo, error) + GetObjectTagging(ctx context.Context, p *ObjectInfo) (map[string]string, error) PutObject(ctx context.Context, p *PutObjectParams) (*ObjectInfo, error) PutObjectTagging(ctx context.Context, p *PutTaggingParams) error @@ -371,6 +372,32 @@ func (n *layer) PutObject(ctx context.Context, p *PutObjectParams) (*ObjectInfo, return n.objectPut(ctx, bkt, p) } +// GetObjectTagging from storage. +func (n *layer) GetObjectTagging(ctx context.Context, oi *ObjectInfo) (map[string]string, error) { + bktInfo := &BucketInfo{ + Name: oi.Bucket, + CID: oi.CID(), + Owner: oi.Owner, + } + + objInfo, err := n.getSystemObject(ctx, bktInfo, oi.TagsObject()) + if err != nil && !errors.IsS3Error(err, errors.ErrNoSuchKey) { + return nil, err + } + + var tagSet map[string]string + if objInfo != nil { + tagSet = make(map[string]string, len(objInfo.Headers)) + for k, v := range objInfo.Headers { + if strings.HasPrefix(k, tagPrefix) { + tagSet[strings.TrimPrefix(k, tagPrefix)] = v + } + } + } + + return tagSet, nil +} + // PutObjectTagging into storage. func (n *layer) PutObjectTagging(ctx context.Context, p *PutTaggingParams) error { bktInfo := &cache.BucketInfo{ @@ -438,6 +465,20 @@ func (n *layer) putSystemObject(ctx context.Context, bktInfo *cache.BucketInfo, return oid, nil } +func (n *layer) getSystemObject(ctx context.Context, bkt *BucketInfo, objName string) (*ObjectInfo, error) { + oid, err := n.objectFindID(ctx, &findParams{cid: bkt.CID, attr: objectSystemAttributeName, val: objName}) + if err != nil { + return nil, err + } + + meta, err := n.objectHead(ctx, bkt.CID, oid) + if err != nil { + return nil, err + } + + return objectInfoFromMeta(bkt, meta, "", ""), nil +} + // CopyObject from one bucket into another bucket. func (n *layer) CopyObject(ctx context.Context, p *CopyObjectParams) (*ObjectInfo, error) { pr, pw := io.Pipe() diff --git a/docs/aws_s3_compat.md b/docs/aws_s3_compat.md index 24e8aa68d..31a68ae3f 100644 --- a/docs/aws_s3_compat.md +++ b/docs/aws_s3_compat.md @@ -69,7 +69,7 @@ Should be supported soon. | | Method | Comments | |----|---------------------|----------| | 🔴 | DeleteObjectTagging | | -| 🔴 | GetObjectTagging | | +| 🟢 | GetObjectTagging | | | 🟢 | PutObjectTagging | | ## Versioning