Skip to content

Commit

Permalink
Add new CopyObject API in Core client (#836)
Browse files Browse the repository at this point in the history
This PR also fixes the problem of PutObject
call not handling additional headers.

Refer original problem here minio/minio#5000
  • Loading branch information
harshavardhana authored and deekoder committed Oct 2, 2017
1 parent cb3571b commit 9690dc6
Show file tree
Hide file tree
Showing 4 changed files with 186 additions and 5 deletions.
45 changes: 45 additions & 0 deletions api-compose-object.go
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,51 @@ func (s *SourceInfo) getProps(c Client) (size int64, etag string, userMeta map[s
return
}

// Low level implementation of CopyObject API, supports only upto 5GiB worth of copy.
func (c Client) copyObjectDo(ctx context.Context, srcBucket, srcObject, destBucket, destObject string,
metadata map[string]string) (ObjectInfo, error) {

// Build headers.
headers := make(http.Header)

// Set all the metadata headers.
for k, v := range metadata {
headers.Set(k, v)
}

// Set the source header
headers.Set("x-amz-copy-source", s3utils.EncodePath(srcBucket+"/"+srcObject))

// Send upload-part-copy request
resp, err := c.executeMethod(ctx, "PUT", requestMetadata{
bucketName: destBucket,
objectName: destObject,
customHeader: headers,
})
defer closeResponse(resp)
if err != nil {
return ObjectInfo{}, err
}

// Check if we got an error response.
if resp.StatusCode != http.StatusOK {
return ObjectInfo{}, httpRespToErrorResponse(resp, srcBucket, srcObject)
}

cpObjRes := copyObjectResult{}
err = xmlDecoder(resp.Body, &cpObjRes)
if err != nil {
return ObjectInfo{}, err
}

objInfo := ObjectInfo{
Key: destObject,
ETag: strings.Trim(cpObjRes.ETag, "\""),
LastModified: cpObjRes.LastModified,
}
return objInfo, nil
}

// uploadPartCopy - helper function to create a part in a multipart
// upload via an upload-part-copy request
// https://docs.aws.amazon.com/AmazonS3/latest/API/mpUploadUploadPartCopy.html
Expand Down
2 changes: 1 addition & 1 deletion api-s3-datatypes.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ type initiator struct {
// copyObjectResult container for copy object response.
type copyObjectResult struct {
ETag string
LastModified string // time string format "2006-01-02T15:04:05.000Z"
LastModified time.Time // time string format "2006-01-02T15:04:05.000Z"
}

// ObjectPart container for particular part of an object.
Expand Down
24 changes: 23 additions & 1 deletion core.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package minio
import (
"context"
"io"
"strings"

"github.com/minio/minio-go/pkg/policy"
)
Expand Down Expand Up @@ -53,9 +54,30 @@ func (c Core) ListObjectsV2(bucketName, objectPrefix, continuationToken string,
return c.listObjectsV2Query(bucketName, objectPrefix, continuationToken, fetchOwner, delimiter, maxkeys)
}

// CopyObject - copies an object from source object to destination object on server side.
func (c Core) CopyObject(sourceBucket, sourceObject, destBucket, destObject string, metadata map[string]string) (ObjectInfo, error) {
return c.copyObjectDo(context.Background(), sourceBucket, sourceObject, destBucket, destObject, metadata)
}

// PutObject - Upload object. Uploads using single PUT call.
func (c Core) PutObject(bucket, object string, data io.Reader, size int64, md5Sum, sha256Sum []byte, metadata map[string]string) (ObjectInfo, error) {
return c.putObjectDo(context.Background(), bucket, object, data, md5Sum, sha256Sum, size, PutObjectOptions{UserMetadata: metadata})
opts := PutObjectOptions{}
m := make(map[string]string)
for k, v := range metadata {
if strings.ToLower(k) == "content-encoding" {
opts.ContentEncoding = v
} else if strings.ToLower(k) == "content-disposition" {
opts.ContentDisposition = v
} else if strings.ToLower(k) == "content-type" {
opts.ContentType = v
} else if strings.ToLower(k) == "cache-control" {
opts.CacheControl = v
} else {
m[k] = metadata[k]
}
}
opts.UserMetadata = m
return c.putObjectDo(context.Background(), bucket, object, data, md5Sum, sha256Sum, size, opts)
}

// NewMultipartUpload - Initiates new multipart upload and returns the new uploadID.
Expand Down
120 changes: 117 additions & 3 deletions core_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,14 @@ package minio

import (
"bytes"
"crypto/md5"
"io"
"log"
"os"
"reflect"
"testing"
"time"

"crypto/md5"
"math/rand"
)

Expand Down Expand Up @@ -366,6 +366,120 @@ func TestGetBucketPolicy(t *testing.T) {
}
}

// Tests Core CopyObject API implementation.
func TestCoreCopyObject(t *testing.T) {
if testing.Short() {
t.Skip("skipping functional tests for short runs")
}

// Seed random based on current time.
rand.Seed(time.Now().Unix())

// Instantiate new minio client object.
c, err := NewCore(
os.Getenv(serverEndpoint),
os.Getenv(accessKey),
os.Getenv(secretKey),
mustParseBool(os.Getenv(enableSecurity)),
)
if err != nil {
t.Fatal("Error:", err)
}

// Enable tracing, write to stderr.
// c.TraceOn(os.Stderr)

// Set user agent.
c.SetAppInfo("Minio-go-FunctionalTest", "0.1.0")

// Generate a new random bucket name.
bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test")

// Make a new bucket.
err = c.MakeBucket(bucketName, "us-east-1")
if err != nil {
t.Fatal("Error:", err, bucketName)
}

buf := bytes.Repeat([]byte("a"), 32*1024)

// Save the data
objectName := randString(60, rand.NewSource(time.Now().UnixNano()), "")
objInfo, err := c.PutObject(bucketName, objectName, bytes.NewReader(buf), int64(len(buf)), nil, nil, map[string]string{
"Content-Type": "binary/octet-stream",
})
if err != nil {
t.Fatal("Error:", err, bucketName, objectName)
}

if objInfo.Size != int64(len(buf)) {
t.Fatalf("Error: number of bytes does not match, want %v, got %v\n", len(buf), objInfo.Size)
}

destBucketName := bucketName
destObjectName := objectName + "-dest"

cobjInfo, err := c.CopyObject(bucketName, objectName, destBucketName, destObjectName, map[string]string{
"X-Amz-Metadata-Directive": "REPLACE",
"Content-Type": "application/javascript",
})
if err != nil {
t.Fatal("Error:", err, bucketName, objectName, destBucketName, destObjectName)
}
if cobjInfo.ETag != objInfo.ETag {
t.Fatalf("Error: expected etag to be same as source object %s, but found different etag :%s", objInfo.ETag, cobjInfo.ETag)
}

// Attempt to read from destBucketName and object name.
r, err := c.Client.GetObject(destBucketName, destObjectName, GetObjectOptions{})
if err != nil {
t.Fatal("Error:", err, bucketName, objectName)
}

st, err := r.Stat()
if err != nil {
t.Fatal("Error:", err, bucketName, objectName)
}

if st.Size != int64(len(buf)) {
t.Fatalf("Error: number of bytes in stat does not match, want %v, got %v\n",
len(buf), st.Size)
}

if st.ContentType != "application/javascript" {
t.Fatalf("Error: Content types don't match, expected: application/javascript, found: %+v\n", st.ContentType)
}

if st.ETag != objInfo.ETag {
t.Fatalf("Error: expected etag to be same as source object %s, but found different etag :%s", objInfo.ETag, st.ETag)
}

if err := r.Close(); err != nil {
t.Fatal("Error:", err)
}

if err := r.Close(); err == nil {
t.Fatal("Error: object is already closed, should return error")
}

err = c.RemoveObject(bucketName, objectName)
if err != nil {
t.Fatal("Error: ", err)
}

err = c.RemoveObject(destBucketName, destObjectName)
if err != nil {
t.Fatal("Error: ", err)
}

err = c.RemoveBucket(bucketName)
if err != nil {
t.Fatal("Error:", err)
}

// Do not need to remove destBucketName its same as bucketName.
}

// Test Core PutObject.
func TestCorePutObject(t *testing.T) {
if testing.Short() {
Expand Down Expand Up @@ -401,7 +515,7 @@ func TestCorePutObject(t *testing.T) {
t.Fatal("Error:", err, bucketName)
}

buf := bytes.Repeat([]byte("a"), minPartSize)
buf := bytes.Repeat([]byte("a"), 32*1024)

// Save the data
objectName := randString(60, rand.NewSource(time.Now().UnixNano()), "")
Expand All @@ -412,7 +526,7 @@ func TestCorePutObject(t *testing.T) {

objInfo, err := c.PutObject(bucketName, objectName, bytes.NewReader(buf), int64(len(buf)), md5.New().Sum(nil), nil, metadata)
if err == nil {
t.Fatal("Error expected: nil, got: ", err)
t.Fatal("Error expected: error, got: nil(success)")
}

objInfo, err = c.PutObject(bucketName, objectName, bytes.NewReader(buf), int64(len(buf)), nil, nil, metadata)
Expand Down

0 comments on commit 9690dc6

Please sign in to comment.