Allow CopyObject() in S3 gateway to support metadata (#5000)

Fixes #4924
This commit is contained in:
Harshavardhana 2017-10-03 10:38:25 -07:00 committed by Dee Koder
parent 53f3d2fd65
commit 89d528a4ed
14 changed files with 194 additions and 6045 deletions

View File

@ -55,11 +55,11 @@ func (l *s3Objects) AnonPutObject(bucket string, object string, size int64, data
// AnonGetObject - Get object anonymously // AnonGetObject - Get object anonymously
func (l *s3Objects) AnonGetObject(bucket string, key string, startOffset int64, length int64, writer io.Writer) error { func (l *s3Objects) AnonGetObject(bucket string, key string, startOffset int64, length int64, writer io.Writer) error {
r := minio.NewGetReqHeaders() opts := minio.GetObjectOptions{}
if err := r.SetRange(startOffset, startOffset+length-1); err != nil { if err := opts.SetRange(startOffset, startOffset+length-1); err != nil {
return s3ToObjectError(traceError(err), bucket, key) return s3ToObjectError(traceError(err), bucket, key)
} }
object, _, err := l.anonClient.GetObject(bucket, key, r) object, _, err := l.anonClient.GetObject(bucket, key, opts)
if err != nil { if err != nil {
return s3ToObjectError(traceError(err), bucket, key) return s3ToObjectError(traceError(err), bucket, key)
} }
@ -75,8 +75,7 @@ func (l *s3Objects) AnonGetObject(bucket string, key string, startOffset int64,
// AnonGetObjectInfo - Get object info anonymously // AnonGetObjectInfo - Get object info anonymously
func (l *s3Objects) AnonGetObjectInfo(bucket string, object string) (objInfo ObjectInfo, e error) { func (l *s3Objects) AnonGetObjectInfo(bucket string, object string) (objInfo ObjectInfo, e error) {
r := minio.NewHeadReqHeaders() oi, err := l.anonClient.StatObject(bucket, object, minio.StatObjectOptions{})
oi, err := l.anonClient.StatObject(bucket, object, r)
if err != nil { if err != nil {
return objInfo, s3ToObjectError(traceError(err), bucket, object) return objInfo, s3ToObjectError(traceError(err), bucket, object)
} }

View File

@ -19,7 +19,6 @@ package cmd
import ( import (
"io" "io"
"net/http" "net/http"
"strings"
"encoding/hex" "encoding/hex"
@ -290,22 +289,20 @@ func fromMinioClientListBucketResult(bucket string, result minio.ListBucketResul
// startOffset indicates the starting read location of the object. // startOffset indicates the starting read location of the object.
// length indicates the total length of the object. // length indicates the total length of the object.
func (l *s3Objects) GetObject(bucket string, key string, startOffset int64, length int64, writer io.Writer) error { func (l *s3Objects) GetObject(bucket string, key string, startOffset int64, length int64, writer io.Writer) error {
r := minio.NewGetReqHeaders()
if length < 0 && length != -1 { if length < 0 && length != -1 {
return s3ToObjectError(traceError(errInvalidArgument), bucket, key) return s3ToObjectError(traceError(errInvalidArgument), bucket, key)
} }
opts := minio.GetObjectOptions{}
if startOffset >= 0 && length >= 0 { if startOffset >= 0 && length >= 0 {
if err := r.SetRange(startOffset, startOffset+length-1); err != nil { if err := opts.SetRange(startOffset, startOffset+length-1); err != nil {
return s3ToObjectError(traceError(err), bucket, key) return s3ToObjectError(traceError(err), bucket, key)
} }
} }
object, _, err := l.Client.GetObject(bucket, key, r) object, _, err := l.Client.GetObject(bucket, key, opts)
if err != nil { if err != nil {
return s3ToObjectError(traceError(err), bucket, key) return s3ToObjectError(traceError(err), bucket, key)
} }
defer object.Close() defer object.Close()
if _, err := io.Copy(writer, object); err != nil { if _, err := io.Copy(writer, object); err != nil {
@ -333,8 +330,7 @@ func fromMinioClientObjectInfo(bucket string, oi minio.ObjectInfo) ObjectInfo {
// GetObjectInfo reads object info and replies back ObjectInfo // GetObjectInfo reads object info and replies back ObjectInfo
func (l *s3Objects) GetObjectInfo(bucket string, object string) (objInfo ObjectInfo, err error) { func (l *s3Objects) GetObjectInfo(bucket string, object string) (objInfo ObjectInfo, err error) {
r := minio.NewHeadReqHeaders() oi, err := l.Client.StatObject(bucket, object, minio.StatObjectOptions{})
oi, err := l.Client.StatObject(bucket, object, r)
if err != nil { if err != nil {
return ObjectInfo{}, s3ToObjectError(traceError(err), bucket, object) return ObjectInfo{}, s3ToObjectError(traceError(err), bucket, object)
} }
@ -361,35 +357,17 @@ func (l *s3Objects) PutObject(bucket string, object string, data *HashReader, me
return fromMinioClientObjectInfo(bucket, oi), nil return fromMinioClientObjectInfo(bucket, oi), nil
} }
// CopyObject copies a blob from source container to destination container. // CopyObject copies an object from source bucket to a destination bucket.
func (l *s3Objects) CopyObject(srcBucket string, srcObject string, dstBucket string, dstObject string, metadata map[string]string) (objInfo ObjectInfo, err error) { func (l *s3Objects) CopyObject(srcBucket string, srcObject string, dstBucket string, dstObject string, metadata map[string]string) (objInfo ObjectInfo, err error) {
// Source object // Set this header such that following CopyObject() always sets the right metadata on the destination.
src := minio.NewSourceInfo(srcBucket, srcObject, nil) // metadata input is already a trickled down value from interpreting x-amz-metadata-directive at
// handler layer. So what we have right now is supposed to be applied on the destination object anyways.
// Destination object // So preserve it by adding "REPLACE" directive to save all the metadata set by CopyObject API.
var xamzMeta = map[string]string{} metadata["x-amz-metadata-directive"] = "REPLACE"
for key := range metadata { if _, err = l.Client.CopyObject(srcBucket, srcObject, dstBucket, dstObject, metadata); err != nil {
for _, prefix := range userMetadataKeyPrefixes {
if strings.HasPrefix(key, prefix) {
xamzMeta[key] = metadata[key]
}
}
}
dst, err := minio.NewDestinationInfo(dstBucket, dstObject, nil, xamzMeta)
if err != nil {
return objInfo, s3ToObjectError(traceError(err), dstBucket, dstObject)
}
if err = l.Client.CopyObject(dst, src); err != nil {
return objInfo, s3ToObjectError(traceError(err), srcBucket, srcObject) return objInfo, s3ToObjectError(traceError(err), srcBucket, srcObject)
} }
return l.GetObjectInfo(dstBucket, dstObject)
oi, err := l.GetObjectInfo(dstBucket, dstObject)
if err != nil {
return objInfo, s3ToObjectError(traceError(err), dstBucket, dstObject)
}
return oi, nil
} }
// DeleteObject deletes a blob in bucket // DeleteObject deletes a blob in bucket

View File

@ -398,7 +398,9 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re
if err != nil { if err != nil {
errorIf(err, "found invalid http request header") errorIf(err, "found invalid http request header")
writeErrorResponse(w, ErrInternalError, r.URL) writeErrorResponse(w, ErrInternalError, r.URL)
return
} }
// Check if x-amz-metadata-directive was not set to REPLACE and source, // Check if x-amz-metadata-directive was not set to REPLACE and source,
// desination are same objects. // desination are same objects.
if !isMetadataReplace(r.Header) && cpSrcDstSame { if !isMetadataReplace(r.Header) && cpSrcDstSame {

View File

@ -224,6 +224,7 @@ The full API Reference is available here.
### Full Examples : Encrypted Object Operations ### Full Examples : Encrypted Object Operations
* [put-encrypted-object.go](https://github.com/minio/minio-go/blob/master/examples/s3/put-encrypted-object.go) * [put-encrypted-object.go](https://github.com/minio/minio-go/blob/master/examples/s3/put-encrypted-object.go)
* [get-encrypted-object.go](https://github.com/minio/minio-go/blob/master/examples/s3/get-encrypted-object.go) * [get-encrypted-object.go](https://github.com/minio/minio-go/blob/master/examples/s3/get-encrypted-object.go)
* [fput-encrypted-object.go](https://github.com/minio/minio-go/blob/master/examples/s3/fputencrypted-object.go)
### Full Examples : Presigned Operations ### Full Examples : Presigned Operations
* [presignedgetobject.go](https://github.com/minio/minio-go/blob/master/examples/s3/presignedgetobject.go) * [presignedgetobject.go](https://github.com/minio/minio-go/blob/master/examples/s3/presignedgetobject.go)

View File

@ -244,11 +244,11 @@ func (s *SourceInfo) getProps(c Client) (size int64, etag string, userMeta map[s
// Get object info - need size and etag here. Also, decryption // Get object info - need size and etag here. Also, decryption
// headers are added to the stat request if given. // headers are added to the stat request if given.
var objInfo ObjectInfo var objInfo ObjectInfo
rh := NewGetReqHeaders() opts := StatObjectOptions{}
for k, v := range s.decryptKey.getSSEHeaders(false) { for k, v := range s.decryptKey.getSSEHeaders(false) {
rh.Set(k, v) opts.Set(k, v)
} }
objInfo, err = c.statObject(s.bucket, s.object, rh) objInfo, err = c.statObject(s.bucket, s.object, opts)
if err != nil { if err != nil {
err = fmt.Errorf("Could not stat object - %s/%s: %v", s.bucket, s.object, err) err = fmt.Errorf("Could not stat object - %s/%s: %v", s.bucket, s.object, err)
} else { } else {
@ -266,6 +266,51 @@ func (s *SourceInfo) getProps(c Client) (size int64, etag string, userMeta map[s
return 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 // uploadPartCopy - helper function to create a part in a multipart
// upload via an upload-part-copy request // upload via an upload-part-copy request
// https://docs.aws.amazon.com/AmazonS3/latest/API/mpUploadUploadPartCopy.html // https://docs.aws.amazon.com/AmazonS3/latest/API/mpUploadUploadPartCopy.html

View File

@ -19,6 +19,7 @@ package minio
import "context" import "context"
// GetObjectWithContext - returns an seekable, readable object. // GetObjectWithContext - returns an seekable, readable object.
func (c Client) GetObjectWithContext(ctx context.Context, bucketName, objectName string) (*Object, error) { // The options can be used to specify the GET request further.
return c.getObjectWithContext(ctx, bucketName, objectName) func (c Client) GetObjectWithContext(ctx context.Context, bucketName, objectName string, opts GetObjectOptions) (*Object, error) {
return c.getObjectWithContext(ctx, bucketName, objectName, opts)
} }

View File

@ -21,23 +21,34 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"github.com/minio/minio-go/pkg/encrypt"
"context" "context"
"github.com/minio/minio-go/pkg/s3utils" "github.com/minio/minio-go/pkg/s3utils"
) )
// FGetObjectWithContext - download contents of an object to a local file. // FGetObjectWithContext - download contents of an object to a local file.
func (c Client) FGetObjectWithContext(ctx context.Context, bucketName, objectName, filePath string) error { // The options can be used to specify the GET request further.
return c.fGetObjectWithContext(ctx, bucketName, objectName, filePath) func (c Client) FGetObjectWithContext(ctx context.Context, bucketName, objectName, filePath string, opts GetObjectOptions) error {
return c.fGetObjectWithContext(ctx, bucketName, objectName, filePath, opts)
} }
// FGetObject - download contents of an object to a local file. // FGetObject - download contents of an object to a local file.
func (c Client) FGetObject(bucketName, objectName, filePath string) error { func (c Client) FGetObject(bucketName, objectName, filePath string, opts GetObjectOptions) error {
return c.fGetObjectWithContext(context.Background(), bucketName, objectName, filePath) return c.fGetObjectWithContext(context.Background(), bucketName, objectName, filePath, opts)
}
// FGetEncryptedObject - Decrypt and store an object at filePath.
func (c Client) FGetEncryptedObject(bucketName, objectName, filePath string, materials encrypt.Materials) error {
if materials == nil {
return ErrInvalidArgument("Unable to recognize empty encryption properties")
}
return c.FGetObject(bucketName, objectName, filePath, GetObjectOptions{Materials: materials})
} }
// fGetObjectWithContext - fgetObject wrapper function with context // fGetObjectWithContext - fgetObject wrapper function with context
func (c Client) fGetObjectWithContext(ctx context.Context, bucketName, objectName, filePath string) error { func (c Client) fGetObjectWithContext(ctx context.Context, bucketName, objectName, filePath string, opts GetObjectOptions) error {
// Input validation. // Input validation.
if err := s3utils.CheckValidBucketName(bucketName); err != nil { if err := s3utils.CheckValidBucketName(bucketName); err != nil {
return err return err
@ -72,7 +83,7 @@ func (c Client) fGetObjectWithContext(ctx context.Context, bucketName, objectNam
} }
// Gather md5sum. // Gather md5sum.
objectStat, err := c.StatObject(bucketName, objectName) objectStat, err := c.StatObject(bucketName, objectName, StatObjectOptions{opts})
if err != nil { if err != nil {
return err return err
} }
@ -94,13 +105,12 @@ func (c Client) fGetObjectWithContext(ctx context.Context, bucketName, objectNam
// Initialize get object request headers to set the // Initialize get object request headers to set the
// appropriate range offsets to read from. // appropriate range offsets to read from.
reqHeaders := NewGetReqHeaders()
if st.Size() > 0 { if st.Size() > 0 {
reqHeaders.SetRange(st.Size(), 0) opts.SetRange(st.Size(), 0)
} }
// Seek to current position for incoming reader. // Seek to current position for incoming reader.
objectReader, objectStat, err := c.getObject(ctx, bucketName, objectName, reqHeaders) objectReader, objectStat, err := c.getObject(ctx, bucketName, objectName, opts)
if err != nil { if err != nil {
return err return err
} }

View File

@ -37,32 +37,16 @@ func (c Client) GetEncryptedObject(bucketName, objectName string, encryptMateria
return nil, ErrInvalidArgument("Unable to recognize empty encryption properties") return nil, ErrInvalidArgument("Unable to recognize empty encryption properties")
} }
// Fetch encrypted object return c.GetObject(bucketName, objectName, GetObjectOptions{Materials: encryptMaterials})
encReader, err := c.GetObject(bucketName, objectName)
if err != nil {
return nil, err
}
// Stat object to get its encryption metadata
st, err := encReader.Stat()
if err != nil {
return nil, err
}
// Setup object for decrytion, object is transparently
// decrypted as the consumer starts reading.
encryptMaterials.SetupDecryptMode(encReader, st.Metadata.Get(amzHeaderIV), st.Metadata.Get(amzHeaderKey))
// Success.
return encryptMaterials, nil
} }
// GetObject - returns an seekable, readable object. // GetObject - returns an seekable, readable object.
func (c Client) GetObject(bucketName, objectName string) (*Object, error) { func (c Client) GetObject(bucketName, objectName string, opts GetObjectOptions) (*Object, error) {
return c.getObjectWithContext(context.Background(), bucketName, objectName) return c.getObjectWithContext(context.Background(), bucketName, objectName, opts)
} }
// GetObject wrapper function that accepts a request context // GetObject wrapper function that accepts a request context
func (c Client) getObjectWithContext(ctx context.Context, bucketName, objectName string) (*Object, error) { func (c Client) getObjectWithContext(ctx context.Context, bucketName, objectName string, opts GetObjectOptions) (*Object, error) {
// Input validation. // Input validation.
if err := s3utils.CheckValidBucketName(bucketName); err != nil { if err := s3utils.CheckValidBucketName(bucketName); err != nil {
return nil, err return nil, err
@ -108,34 +92,26 @@ func (c Client) getObjectWithContext(ctx context.Context, bucketName, objectName
if req.isFirstReq { if req.isFirstReq {
// First request is a Read/ReadAt. // First request is a Read/ReadAt.
if req.isReadOp { if req.isReadOp {
reqHeaders := NewGetReqHeaders()
// Differentiate between wanting the whole object and just a range. // Differentiate between wanting the whole object and just a range.
if req.isReadAt { if req.isReadAt {
// If this is a ReadAt request only get the specified range. // If this is a ReadAt request only get the specified range.
// Range is set with respect to the offset and length of the buffer requested. // Range is set with respect to the offset and length of the buffer requested.
// Do not set objectInfo from the first readAt request because it will not get // Do not set objectInfo from the first readAt request because it will not get
// the whole object. // the whole object.
reqHeaders.SetRange(req.Offset, req.Offset+int64(len(req.Buffer))-1) opts.SetRange(req.Offset, req.Offset+int64(len(req.Buffer))-1)
httpReader, objectInfo, err = c.getObject(ctx, bucketName, objectName, reqHeaders) } else if req.Offset > 0 {
} else { opts.SetRange(req.Offset, 0)
if req.Offset > 0 {
reqHeaders.SetRange(req.Offset, 0)
}
// First request is a Read request.
httpReader, objectInfo, err = c.getObject(ctx, bucketName, objectName, reqHeaders)
} }
httpReader, objectInfo, err = c.getObject(ctx, bucketName, objectName, opts)
if err != nil { if err != nil {
resCh <- getResponse{ resCh <- getResponse{Error: err}
Error: err,
}
return return
} }
etag = objectInfo.ETag etag = objectInfo.ETag
// Read at least firstReq.Buffer bytes, if not we have // Read at least firstReq.Buffer bytes, if not we have
// reached our EOF. // reached our EOF.
size, err := io.ReadFull(httpReader, req.Buffer) size, err := io.ReadFull(httpReader, req.Buffer)
if err == io.ErrUnexpectedEOF { if size > 0 && err == io.ErrUnexpectedEOF {
// If an EOF happens after reading some but not // If an EOF happens after reading some but not
// all the bytes ReadFull returns ErrUnexpectedEOF // all the bytes ReadFull returns ErrUnexpectedEOF
err = io.EOF err = io.EOF
@ -150,7 +126,7 @@ func (c Client) getObjectWithContext(ctx context.Context, bucketName, objectName
} else { } else {
// First request is a Stat or Seek call. // First request is a Stat or Seek call.
// Only need to run a StatObject until an actual Read or ReadAt request comes through. // Only need to run a StatObject until an actual Read or ReadAt request comes through.
objectInfo, err = c.StatObject(bucketName, objectName) objectInfo, err = c.statObject(bucketName, objectName, StatObjectOptions{opts})
if err != nil { if err != nil {
resCh <- getResponse{ resCh <- getResponse{
Error: err, Error: err,
@ -165,11 +141,10 @@ func (c Client) getObjectWithContext(ctx context.Context, bucketName, objectName
} }
} }
} else if req.settingObjectInfo { // Request is just to get objectInfo. } else if req.settingObjectInfo { // Request is just to get objectInfo.
reqHeaders := NewGetReqHeaders()
if etag != "" { if etag != "" {
reqHeaders.SetMatchETag(etag) opts.SetMatchETag(etag)
} }
objectInfo, err := c.statObject(bucketName, objectName, reqHeaders) objectInfo, err := c.statObject(bucketName, objectName, StatObjectOptions{opts})
if err != nil { if err != nil {
resCh <- getResponse{ resCh <- getResponse{
Error: err, Error: err,
@ -189,9 +164,8 @@ func (c Client) getObjectWithContext(ctx context.Context, bucketName, objectName
// new ones when they haven't been already. // new ones when they haven't been already.
// All readAt requests are new requests. // All readAt requests are new requests.
if req.DidOffsetChange || !req.beenRead { if req.DidOffsetChange || !req.beenRead {
reqHeaders := NewGetReqHeaders()
if etag != "" { if etag != "" {
reqHeaders.SetMatchETag(etag) opts.SetMatchETag(etag)
} }
if httpReader != nil { if httpReader != nil {
// Close previously opened http reader. // Close previously opened http reader.
@ -200,16 +174,11 @@ func (c Client) getObjectWithContext(ctx context.Context, bucketName, objectName
// If this request is a readAt only get the specified range. // If this request is a readAt only get the specified range.
if req.isReadAt { if req.isReadAt {
// Range is set with respect to the offset and length of the buffer requested. // Range is set with respect to the offset and length of the buffer requested.
reqHeaders.SetRange(req.Offset, req.Offset+int64(len(req.Buffer))-1) opts.SetRange(req.Offset, req.Offset+int64(len(req.Buffer))-1)
httpReader, _, err = c.getObject(ctx, bucketName, objectName, reqHeaders) } else if req.Offset > 0 { // Range is set with respect to the offset.
} else { opts.SetRange(req.Offset, 0)
// Range is set with respect to the offset.
if req.Offset > 0 {
reqHeaders.SetRange(req.Offset, 0)
}
httpReader, objectInfo, err = c.getObject(ctx, bucketName, objectName, reqHeaders)
} }
httpReader, objectInfo, err = c.getObject(ctx, bucketName, objectName, opts)
if err != nil { if err != nil {
resCh <- getResponse{ resCh <- getResponse{
Error: err, Error: err,
@ -632,7 +601,7 @@ func newObject(reqCh chan<- getRequest, resCh <-chan getResponse, doneCh chan<-
// //
// For more information about the HTTP Range header. // For more information about the HTTP Range header.
// go to http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35. // go to http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35.
func (c Client) getObject(ctx context.Context, bucketName, objectName string, reqHeaders RequestHeaders) (io.ReadCloser, ObjectInfo, error) { func (c Client) getObject(ctx context.Context, bucketName, objectName string, opts GetObjectOptions) (io.ReadCloser, ObjectInfo, error) {
// Validate input arguments. // Validate input arguments.
if err := s3utils.CheckValidBucketName(bucketName); err != nil { if err := s3utils.CheckValidBucketName(bucketName); err != nil {
return nil, ObjectInfo{}, err return nil, ObjectInfo{}, err
@ -641,17 +610,11 @@ func (c Client) getObject(ctx context.Context, bucketName, objectName string, re
return nil, ObjectInfo{}, err return nil, ObjectInfo{}, err
} }
// Set all the necessary reqHeaders.
customHeader := make(http.Header)
for key, value := range reqHeaders.Header {
customHeader[key] = value
}
// Execute GET on objectName. // Execute GET on objectName.
resp, err := c.executeMethod(ctx, "GET", requestMetadata{ resp, err := c.executeMethod(ctx, "GET", requestMetadata{
bucketName: bucketName, bucketName: bucketName,
objectName: objectName, objectName: objectName,
customHeader: customHeader, customHeader: opts.Header(),
contentSHA256Bytes: emptySHA256, contentSHA256Bytes: emptySHA256,
}) })
if err != nil { if err != nil {
@ -698,6 +661,15 @@ func (c Client) getObject(ctx context.Context, bucketName, objectName string, re
Metadata: extractObjMetadata(resp.Header), Metadata: extractObjMetadata(resp.Header),
} }
reader := resp.Body
if opts.Materials != nil {
err = opts.Materials.SetupDecryptMode(reader, objectStat.Metadata.Get(amzHeaderIV), objectStat.Metadata.Get(amzHeaderKey))
if err != nil {
return nil, ObjectInfo{}, err
}
reader = opts.Materials
}
// do not close body here, caller will close // do not close body here, caller will close
return resp.Body, objectStat, nil return reader, objectStat, nil
} }

View File

@ -20,80 +20,94 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"time" "time"
"github.com/minio/minio-go/pkg/encrypt"
) )
// RequestHeaders - implement methods for setting special // GetObjectOptions are used to specify additional headers or options
// request headers for GET, HEAD object operations. // during GET requests.
// http://docs.aws.amazon.com/AmazonS3/latest/API/RESTObjectGET.html type GetObjectOptions struct {
type RequestHeaders struct { headers map[string]string
http.Header
Materials encrypt.Materials
} }
// NewGetReqHeaders - initializes a new request headers for GET request. // StatObjectOptions are used to specify additional headers or options
func NewGetReqHeaders() RequestHeaders { // during GET info/stat requests.
return RequestHeaders{ type StatObjectOptions struct {
Header: make(http.Header), GetObjectOptions
}
} }
// NewHeadReqHeaders - initializes a new request headers for HEAD request. // Header returns the http.Header representation of the GET options.
func NewHeadReqHeaders() RequestHeaders { func (o GetObjectOptions) Header() http.Header {
return RequestHeaders{ headers := make(http.Header, len(o.headers))
Header: make(http.Header), for k, v := range o.headers {
headers.Set(k, v)
} }
return headers
}
// Set adds a key value pair to the options. The
// key-value pair will be part of the HTTP GET request
// headers.
func (o *GetObjectOptions) Set(key, value string) {
if o.headers == nil {
o.headers = make(map[string]string)
}
o.headers[http.CanonicalHeaderKey(key)] = value
} }
// SetMatchETag - set match etag. // SetMatchETag - set match etag.
func (c RequestHeaders) SetMatchETag(etag string) error { func (o *GetObjectOptions) SetMatchETag(etag string) error {
if etag == "" { if etag == "" {
return ErrInvalidArgument("ETag cannot be empty.") return ErrInvalidArgument("ETag cannot be empty.")
} }
c.Set("If-Match", "\""+etag+"\"") o.Set("If-Match", "\""+etag+"\"")
return nil return nil
} }
// SetMatchETagExcept - set match etag except. // SetMatchETagExcept - set match etag except.
func (c RequestHeaders) SetMatchETagExcept(etag string) error { func (o *GetObjectOptions) SetMatchETagExcept(etag string) error {
if etag == "" { if etag == "" {
return ErrInvalidArgument("ETag cannot be empty.") return ErrInvalidArgument("ETag cannot be empty.")
} }
c.Set("If-None-Match", "\""+etag+"\"") o.Set("If-None-Match", "\""+etag+"\"")
return nil return nil
} }
// SetUnmodified - set unmodified time since. // SetUnmodified - set unmodified time since.
func (c RequestHeaders) SetUnmodified(modTime time.Time) error { func (o *GetObjectOptions) SetUnmodified(modTime time.Time) error {
if modTime.IsZero() { if modTime.IsZero() {
return ErrInvalidArgument("Modified since cannot be empty.") return ErrInvalidArgument("Modified since cannot be empty.")
} }
c.Set("If-Unmodified-Since", modTime.Format(http.TimeFormat)) o.Set("If-Unmodified-Since", modTime.Format(http.TimeFormat))
return nil return nil
} }
// SetModified - set modified time since. // SetModified - set modified time since.
func (c RequestHeaders) SetModified(modTime time.Time) error { func (o *GetObjectOptions) SetModified(modTime time.Time) error {
if modTime.IsZero() { if modTime.IsZero() {
return ErrInvalidArgument("Modified since cannot be empty.") return ErrInvalidArgument("Modified since cannot be empty.")
} }
c.Set("If-Modified-Since", modTime.Format(http.TimeFormat)) o.Set("If-Modified-Since", modTime.Format(http.TimeFormat))
return nil return nil
} }
// SetRange - set the start and end offset of the object to be read. // SetRange - set the start and end offset of the object to be read.
// See https://tools.ietf.org/html/rfc7233#section-3.1 for reference. // See https://tools.ietf.org/html/rfc7233#section-3.1 for reference.
func (c RequestHeaders) SetRange(start, end int64) error { func (o *GetObjectOptions) SetRange(start, end int64) error {
switch { switch {
case start == 0 && end < 0: case start == 0 && end < 0:
// Read last '-end' bytes. `bytes=-N`. // Read last '-end' bytes. `bytes=-N`.
c.Set("Range", fmt.Sprintf("bytes=%d", end)) o.Set("Range", fmt.Sprintf("bytes=%d", end))
case 0 < start && end == 0: case 0 < start && end == 0:
// Read everything starting from offset // Read everything starting from offset
// 'start'. `bytes=N-`. // 'start'. `bytes=N-`.
c.Set("Range", fmt.Sprintf("bytes=%d-", start)) o.Set("Range", fmt.Sprintf("bytes=%d-", start))
case 0 <= start && start <= end: case 0 <= start && start <= end:
// Read everything starting at 'start' till the // Read everything starting at 'start' till the
// 'end'. `bytes=N-M` // 'end'. `bytes=N-M`
c.Set("Range", fmt.Sprintf("bytes=%d-%d", start, end)) o.Set("Range", fmt.Sprintf("bytes=%d-%d", start, end))
default: default:
// All other cases such as // All other cases such as
// bytes=-3- // bytes=-3-

View File

@ -128,7 +128,7 @@ type initiator struct {
// copyObjectResult container for copy object response. // copyObjectResult container for copy object response.
type copyObjectResult struct { type copyObjectResult struct {
ETag string 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. // ObjectPart container for particular part of an object.

View File

@ -81,7 +81,7 @@ func extractObjMetadata(header http.Header) http.Header {
} }
// StatObject verifies if object exists and you have permission to access. // StatObject verifies if object exists and you have permission to access.
func (c Client) StatObject(bucketName, objectName string) (ObjectInfo, error) { func (c Client) StatObject(bucketName, objectName string, opts StatObjectOptions) (ObjectInfo, error) {
// Input validation. // Input validation.
if err := s3utils.CheckValidBucketName(bucketName); err != nil { if err := s3utils.CheckValidBucketName(bucketName); err != nil {
return ObjectInfo{}, err return ObjectInfo{}, err
@ -89,12 +89,11 @@ func (c Client) StatObject(bucketName, objectName string) (ObjectInfo, error) {
if err := s3utils.CheckValidObjectName(objectName); err != nil { if err := s3utils.CheckValidObjectName(objectName); err != nil {
return ObjectInfo{}, err return ObjectInfo{}, err
} }
reqHeaders := NewHeadReqHeaders() return c.statObject(bucketName, objectName, opts)
return c.statObject(bucketName, objectName, reqHeaders)
} }
// Lower level API for statObject supporting pre-conditions and range headers. // Lower level API for statObject supporting pre-conditions and range headers.
func (c Client) statObject(bucketName, objectName string, reqHeaders RequestHeaders) (ObjectInfo, error) { func (c Client) statObject(bucketName, objectName string, opts StatObjectOptions) (ObjectInfo, error) {
// Input validation. // Input validation.
if err := s3utils.CheckValidBucketName(bucketName); err != nil { if err := s3utils.CheckValidBucketName(bucketName); err != nil {
return ObjectInfo{}, err return ObjectInfo{}, err
@ -103,17 +102,12 @@ func (c Client) statObject(bucketName, objectName string, reqHeaders RequestHead
return ObjectInfo{}, err return ObjectInfo{}, err
} }
customHeader := make(http.Header)
for k, v := range reqHeaders.Header {
customHeader[k] = v
}
// Execute HEAD on objectName. // Execute HEAD on objectName.
resp, err := c.executeMethod(context.Background(), "HEAD", requestMetadata{ resp, err := c.executeMethod(context.Background(), "HEAD", requestMetadata{
bucketName: bucketName, bucketName: bucketName,
objectName: objectName, objectName: objectName,
contentSHA256Bytes: emptySHA256, contentSHA256Bytes: emptySHA256,
customHeader: customHeader, customHeader: opts.Header(),
}) })
defer closeResponse(resp) defer closeResponse(resp)
if err != nil { if err != nil {

View File

@ -19,6 +19,7 @@ package minio
import ( import (
"context" "context"
"io" "io"
"strings"
"github.com/minio/minio-go/pkg/policy" "github.com/minio/minio-go/pkg/policy"
) )
@ -53,9 +54,30 @@ func (c Core) ListObjectsV2(bucketName, objectPrefix, continuationToken string,
return c.listObjectsV2Query(bucketName, objectPrefix, continuationToken, fetchOwner, delimiter, maxkeys) 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. // 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) { 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. // NewMultipartUpload - Initiates new multipart upload and returns the new uploadID.
@ -111,12 +133,12 @@ func (c Core) PutBucketPolicy(bucket string, bucketPolicy policy.BucketAccessPol
// GetObject is a lower level API implemented to support reading // GetObject is a lower level API implemented to support reading
// partial objects and also downloading objects with special conditions // partial objects and also downloading objects with special conditions
// matching etag, modtime etc. // matching etag, modtime etc.
func (c Core) GetObject(bucketName, objectName string, reqHeaders RequestHeaders) (io.ReadCloser, ObjectInfo, error) { func (c Core) GetObject(bucketName, objectName string, opts GetObjectOptions) (io.ReadCloser, ObjectInfo, error) {
return c.getObject(context.Background(), bucketName, objectName, reqHeaders) return c.getObject(context.Background(), bucketName, objectName, opts)
} }
// StatObject is a lower level API implemented to support special // StatObject is a lower level API implemented to support special
// conditions matching etag, modtime on a request. // conditions matching etag, modtime on a request.
func (c Core) StatObject(bucketName, objectName string, reqHeaders RequestHeaders) (ObjectInfo, error) { func (c Core) StatObject(bucketName, objectName string, opts StatObjectOptions) (ObjectInfo, error) {
return c.statObject(bucketName, objectName, reqHeaders) return c.statObject(bucketName, objectName, opts)
} }

File diff suppressed because it is too large Load Diff

6
vendor/vendor.json vendored
View File

@ -336,10 +336,10 @@
"revisionTime": "2016-02-29T08:42:30-08:00" "revisionTime": "2016-02-29T08:42:30-08:00"
}, },
{ {
"checksumSHA1": "mqxOM3CsubB09O0nDEe4efu0JLQ=", "checksumSHA1": "EkdIh5Mk2bRiARtdoqUfnBuyndk=",
"path": "github.com/minio/minio-go", "path": "github.com/minio/minio-go",
"revision": "414c6b6a2e97428776cd831d9745589ebcf873e5", "revision": "9690dc6c40e6ef271727848c04f974e801212ac1",
"revisionTime": "2017-09-27T19:03:45Z" "revisionTime": "2017-10-02T19:34:27Z"
}, },
{ {
"checksumSHA1": "5juljGXPkBWENR2Os7dlnPQER48=", "checksumSHA1": "5juljGXPkBWENR2Os7dlnPQER48=",