mirror of
https://github.com/minio/minio.git
synced 2025-11-09 21:49:46 -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:
committed by
kannappanr
parent
dd93eee1e3
commit
61c17c8933
@@ -36,6 +36,7 @@ import (
|
||||
"github.com/minio/minio/pkg/hash"
|
||||
"github.com/minio/minio/pkg/objectlock"
|
||||
"github.com/minio/minio/pkg/policy"
|
||||
"github.com/minio/minio/pkg/tagging"
|
||||
)
|
||||
|
||||
// APIError structure
|
||||
@@ -150,6 +151,7 @@ const (
|
||||
ErrPastObjectLockRetainDate
|
||||
ErrUnknownWORMModeDirective
|
||||
ErrObjectLockInvalidHeaders
|
||||
ErrInvalidTagDirective
|
||||
// Add new error codes here.
|
||||
|
||||
// SSE-S3 related API errors
|
||||
@@ -830,6 +832,11 @@ var errorCodes = errorCodeMap{
|
||||
Description: "Your metadata headers exceed the maximum allowed metadata size.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrInvalidTagDirective: {
|
||||
Code: "InvalidArgument",
|
||||
Description: "Unknown tag directive.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrInvalidEncryptionMethod: {
|
||||
Code: "InvalidRequest",
|
||||
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
|
||||
// useful with gateway implementations.
|
||||
switch e := err.(type) {
|
||||
case tagging.Error:
|
||||
apiErr = APIError{
|
||||
Code: "InvalidTag",
|
||||
Description: e.Error(),
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
}
|
||||
case policy.Error:
|
||||
apiErr = APIError{
|
||||
Code: "MalformedPolicy",
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"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.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.
|
||||
for k, v := range objInfo.UserDefined {
|
||||
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:.*}")
|
||||
// GetObjectACL - this is a dummy call.
|
||||
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", "")
|
||||
// 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
|
||||
bucket.Methods(http.MethodPost).Path("/{object:.+}").HandlerFunc(collectAPIStats("selectobjectcontent", httpTraceHdrs(api.SelectObjectContentHandler))).Queries("select", "").Queries("select-type", "2")
|
||||
// GetObjectRetention
|
||||
|
||||
@@ -22,26 +22,13 @@ import (
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/minio/minio/pkg/policy"
|
||||
"github.com/minio/minio/pkg/tagging"
|
||||
)
|
||||
|
||||
// Data types used for returning dummy tagging XML.
|
||||
// These variables shouldn't be used elsewhere.
|
||||
// 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
|
||||
func (api objectAPIHandlers) GetBucketWebsiteHandler(w http.ResponseWriter, r *http.Request) {
|
||||
writeSuccessResponseHeadersOnly(w)
|
||||
@@ -171,47 +158,8 @@ func (api objectAPIHandlers) GetBucketTaggingHandler(w http.ResponseWriter, r *h
|
||||
return
|
||||
}
|
||||
|
||||
tags := &tagging{}
|
||||
tags.TagSet.Tag = append(tags.TagSet.Tag, tagElem{})
|
||||
|
||||
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{})
|
||||
tags := &tagging.Tagging{}
|
||||
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))
|
||||
|
||||
@@ -182,9 +182,14 @@ func (m fsMetaV1) ToObjectInfo(bucket, object string, fi os.FileInfo) ObjectInfo
|
||||
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
|
||||
// remove to avoid it from appearing as part of
|
||||
// response headers. e.g, X-Minio-* or X-Amz-*.
|
||||
// Tags have also been extracted, we remove that as well.
|
||||
objInfo.UserDefined = cleanMetadata(m.Meta)
|
||||
|
||||
// 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"
|
||||
"github.com/minio/minio-go/v6/pkg/s3utils"
|
||||
"github.com/minio/minio/cmd/config"
|
||||
xhttp "github.com/minio/minio/cmd/http"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/lifecycle"
|
||||
"github.com/minio/minio/pkg/lock"
|
||||
@@ -44,6 +45,7 @@ import (
|
||||
"github.com/minio/minio/pkg/mimedb"
|
||||
"github.com/minio/minio/pkg/mountinfo"
|
||||
"github.com/minio/minio/pkg/policy"
|
||||
"github.com/minio/minio/pkg/tagging"
|
||||
)
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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.
|
||||
func (fs *FSObjects) ReloadFormat(ctx context.Context, dryRun bool) error {
|
||||
logger.LogIf(ctx, NotImplemented{})
|
||||
|
||||
@@ -24,6 +24,7 @@ import (
|
||||
"github.com/minio/minio/pkg/lifecycle"
|
||||
"github.com/minio/minio/pkg/madmin"
|
||||
"github.com/minio/minio/pkg/policy"
|
||||
"github.com/minio/minio/pkg/tagging"
|
||||
)
|
||||
|
||||
// GatewayLocker implements custom NeNSLock implementation
|
||||
@@ -173,6 +174,24 @@ func (a GatewayUnsupported) GetMetrics(ctx context.Context) (*Metrics, error) {
|
||||
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.
|
||||
func (a GatewayUnsupported) IsNotificationSupported() bool {
|
||||
return false
|
||||
|
||||
@@ -467,8 +467,8 @@ func ignoreNotImplementedBucketResources(req *http.Request) bool {
|
||||
// Checks requests for not implemented Object resources
|
||||
func ignoreNotImplementedObjectResources(req *http.Request) bool {
|
||||
for name := range req.URL.Query() {
|
||||
// Enable GetObjectACL and GetObjectTagging dummy calls specifically.
|
||||
if (name == "acl" || name == "tagging") && req.Method == http.MethodGet {
|
||||
// Enable GetObjectACL dummy call specifically.
|
||||
if name == "acl" && req.Method == http.MethodGet {
|
||||
return false
|
||||
}
|
||||
if notimplementedObjectResourceNames[name] {
|
||||
@@ -497,7 +497,6 @@ var notimplementedBucketResourceNames = map[string]bool{
|
||||
var notimplementedObjectResourceNames = map[string]bool{
|
||||
"acl": true,
|
||||
"restore": true,
|
||||
"tagging": true,
|
||||
"torrent": true,
|
||||
}
|
||||
|
||||
|
||||
@@ -34,6 +34,12 @@ import (
|
||||
"github.com/minio/minio/pkg/auth"
|
||||
"github.com/minio/minio/pkg/handlers"
|
||||
"github.com/minio/minio/pkg/madmin"
|
||||
"github.com/minio/minio/pkg/tagging"
|
||||
)
|
||||
|
||||
const (
|
||||
copyDirective = "COPY"
|
||||
replaceDirective = "REPLACE"
|
||||
)
|
||||
|
||||
// Parses location constraint from the incoming reader.
|
||||
@@ -69,30 +75,27 @@ var supportedHeaders = []string{
|
||||
"content-encoding",
|
||||
"content-disposition",
|
||||
xhttp.AmzStorageClass,
|
||||
xhttp.AmzObjectTagging,
|
||||
"expires",
|
||||
// Add more supported headers here.
|
||||
}
|
||||
|
||||
// isMetadataDirectiveValid - check if metadata-directive is valid.
|
||||
func isMetadataDirectiveValid(h http.Header) bool {
|
||||
_, ok := h[http.CanonicalHeaderKey(xhttp.AmzMetadataDirective)]
|
||||
if ok {
|
||||
// Check atleast set metadata-directive is valid.
|
||||
return (isMetadataCopy(h) || isMetadataReplace(h))
|
||||
}
|
||||
// By default if x-amz-metadata-directive is not we
|
||||
// isDirectiveValid - check if tagging-directive is valid.
|
||||
func isDirectiveValid(v string) bool {
|
||||
// Check if set metadata-directive is valid.
|
||||
return isDirectiveCopy(v) || isDirectiveReplace(v)
|
||||
}
|
||||
|
||||
// Check if the directive COPY is requested.
|
||||
func isDirectiveCopy(value string) bool {
|
||||
// By default if directive is not set we
|
||||
// treat it as 'COPY' this function returns true.
|
||||
return true
|
||||
return value == copyDirective || value == ""
|
||||
}
|
||||
|
||||
// Check if the metadata COPY is requested.
|
||||
func isMetadataCopy(h http.Header) bool {
|
||||
return h.Get(xhttp.AmzMetadataDirective) == "COPY"
|
||||
}
|
||||
|
||||
// Check if the metadata REPLACE is requested.
|
||||
func isMetadataReplace(h http.Header) bool {
|
||||
return h.Get(xhttp.AmzMetadataDirective) == "REPLACE"
|
||||
// Check if the directive REPLACE is requested.
|
||||
func isDirectiveReplace(value string) bool {
|
||||
return value == replaceDirective
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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
|
||||
// redirected on successful upload.
|
||||
func getRedirectPostRawQuery(objInfo ObjectInfo) string {
|
||||
|
||||
@@ -56,6 +56,11 @@ const (
|
||||
// S3 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
|
||||
AmzCopySourceIfModifiedSince = "x-amz-copy-source-if-modified-since"
|
||||
AmzCopySourceIfUnmodifiedSince = "x-amz-copy-source-if-unmodified-since"
|
||||
|
||||
@@ -154,6 +154,9 @@ type ObjectInfo struct {
|
||||
// User-Defined metadata
|
||||
UserDefined map[string]string
|
||||
|
||||
// User-Defined object tags
|
||||
UserTags string
|
||||
|
||||
// List of individual parts, maximum size of upto 10,000
|
||||
Parts []ObjectPartInfo `json:"-"`
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ import (
|
||||
"github.com/minio/minio/pkg/lifecycle"
|
||||
"github.com/minio/minio/pkg/madmin"
|
||||
"github.com/minio/minio/pkg/policy"
|
||||
"github.com/minio/minio/pkg/tagging"
|
||||
)
|
||||
|
||||
// CheckCopyPreconditionFn returns true if copy precondition check failed.
|
||||
@@ -125,4 +126,9 @@ type ObjectLayer interface {
|
||||
|
||||
// Check Readiness
|
||||
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 {
|
||||
// Remove STANDARD StorageClass
|
||||
metadata = removeStandardStorageClass(metadata)
|
||||
// Clean meta etag keys 'md5Sum', 'etag', "expires".
|
||||
return cleanMetadataKeys(metadata, "md5Sum", "etag", "expires")
|
||||
// Clean meta etag keys 'md5Sum', 'etag', "expires", "x-amz-tagging".
|
||||
return cleanMetadataKeys(metadata, "md5Sum", "etag", "expires", xhttp.AmzObjectTagging)
|
||||
}
|
||||
|
||||
// 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/policy"
|
||||
"github.com/minio/minio/pkg/s3select"
|
||||
"github.com/minio/minio/pkg/tagging"
|
||||
sha256 "github.com/minio/sha256-simd"
|
||||
"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
|
||||
// we extract metadata from the input headers.
|
||||
if isMetadataReplace(r.Header) {
|
||||
if isDirectiveReplace(r.Header.Get(xhttp.AmzMetadataDirective)) {
|
||||
return extractMetadata(ctx, r)
|
||||
}
|
||||
|
||||
// if x-amz-metadata-directive says COPY then we
|
||||
// return the default metadata.
|
||||
if isMetadataCopy(r.Header) {
|
||||
if isDirectiveCopy(r.Header.Get(xhttp.AmzMetadataDirective)) {
|
||||
return defaultMeta, nil
|
||||
}
|
||||
|
||||
@@ -618,6 +619,24 @@ func getCpObjMetadataFromHeader(ctx context.Context, r *http.Request, userMeta m
|
||||
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
|
||||
// Applicable only in a federated deployment
|
||||
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.
|
||||
if !isMetadataDirectiveValid(r.Header) {
|
||||
if !isDirectiveValid(r.Header.Get(xhttp.AmzMetadataDirective)) {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidMetadataDirective), r.URL, guessIsBrowserReq(r))
|
||||
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
|
||||
if globalAutoEncryption && !crypto.SSEC.IsRequested(r.Header) {
|
||||
r.Header.Add(crypto.SSEHeader, crypto.SSEAlgorithmAES256)
|
||||
@@ -795,7 +820,7 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re
|
||||
defer gr.Close()
|
||||
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) {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrEntityTooLarge), r.URL, guessIsBrowserReq(r))
|
||||
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))
|
||||
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
|
||||
if api.CacheAPI() != nil {
|
||||
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
|
||||
crypto.RemoveSensitiveEntries(srcInfo.UserDefined)
|
||||
// Check if x-amz-metadata-directive was not set to REPLACE and source,
|
||||
// desination are same objects. Apply this restriction also when
|
||||
// Check if x-amz-metadata-directive or x-amz-tagging-directive was not set to REPLACE and source,
|
||||
// destination are same objects. Apply this restriction also when
|
||||
// metadataOnly is true indicating that we are not overwriting the object.
|
||||
// if encryption is enabled we do not need explicit "REPLACE" metadata to
|
||||
// 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
|
||||
// to error out if source and destination are same.
|
||||
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
|
||||
}
|
||||
|
||||
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 contentEncoding, ok := metadata["content-encoding"]; ok {
|
||||
contentEncoding = trimAwsChunkedContentEncoding(contentEncoding)
|
||||
@@ -2774,3 +2819,103 @@ func (api objectAPIHandlers) GetObjectRetentionHandler(w http.ResponseWriter, 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/policy"
|
||||
"github.com/minio/minio/pkg/sync/errgroup"
|
||||
"github.com/minio/minio/pkg/tagging"
|
||||
)
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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
|
||||
func (s *xlSets) GetMetrics(ctx context.Context) (*Metrics, error) {
|
||||
logger.LogIf(ctx, NotImplemented{})
|
||||
|
||||
@@ -245,9 +245,13 @@ func (m xlMetaV1) ToObjectInfo(bucket, object string) ObjectInfo {
|
||||
// Extract etag from metadata.
|
||||
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
|
||||
// remove to avoid it from appearing as part of
|
||||
// response headers. e.g, X-Minio-* or X-Amz-*.
|
||||
// Tags have also been extracted, we remove that as well.
|
||||
objInfo.UserDefined = cleanMetadata(m.Meta)
|
||||
|
||||
// All the parts per object.
|
||||
|
||||
@@ -27,6 +27,7 @@ import (
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/mimedb"
|
||||
"github.com/minio/minio/pkg/sync/errgroup"
|
||||
"github.com/minio/minio/pkg/tagging"
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// Send the successul but partial upload, however ignore
|
||||
// Send the successful but partial upload, however ignore
|
||||
// if the channel is blocked by other items.
|
||||
func (xl xlObjects) addPartialUpload(bucket, key string) {
|
||||
select {
|
||||
@@ -977,3 +978,60 @@ func (xl xlObjects) addPartialUpload(bucket, key string) {
|
||||
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/policy"
|
||||
"github.com/minio/minio/pkg/sync/errgroup"
|
||||
"github.com/minio/minio/pkg/tagging"
|
||||
)
|
||||
|
||||
type xlZones struct {
|
||||
@@ -1374,3 +1375,63 @@ func (z *xlZones) GetMetrics(ctx context.Context) (*Metrics, error) {
|
||||
func (z *xlZones) IsReady(ctx context.Context) bool {
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user