mirror of
https://github.com/minio/minio.git
synced 2024-12-24 06:05:55 -05:00
Add ObjectTagging Support (#8754)
This PR adds support for AWS S3 ObjectTagging API as explained here https://docs.aws.amazon.com/AmazonS3/latest/dev/object-tagging.html
This commit is contained in:
parent
dd93eee1e3
commit
61c17c8933
@ -36,6 +36,7 @@ import (
|
|||||||
"github.com/minio/minio/pkg/hash"
|
"github.com/minio/minio/pkg/hash"
|
||||||
"github.com/minio/minio/pkg/objectlock"
|
"github.com/minio/minio/pkg/objectlock"
|
||||||
"github.com/minio/minio/pkg/policy"
|
"github.com/minio/minio/pkg/policy"
|
||||||
|
"github.com/minio/minio/pkg/tagging"
|
||||||
)
|
)
|
||||||
|
|
||||||
// APIError structure
|
// APIError structure
|
||||||
@ -150,6 +151,7 @@ const (
|
|||||||
ErrPastObjectLockRetainDate
|
ErrPastObjectLockRetainDate
|
||||||
ErrUnknownWORMModeDirective
|
ErrUnknownWORMModeDirective
|
||||||
ErrObjectLockInvalidHeaders
|
ErrObjectLockInvalidHeaders
|
||||||
|
ErrInvalidTagDirective
|
||||||
// Add new error codes here.
|
// Add new error codes here.
|
||||||
|
|
||||||
// SSE-S3 related API errors
|
// SSE-S3 related API errors
|
||||||
@ -830,6 +832,11 @@ var errorCodes = errorCodeMap{
|
|||||||
Description: "Your metadata headers exceed the maximum allowed metadata size.",
|
Description: "Your metadata headers exceed the maximum allowed metadata size.",
|
||||||
HTTPStatusCode: http.StatusBadRequest,
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
},
|
},
|
||||||
|
ErrInvalidTagDirective: {
|
||||||
|
Code: "InvalidArgument",
|
||||||
|
Description: "Unknown tag directive.",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
ErrInvalidEncryptionMethod: {
|
ErrInvalidEncryptionMethod: {
|
||||||
Code: "InvalidRequest",
|
Code: "InvalidRequest",
|
||||||
Description: "The encryption method specified is not supported",
|
Description: "The encryption method specified is not supported",
|
||||||
@ -1780,6 +1787,12 @@ func toAPIError(ctx context.Context, err error) APIError {
|
|||||||
// their internal error types. This code is only
|
// their internal error types. This code is only
|
||||||
// useful with gateway implementations.
|
// useful with gateway implementations.
|
||||||
switch e := err.(type) {
|
switch e := err.(type) {
|
||||||
|
case tagging.Error:
|
||||||
|
apiErr = APIError{
|
||||||
|
Code: "InvalidTag",
|
||||||
|
Description: e.Error(),
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
}
|
||||||
case policy.Error:
|
case policy.Error:
|
||||||
apiErr = APIError{
|
apiErr = APIError{
|
||||||
Code: "MalformedPolicy",
|
Code: "MalformedPolicy",
|
||||||
|
@ -22,6 +22,7 @@ import (
|
|||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -95,6 +96,14 @@ func setObjectHeaders(w http.ResponseWriter, objInfo ObjectInfo, rs *HTTPRangeSp
|
|||||||
w.Header().Set(xhttp.XCache, objInfo.CacheStatus.String())
|
w.Header().Set(xhttp.XCache, objInfo.CacheStatus.String())
|
||||||
w.Header().Set(xhttp.XCacheLookup, objInfo.CacheLookupStatus.String())
|
w.Header().Set(xhttp.XCacheLookup, objInfo.CacheLookupStatus.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set tag count if object has tags
|
||||||
|
tags, _ := url.ParseQuery(objInfo.UserTags)
|
||||||
|
tagCount := len(tags)
|
||||||
|
if tagCount != 0 {
|
||||||
|
w.Header().Set(xhttp.AmzTagCount, strconv.Itoa(tagCount))
|
||||||
|
}
|
||||||
|
|
||||||
// Set all other user defined metadata.
|
// Set all other user defined metadata.
|
||||||
for k, v := range objInfo.UserDefined {
|
for k, v := range objInfo.UserDefined {
|
||||||
if HasPrefix(k, ReservedMetadataPrefix) {
|
if HasPrefix(k, ReservedMetadataPrefix) {
|
||||||
|
@ -105,8 +105,12 @@ func registerAPIRouter(router *mux.Router, encryptionEnabled, allowSSEKMS bool)
|
|||||||
bucket.Methods(http.MethodDelete).Path("/{object:.+}").HandlerFunc(collectAPIStats("abortmultipartupload", httpTraceAll(api.AbortMultipartUploadHandler))).Queries("uploadId", "{uploadId:.*}")
|
bucket.Methods(http.MethodDelete).Path("/{object:.+}").HandlerFunc(collectAPIStats("abortmultipartupload", httpTraceAll(api.AbortMultipartUploadHandler))).Queries("uploadId", "{uploadId:.*}")
|
||||||
// GetObjectACL - this is a dummy call.
|
// GetObjectACL - this is a dummy call.
|
||||||
bucket.Methods(http.MethodGet).Path("/{object:.+}").HandlerFunc(collectAPIStats("getobjectacl", httpTraceHdrs(api.GetObjectACLHandler))).Queries("acl", "")
|
bucket.Methods(http.MethodGet).Path("/{object:.+}").HandlerFunc(collectAPIStats("getobjectacl", httpTraceHdrs(api.GetObjectACLHandler))).Queries("acl", "")
|
||||||
// GetObjectTagging - this is a dummy call.
|
// GetObjectTagging
|
||||||
bucket.Methods(http.MethodGet).Path("/{object:.+}").HandlerFunc(collectAPIStats("getobjecttagging", httpTraceHdrs(api.GetObjectTaggingHandler))).Queries("tagging", "")
|
bucket.Methods(http.MethodGet).Path("/{object:.+}").HandlerFunc(collectAPIStats("getobjecttagging", httpTraceHdrs(api.GetObjectTaggingHandler))).Queries("tagging", "")
|
||||||
|
// PutObjectTagging
|
||||||
|
bucket.Methods(http.MethodPut).Path("/{object:.+}").HandlerFunc(collectAPIStats("putobjecttagging", httpTraceHdrs(api.PutObjectTaggingHandler))).Queries("tagging", "")
|
||||||
|
// DeleteObjectTagging
|
||||||
|
bucket.Methods(http.MethodDelete).Path("/{object:.+}").HandlerFunc(collectAPIStats("deleteobjecttagging", httpTraceHdrs(api.DeleteObjectTaggingHandler))).Queries("tagging", "")
|
||||||
// SelectObjectContent
|
// SelectObjectContent
|
||||||
bucket.Methods(http.MethodPost).Path("/{object:.+}").HandlerFunc(collectAPIStats("selectobjectcontent", httpTraceHdrs(api.SelectObjectContentHandler))).Queries("select", "").Queries("select-type", "2")
|
bucket.Methods(http.MethodPost).Path("/{object:.+}").HandlerFunc(collectAPIStats("selectobjectcontent", httpTraceHdrs(api.SelectObjectContentHandler))).Queries("select", "").Queries("select-type", "2")
|
||||||
// GetObjectRetention
|
// GetObjectRetention
|
||||||
|
@ -22,26 +22,13 @@ import (
|
|||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"github.com/minio/minio/pkg/policy"
|
"github.com/minio/minio/pkg/policy"
|
||||||
|
"github.com/minio/minio/pkg/tagging"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Data types used for returning dummy tagging XML.
|
// Data types used for returning dummy tagging XML.
|
||||||
// These variables shouldn't be used elsewhere.
|
// These variables shouldn't be used elsewhere.
|
||||||
// They are only defined to be used in this file alone.
|
// They are only defined to be used in this file alone.
|
||||||
|
|
||||||
type tagging struct {
|
|
||||||
XMLName xml.Name `xml:"Tagging"`
|
|
||||||
TagSet tagSet `xml:"TagSet"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type tagSet struct {
|
|
||||||
Tag []tagElem `xml:"Tag"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type tagElem struct {
|
|
||||||
Key string `xml:"Key"`
|
|
||||||
Value string `xml:"Value"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetBucketWebsite - GET bucket website, a dummy api
|
// GetBucketWebsite - GET bucket website, a dummy api
|
||||||
func (api objectAPIHandlers) GetBucketWebsiteHandler(w http.ResponseWriter, r *http.Request) {
|
func (api objectAPIHandlers) GetBucketWebsiteHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
writeSuccessResponseHeadersOnly(w)
|
writeSuccessResponseHeadersOnly(w)
|
||||||
@ -171,47 +158,8 @@ func (api objectAPIHandlers) GetBucketTaggingHandler(w http.ResponseWriter, r *h
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
tags := &tagging{}
|
tags := &tagging.Tagging{}
|
||||||
tags.TagSet.Tag = append(tags.TagSet.Tag, tagElem{})
|
tags.TagSet.Tags = append(tags.TagSet.Tags, tagging.Tag{})
|
||||||
|
|
||||||
if err := xml.NewEncoder(w).Encode(tags); err != nil {
|
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
w.(http.Flusher).Flush()
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetObjectTaggingHandler - GET object tagging, a dummy api
|
|
||||||
func (api objectAPIHandlers) GetObjectTaggingHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
ctx := newContext(r, w, "GetObjectTagging")
|
|
||||||
|
|
||||||
vars := mux.Vars(r)
|
|
||||||
bucket := vars["bucket"]
|
|
||||||
object := vars["object"]
|
|
||||||
|
|
||||||
objAPI := api.ObjectAPI()
|
|
||||||
if objAPI == nil {
|
|
||||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL, guessIsBrowserReq(r))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Allow getObjectTagging if policy action is set, since this is a dummy call
|
|
||||||
// we are simply re-purposing the bucketPolicyAction.
|
|
||||||
if s3Error := checkRequestAuthType(ctx, r, policy.GetBucketPolicyAction, bucket, ""); s3Error != ErrNone {
|
|
||||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL, guessIsBrowserReq(r))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate if object exists, before proceeding further...
|
|
||||||
_, err := objAPI.GetObjectInfo(ctx, bucket, object, ObjectOptions{})
|
|
||||||
if err != nil {
|
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
tags := &tagging{}
|
|
||||||
tags.TagSet.Tag = append(tags.TagSet.Tag, tagElem{})
|
|
||||||
|
|
||||||
if err := xml.NewEncoder(w).Encode(tags); err != nil {
|
if err := xml.NewEncoder(w).Encode(tags); err != nil {
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||||
|
@ -182,9 +182,14 @@ func (m fsMetaV1) ToObjectInfo(bucket, object string, fi os.FileInfo) ObjectInfo
|
|||||||
objInfo.Expires = t.UTC()
|
objInfo.Expires = t.UTC()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add user tags to the object info
|
||||||
|
objInfo.UserTags = m.Meta[xhttp.AmzObjectTagging]
|
||||||
|
|
||||||
// etag/md5Sum has already been extracted. We need to
|
// etag/md5Sum has already been extracted. We need to
|
||||||
// remove to avoid it from appearing as part of
|
// remove to avoid it from appearing as part of
|
||||||
// response headers. e.g, X-Minio-* or X-Amz-*.
|
// response headers. e.g, X-Minio-* or X-Amz-*.
|
||||||
|
// Tags have also been extracted, we remove that as well.
|
||||||
objInfo.UserDefined = cleanMetadata(m.Meta)
|
objInfo.UserDefined = cleanMetadata(m.Meta)
|
||||||
|
|
||||||
// All the parts per object.
|
// All the parts per object.
|
||||||
|
54
cmd/fs-v1.go
54
cmd/fs-v1.go
@ -37,6 +37,7 @@ import (
|
|||||||
jsoniter "github.com/json-iterator/go"
|
jsoniter "github.com/json-iterator/go"
|
||||||
"github.com/minio/minio-go/v6/pkg/s3utils"
|
"github.com/minio/minio-go/v6/pkg/s3utils"
|
||||||
"github.com/minio/minio/cmd/config"
|
"github.com/minio/minio/cmd/config"
|
||||||
|
xhttp "github.com/minio/minio/cmd/http"
|
||||||
"github.com/minio/minio/cmd/logger"
|
"github.com/minio/minio/cmd/logger"
|
||||||
"github.com/minio/minio/pkg/lifecycle"
|
"github.com/minio/minio/pkg/lifecycle"
|
||||||
"github.com/minio/minio/pkg/lock"
|
"github.com/minio/minio/pkg/lock"
|
||||||
@ -44,6 +45,7 @@ import (
|
|||||||
"github.com/minio/minio/pkg/mimedb"
|
"github.com/minio/minio/pkg/mimedb"
|
||||||
"github.com/minio/minio/pkg/mountinfo"
|
"github.com/minio/minio/pkg/mountinfo"
|
||||||
"github.com/minio/minio/pkg/policy"
|
"github.com/minio/minio/pkg/policy"
|
||||||
|
"github.com/minio/minio/pkg/tagging"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Default etag is used for pre-existing objects.
|
// Default etag is used for pre-existing objects.
|
||||||
@ -1235,6 +1237,58 @@ func (fs *FSObjects) ListObjects(ctx context.Context, bucket, prefix, marker, de
|
|||||||
fs.listDirFactory(), fs.getObjectInfo, fs.getObjectInfo)
|
fs.listDirFactory(), fs.getObjectInfo, fs.getObjectInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetObjectTag - get object tags from an existing object
|
||||||
|
func (fs *FSObjects) GetObjectTag(ctx context.Context, bucket, object string) (tagging.Tagging, error) {
|
||||||
|
oi, err := fs.GetObjectInfo(ctx, bucket, object, ObjectOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return tagging.Tagging{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tags, err := tagging.FromString(oi.UserTags)
|
||||||
|
if err != nil {
|
||||||
|
return tagging.Tagging{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return tags, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PutObjectTag - replace or add tags to an existing object
|
||||||
|
func (fs *FSObjects) PutObjectTag(ctx context.Context, bucket, object string, tags string) error {
|
||||||
|
fsMetaPath := pathJoin(fs.fsPath, minioMetaBucket, bucketMetaPrefix, bucket, object, fs.metaJSONFile)
|
||||||
|
fsMeta := fsMetaV1{}
|
||||||
|
wlk, err := fs.rwPool.Write(fsMetaPath)
|
||||||
|
if err != nil {
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
return toObjectErr(err, bucket, object)
|
||||||
|
}
|
||||||
|
// This close will allow for locks to be synchronized on `fs.json`.
|
||||||
|
defer wlk.Close()
|
||||||
|
|
||||||
|
// Read objects' metadata in `fs.json`.
|
||||||
|
if _, err = fsMeta.ReadFrom(ctx, wlk); err != nil {
|
||||||
|
// For any error to read fsMeta, set default ETag and proceed.
|
||||||
|
fsMeta = fs.defaultFsJSON(object)
|
||||||
|
}
|
||||||
|
|
||||||
|
// clean fsMeta.Meta of tag key, before updating the new tags
|
||||||
|
delete(fsMeta.Meta, xhttp.AmzObjectTagging)
|
||||||
|
|
||||||
|
// Do not update for empty tags
|
||||||
|
if tags != "" {
|
||||||
|
fsMeta.Meta[xhttp.AmzObjectTagging] = tags
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err = fsMeta.WriteTo(wlk); err != nil {
|
||||||
|
return toObjectErr(err, bucket, object)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteObjectTag - delete object tags from an existing object
|
||||||
|
func (fs *FSObjects) DeleteObjectTag(ctx context.Context, bucket, object string) error {
|
||||||
|
return fs.PutObjectTag(ctx, bucket, object, "")
|
||||||
|
}
|
||||||
|
|
||||||
// ReloadFormat - no-op for fs, Valid only for XL.
|
// ReloadFormat - no-op for fs, Valid only for XL.
|
||||||
func (fs *FSObjects) ReloadFormat(ctx context.Context, dryRun bool) error {
|
func (fs *FSObjects) ReloadFormat(ctx context.Context, dryRun bool) error {
|
||||||
logger.LogIf(ctx, NotImplemented{})
|
logger.LogIf(ctx, NotImplemented{})
|
||||||
|
@ -24,6 +24,7 @@ import (
|
|||||||
"github.com/minio/minio/pkg/lifecycle"
|
"github.com/minio/minio/pkg/lifecycle"
|
||||||
"github.com/minio/minio/pkg/madmin"
|
"github.com/minio/minio/pkg/madmin"
|
||||||
"github.com/minio/minio/pkg/policy"
|
"github.com/minio/minio/pkg/policy"
|
||||||
|
"github.com/minio/minio/pkg/tagging"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GatewayLocker implements custom NeNSLock implementation
|
// GatewayLocker implements custom NeNSLock implementation
|
||||||
@ -173,6 +174,24 @@ func (a GatewayUnsupported) GetMetrics(ctx context.Context) (*Metrics, error) {
|
|||||||
return &Metrics{}, NotImplemented{}
|
return &Metrics{}, NotImplemented{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PutObjectTag - not implemented.
|
||||||
|
func (a GatewayUnsupported) PutObjectTag(ctx context.Context, bucket, object string, tags string) error {
|
||||||
|
logger.LogIf(ctx, NotImplemented{})
|
||||||
|
return NotImplemented{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetObjectTag - not implemented.
|
||||||
|
func (a GatewayUnsupported) GetObjectTag(ctx context.Context, bucket, object string) (tagging.Tagging, error) {
|
||||||
|
logger.LogIf(ctx, NotImplemented{})
|
||||||
|
return tagging.Tagging{}, NotImplemented{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteObjectTag - not implemented.
|
||||||
|
func (a GatewayUnsupported) DeleteObjectTag(ctx context.Context, bucket, object string) error {
|
||||||
|
logger.LogIf(ctx, NotImplemented{})
|
||||||
|
return NotImplemented{}
|
||||||
|
}
|
||||||
|
|
||||||
// IsNotificationSupported returns whether bucket notification is applicable for this layer.
|
// IsNotificationSupported returns whether bucket notification is applicable for this layer.
|
||||||
func (a GatewayUnsupported) IsNotificationSupported() bool {
|
func (a GatewayUnsupported) IsNotificationSupported() bool {
|
||||||
return false
|
return false
|
||||||
|
@ -467,8 +467,8 @@ func ignoreNotImplementedBucketResources(req *http.Request) bool {
|
|||||||
// Checks requests for not implemented Object resources
|
// Checks requests for not implemented Object resources
|
||||||
func ignoreNotImplementedObjectResources(req *http.Request) bool {
|
func ignoreNotImplementedObjectResources(req *http.Request) bool {
|
||||||
for name := range req.URL.Query() {
|
for name := range req.URL.Query() {
|
||||||
// Enable GetObjectACL and GetObjectTagging dummy calls specifically.
|
// Enable GetObjectACL dummy call specifically.
|
||||||
if (name == "acl" || name == "tagging") && req.Method == http.MethodGet {
|
if name == "acl" && req.Method == http.MethodGet {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if notimplementedObjectResourceNames[name] {
|
if notimplementedObjectResourceNames[name] {
|
||||||
@ -497,7 +497,6 @@ var notimplementedBucketResourceNames = map[string]bool{
|
|||||||
var notimplementedObjectResourceNames = map[string]bool{
|
var notimplementedObjectResourceNames = map[string]bool{
|
||||||
"acl": true,
|
"acl": true,
|
||||||
"restore": true,
|
"restore": true,
|
||||||
"tagging": true,
|
|
||||||
"torrent": true,
|
"torrent": true,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -34,6 +34,12 @@ import (
|
|||||||
"github.com/minio/minio/pkg/auth"
|
"github.com/minio/minio/pkg/auth"
|
||||||
"github.com/minio/minio/pkg/handlers"
|
"github.com/minio/minio/pkg/handlers"
|
||||||
"github.com/minio/minio/pkg/madmin"
|
"github.com/minio/minio/pkg/madmin"
|
||||||
|
"github.com/minio/minio/pkg/tagging"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
copyDirective = "COPY"
|
||||||
|
replaceDirective = "REPLACE"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Parses location constraint from the incoming reader.
|
// Parses location constraint from the incoming reader.
|
||||||
@ -69,30 +75,27 @@ var supportedHeaders = []string{
|
|||||||
"content-encoding",
|
"content-encoding",
|
||||||
"content-disposition",
|
"content-disposition",
|
||||||
xhttp.AmzStorageClass,
|
xhttp.AmzStorageClass,
|
||||||
|
xhttp.AmzObjectTagging,
|
||||||
"expires",
|
"expires",
|
||||||
// Add more supported headers here.
|
// Add more supported headers here.
|
||||||
}
|
}
|
||||||
|
|
||||||
// isMetadataDirectiveValid - check if metadata-directive is valid.
|
// isDirectiveValid - check if tagging-directive is valid.
|
||||||
func isMetadataDirectiveValid(h http.Header) bool {
|
func isDirectiveValid(v string) bool {
|
||||||
_, ok := h[http.CanonicalHeaderKey(xhttp.AmzMetadataDirective)]
|
// Check if set metadata-directive is valid.
|
||||||
if ok {
|
return isDirectiveCopy(v) || isDirectiveReplace(v)
|
||||||
// Check atleast set metadata-directive is valid.
|
}
|
||||||
return (isMetadataCopy(h) || isMetadataReplace(h))
|
|
||||||
}
|
// Check if the directive COPY is requested.
|
||||||
// By default if x-amz-metadata-directive is not we
|
func isDirectiveCopy(value string) bool {
|
||||||
|
// By default if directive is not set we
|
||||||
// treat it as 'COPY' this function returns true.
|
// treat it as 'COPY' this function returns true.
|
||||||
return true
|
return value == copyDirective || value == ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the metadata COPY is requested.
|
// Check if the directive REPLACE is requested.
|
||||||
func isMetadataCopy(h http.Header) bool {
|
func isDirectiveReplace(value string) bool {
|
||||||
return h.Get(xhttp.AmzMetadataDirective) == "COPY"
|
return value == replaceDirective
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the metadata REPLACE is requested.
|
|
||||||
func isMetadataReplace(h http.Header) bool {
|
|
||||||
return h.Get(xhttp.AmzMetadataDirective) == "REPLACE"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Splits an incoming path into bucket and object components.
|
// Splits an incoming path into bucket and object components.
|
||||||
@ -174,6 +177,26 @@ func extractMetadataFromMap(ctx context.Context, v map[string][]string, m map[st
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// extractTags extracts tag key and value from given http header. It then
|
||||||
|
// - Parses the input format X-Amz-Tagging:"Key1=Value1&Key2=Value2" into a map[string]string
|
||||||
|
// with entries in the format X-Amg-Tag-Key1:Value1, X-Amz-Tag-Key2:Value2
|
||||||
|
// - Validates the tags
|
||||||
|
// - Returns the Tag in original string format "Key1=Value1&Key2=Value2"
|
||||||
|
func extractTags(ctx context.Context, tags string) (string, error) {
|
||||||
|
// Check if the metadata has tagging related header
|
||||||
|
if tags != "" {
|
||||||
|
tagging, err := tagging.FromString(tags)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if err := tagging.Validate(); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return tagging.String(), nil
|
||||||
|
}
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
// The Query string for the redirect URL the client is
|
// The Query string for the redirect URL the client is
|
||||||
// redirected on successful upload.
|
// redirected on successful upload.
|
||||||
func getRedirectPostRawQuery(objInfo ObjectInfo) string {
|
func getRedirectPostRawQuery(objInfo ObjectInfo) string {
|
||||||
|
@ -56,6 +56,11 @@ const (
|
|||||||
// S3 storage class
|
// S3 storage class
|
||||||
AmzStorageClass = "x-amz-storage-class"
|
AmzStorageClass = "x-amz-storage-class"
|
||||||
|
|
||||||
|
// S3 object tagging
|
||||||
|
AmzObjectTagging = "X-Amz-Tagging"
|
||||||
|
AmzTagCount = "X-Amz-Tag-Count"
|
||||||
|
AmzTagDirective = "X-Amz-Tagging-Directive"
|
||||||
|
|
||||||
// S3 extensions
|
// S3 extensions
|
||||||
AmzCopySourceIfModifiedSince = "x-amz-copy-source-if-modified-since"
|
AmzCopySourceIfModifiedSince = "x-amz-copy-source-if-modified-since"
|
||||||
AmzCopySourceIfUnmodifiedSince = "x-amz-copy-source-if-unmodified-since"
|
AmzCopySourceIfUnmodifiedSince = "x-amz-copy-source-if-unmodified-since"
|
||||||
|
@ -154,6 +154,9 @@ type ObjectInfo struct {
|
|||||||
// User-Defined metadata
|
// User-Defined metadata
|
||||||
UserDefined map[string]string
|
UserDefined map[string]string
|
||||||
|
|
||||||
|
// User-Defined object tags
|
||||||
|
UserTags string
|
||||||
|
|
||||||
// List of individual parts, maximum size of upto 10,000
|
// List of individual parts, maximum size of upto 10,000
|
||||||
Parts []ObjectPartInfo `json:"-"`
|
Parts []ObjectPartInfo `json:"-"`
|
||||||
|
|
||||||
|
@ -25,6 +25,7 @@ import (
|
|||||||
"github.com/minio/minio/pkg/lifecycle"
|
"github.com/minio/minio/pkg/lifecycle"
|
||||||
"github.com/minio/minio/pkg/madmin"
|
"github.com/minio/minio/pkg/madmin"
|
||||||
"github.com/minio/minio/pkg/policy"
|
"github.com/minio/minio/pkg/policy"
|
||||||
|
"github.com/minio/minio/pkg/tagging"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CheckCopyPreconditionFn returns true if copy precondition check failed.
|
// CheckCopyPreconditionFn returns true if copy precondition check failed.
|
||||||
@ -125,4 +126,9 @@ type ObjectLayer interface {
|
|||||||
|
|
||||||
// Check Readiness
|
// Check Readiness
|
||||||
IsReady(ctx context.Context) bool
|
IsReady(ctx context.Context) bool
|
||||||
|
|
||||||
|
// ObjectTagging operations
|
||||||
|
PutObjectTag(context.Context, string, string, string) error
|
||||||
|
GetObjectTag(context.Context, string, string) (tagging.Tagging, error)
|
||||||
|
DeleteObjectTag(context.Context, string, string) error
|
||||||
}
|
}
|
||||||
|
@ -239,8 +239,8 @@ func getCompleteMultipartMD5(parts []CompletePart) string {
|
|||||||
func cleanMetadata(metadata map[string]string) map[string]string {
|
func cleanMetadata(metadata map[string]string) map[string]string {
|
||||||
// Remove STANDARD StorageClass
|
// Remove STANDARD StorageClass
|
||||||
metadata = removeStandardStorageClass(metadata)
|
metadata = removeStandardStorageClass(metadata)
|
||||||
// Clean meta etag keys 'md5Sum', 'etag', "expires".
|
// Clean meta etag keys 'md5Sum', 'etag', "expires", "x-amz-tagging".
|
||||||
return cleanMetadataKeys(metadata, "md5Sum", "etag", "expires")
|
return cleanMetadataKeys(metadata, "md5Sum", "etag", "expires", xhttp.AmzObjectTagging)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter X-Amz-Storage-Class field only if it is set to STANDARD.
|
// Filter X-Amz-Storage-Class field only if it is set to STANDARD.
|
||||||
|
@ -50,6 +50,7 @@ import (
|
|||||||
"github.com/minio/minio/pkg/ioutil"
|
"github.com/minio/minio/pkg/ioutil"
|
||||||
"github.com/minio/minio/pkg/policy"
|
"github.com/minio/minio/pkg/policy"
|
||||||
"github.com/minio/minio/pkg/s3select"
|
"github.com/minio/minio/pkg/s3select"
|
||||||
|
"github.com/minio/minio/pkg/tagging"
|
||||||
sha256 "github.com/minio/sha256-simd"
|
sha256 "github.com/minio/sha256-simd"
|
||||||
"github.com/minio/sio"
|
"github.com/minio/sio"
|
||||||
)
|
)
|
||||||
@ -604,13 +605,13 @@ func getCpObjMetadataFromHeader(ctx context.Context, r *http.Request, userMeta m
|
|||||||
|
|
||||||
// if x-amz-metadata-directive says REPLACE then
|
// if x-amz-metadata-directive says REPLACE then
|
||||||
// we extract metadata from the input headers.
|
// we extract metadata from the input headers.
|
||||||
if isMetadataReplace(r.Header) {
|
if isDirectiveReplace(r.Header.Get(xhttp.AmzMetadataDirective)) {
|
||||||
return extractMetadata(ctx, r)
|
return extractMetadata(ctx, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
// if x-amz-metadata-directive says COPY then we
|
// if x-amz-metadata-directive says COPY then we
|
||||||
// return the default metadata.
|
// return the default metadata.
|
||||||
if isMetadataCopy(r.Header) {
|
if isDirectiveCopy(r.Header.Get(xhttp.AmzMetadataDirective)) {
|
||||||
return defaultMeta, nil
|
return defaultMeta, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -618,6 +619,24 @@ func getCpObjMetadataFromHeader(ctx context.Context, r *http.Request, userMeta m
|
|||||||
return defaultMeta, nil
|
return defaultMeta, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Extract tags relevant for an CopyObject operation based on conditional
|
||||||
|
// header values specified in X-Amz-Tagging-Directive.
|
||||||
|
func getCpObjTagsFromHeader(ctx context.Context, r *http.Request, tags string) (string, error) {
|
||||||
|
// if x-amz-tagging-directive says REPLACE then
|
||||||
|
// we extract tags from the input headers.
|
||||||
|
if isDirectiveReplace(r.Header.Get(xhttp.AmzTagDirective)) {
|
||||||
|
if tags := r.Header.Get(xhttp.AmzObjectTagging); tags != "" {
|
||||||
|
return extractTags(ctx, tags)
|
||||||
|
}
|
||||||
|
// Copy is default behavior if x-amz-tagging-directive is set, but x-amz-tagging is
|
||||||
|
// is not set
|
||||||
|
return tags, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy is default behavior if x-amz-tagging-directive is not set.
|
||||||
|
return tags, nil
|
||||||
|
}
|
||||||
|
|
||||||
// Returns a minio-go Client configured to access remote host described by destDNSRecord
|
// Returns a minio-go Client configured to access remote host described by destDNSRecord
|
||||||
// Applicable only in a federated deployment
|
// Applicable only in a federated deployment
|
||||||
var getRemoteInstanceClient = func(r *http.Request, host string) (*miniogo.Core, error) {
|
var getRemoteInstanceClient = func(r *http.Request, host string) (*miniogo.Core, error) {
|
||||||
@ -737,11 +756,17 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check if metadata directive is valid.
|
// Check if metadata directive is valid.
|
||||||
if !isMetadataDirectiveValid(r.Header) {
|
if !isDirectiveValid(r.Header.Get(xhttp.AmzMetadataDirective)) {
|
||||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidMetadataDirective), r.URL, guessIsBrowserReq(r))
|
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidMetadataDirective), r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// check if tag directive is valid
|
||||||
|
if !isDirectiveValid(r.Header.Get(xhttp.AmzTagDirective)) {
|
||||||
|
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidTagDirective), r.URL, guessIsBrowserReq(r))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// This request header needs to be set prior to setting ObjectOptions
|
// This request header needs to be set prior to setting ObjectOptions
|
||||||
if globalAutoEncryption && !crypto.SSEC.IsRequested(r.Header) {
|
if globalAutoEncryption && !crypto.SSEC.IsRequested(r.Header) {
|
||||||
r.Header.Add(crypto.SSEHeader, crypto.SSEAlgorithmAES256)
|
r.Header.Add(crypto.SSEHeader, crypto.SSEAlgorithmAES256)
|
||||||
@ -795,7 +820,7 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re
|
|||||||
defer gr.Close()
|
defer gr.Close()
|
||||||
srcInfo := gr.ObjInfo
|
srcInfo := gr.ObjInfo
|
||||||
|
|
||||||
/// maximum Upload size for object in a single CopyObject operation.
|
// maximum Upload size for object in a single CopyObject operation.
|
||||||
if isMaxObjectSize(srcInfo.Size) {
|
if isMaxObjectSize(srcInfo.Size) {
|
||||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrEntityTooLarge), r.URL, guessIsBrowserReq(r))
|
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrEntityTooLarge), r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
@ -968,6 +993,17 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re
|
|||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tags, err := getCpObjTagsFromHeader(ctx, r, srcInfo.UserTags)
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if tags != "" {
|
||||||
|
srcInfo.UserDefined[xhttp.AmzObjectTagging] = tags
|
||||||
|
}
|
||||||
|
|
||||||
getObjectInfo := objectAPI.GetObjectInfo
|
getObjectInfo := objectAPI.GetObjectInfo
|
||||||
if api.CacheAPI() != nil {
|
if api.CacheAPI() != nil {
|
||||||
getObjectInfo = api.CacheAPI().GetObjectInfo
|
getObjectInfo = api.CacheAPI().GetObjectInfo
|
||||||
@ -1002,12 +1038,13 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re
|
|||||||
|
|
||||||
// Ensure that metadata does not contain sensitive information
|
// Ensure that metadata does not contain sensitive information
|
||||||
crypto.RemoveSensitiveEntries(srcInfo.UserDefined)
|
crypto.RemoveSensitiveEntries(srcInfo.UserDefined)
|
||||||
// Check if x-amz-metadata-directive was not set to REPLACE and source,
|
// Check if x-amz-metadata-directive or x-amz-tagging-directive was not set to REPLACE and source,
|
||||||
// desination are same objects. Apply this restriction also when
|
// destination are same objects. Apply this restriction also when
|
||||||
// metadataOnly is true indicating that we are not overwriting the object.
|
// metadataOnly is true indicating that we are not overwriting the object.
|
||||||
// if encryption is enabled we do not need explicit "REPLACE" metadata to
|
// if encryption is enabled we do not need explicit "REPLACE" metadata to
|
||||||
// be enabled as well - this is to allow for key-rotation.
|
// be enabled as well - this is to allow for key-rotation.
|
||||||
if !isMetadataReplace(r.Header) && srcInfo.metadataOnly && !crypto.IsEncrypted(srcInfo.UserDefined) {
|
if !isDirectiveReplace(r.Header.Get(xhttp.AmzMetadataDirective)) && !isDirectiveReplace(r.Header.Get(xhttp.AmzTagDirective)) &&
|
||||||
|
srcInfo.metadataOnly && !crypto.IsEncrypted(srcInfo.UserDefined) {
|
||||||
// If x-amz-metadata-directive is not set to REPLACE then we need
|
// If x-amz-metadata-directive is not set to REPLACE then we need
|
||||||
// to error out if source and destination are same.
|
// to error out if source and destination are same.
|
||||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidCopyDest), r.URL, guessIsBrowserReq(r))
|
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidCopyDest), r.URL, guessIsBrowserReq(r))
|
||||||
@ -1155,6 +1192,14 @@ func (api objectAPIHandlers) PutObjectHandler(w http.ResponseWriter, r *http.Req
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if tags := r.Header.Get(http.CanonicalHeaderKey(xhttp.AmzObjectTagging)); tags != "" {
|
||||||
|
metadata[xhttp.AmzObjectTagging], err = extractTags(ctx, tags)
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if rAuthType == authTypeStreamingSigned {
|
if rAuthType == authTypeStreamingSigned {
|
||||||
if contentEncoding, ok := metadata["content-encoding"]; ok {
|
if contentEncoding, ok := metadata["content-encoding"]; ok {
|
||||||
contentEncoding = trimAwsChunkedContentEncoding(contentEncoding)
|
contentEncoding = trimAwsChunkedContentEncoding(contentEncoding)
|
||||||
@ -2774,3 +2819,103 @@ func (api objectAPIHandlers) GetObjectRetentionHandler(w http.ResponseWriter, r
|
|||||||
Host: handlers.GetSourceIP(r),
|
Host: handlers.GetSourceIP(r),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetObjectTaggingHandler - GET object tagging
|
||||||
|
func (api objectAPIHandlers) GetObjectTaggingHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := newContext(r, w, "GetObjectTagging")
|
||||||
|
defer logger.AuditLog(w, r, "GetObjectTagging", mustGetClaimsFromToken(r))
|
||||||
|
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
bucket := vars["bucket"]
|
||||||
|
object := vars["object"]
|
||||||
|
|
||||||
|
objAPI := api.ObjectAPI()
|
||||||
|
if objAPI == nil {
|
||||||
|
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL, guessIsBrowserReq(r))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow getObjectTagging if policy action is set.
|
||||||
|
if s3Error := checkRequestAuthType(ctx, r, policy.GetObjectTaggingAction, bucket, object); s3Error != ErrNone {
|
||||||
|
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL, guessIsBrowserReq(r))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get object tags
|
||||||
|
tags, err := objAPI.GetObjectTag(ctx, bucket, object)
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
writeSuccessResponseXML(w, encodeResponse(tags))
|
||||||
|
}
|
||||||
|
|
||||||
|
// PutObjectTaggingHandler - PUT object tagging
|
||||||
|
func (api objectAPIHandlers) PutObjectTaggingHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := newContext(r, w, "PutObjectTagging")
|
||||||
|
defer logger.AuditLog(w, r, "PutObjectTagging", mustGetClaimsFromToken(r))
|
||||||
|
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
bucket := vars["bucket"]
|
||||||
|
object := vars["object"]
|
||||||
|
|
||||||
|
objAPI := api.ObjectAPI()
|
||||||
|
if objAPI == nil {
|
||||||
|
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL, guessIsBrowserReq(r))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow putObjectTagging if policy action is set
|
||||||
|
if s3Error := checkRequestAuthType(ctx, r, policy.PutObjectTaggingAction, bucket, object); s3Error != ErrNone {
|
||||||
|
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL, guessIsBrowserReq(r))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tagging, err := tagging.ParseTagging(io.LimitReader(r.Body, r.ContentLength))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Put object tags
|
||||||
|
err = objAPI.PutObjectTag(ctx, bucket, object, tagging.String())
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
writeSuccessResponseHeadersOnly(w)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteObjectTaggingHandler - DELETE object tagging
|
||||||
|
func (api objectAPIHandlers) DeleteObjectTaggingHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := newContext(r, w, "DeleteObjectTagging")
|
||||||
|
defer logger.AuditLog(w, r, "DeleteObjectTagging", mustGetClaimsFromToken(r))
|
||||||
|
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
bucket := vars["bucket"]
|
||||||
|
object := vars["object"]
|
||||||
|
|
||||||
|
objAPI := api.ObjectAPI()
|
||||||
|
if objAPI == nil {
|
||||||
|
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL, guessIsBrowserReq(r))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow deleteObjectTagging if policy action is set
|
||||||
|
if s3Error := checkRequestAuthType(ctx, r, policy.DeleteObjectTaggingAction, bucket, object); s3Error != ErrNone {
|
||||||
|
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL, guessIsBrowserReq(r))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete object tags
|
||||||
|
err := objAPI.DeleteObjectTag(ctx, bucket, object)
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
writeSuccessResponseHeadersOnly(w)
|
||||||
|
}
|
||||||
|
@ -35,6 +35,7 @@ import (
|
|||||||
"github.com/minio/minio/pkg/madmin"
|
"github.com/minio/minio/pkg/madmin"
|
||||||
"github.com/minio/minio/pkg/policy"
|
"github.com/minio/minio/pkg/policy"
|
||||||
"github.com/minio/minio/pkg/sync/errgroup"
|
"github.com/minio/minio/pkg/sync/errgroup"
|
||||||
|
"github.com/minio/minio/pkg/tagging"
|
||||||
)
|
)
|
||||||
|
|
||||||
// setsStorageAPI is encapsulated type for Close()
|
// setsStorageAPI is encapsulated type for Close()
|
||||||
@ -1661,6 +1662,21 @@ func (s *xlSets) ListObjectsHeal(ctx context.Context, bucket, prefix, marker, de
|
|||||||
return s.listObjects(ctx, bucket, prefix, marker, delimiter, maxKeys, true)
|
return s.listObjects(ctx, bucket, prefix, marker, delimiter, maxKeys, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PutObjectTag - replace or add tags to an existing object
|
||||||
|
func (s *xlSets) PutObjectTag(ctx context.Context, bucket, object string, tags string) error {
|
||||||
|
return s.getHashedSet(object).PutObjectTag(ctx, bucket, object, tags)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteObjectTag - delete object tags from an existing object
|
||||||
|
func (s *xlSets) DeleteObjectTag(ctx context.Context, bucket, object string) error {
|
||||||
|
return s.getHashedSet(object).DeleteObjectTag(ctx, bucket, object)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetObjectTag - get object tags from an existing object
|
||||||
|
func (s *xlSets) GetObjectTag(ctx context.Context, bucket, object string) (tagging.Tagging, error) {
|
||||||
|
return s.getHashedSet(object).GetObjectTag(ctx, bucket, object)
|
||||||
|
}
|
||||||
|
|
||||||
// GetMetrics - no op
|
// GetMetrics - no op
|
||||||
func (s *xlSets) GetMetrics(ctx context.Context) (*Metrics, error) {
|
func (s *xlSets) GetMetrics(ctx context.Context) (*Metrics, error) {
|
||||||
logger.LogIf(ctx, NotImplemented{})
|
logger.LogIf(ctx, NotImplemented{})
|
||||||
|
@ -245,9 +245,13 @@ func (m xlMetaV1) ToObjectInfo(bucket, object string) ObjectInfo {
|
|||||||
// Extract etag from metadata.
|
// Extract etag from metadata.
|
||||||
objInfo.ETag = extractETag(m.Meta)
|
objInfo.ETag = extractETag(m.Meta)
|
||||||
|
|
||||||
|
// Add user tags to the object info
|
||||||
|
objInfo.UserTags = m.Meta[xhttp.AmzObjectTagging]
|
||||||
|
|
||||||
// etag/md5Sum has already been extracted. We need to
|
// etag/md5Sum has already been extracted. We need to
|
||||||
// remove to avoid it from appearing as part of
|
// remove to avoid it from appearing as part of
|
||||||
// response headers. e.g, X-Minio-* or X-Amz-*.
|
// response headers. e.g, X-Minio-* or X-Amz-*.
|
||||||
|
// Tags have also been extracted, we remove that as well.
|
||||||
objInfo.UserDefined = cleanMetadata(m.Meta)
|
objInfo.UserDefined = cleanMetadata(m.Meta)
|
||||||
|
|
||||||
// All the parts per object.
|
// All the parts per object.
|
||||||
|
@ -27,6 +27,7 @@ import (
|
|||||||
"github.com/minio/minio/cmd/logger"
|
"github.com/minio/minio/cmd/logger"
|
||||||
"github.com/minio/minio/pkg/mimedb"
|
"github.com/minio/minio/pkg/mimedb"
|
||||||
"github.com/minio/minio/pkg/sync/errgroup"
|
"github.com/minio/minio/pkg/sync/errgroup"
|
||||||
|
"github.com/minio/minio/pkg/tagging"
|
||||||
)
|
)
|
||||||
|
|
||||||
// list all errors which can be ignored in object operations.
|
// list all errors which can be ignored in object operations.
|
||||||
@ -969,7 +970,7 @@ func (xl xlObjects) ListObjectsV2(ctx context.Context, bucket, prefix, continuat
|
|||||||
return listObjectsV2Info, err
|
return listObjectsV2Info, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send the successul but partial upload, however ignore
|
// Send the successful but partial upload, however ignore
|
||||||
// if the channel is blocked by other items.
|
// if the channel is blocked by other items.
|
||||||
func (xl xlObjects) addPartialUpload(bucket, key string) {
|
func (xl xlObjects) addPartialUpload(bucket, key string) {
|
||||||
select {
|
select {
|
||||||
@ -977,3 +978,60 @@ func (xl xlObjects) addPartialUpload(bucket, key string) {
|
|||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PutObjectTag - replace or add tags to an existing object
|
||||||
|
func (xl xlObjects) PutObjectTag(ctx context.Context, bucket, object string, tags string) error {
|
||||||
|
disks := xl.getDisks()
|
||||||
|
|
||||||
|
// Read metadata associated with the object from all disks.
|
||||||
|
metaArr, errs := readAllXLMetadata(ctx, disks, bucket, object)
|
||||||
|
|
||||||
|
_, writeQuorum, err := objectQuorumFromMeta(ctx, xl, metaArr, errs)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, xlMeta := range metaArr {
|
||||||
|
// clean xlMeta.Meta of tag key, before updating the new tags
|
||||||
|
delete(xlMeta.Meta, xhttp.AmzObjectTagging)
|
||||||
|
// Don't update for empty tags
|
||||||
|
if tags != "" {
|
||||||
|
xlMeta.Meta[xhttp.AmzObjectTagging] = tags
|
||||||
|
}
|
||||||
|
metaArr[i].Meta = xlMeta.Meta
|
||||||
|
}
|
||||||
|
|
||||||
|
tempObj := mustGetUUID()
|
||||||
|
|
||||||
|
// Write unique `xl.json` for each disk.
|
||||||
|
if disks, err = writeUniqueXLMetadata(ctx, disks, minioMetaTmpBucket, tempObj, metaArr, writeQuorum); err != nil {
|
||||||
|
return toObjectErr(err, bucket, object)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Atomically rename `xl.json` from tmp location to destination for each disk.
|
||||||
|
if _, err = renameXLMetadata(ctx, disks, minioMetaTmpBucket, tempObj, bucket, object, writeQuorum); err != nil {
|
||||||
|
return toObjectErr(err, bucket, object)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteObjectTag - delete object tags from an existing object
|
||||||
|
func (xl xlObjects) DeleteObjectTag(ctx context.Context, bucket, object string) error {
|
||||||
|
return xl.PutObjectTag(ctx, bucket, object, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetObjectTag - get object tags from an existing object
|
||||||
|
func (xl xlObjects) GetObjectTag(ctx context.Context, bucket, object string) (tagging.Tagging, error) {
|
||||||
|
// GetObjectInfo will return tag value as well
|
||||||
|
oi, err := xl.GetObjectInfo(ctx, bucket, object, ObjectOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return tagging.Tagging{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tags, err := tagging.FromString(oi.UserTags)
|
||||||
|
if err != nil {
|
||||||
|
return tagging.Tagging{}, err
|
||||||
|
}
|
||||||
|
return tags, nil
|
||||||
|
}
|
||||||
|
@ -31,6 +31,7 @@ import (
|
|||||||
"github.com/minio/minio/pkg/madmin"
|
"github.com/minio/minio/pkg/madmin"
|
||||||
"github.com/minio/minio/pkg/policy"
|
"github.com/minio/minio/pkg/policy"
|
||||||
"github.com/minio/minio/pkg/sync/errgroup"
|
"github.com/minio/minio/pkg/sync/errgroup"
|
||||||
|
"github.com/minio/minio/pkg/tagging"
|
||||||
)
|
)
|
||||||
|
|
||||||
type xlZones struct {
|
type xlZones struct {
|
||||||
@ -1374,3 +1375,63 @@ func (z *xlZones) GetMetrics(ctx context.Context) (*Metrics, error) {
|
|||||||
func (z *xlZones) IsReady(ctx context.Context) bool {
|
func (z *xlZones) IsReady(ctx context.Context) bool {
|
||||||
return z.zones[0].IsReady(ctx)
|
return z.zones[0].IsReady(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PutObjectTag - replace or add tags to an existing object
|
||||||
|
func (z *xlZones) PutObjectTag(ctx context.Context, bucket, object string, tags string) error {
|
||||||
|
if z.SingleZone() {
|
||||||
|
return z.zones[0].PutObjectTag(ctx, bucket, object, tags)
|
||||||
|
}
|
||||||
|
for _, zone := range z.zones {
|
||||||
|
err := zone.PutObjectTag(ctx, bucket, object, tags)
|
||||||
|
if err != nil {
|
||||||
|
if isErrBucketNotFound(err) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return BucketNotFound{
|
||||||
|
Bucket: bucket,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteObjectTag - delete object tags from an existing object
|
||||||
|
func (z *xlZones) DeleteObjectTag(ctx context.Context, bucket, object string) error {
|
||||||
|
if z.SingleZone() {
|
||||||
|
return z.zones[0].DeleteObjectTag(ctx, bucket, object)
|
||||||
|
}
|
||||||
|
for _, zone := range z.zones {
|
||||||
|
err := zone.DeleteObjectTag(ctx, bucket, object)
|
||||||
|
if err != nil {
|
||||||
|
if isErrBucketNotFound(err) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return BucketNotFound{
|
||||||
|
Bucket: bucket,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetObjectTag - get object tags from an existing object
|
||||||
|
func (z *xlZones) GetObjectTag(ctx context.Context, bucket, object string) (tagging.Tagging, error) {
|
||||||
|
if z.SingleZone() {
|
||||||
|
return z.zones[0].GetObjectTag(ctx, bucket, object)
|
||||||
|
}
|
||||||
|
for _, zone := range z.zones {
|
||||||
|
tags, err := zone.GetObjectTag(ctx, bucket, object)
|
||||||
|
if err != nil {
|
||||||
|
if isErrBucketNotFound(err) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return tags, err
|
||||||
|
}
|
||||||
|
return tags, nil
|
||||||
|
}
|
||||||
|
return tagging.Tagging{}, BucketNotFound{
|
||||||
|
Bucket: bucket,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -51,7 +51,6 @@ We found the following APIs to be redundant or less useful outside of AWS S3. If
|
|||||||
- ObjectACL (Use [bucket policies](https://docs.min.io/docs/minio-client-complete-guide#policy) instead)
|
- ObjectACL (Use [bucket policies](https://docs.min.io/docs/minio-client-complete-guide#policy) instead)
|
||||||
- ObjectTorrent
|
- ObjectTorrent
|
||||||
- ObjectVersions
|
- ObjectVersions
|
||||||
- ObjectTagging
|
|
||||||
|
|
||||||
### Object name restrictions on MinIO
|
### Object name restrictions on MinIO
|
||||||
Object names that contain characters `^*|\/&";` are unsupported on Windows and other file systems which do not support filenames with these characters. Note that this list is not exhaustive, and depends on the maintainers of the filesystem itself.
|
Object names that contain characters `^*|\/&";` are unsupported on Windows and other file systems which do not support filenames with these characters. Note that this list is not exhaustive, and depends on the maintainers of the filesystem itself.
|
||||||
|
@ -28,10 +28,12 @@ import (
|
|||||||
"math/rand"
|
"math/rand"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/aws/aws-sdk-go/aws"
|
"github.com/aws/aws-sdk-go/aws"
|
||||||
|
"github.com/aws/aws-sdk-go/aws/awserr"
|
||||||
"github.com/aws/aws-sdk-go/aws/credentials"
|
"github.com/aws/aws-sdk-go/aws/credentials"
|
||||||
"github.com/aws/aws-sdk-go/aws/session"
|
"github.com/aws/aws-sdk-go/aws/session"
|
||||||
"github.com/aws/aws-sdk-go/service/s3"
|
"github.com/aws/aws-sdk-go/service/s3"
|
||||||
@ -132,6 +134,50 @@ func randString(n int, src rand.Source, prefix string) string {
|
|||||||
return prefix + string(b[0:30-len(prefix)])
|
return prefix + string(b[0:30-len(prefix)])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isObjectTaggingImplemented(s3Client *s3.S3) bool {
|
||||||
|
bucket := randString(60, rand.NewSource(time.Now().UnixNano()), "aws-sdk-go-test-")
|
||||||
|
object := randString(60, rand.NewSource(time.Now().UnixNano()), "")
|
||||||
|
startTime := time.Now()
|
||||||
|
function := "isObjectTaggingImplemented"
|
||||||
|
args := map[string]interface{}{
|
||||||
|
"bucketName": bucket,
|
||||||
|
"objectName": object,
|
||||||
|
}
|
||||||
|
defer cleanup(s3Client, bucket, object, function, args, startTime, true)
|
||||||
|
|
||||||
|
_, err := s3Client.CreateBucket(&s3.CreateBucketInput{
|
||||||
|
Bucket: aws.String(bucket),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
failureLog(function, args, startTime, "", "AWS SDK Go CreateBucket Failed", err).Fatal()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = s3Client.PutObject(&s3.PutObjectInput{
|
||||||
|
Body: aws.ReadSeekCloser(strings.NewReader("testfile")),
|
||||||
|
Bucket: aws.String(bucket),
|
||||||
|
Key: aws.String(object),
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
failureLog(function, args, startTime, "", fmt.Sprintf("AWS SDK Go PUT expected to success but got %v", err), err).Fatal()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = s3Client.GetObjectTagging(&s3.GetObjectTaggingInput{
|
||||||
|
Bucket: aws.String(bucket),
|
||||||
|
Key: aws.String(object),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
if awsErr, ok := err.(awserr.Error); ok {
|
||||||
|
if awsErr.Code() == "NotImplemented" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
func cleanup(s3Client *s3.S3, bucket string, object string, function string,
|
func cleanup(s3Client *s3.S3, bucket string, object string, function string,
|
||||||
args map[string]interface{}, startTime time.Time, deleteBucket bool) {
|
args map[string]interface{}, startTime time.Time, deleteBucket bool) {
|
||||||
|
|
||||||
@ -474,6 +520,95 @@ func testSelectObject(s3Client *s3.S3) {
|
|||||||
successLogger(function, args, startTime).Info()
|
successLogger(function, args, startTime).Info()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testObjectTagging(s3Client *s3.S3) {
|
||||||
|
startTime := time.Now()
|
||||||
|
function := "testObjectTagging"
|
||||||
|
bucket := randString(60, rand.NewSource(time.Now().UnixNano()), "aws-sdk-go-test-")
|
||||||
|
object := randString(60, rand.NewSource(time.Now().UnixNano()), "")
|
||||||
|
args := map[string]interface{}{
|
||||||
|
"bucketName": bucket,
|
||||||
|
"objectName": object,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := s3Client.CreateBucket(&s3.CreateBucketInput{
|
||||||
|
Bucket: aws.String(bucket),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
failureLog(function, args, startTime, "", "AWS SDK Go CreateBucket Failed", err).Fatal()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer cleanup(s3Client, bucket, object, function, args, startTime, true)
|
||||||
|
|
||||||
|
taginput := "Tag1=Value1"
|
||||||
|
tagInputSet := []*s3.Tag{
|
||||||
|
{
|
||||||
|
Key: aws.String("Tag1"),
|
||||||
|
Value: aws.String("Value1"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
_, err = s3Client.PutObject(&s3.PutObjectInput{
|
||||||
|
Body: aws.ReadSeekCloser(strings.NewReader("testfile")),
|
||||||
|
Bucket: aws.String(bucket),
|
||||||
|
Key: aws.String(object),
|
||||||
|
Tagging: &taginput,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
failureLog(function, args, startTime, "", fmt.Sprintf("AWS SDK Go PUT expected to success but got %v", err), err).Fatal()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tagop, err := s3Client.GetObjectTagging(&s3.GetObjectTaggingInput{
|
||||||
|
Bucket: aws.String(bucket),
|
||||||
|
Key: aws.String(object),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
if awsErr, ok := err.(awserr.Error); ok {
|
||||||
|
failureLog(function, args, startTime, "", fmt.Sprintf("AWS SDK Go PUTObjectTagging expected to success but got %v", awsErr.Code()), err).Fatal()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(tagop.TagSet, tagInputSet) {
|
||||||
|
failureLog(function, args, startTime, "", fmt.Sprintf("AWS SDK Go PUTObject Tag input did not match with GetObjectTagging output %v", nil), nil).Fatal()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
taginputSet1 := []*s3.Tag{
|
||||||
|
{
|
||||||
|
Key: aws.String("Key4"),
|
||||||
|
Value: aws.String("Value4"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
_, err = s3Client.PutObjectTagging(&s3.PutObjectTaggingInput{
|
||||||
|
Bucket: aws.String(bucket),
|
||||||
|
Key: aws.String(object),
|
||||||
|
Tagging: &s3.Tagging{
|
||||||
|
TagSet: taginputSet1,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
if awsErr, ok := err.(awserr.Error); ok {
|
||||||
|
failureLog(function, args, startTime, "", fmt.Sprintf("AWS SDK Go PUTObjectTagging expected to success but got %v", awsErr.Code()), err).Fatal()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tagop, err = s3Client.GetObjectTagging(&s3.GetObjectTaggingInput{
|
||||||
|
Bucket: aws.String(bucket),
|
||||||
|
Key: aws.String(object),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
if awsErr, ok := err.(awserr.Error); ok {
|
||||||
|
failureLog(function, args, startTime, "", fmt.Sprintf("AWS SDK Go PUTObjectTagging expected to success but got %v", awsErr.Code()), err).Fatal()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(tagop.TagSet, taginputSet1) {
|
||||||
|
failureLog(function, args, startTime, "", fmt.Sprintf("AWS SDK Go PUTObjectTagging input did not match with GetObjectTagging output %v", nil), nil).Fatal()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
endpoint := os.Getenv("SERVER_ENDPOINT")
|
endpoint := os.Getenv("SERVER_ENDPOINT")
|
||||||
accessKey := os.Getenv("ACCESS_KEY")
|
accessKey := os.Getenv("ACCESS_KEY")
|
||||||
@ -508,4 +643,7 @@ func main() {
|
|||||||
testPresignedPutInvalidHash(s3Client)
|
testPresignedPutInvalidHash(s3Client)
|
||||||
testListObjects(s3Client)
|
testListObjects(s3Client)
|
||||||
testSelectObject(s3Client)
|
testSelectObject(s3Client)
|
||||||
|
if isObjectTaggingImplemented(s3Client) {
|
||||||
|
testObjectTagging(s3Client)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -114,6 +114,15 @@ const (
|
|||||||
// PutBucketObjectLockConfigurationAction - PutBucketObjectLockConfiguration Rest API action
|
// PutBucketObjectLockConfigurationAction - PutBucketObjectLockConfiguration Rest API action
|
||||||
PutBucketObjectLockConfigurationAction = "s3:PutBucketObjectLockConfiguration"
|
PutBucketObjectLockConfigurationAction = "s3:PutBucketObjectLockConfiguration"
|
||||||
|
|
||||||
|
// GetObjectTaggingAction - Get Object Tags API action
|
||||||
|
GetObjectTaggingAction = "s3:GetObjectTagging"
|
||||||
|
|
||||||
|
// PutObjectTaggingAction - Put Object Tags API action
|
||||||
|
PutObjectTaggingAction = "s3:PutObjectTagging"
|
||||||
|
|
||||||
|
// DeleteObjectTaggingAction - Delete Object Tags API action
|
||||||
|
DeleteObjectTaggingAction = "s3:DeleteObjectTagging"
|
||||||
|
|
||||||
// AllActions - all API actions
|
// AllActions - all API actions
|
||||||
AllActions = "s3:*"
|
AllActions = "s3:*"
|
||||||
)
|
)
|
||||||
@ -149,6 +158,9 @@ var supportedActions = map[Action]struct{}{
|
|||||||
GetBucketObjectLockConfigurationAction: {},
|
GetBucketObjectLockConfigurationAction: {},
|
||||||
BypassGovernanceModeAction: {},
|
BypassGovernanceModeAction: {},
|
||||||
BypassGovernanceRetentionAction: {},
|
BypassGovernanceRetentionAction: {},
|
||||||
|
GetObjectTaggingAction: {},
|
||||||
|
PutObjectTaggingAction: {},
|
||||||
|
DeleteObjectTaggingAction: {},
|
||||||
}
|
}
|
||||||
|
|
||||||
// isObjectAction - returns whether action is object type or not.
|
// isObjectAction - returns whether action is object type or not.
|
||||||
@ -164,6 +176,8 @@ func (action Action) isObjectAction() bool {
|
|||||||
return true
|
return true
|
||||||
case PutObjectLegalHoldAction, GetObjectLegalHoldAction:
|
case PutObjectLegalHoldAction, GetObjectLegalHoldAction:
|
||||||
return true
|
return true
|
||||||
|
case GetObjectTaggingAction, PutObjectTaggingAction, DeleteObjectTaggingAction:
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
@ -283,4 +297,7 @@ var actionConditionKeyMap = map[Action]condition.KeySet{
|
|||||||
BypassGovernanceRetentionAction: condition.NewKeySet(condition.CommonKeys...),
|
BypassGovernanceRetentionAction: condition.NewKeySet(condition.CommonKeys...),
|
||||||
GetBucketObjectLockConfigurationAction: condition.NewKeySet(condition.CommonKeys...),
|
GetBucketObjectLockConfigurationAction: condition.NewKeySet(condition.CommonKeys...),
|
||||||
PutBucketObjectLockConfigurationAction: condition.NewKeySet(condition.CommonKeys...),
|
PutBucketObjectLockConfigurationAction: condition.NewKeySet(condition.CommonKeys...),
|
||||||
|
PutObjectTaggingAction: condition.NewKeySet(condition.CommonKeys...),
|
||||||
|
GetObjectTaggingAction: condition.NewKeySet(condition.CommonKeys...),
|
||||||
|
DeleteObjectTaggingAction: condition.NewKeySet(condition.CommonKeys...),
|
||||||
}
|
}
|
||||||
|
@ -106,6 +106,13 @@ const (
|
|||||||
GetBucketObjectLockConfigurationAction = "s3:GetBucketObjectLockConfiguration"
|
GetBucketObjectLockConfigurationAction = "s3:GetBucketObjectLockConfiguration"
|
||||||
// PutBucketObjectLockConfigurationAction - PutObjectLockConfiguration Rest API action
|
// PutBucketObjectLockConfigurationAction - PutObjectLockConfiguration Rest API action
|
||||||
PutBucketObjectLockConfigurationAction = "s3:PutBucketObjectLockConfiguration"
|
PutBucketObjectLockConfigurationAction = "s3:PutBucketObjectLockConfiguration"
|
||||||
|
|
||||||
|
// GetObjectTaggingAction - Get Object Tags API action
|
||||||
|
GetObjectTaggingAction = "s3:GetObjectTagging"
|
||||||
|
// PutObjectTaggingAction - Put Object Tags API action
|
||||||
|
PutObjectTaggingAction = "s3:PutObjectTagging"
|
||||||
|
// DeleteObjectTaggingAction - Delete Object Tags API action
|
||||||
|
DeleteObjectTaggingAction = "s3:DeleteObjectTagging"
|
||||||
)
|
)
|
||||||
|
|
||||||
// isObjectAction - returns whether action is object type or not.
|
// isObjectAction - returns whether action is object type or not.
|
||||||
@ -121,6 +128,8 @@ func (action Action) isObjectAction() bool {
|
|||||||
return true
|
return true
|
||||||
case BypassGovernanceModeAction, BypassGovernanceRetentionAction:
|
case BypassGovernanceModeAction, BypassGovernanceRetentionAction:
|
||||||
return true
|
return true
|
||||||
|
case GetObjectTaggingAction, PutObjectTaggingAction, DeleteObjectTaggingAction:
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
@ -153,6 +162,8 @@ func (action Action) IsValid() bool {
|
|||||||
return true
|
return true
|
||||||
case PutBucketObjectLockConfigurationAction, GetBucketObjectLockConfigurationAction:
|
case PutBucketObjectLockConfigurationAction, GetBucketObjectLockConfigurationAction:
|
||||||
return true
|
return true
|
||||||
|
case GetObjectTaggingAction, PutObjectTaggingAction, DeleteObjectTaggingAction:
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
@ -243,4 +254,7 @@ var actionConditionKeyMap = map[Action]condition.KeySet{
|
|||||||
GetObjectLegalHoldAction: condition.NewKeySet(condition.CommonKeys...),
|
GetObjectLegalHoldAction: condition.NewKeySet(condition.CommonKeys...),
|
||||||
GetBucketObjectLockConfigurationAction: condition.NewKeySet(condition.CommonKeys...),
|
GetBucketObjectLockConfigurationAction: condition.NewKeySet(condition.CommonKeys...),
|
||||||
PutBucketObjectLockConfigurationAction: condition.NewKeySet(condition.CommonKeys...),
|
PutBucketObjectLockConfigurationAction: condition.NewKeySet(condition.CommonKeys...),
|
||||||
|
PutObjectTaggingAction: condition.NewKeySet(condition.CommonKeys...),
|
||||||
|
GetObjectTaggingAction: condition.NewKeySet(condition.CommonKeys...),
|
||||||
|
DeleteObjectTaggingAction: condition.NewKeySet(condition.CommonKeys...),
|
||||||
}
|
}
|
||||||
|
44
pkg/tagging/error.go
Normal file
44
pkg/tagging/error.go
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
/*
|
||||||
|
* MinIO Cloud Storage, (C) 2020 MinIO, Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package tagging
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Error is the generic type for any error happening during tag
|
||||||
|
// parsing.
|
||||||
|
type Error struct {
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Errorf - formats according to a format specifier and returns
|
||||||
|
// the string as a value that satisfies error of type tagging.Error
|
||||||
|
func Errorf(format string, a ...interface{}) error {
|
||||||
|
return Error{err: fmt.Errorf(format, a...)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unwrap the internal error.
|
||||||
|
func (e Error) Unwrap() error { return e.err }
|
||||||
|
|
||||||
|
// Error 'error' compatible method.
|
||||||
|
func (e Error) Error() string {
|
||||||
|
if e.err == nil {
|
||||||
|
return "tagging: cause <nil>"
|
||||||
|
}
|
||||||
|
return e.err.Error()
|
||||||
|
}
|
62
pkg/tagging/tag.go
Normal file
62
pkg/tagging/tag.go
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
/*
|
||||||
|
* MinIO Cloud Storage, (C) 2020 MinIO, Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package tagging
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
"unicode/utf8"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Tag - single tag
|
||||||
|
type Tag struct {
|
||||||
|
XMLName xml.Name `xml:"Tag"`
|
||||||
|
Key string `xml:"Key"`
|
||||||
|
Value string `xml:"Value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate - validates the tag element
|
||||||
|
func (t Tag) Validate() error {
|
||||||
|
if err := t.validateKey(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := t.validateValue(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateKey - checks if key is valid or not.
|
||||||
|
func (t Tag) validateKey() error {
|
||||||
|
// cannot be longer than maxTagKeyLength characters
|
||||||
|
if utf8.RuneCountInString(t.Key) > maxTagKeyLength {
|
||||||
|
return ErrInvalidTagKey
|
||||||
|
}
|
||||||
|
// cannot be empty
|
||||||
|
if len(t.Key) == 0 {
|
||||||
|
return ErrInvalidTagKey
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateValue - checks if value is valid or not.
|
||||||
|
func (t Tag) validateValue() error {
|
||||||
|
// cannot be longer than maxTagValueLength characters
|
||||||
|
if utf8.RuneCountInString(t.Value) > maxTagValueLength {
|
||||||
|
return ErrInvalidTagValue
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
113
pkg/tagging/tagging.go
Normal file
113
pkg/tagging/tagging.go
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
/*
|
||||||
|
* MinIO Cloud Storage, (C) 2020 MinIO, Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package tagging
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/xml"
|
||||||
|
"io"
|
||||||
|
"net/url"
|
||||||
|
)
|
||||||
|
|
||||||
|
// S3 API limits for tags
|
||||||
|
// Ref: https://docs.aws.amazon.com/AmazonS3/latest/dev/object-tagging.html
|
||||||
|
const (
|
||||||
|
maxTags = 10
|
||||||
|
maxTagKeyLength = 128
|
||||||
|
maxTagValueLength = 256
|
||||||
|
)
|
||||||
|
|
||||||
|
// errors returned by tagging package
|
||||||
|
var (
|
||||||
|
ErrTooManyTags = Errorf("Cannot have more than 10 object tags")
|
||||||
|
ErrInvalidTagKey = Errorf("The TagKey you have provided is invalid")
|
||||||
|
ErrInvalidTagValue = Errorf("The TagValue you have provided is invalid")
|
||||||
|
ErrInvalidTag = Errorf("Cannot provide multiple Tags with the same key")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Tagging - object tagging interface
|
||||||
|
type Tagging struct {
|
||||||
|
XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ Tagging"`
|
||||||
|
TagSet TagSet `xml:"TagSet"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate - validates the tagging configuration
|
||||||
|
func (t Tagging) Validate() error {
|
||||||
|
// Tagging can't have more than 10 tags
|
||||||
|
if len(t.TagSet.Tags) > maxTags {
|
||||||
|
return ErrTooManyTags
|
||||||
|
}
|
||||||
|
// Validate all the rules in the tagging config
|
||||||
|
for _, ts := range t.TagSet.Tags {
|
||||||
|
if t.TagSet.ContainsDuplicate(ts.Key) {
|
||||||
|
return ErrInvalidTag
|
||||||
|
}
|
||||||
|
if err := ts.Validate(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// String - returns a string in format "tag1=value1&tag2=value2" with all the
|
||||||
|
// tags in this Tagging Struct
|
||||||
|
func (t Tagging) String() string {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
for _, tag := range t.TagSet.Tags {
|
||||||
|
if buf.Len() > 0 {
|
||||||
|
buf.WriteString("&")
|
||||||
|
}
|
||||||
|
buf.WriteString(tag.Key + "=")
|
||||||
|
buf.WriteString(tag.Value)
|
||||||
|
}
|
||||||
|
return buf.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// FromString - returns a Tagging struct when given a string in format
|
||||||
|
// "tag1=value1&tag2=value2"
|
||||||
|
func FromString(tagStr string) (Tagging, error) {
|
||||||
|
tags, err := url.ParseQuery(tagStr)
|
||||||
|
if err != nil {
|
||||||
|
return Tagging{}, err
|
||||||
|
}
|
||||||
|
var idx = 0
|
||||||
|
parsedTags := make([]Tag, len(tags))
|
||||||
|
for k := range tags {
|
||||||
|
parsedTags[idx].Key = k
|
||||||
|
parsedTags[idx].Value = tags.Get(k)
|
||||||
|
idx++
|
||||||
|
}
|
||||||
|
return Tagging{
|
||||||
|
TagSet: TagSet{
|
||||||
|
Tags: parsedTags,
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseTagging - parses incoming xml data in given reader
|
||||||
|
// into Tagging interface. After parsing, also validates the
|
||||||
|
// parsed fields based on S3 API constraints.
|
||||||
|
func ParseTagging(reader io.Reader) (*Tagging, error) {
|
||||||
|
var t Tagging
|
||||||
|
if err := xml.NewDecoder(reader).Decode(&t); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := t.Validate(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &t, nil
|
||||||
|
}
|
41
pkg/tagging/tagset.go
Normal file
41
pkg/tagging/tagset.go
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
/*
|
||||||
|
* MinIO Cloud Storage, (C) 2020 MinIO, Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package tagging
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TagSet - Set of tags under Tagging
|
||||||
|
type TagSet struct {
|
||||||
|
XMLName xml.Name `xml:"TagSet"`
|
||||||
|
Tags []Tag `xml:"Tag"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ContainsDuplicate - returns true if duplicate keys are present in TagSet
|
||||||
|
func (t TagSet) ContainsDuplicate(key string) bool {
|
||||||
|
var found bool
|
||||||
|
for _, tag := range t.Tags {
|
||||||
|
if tag.Key == key {
|
||||||
|
if found {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user