Skip to content

Commit

Permalink
Add new CopyObject API in Core client
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 minio-trusted committed Oct 2, 2017
1 parent cb3571b commit 9af77c8
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 9af77c8

Please sign in to comment.