Browse Source
s3: support object tagging
s3: support object tagging
* GetObjectTagging * PutObjectTagging * DeleteObjectTaggingpull/1508/head
Chris Lu
4 years ago
7 changed files with 404 additions and 0 deletions
-
82test/s3/basic/object_tagging_test.go
-
104weed/s3api/filer_util_tags.go
-
117weed/s3api/s3api_object_tagging_handlers.go
-
7weed/s3api/s3api_server.go
-
6weed/s3api/s3err/s3api_errors.go
-
38weed/s3api/tags.go
-
50weed/s3api/tags_test.go
@ -0,0 +1,82 @@ |
|||
package basic |
|||
|
|||
import ( |
|||
"fmt" |
|||
"github.com/aws/aws-sdk-go/aws" |
|||
"github.com/aws/aws-sdk-go/service/s3" |
|||
"testing" |
|||
) |
|||
|
|||
func TestObjectTagging(t *testing.T) { |
|||
|
|||
input := &s3.PutObjectInput{ |
|||
Bucket: aws.String("theBucket"), |
|||
Key: aws.String("testDir/testObject"), |
|||
} |
|||
|
|||
svc.PutObject(input) |
|||
|
|||
printTags() |
|||
|
|||
setTags() |
|||
|
|||
printTags() |
|||
|
|||
clearTags() |
|||
|
|||
printTags() |
|||
|
|||
} |
|||
|
|||
func printTags() { |
|||
response, err := svc.GetObjectTagging( |
|||
&s3.GetObjectTaggingInput{ |
|||
Bucket: aws.String("theBucket"), |
|||
Key: aws.String("testDir/testObject"), |
|||
}) |
|||
|
|||
fmt.Println("printTags") |
|||
if err != nil { |
|||
fmt.Println(err.Error()) |
|||
} |
|||
|
|||
fmt.Println(response.TagSet) |
|||
} |
|||
|
|||
func setTags() { |
|||
|
|||
response, err := svc.PutObjectTagging(&s3.PutObjectTaggingInput{ |
|||
Bucket: aws.String("theBucket"), |
|||
Key: aws.String("testDir/testObject"), |
|||
Tagging: &s3.Tagging{ |
|||
TagSet: []*s3.Tag{ |
|||
{ |
|||
Key: aws.String("kye2"), |
|||
Value: aws.String("value2"), |
|||
}, |
|||
}, |
|||
}, |
|||
}) |
|||
|
|||
fmt.Println("setTags") |
|||
if err != nil { |
|||
fmt.Println(err.Error()) |
|||
} |
|||
|
|||
fmt.Println(response.String()) |
|||
} |
|||
|
|||
func clearTags() { |
|||
|
|||
response, err := svc.DeleteObjectTagging(&s3.DeleteObjectTaggingInput{ |
|||
Bucket: aws.String("theBucket"), |
|||
Key: aws.String("testDir/testObject"), |
|||
}) |
|||
|
|||
fmt.Println("clearTags") |
|||
if err != nil { |
|||
fmt.Println(err.Error()) |
|||
} |
|||
|
|||
fmt.Println(response.String()) |
|||
} |
@ -0,0 +1,104 @@ |
|||
package s3api |
|||
|
|||
import ( |
|||
"strings" |
|||
|
|||
"github.com/chrislusf/seaweedfs/weed/pb/filer_pb" |
|||
) |
|||
|
|||
const( |
|||
S3TAG_PREFIX = "s3-" |
|||
) |
|||
|
|||
func (s3a *S3ApiServer) getTags(parentDirectoryPath string, entryName string) (tags map[string]string, err error) { |
|||
|
|||
err = s3a.WithFilerClient(func(client filer_pb.SeaweedFilerClient) error { |
|||
|
|||
resp, err := filer_pb.LookupEntry(client, &filer_pb.LookupDirectoryEntryRequest{ |
|||
Directory: parentDirectoryPath, |
|||
Name: entryName, |
|||
}) |
|||
if err != nil { |
|||
return err |
|||
} |
|||
tags = make(map[string]string) |
|||
for k, v := range resp.Entry.Extended { |
|||
if strings.HasPrefix(k, S3TAG_PREFIX) { |
|||
tags[k[len(S3TAG_PREFIX):]] = string(v) |
|||
} |
|||
} |
|||
return nil |
|||
}) |
|||
return |
|||
} |
|||
|
|||
func (s3a *S3ApiServer) setTags(parentDirectoryPath string, entryName string, tags map[string]string) (err error) { |
|||
|
|||
return s3a.WithFilerClient(func(client filer_pb.SeaweedFilerClient) error { |
|||
|
|||
resp, err := filer_pb.LookupEntry(client, &filer_pb.LookupDirectoryEntryRequest{ |
|||
Directory: parentDirectoryPath, |
|||
Name: entryName, |
|||
}) |
|||
if err != nil { |
|||
return err |
|||
} |
|||
|
|||
for k, _ := range resp.Entry.Extended { |
|||
if strings.HasPrefix(k, S3TAG_PREFIX) { |
|||
delete(resp.Entry.Extended, k) |
|||
} |
|||
} |
|||
|
|||
if resp.Entry.Extended == nil { |
|||
resp.Entry.Extended = make(map[string][]byte) |
|||
} |
|||
for k, v := range tags { |
|||
resp.Entry.Extended[S3TAG_PREFIX+k] = []byte(v) |
|||
} |
|||
|
|||
return filer_pb.UpdateEntry(client, &filer_pb.UpdateEntryRequest{ |
|||
Directory: parentDirectoryPath, |
|||
Entry: resp.Entry, |
|||
IsFromOtherCluster: false, |
|||
Signatures: nil, |
|||
}) |
|||
|
|||
}) |
|||
|
|||
} |
|||
|
|||
func (s3a *S3ApiServer) rmTags(parentDirectoryPath string, entryName string) (err error) { |
|||
|
|||
return s3a.WithFilerClient(func(client filer_pb.SeaweedFilerClient) error { |
|||
|
|||
resp, err := filer_pb.LookupEntry(client, &filer_pb.LookupDirectoryEntryRequest{ |
|||
Directory: parentDirectoryPath, |
|||
Name: entryName, |
|||
}) |
|||
if err != nil { |
|||
return err |
|||
} |
|||
|
|||
hasDeletion := false |
|||
for k, _ := range resp.Entry.Extended { |
|||
if strings.HasPrefix(k, S3TAG_PREFIX) { |
|||
delete(resp.Entry.Extended, k) |
|||
hasDeletion = true |
|||
} |
|||
} |
|||
|
|||
if !hasDeletion { |
|||
return nil |
|||
} |
|||
|
|||
return filer_pb.UpdateEntry(client, &filer_pb.UpdateEntryRequest{ |
|||
Directory: parentDirectoryPath, |
|||
Entry: resp.Entry, |
|||
IsFromOtherCluster: false, |
|||
Signatures: nil, |
|||
}) |
|||
|
|||
}) |
|||
|
|||
} |
@ -0,0 +1,117 @@ |
|||
package s3api |
|||
|
|||
import ( |
|||
"encoding/xml" |
|||
"fmt" |
|||
"github.com/chrislusf/seaweedfs/weed/glog" |
|||
"github.com/chrislusf/seaweedfs/weed/pb/filer_pb" |
|||
"github.com/chrislusf/seaweedfs/weed/s3api/s3err" |
|||
"github.com/chrislusf/seaweedfs/weed/util" |
|||
"io" |
|||
"io/ioutil" |
|||
"net/http" |
|||
) |
|||
|
|||
// GetObjectTaggingHandler - GET object tagging
|
|||
// API reference: https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetObjectTagging.html
|
|||
func (s3a *S3ApiServer) GetObjectTaggingHandler(w http.ResponseWriter, r *http.Request) { |
|||
|
|||
bucket, object := getBucketAndObject(r) |
|||
|
|||
target := util.FullPath(fmt.Sprintf("%s/%s%s", s3a.option.BucketsPath, bucket, object)) |
|||
dir, name := target.DirAndName() |
|||
|
|||
tags, err := s3a.getTags(dir, name) |
|||
if err != nil { |
|||
if err == filer_pb.ErrNotFound { |
|||
glog.Errorf("GetObjectTaggingHandler %s: %v", r.URL, err) |
|||
writeErrorResponse(w, s3err.ErrNoSuchKey, r.URL) |
|||
} else { |
|||
glog.Errorf("GetObjectTaggingHandler %s: %v", r.URL, err) |
|||
writeErrorResponse(w, s3err.ErrInternalError, r.URL) |
|||
} |
|||
return |
|||
} |
|||
|
|||
writeSuccessResponseXML(w, encodeResponse(FromTags(tags))) |
|||
|
|||
} |
|||
|
|||
// PutObjectTaggingHandler Put object tagging
|
|||
// API reference: https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutObjectTagging.html
|
|||
func (s3a *S3ApiServer) PutObjectTaggingHandler(w http.ResponseWriter, r *http.Request) { |
|||
|
|||
bucket, object := getBucketAndObject(r) |
|||
|
|||
target := util.FullPath(fmt.Sprintf("%s/%s%s", s3a.option.BucketsPath, bucket, object)) |
|||
dir, name := target.DirAndName() |
|||
|
|||
tagging := &Tagging{} |
|||
input, err := ioutil.ReadAll(io.LimitReader(r.Body, r.ContentLength)) |
|||
if err != nil { |
|||
glog.Errorf("PutObjectTaggingHandler read input %s: %v", r.URL, err) |
|||
writeErrorResponse(w, s3err.ErrInternalError, r.URL) |
|||
return |
|||
} |
|||
if err = xml.Unmarshal(input, tagging); err != nil { |
|||
glog.Errorf("PutObjectTaggingHandler Unmarshal %s: %v", r.URL, err) |
|||
writeErrorResponse(w, s3err.ErrMalformedXML, r.URL) |
|||
return |
|||
} |
|||
tags := tagging.ToTags() |
|||
if len(tags) > 10 { |
|||
glog.Errorf("PutObjectTaggingHandler tags %s: %d tags more than 10", r.URL, len(tags)) |
|||
writeErrorResponse(w, s3err.ErrInvalidTag, r.URL) |
|||
return |
|||
} |
|||
for k, v := range tags { |
|||
if len(k) > 128 { |
|||
glog.Errorf("PutObjectTaggingHandler tags %s: tag key %s longer than 128", r.URL, k) |
|||
writeErrorResponse(w, s3err.ErrInvalidTag, r.URL) |
|||
return |
|||
} |
|||
if len(v) > 256 { |
|||
glog.Errorf("PutObjectTaggingHandler tags %s: tag value %s longer than 256", r.URL, v) |
|||
writeErrorResponse(w, s3err.ErrInvalidTag, r.URL) |
|||
return |
|||
} |
|||
} |
|||
|
|||
if err = s3a.setTags(dir, name, tagging.ToTags()); err != nil { |
|||
if err == filer_pb.ErrNotFound { |
|||
glog.Errorf("PutObjectTaggingHandler setTags %s: %v", r.URL, err) |
|||
writeErrorResponse(w, s3err.ErrNoSuchKey, r.URL) |
|||
} else { |
|||
glog.Errorf("PutObjectTaggingHandler setTags %s: %v", r.URL, err) |
|||
writeErrorResponse(w, s3err.ErrInternalError, r.URL) |
|||
} |
|||
return |
|||
} |
|||
|
|||
w.WriteHeader(http.StatusNoContent) |
|||
|
|||
} |
|||
|
|||
// DeleteObjectTaggingHandler Delete object tagging
|
|||
// API reference: https://docs.aws.amazon.com/AmazonS3/latest/API/API_DeleteObjectTagging.html
|
|||
func (s3a *S3ApiServer) DeleteObjectTaggingHandler(w http.ResponseWriter, r *http.Request) { |
|||
|
|||
bucket, object := getBucketAndObject(r) |
|||
|
|||
target := util.FullPath(fmt.Sprintf("%s/%s%s", s3a.option.BucketsPath, bucket, object)) |
|||
dir, name := target.DirAndName() |
|||
|
|||
err := s3a.rmTags(dir, name) |
|||
if err != nil { |
|||
if err == filer_pb.ErrNotFound { |
|||
glog.Errorf("DeleteObjectTaggingHandler %s: %v", r.URL, err) |
|||
writeErrorResponse(w, s3err.ErrNoSuchKey, r.URL) |
|||
} else { |
|||
glog.Errorf("DeleteObjectTaggingHandler %s: %v", r.URL, err) |
|||
writeErrorResponse(w, s3err.ErrInternalError, r.URL) |
|||
} |
|||
return |
|||
} |
|||
|
|||
w.WriteHeader(http.StatusNoContent) |
|||
} |
@ -0,0 +1,38 @@ |
|||
package s3api |
|||
|
|||
import ( |
|||
"encoding/xml" |
|||
) |
|||
|
|||
type Tag struct { |
|||
Key string `xml:"Key"` |
|||
Value string `xml:"Value"` |
|||
} |
|||
|
|||
type TagSet struct { |
|||
Tag []Tag `xml:"Tag"` |
|||
} |
|||
|
|||
type Tagging struct { |
|||
XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ Tagging"` |
|||
TagSet TagSet `xml:"TagSet"` |
|||
} |
|||
|
|||
func (t *Tagging) ToTags() map[string]string { |
|||
output := make(map[string]string) |
|||
for _, tag := range t.TagSet.Tag { |
|||
output[tag.Key] = tag.Value |
|||
} |
|||
return output |
|||
} |
|||
|
|||
func FromTags(tags map[string]string) (t *Tagging) { |
|||
t = &Tagging{} |
|||
for k, v := range tags { |
|||
t.TagSet.Tag = append(t.TagSet.Tag, Tag{ |
|||
Key: k, |
|||
Value: v, |
|||
}) |
|||
} |
|||
return |
|||
} |
@ -0,0 +1,50 @@ |
|||
package s3api |
|||
|
|||
import ( |
|||
"encoding/xml" |
|||
"github.com/stretchr/testify/assert" |
|||
"testing" |
|||
) |
|||
|
|||
func TestXMLUnmarshall(t *testing.T) { |
|||
|
|||
input := `<?xml version="1.0" encoding="UTF-8"?> |
|||
<Tagging xmlns="http://s3.amazonaws.com/doc/2006-03-01/"> |
|||
<TagSet> |
|||
<Tag> |
|||
<Key>key1</Key> |
|||
<Value>value1</Value> |
|||
</Tag> |
|||
</TagSet> |
|||
</Tagging> |
|||
` |
|||
|
|||
tags := &Tagging{} |
|||
|
|||
xml.Unmarshal([]byte(input), tags) |
|||
|
|||
assert.Equal(t, len(tags.TagSet.Tag), 1) |
|||
assert.Equal(t, tags.TagSet.Tag[0].Key, "key1") |
|||
assert.Equal(t, tags.TagSet.Tag[0].Value, "value1") |
|||
|
|||
} |
|||
|
|||
func TestXMLMarshall(t *testing.T) { |
|||
tags := &Tagging{ |
|||
TagSet: TagSet{ |
|||
[]Tag{ |
|||
{ |
|||
Key: "key1", |
|||
Value: "value1", |
|||
}, |
|||
}, |
|||
}, |
|||
} |
|||
|
|||
actual := string(encodeResponse(tags)) |
|||
|
|||
expected := `<?xml version="1.0" encoding="UTF-8"?> |
|||
<Tagging xmlns="http://s3.amazonaws.com/doc/2006-03-01/"><TagSet><Tag><Key>key1</Key><Value>value1</Value></Tag></TagSet></Tagging>` |
|||
assert.Equal(t, expected, actual) |
|||
|
|||
} |
Write
Preview
Loading…
Cancel
Save
Reference in new issue