forked from TrueCloudLab/frostfs-s3-gw
[#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
|
ErrNoSuchUpload
|
||||||
ErrNoSuchVersion
|
ErrNoSuchVersion
|
||||||
ErrInvalidVersion
|
ErrInvalidVersion
|
||||||
|
ErrInvalidTag
|
||||||
ErrNotImplemented
|
ErrNotImplemented
|
||||||
ErrPreconditionFailed
|
ErrPreconditionFailed
|
||||||
ErrNotModified
|
ErrNotModified
|
||||||
|
@ -536,6 +537,12 @@ var errorCodes = errorCodeMap{
|
||||||
Description: "Invalid version id specified",
|
Description: "Invalid version id specified",
|
||||||
HTTPStatusCode: http.StatusBadRequest,
|
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: {
|
ErrNotImplemented: {
|
||||||
ErrCode: ErrNotImplemented,
|
ErrCode: ErrNotImplemented,
|
||||||
Code: "NotImplemented",
|
Code: "NotImplemented",
|
||||||
|
|
|
@ -171,6 +171,18 @@ type VersioningConfiguration struct {
|
||||||
MfaDelete string `xml:"MfaDelete,omitempty"`
|
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.
|
// MarshalXML - StringMap marshals into XML.
|
||||||
func (s StringMap) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
func (s StringMap) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
||||||
tokens := []xml.Token{start}
|
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))
|
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) {
|
func (h *handler) DeleteObjectTaggingHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
h.logAndSendError(w, "not implemented", api.GetReqInfo(r.Context()), errors.GetAPIError(errors.ErrNotImplemented))
|
h.logAndSendError(w, "not implemented", api.GetReqInfo(r.Context()), errors.GetAPIError(errors.ErrNotImplemented))
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/nspcc-dev/neofs-api-go/pkg/acl/eacl"
|
"github.com/nspcc-dev/neofs-api-go/pkg/acl/eacl"
|
||||||
|
@ -134,6 +135,12 @@ type (
|
||||||
VersionID string
|
VersionID string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PutTaggingParams stores tag set params.
|
||||||
|
PutTaggingParams struct {
|
||||||
|
ObjectInfo *ObjectInfo
|
||||||
|
TagSet map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
// NeoFS provides basic NeoFS interface.
|
// NeoFS provides basic NeoFS interface.
|
||||||
NeoFS interface {
|
NeoFS interface {
|
||||||
Get(ctx context.Context, address *object.Address) (*object.Object, error)
|
Get(ctx context.Context, address *object.Address) (*object.Object, error)
|
||||||
|
@ -157,6 +164,7 @@ type (
|
||||||
GetObjectInfo(ctx context.Context, p *HeadObjectParams) (*ObjectInfo, error)
|
GetObjectInfo(ctx context.Context, p *HeadObjectParams) (*ObjectInfo, error)
|
||||||
|
|
||||||
PutObject(ctx context.Context, p *PutObjectParams) (*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)
|
CopyObject(ctx context.Context, p *CopyObjectParams) (*ObjectInfo, error)
|
||||||
|
|
||||||
|
@ -168,6 +176,8 @@ type (
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const tagPrefix = "S3-Tag-"
|
||||||
|
|
||||||
func (t *VersionedObject) String() string {
|
func (t *VersionedObject) String() string {
|
||||||
return t.Name + ":" + t.VersionID
|
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)
|
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.
|
// CopyObject from one bucket into another bucket.
|
||||||
func (n *layer) CopyObject(ctx context.Context, p *CopyObjectParams) (*ObjectInfo, error) {
|
func (n *layer) CopyObject(ctx context.Context, p *CopyObjectParams) (*ObjectInfo, error) {
|
||||||
pr, pw := io.Pipe()
|
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.Stringer("version id", id),
|
||||||
zap.Error(err))
|
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{
|
return &ObjectInfo{
|
||||||
|
|
|
@ -175,6 +175,9 @@ func (o *ObjectInfo) NiceName() string { return o.Bucket + "/" + o.Name }
|
||||||
// Address returns object address.
|
// Address returns object address.
|
||||||
func (o *ObjectInfo) Address() *object.Address { return newAddress(o.bucketID, o.id) }
|
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.
|
// CID returns bucket ID from ObjectInfo.
|
||||||
func (o *ObjectInfo) CID() *cid.ID { return o.bucketID }
|
func (o *ObjectInfo) CID() *cid.ID { return o.bucketID }
|
||||||
|
|
||||||
|
|
|
@ -70,7 +70,7 @@ Should be supported soon.
|
||||||
|----|---------------------|----------|
|
|----|---------------------|----------|
|
||||||
| 🔴 | DeleteObjectTagging | |
|
| 🔴 | DeleteObjectTagging | |
|
||||||
| 🔴 | GetObjectTagging | |
|
| 🔴 | GetObjectTagging | |
|
||||||
| 🔴 | PutObjectTagging | |
|
| 🟢 | PutObjectTagging | |
|
||||||
|
|
||||||
## Versioning
|
## Versioning
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue