[#196] Add PutObjectTagging
Signed-off-by: Denis Kirillov <denis@nspcc.ru>
This commit is contained in:
parent
44a2f1b471
commit
8b5ebe2ec2
9 changed files with 263 additions and 5 deletions
|
@ -67,6 +67,7 @@ const (
|
|||
ErrNoSuchUpload
|
||||
ErrNoSuchVersion
|
||||
ErrInvalidVersion
|
||||
ErrInvalidTag
|
||||
ErrNotImplemented
|
||||
ErrPreconditionFailed
|
||||
ErrNotModified
|
||||
|
@ -536,6 +537,12 @@ var errorCodes = errorCodeMap{
|
|||
Description: "Invalid version id specified",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrInvalidTag: {
|
||||
ErrCode: ErrInvalidTag,
|
||||
Code: "InvalidTag",
|
||||
Description: "You have passed bad tag input - duplicate keys, key/values are too long, system tags were sent.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrNotImplemented: {
|
||||
ErrCode: ErrNotImplemented,
|
||||
Code: "NotImplemented",
|
||||
|
|
|
@ -171,6 +171,18 @@ type VersioningConfiguration struct {
|
|||
MfaDelete string `xml:"MfaDelete,omitempty"`
|
||||
}
|
||||
|
||||
// Tagging contains tag set.
|
||||
type Tagging struct {
|
||||
XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ Tagging"`
|
||||
TagSet []Tag `xml:"TagSet>Tag"`
|
||||
}
|
||||
|
||||
// Tag is AWS key-value tag.
|
||||
type Tag struct {
|
||||
Key string
|
||||
Value string
|
||||
}
|
||||
|
||||
// MarshalXML - StringMap marshals into XML.
|
||||
func (s StringMap) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
||||
tokens := []xml.Token{start}
|
||||
|
|
107
api/handler/tagging.go
Normal file
107
api/handler/tagging.go
Normal file
|
@ -0,0 +1,107 @@
|
|||
package handler
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"net/http"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
const (
|
||||
allowedTagChars = "+-=._:/@"
|
||||
|
||||
keyTagMaxLength = 128
|
||||
valueTagMaxLength = 256
|
||||
)
|
||||
|
||||
func (h *handler) PutObjectTaggingHandler(w http.ResponseWriter, r *http.Request) {
|
||||
reqInfo := api.GetReqInfo(r.Context())
|
||||
|
||||
tagging := new(Tagging)
|
||||
if err := xml.NewDecoder(r.Body).Decode(tagging); err != nil {
|
||||
h.logAndSendError(w, "could not decode body", reqInfo, errors.GetAPIError(errors.ErrMalformedXML))
|
||||
return
|
||||
}
|
||||
|
||||
if err := checkTagSet(tagging.TagSet); err != nil {
|
||||
h.logAndSendError(w, "some tags are invalid", reqInfo, err)
|
||||
return
|
||||
}
|
||||
|
||||
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 := make(map[string]string, len(tagging.TagSet))
|
||||
for _, tag := range tagging.TagSet {
|
||||
tagSet[tag.Key] = tag.Value
|
||||
}
|
||||
|
||||
p2 := &layer.PutTaggingParams{
|
||||
ObjectInfo: objInfo,
|
||||
TagSet: tagSet,
|
||||
}
|
||||
|
||||
if err = h.obj.PutObjectTagging(r.Context(), p2); err != nil {
|
||||
h.logAndSendError(w, "could not put object tagging", reqInfo, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func checkTagSet(tagSet []Tag) error {
|
||||
if len(tagSet) > 10 {
|
||||
return errors.GetAPIError(errors.ErrInvalidTag)
|
||||
}
|
||||
|
||||
for _, tag := range tagSet {
|
||||
if err := checkTag(tag); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkTag(tag Tag) error {
|
||||
if len(tag.Key) < 1 || len(tag.Key) > keyTagMaxLength {
|
||||
return errors.GetAPIError(errors.ErrInvalidTag)
|
||||
}
|
||||
if len(tag.Value) < 1 || len(tag.Value) > valueTagMaxLength {
|
||||
return errors.GetAPIError(errors.ErrInvalidTag)
|
||||
}
|
||||
|
||||
if strings.HasPrefix(tag.Key, "aws:") {
|
||||
return errors.GetAPIError(errors.ErrInvalidTag)
|
||||
}
|
||||
|
||||
if err := checkCharacters(tag.Key); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := checkCharacters(tag.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkCharacters(str string) error {
|
||||
for _, r := range str {
|
||||
if !unicode.IsLetter(r) && !unicode.IsDigit(r) &&
|
||||
!unicode.IsSpace(r) && !strings.ContainsRune(allowedTagChars, r) {
|
||||
return errors.GetAPIError(errors.ErrInvalidTag)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
47
api/handler/tagging_test.go
Normal file
47
api/handler/tagging_test.go
Normal file
|
@ -0,0 +1,47 @@
|
|||
package handler
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestTagsValidity(t *testing.T) {
|
||||
sbKey := strings.Builder{}
|
||||
for i := 0; i < keyTagMaxLength; i++ {
|
||||
sbKey.WriteByte('a')
|
||||
}
|
||||
sbValue := strings.Builder{}
|
||||
for i := 0; i < valueTagMaxLength; i++ {
|
||||
sbValue.WriteByte('a')
|
||||
}
|
||||
|
||||
for _, tc := range []struct {
|
||||
tag Tag
|
||||
valid bool
|
||||
}{
|
||||
{tag: Tag{}, valid: false},
|
||||
{tag: Tag{Key: "", Value: "1"}, valid: false},
|
||||
{tag: Tag{Key: "2", Value: ""}, valid: false},
|
||||
{tag: Tag{Key: "aws:key", Value: "val"}, valid: false},
|
||||
{tag: Tag{Key: "key~", Value: "val"}, valid: false},
|
||||
{tag: Tag{Key: "key\\", Value: "val"}, valid: false},
|
||||
{tag: Tag{Key: "key?", Value: "val"}, valid: false},
|
||||
{tag: Tag{Key: sbKey.String() + "b", Value: "val"}, valid: false},
|
||||
{tag: Tag{Key: "key", Value: sbValue.String() + "b"}, valid: false},
|
||||
|
||||
{tag: Tag{Key: sbKey.String(), Value: "val"}, valid: true},
|
||||
{tag: Tag{Key: "key", Value: sbValue.String()}, valid: true},
|
||||
{tag: Tag{Key: "k e y", Value: "v a l"}, valid: true},
|
||||
{tag: Tag{Key: "12345", Value: "1234"}, valid: true},
|
||||
{tag: Tag{Key: allowedTagChars, Value: allowedTagChars}, valid: true},
|
||||
} {
|
||||
err := checkTag(tc.tag)
|
||||
if tc.valid {
|
||||
require.NoError(t, err)
|
||||
} else {
|
||||
require.Error(t, err)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -35,10 +35,6 @@ 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) PutObjectTaggingHandler(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))
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/nspcc-dev/neofs-api-go/pkg/acl/eacl"
|
||||
|
@ -134,6 +135,12 @@ type (
|
|||
VersionID string
|
||||
}
|
||||
|
||||
// PutTaggingParams stores tag set params.
|
||||
PutTaggingParams struct {
|
||||
ObjectInfo *ObjectInfo
|
||||
TagSet map[string]string
|
||||
}
|
||||
|
||||
// NeoFS provides basic NeoFS interface.
|
||||
NeoFS interface {
|
||||
Get(ctx context.Context, address *object.Address) (*object.Object, error)
|
||||
|
@ -157,6 +164,7 @@ type (
|
|||
GetObjectInfo(ctx context.Context, p *HeadObjectParams) (*ObjectInfo, error)
|
||||
|
||||
PutObject(ctx context.Context, p *PutObjectParams) (*ObjectInfo, error)
|
||||
PutObjectTagging(ctx context.Context, p *PutTaggingParams) error
|
||||
|
||||
CopyObject(ctx context.Context, p *CopyObjectParams) (*ObjectInfo, error)
|
||||
|
||||
|
@ -168,6 +176,8 @@ type (
|
|||
}
|
||||
)
|
||||
|
||||
const tagPrefix = "S3-Tag-"
|
||||
|
||||
func (t *VersionedObject) String() string {
|
||||
return t.Name + ":" + t.VersionID
|
||||
}
|
||||
|
@ -361,6 +371,73 @@ func (n *layer) PutObject(ctx context.Context, p *PutObjectParams) (*ObjectInfo,
|
|||
return n.objectPut(ctx, bkt, p)
|
||||
}
|
||||
|
||||
// PutObjectTagging into storage.
|
||||
func (n *layer) PutObjectTagging(ctx context.Context, p *PutTaggingParams) error {
|
||||
bktInfo := &cache.BucketInfo{
|
||||
Name: p.ObjectInfo.Bucket,
|
||||
CID: p.ObjectInfo.CID(),
|
||||
Owner: p.ObjectInfo.Owner,
|
||||
}
|
||||
|
||||
if _, err := n.putSystemObject(ctx, bktInfo, p.ObjectInfo.TagsObject(), p.TagSet, tagPrefix); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *layer) putSystemObject(ctx context.Context, bktInfo *cache.BucketInfo, objName string, metadata map[string]string, prefix string) (*object.ID, error) {
|
||||
oldOID, err := n.objectFindID(ctx, &findParams{cid: bktInfo.CID, attr: objectSystemAttributeName, val: objName})
|
||||
if err != nil && !errors.IsS3Error(err, errors.ErrNoSuchKey) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
attributes := make([]*object.Attribute, 0, 3)
|
||||
|
||||
filename := object.NewAttribute()
|
||||
filename.SetKey(objectSystemAttributeName)
|
||||
filename.SetValue(objName)
|
||||
|
||||
createdAt := object.NewAttribute()
|
||||
createdAt.SetKey(object.AttributeTimestamp)
|
||||
createdAt.SetValue(strconv.FormatInt(time.Now().UTC().Unix(), 10))
|
||||
|
||||
versioningIgnore := object.NewAttribute()
|
||||
versioningIgnore.SetKey(attrVersionsIgnore)
|
||||
versioningIgnore.SetValue(strconv.FormatBool(true))
|
||||
|
||||
attributes = append(attributes, filename, createdAt, versioningIgnore)
|
||||
|
||||
for k, v := range metadata {
|
||||
attr := object.NewAttribute()
|
||||
attr.SetKey(prefix + k)
|
||||
attr.SetValue(v)
|
||||
attributes = append(attributes, attr)
|
||||
}
|
||||
|
||||
raw := object.NewRaw()
|
||||
raw.SetOwnerID(bktInfo.Owner)
|
||||
raw.SetContainerID(bktInfo.CID)
|
||||
raw.SetAttributes(attributes...)
|
||||
|
||||
ops := new(client.PutObjectParams).WithObject(raw.Object())
|
||||
oid, err := n.pool.PutObject(ctx, ops, n.BearerOpt(ctx))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if _, err = n.objectHead(ctx, bktInfo.CID, oid); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if oldOID != nil {
|
||||
if err = n.objectDelete(ctx, bktInfo.CID, oldOID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return oid, nil
|
||||
}
|
||||
|
||||
// CopyObject from one bucket into another bucket.
|
||||
func (n *layer) CopyObject(ctx context.Context, p *CopyObjectParams) (*ObjectInfo, error) {
|
||||
pr, pw := io.Pipe()
|
||||
|
|
|
@ -179,6 +179,15 @@ func (n *layer) objectPut(ctx context.Context, bkt *cache.BucketInfo, p *PutObje
|
|||
zap.Stringer("version id", id),
|
||||
zap.Error(err))
|
||||
}
|
||||
if !versioningEnabled {
|
||||
if objVersion := versions.getVersion(id); objVersion != nil {
|
||||
if err = n.DeleteObjectTagging(ctx, objVersion); err != nil {
|
||||
n.log.Warn("couldn't delete object tagging",
|
||||
zap.Stringer("version id", id),
|
||||
zap.Error(err))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &ObjectInfo{
|
||||
|
|
|
@ -175,6 +175,9 @@ func (o *ObjectInfo) NiceName() string { return o.Bucket + "/" + o.Name }
|
|||
// Address returns object address.
|
||||
func (o *ObjectInfo) Address() *object.Address { return newAddress(o.bucketID, o.id) }
|
||||
|
||||
// TagsObject returns name of system object for tags.
|
||||
func (o *ObjectInfo) TagsObject() string { return ".tagset." + o.Name + "." + o.Version() }
|
||||
|
||||
// CID returns bucket ID from ObjectInfo.
|
||||
func (o *ObjectInfo) CID() *cid.ID { return o.bucketID }
|
||||
|
||||
|
|
|
@ -70,7 +70,7 @@ Should be supported soon.
|
|||
|----|---------------------|----------|
|
||||
| 🔴 | DeleteObjectTagging | |
|
||||
| 🔴 | GetObjectTagging | |
|
||||
| 🔴 | PutObjectTagging | |
|
||||
| 🟢 | PutObjectTagging | |
|
||||
|
||||
## Versioning
|
||||
|
||||
|
|
Loading…
Reference in a new issue