minio-go: Move request into Client, rename API as Client.

- This cleanup merges newRequest into Client and uses httpClient
  instance properly in a single Client object context.
- This cleanup also ensures that we use getBucketLocation, inside
  request constructor to avoid the overkill of each requests handling
  them.
- ListBuckets now replies a list of buckets not an iterator.
- Remove mocked tests, we will write them through functional tests.
- ListObjectsPartsRecursive now cleans itself up in putParts through
  read channel.
This commit is contained in:
Harshavardhana 2015-12-14 02:08:20 -08:00
parent 2142093176
commit cff99100a5
27 changed files with 1329 additions and 2065 deletions

View File

@ -4,11 +4,6 @@
Please go through this link [Maintainer Responsibility](https://gist.github.com/abperiasamy/f4d9b31d3186bbd26522)
## Current Maintainers
- Harshavardhana
- Krishna Srinivas
### Making new releases
Edit `libraryVersion` constant in `api.go`.

View File

@ -49,10 +49,11 @@ func main() {
if err != nil {
log.Fatalln(err)
}
for bucket := range s3Client.ListBuckets() {
if bucket.Err != nil {
log.Fatalln(bucket.Err)
}
buckets, err := s3Client.ListBuckets()
if err != nil {
log.Fatalln(err)
}
for _, bucket := range buckets {
log.Println(bucket)
}
}
@ -66,7 +67,7 @@ func main() {
* [RemoveBucket(bucketName) error](examples/s3/removebucket.go)
* [GetBucketACL(bucketName) (BucketACL, error)](examples/s3/getbucketacl.go)
* [SetBucketACL(bucketName, BucketACL) error)](examples/s3/setbucketacl.go)
* [ListBuckets() <-chan BucketStat](examples/s3/listbuckets.go)
* [ListBuckets() []BucketStat](examples/s3/listbuckets.go)
* [ListObjects(bucketName, objectPrefix, recursive) <-chan ObjectStat](examples/s3/listobjects.go)
* [ListIncompleteUploads(bucketName, prefix, recursive) <-chan ObjectMultipartStat](examples/s3/listincompleteuploads.go)

View File

@ -27,8 +27,6 @@ type BucketStat struct {
Name string
// Date the bucket was created.
CreationDate time.Time
// Error
Err error
}
// ObjectStat container for object metadata.

View File

@ -20,7 +20,7 @@ import (
"encoding/json"
"encoding/xml"
"fmt"
"io"
"net/http"
"strconv"
)
@ -96,12 +96,71 @@ func (e ErrorResponse) Error() string {
return e.Message
}
// BodyToErrorResponse returns a new encoded ErrorResponse structure
func BodyToErrorResponse(errBody io.Reader) error {
// Common reporting string
const (
reportIssue = "Please report this issue at https://github.com/minio/minio-go/issues."
)
// HTTPRespToErrorResponse returns a new encoded ErrorResponse structure
func HTTPRespToErrorResponse(resp *http.Response, bucketName, objectName string) error {
if resp == nil {
msg := "Response is empty. " + reportIssue
return ErrInvalidArgument(msg)
}
var errorResponse ErrorResponse
err := xmlDecoder(errBody, &errorResponse)
err := xmlDecoder(resp.Body, &errorResponse)
if err != nil {
return err
switch resp.StatusCode {
case http.StatusNotFound:
if objectName == "" {
errorResponse = ErrorResponse{
Code: "NoSuchBucket",
Message: "The specified bucket does not exist.",
BucketName: bucketName,
RequestID: resp.Header.Get("x-amz-request-id"),
HostID: resp.Header.Get("x-amz-id-2"),
AmzBucketRegion: resp.Header.Get("x-amz-bucket-region"),
}
} else {
errorResponse = ErrorResponse{
Code: "NoSuchKey",
Message: "The specified key does not exist.",
BucketName: bucketName,
Key: objectName,
RequestID: resp.Header.Get("x-amz-request-id"),
HostID: resp.Header.Get("x-amz-id-2"),
AmzBucketRegion: resp.Header.Get("x-amz-bucket-region"),
}
}
case http.StatusForbidden:
errorResponse = ErrorResponse{
Code: "AccessDenied",
Message: "Access Denied.",
BucketName: bucketName,
Key: objectName,
RequestID: resp.Header.Get("x-amz-request-id"),
HostID: resp.Header.Get("x-amz-id-2"),
AmzBucketRegion: resp.Header.Get("x-amz-bucket-region"),
}
case http.StatusConflict:
errorResponse = ErrorResponse{
Code: "Conflict",
Message: "Bucket not empty.",
BucketName: bucketName,
RequestID: resp.Header.Get("x-amz-request-id"),
HostID: resp.Header.Get("x-amz-id-2"),
AmzBucketRegion: resp.Header.Get("x-amz-bucket-region"),
}
default:
errorResponse = ErrorResponse{
Code: resp.Status,
Message: resp.Status,
BucketName: bucketName,
RequestID: resp.Header.Get("x-amz-request-id"),
HostID: resp.Header.Get("x-amz-id-2"),
AmzBucketRegion: resp.Header.Get("x-amz-bucket-region"),
}
}
}
return errorResponse
}

View File

@ -1,3 +1,19 @@
/*
* Minio Go Library for Amazon S3 Compatible Cloud Storage (C) 2015 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package minio
import (
@ -19,24 +35,33 @@ import (
// public-read - owner gets full access, others get read access.
// public-read-write - owner gets full access, others get full access too.
// authenticated-read - owner gets full access, authenticated users get read access.
func (a API) GetBucketACL(bucketName string) (BucketACL, error) {
func (c Client) GetBucketACL(bucketName string) (BucketACL, error) {
if err := isValidBucketName(bucketName); err != nil {
return "", err
}
req, err := a.getBucketACLRequest(bucketName)
// Set acl query.
urlValues := make(url.Values)
urlValues.Set("acl", "")
// Instantiate a new request.
req, err := c.newRequest("GET", requestMetadata{
bucketName: bucketName,
queryValues: urlValues,
})
if err != nil {
return "", err
}
// Initiate the request.
resp, err := req.Do()
resp, err := c.httpClient.Do(req)
defer closeResponse(resp)
if err != nil {
return "", err
}
if resp != nil {
if resp.StatusCode != http.StatusOK {
return "", BodyToErrorResponse(resp.Body)
return "", HTTPRespToErrorResponse(resp, bucketName, "")
}
}
@ -47,12 +72,14 @@ func (a API) GetBucketACL(bucketName string) (BucketACL, error) {
return "", err
}
// If Google private bucket policy doesn't have any Grant list.
if !isGoogleEndpoint(a.endpointURL) {
// We need to avoid following de-serialization check for Google Cloud Storage.
// On Google Cloud Storage "private" canned ACL's policy do not have grant list.
// Treat it as a valid case, check for all other vendors.
if !isGoogleEndpoint(c.endpointURL) {
if policy.AccessControlList.Grant == nil {
errorResponse := ErrorResponse{
Code: "InternalError",
Message: "Access control Grant list is empty, please report this at https://github.com/minio/minio-go/issues.",
Message: "Access control Grant list is empty. " + reportIssue,
BucketName: bucketName,
RequestID: resp.Header.Get("x-amz-request-id"),
HostID: resp.Header.Get("x-amz-id-2"),
@ -101,39 +128,9 @@ func (a API) GetBucketACL(bucketName string) (BucketACL, error) {
}
}
// getBucketACLRequest wrapper creates a new getBucketACL request.
func (a API) getBucketACLRequest(bucketName string) (*Request, error) {
// Set acl query.
urlValues := make(url.Values)
urlValues.Set("acl", "")
// get target URL.
targetURL, err := getTargetURL(a.endpointURL, bucketName, "", urlValues)
if err != nil {
return nil, err
}
// get bucket region.
region, err := a.getRegion(bucketName)
if err != nil {
return nil, err
}
// Instantiate a new request.
req, err := newRequest("GET", targetURL, requestMetadata{
bucketRegion: region,
credentials: a.credentials,
contentTransport: a.httpTransport,
})
if err != nil {
return nil, err
}
return req, nil
}
// GetObject gets object content from specified bucket.
// You may also look at GetPartialObject.
func (a API) GetObject(bucketName, objectName string) (io.ReadSeeker, error) {
func (c Client) GetObject(bucketName, objectName string) (io.ReadSeeker, error) {
if err := isValidBucketName(bucketName); err != nil {
return nil, err
}
@ -141,7 +138,7 @@ func (a API) GetObject(bucketName, objectName string) (io.ReadSeeker, error) {
return nil, err
}
// get object.
return newObjectReadSeeker(a, bucketName, objectName), nil
return newObjectReadSeeker(c, bucketName, objectName), nil
}
// GetPartialObject gets partial object content as specified by the Range.
@ -149,7 +146,7 @@ func (a API) GetObject(bucketName, objectName string) (io.ReadSeeker, error) {
// Setting offset and length = 0 will download the full object.
// For more information about the HTTP Range header,
// go to http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35
func (a API) GetPartialObject(bucketName, objectName string, offset, length int64) (io.ReadSeeker, error) {
func (c Client) GetPartialObject(bucketName, objectName string, offset, length int64) (io.ReadSeeker, error) {
if err := isValidBucketName(bucketName); err != nil {
return nil, err
}
@ -157,7 +154,7 @@ func (a API) GetPartialObject(bucketName, objectName string, offset, length int6
return nil, err
}
// get partial object.
return newObjectReadSeeker(a, bucketName, objectName), nil
return newObjectReadSeeker(c, bucketName, objectName), nil
}
// objectReadSeeker container for io.ReadSeeker.
@ -165,7 +162,7 @@ type objectReadSeeker struct {
// mutex.
mutex *sync.Mutex
api API
client Client
reader io.ReadCloser
isRead bool
stat ObjectStat
@ -175,12 +172,12 @@ type objectReadSeeker struct {
}
// newObjectReadSeeker wraps getObject request returning a io.ReadSeeker.
func newObjectReadSeeker(api API, bucket, object string) *objectReadSeeker {
func newObjectReadSeeker(client Client, bucket, object string) *objectReadSeeker {
return &objectReadSeeker{
mutex: new(sync.Mutex),
reader: nil,
isRead: false,
api: api,
client: client,
offset: 0,
bucketName: bucket,
objectName: object,
@ -206,7 +203,7 @@ func (r *objectReadSeeker) Read(p []byte) (int, error) {
defer r.mutex.Unlock()
if !r.isRead {
reader, _, err := r.api.getObject(r.bucketName, r.objectName, r.offset, 0)
reader, _, err := r.client.getObject(r.bucketName, r.objectName, r.offset, 0)
if err != nil {
return 0, err
}
@ -246,47 +243,11 @@ func (r *objectReadSeeker) Seek(offset int64, whence int) (int64, error) {
// Size returns the size of the object.
func (r *objectReadSeeker) Size() (int64, error) {
objectSt, err := r.api.StatObject(r.bucketName, r.objectName)
objectSt, err := r.client.StatObject(r.bucketName, r.objectName)
r.stat = objectSt
return r.stat.Size, err
}
// getObjectRequest wrapper creates a new getObject request.
func (a API) getObjectRequest(bucketName, objectName string, offset, length int64) (*Request, error) {
// get targetURL.
targetURL, err := getTargetURL(a.endpointURL, bucketName, objectName, url.Values{})
if err != nil {
return nil, err
}
// get bucket region.
region, err := a.getRegion(bucketName)
if err != nil {
return nil, err
}
// Instantiate a new request.
req, err := newRequest("GET", targetURL, requestMetadata{
credentials: a.credentials,
userAgent: a.userAgent,
bucketRegion: region,
contentTransport: a.httpTransport,
})
if err != nil {
return nil, err
}
// Set ranges if length and offset are valid.
if length > 0 && offset >= 0 {
req.Set("Range", fmt.Sprintf("bytes=%d-%d", offset, offset+length-1))
} else if offset > 0 && length == 0 {
req.Set("Range", fmt.Sprintf("bytes=%d-", offset))
} else if length < 0 && offset == 0 {
req.Set("Range", fmt.Sprintf("bytes=%d", length))
}
return req, nil
}
// getObject - retrieve object from Object Storage.
//
// Additionally this function also takes range arguments to download the specified
@ -294,32 +255,53 @@ func (a API) getObjectRequest(bucketName, objectName string, offset, length int6
//
// For more information about the HTTP Range header.
// go to http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35.
func (a API) getObject(bucketName, objectName string, offset, length int64) (io.ReadCloser, ObjectStat, error) {
func (c Client) getObject(bucketName, objectName string, offset, length int64) (io.ReadCloser, ObjectStat, error) {
// Validate input arguments.
if err := isValidBucketName(bucketName); err != nil {
return nil, ObjectStat{}, err
}
if err := isValidObjectName(objectName); err != nil {
return nil, ObjectStat{}, err
}
req, err := a.getObjectRequest(bucketName, objectName, offset, length)
customHeader := make(http.Header)
// Set ranges if length and offset are valid.
if length > 0 && offset >= 0 {
customHeader.Set("Range", fmt.Sprintf("bytes=%d-%d", offset, offset+length-1))
} else if offset > 0 && length == 0 {
customHeader.Set("Range", fmt.Sprintf("bytes=%d-", offset))
} else if length < 0 && offset == 0 {
customHeader.Set("Range", fmt.Sprintf("bytes=%d", length))
}
// Instantiate a new request.
req, err := c.newRequest("GET", requestMetadata{
bucketName: bucketName,
objectName: objectName,
customHeader: customHeader,
})
if err != nil {
return nil, ObjectStat{}, err
}
resp, err := req.Do()
// Execute the request.
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, ObjectStat{}, err
}
if resp != nil {
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusPartialContent {
return nil, ObjectStat{}, BodyToErrorResponse(resp.Body)
return nil, ObjectStat{}, HTTPRespToErrorResponse(resp, bucketName, objectName)
}
}
md5sum := strings.Trim(resp.Header.Get("ETag"), "\"") // trim off the odd double quotes
// trim off the odd double quotes.
md5sum := strings.Trim(resp.Header.Get("ETag"), "\"")
// parse the date.
date, err := time.Parse(http.TimeFormat, resp.Header.Get("Last-Modified"))
if err != nil {
msg := "Last-Modified time format not recognized. " + reportIssue
return nil, ObjectStat{}, ErrorResponse{
Code: "InternalError",
Message: "Last-Modified time format not recognized, please report this issue at https://github.com/minio/minio-go/issues.",
Message: msg,
RequestID: resp.Header.Get("x-amz-request-id"),
HostID: resp.Header.Get("x-amz-id-2"),
AmzBucketRegion: resp.Header.Get("x-amz-bucket-region"),

View File

@ -32,82 +32,29 @@ import (
// fmt.Println(message)
// }
//
func (a API) ListBuckets() <-chan BucketStat {
ch := make(chan BucketStat, 100)
go a.listBucketsInRoutine(ch)
return ch
}
// listBucketsInRoutine goroutine based iterator for listBuckets.
func (a API) listBucketsInRoutine(ch chan<- BucketStat) {
defer close(ch)
req, err := a.listBucketsRequest()
func (c Client) ListBuckets() ([]BucketStat, error) {
// Instantiate a new request.
req, err := c.newRequest("GET", requestMetadata{})
if err != nil {
ch <- BucketStat{
Err: err,
}
return
return nil, err
}
resp, err := req.Do()
// Initiate the request.
resp, err := c.httpClient.Do(req)
defer closeResponse(resp)
if err != nil {
ch <- BucketStat{
Err: err,
}
return
return nil, err
}
if resp != nil {
// for un-authenticated requests, amazon sends a redirect handle it.
if resp.StatusCode == http.StatusTemporaryRedirect {
ch <- BucketStat{
Err: ErrorResponse{
Code: "AccessDenied",
Message: "Anonymous access is forbidden for this operation.",
RequestID: resp.Header.Get("x-amz-request-id"),
HostID: resp.Header.Get("x-amz-id-2"),
AmzBucketRegion: resp.Header.Get("x-amz-bucket-region"),
},
}
return
}
if resp.StatusCode != http.StatusOK {
ch <- BucketStat{
Err: BodyToErrorResponse(resp.Body),
}
return
return nil, HTTPRespToErrorResponse(resp, "", "")
}
}
listAllMyBucketsResult := listAllMyBucketsResult{}
err = xmlDecoder(resp.Body, &listAllMyBucketsResult)
if err != nil {
ch <- BucketStat{
Err: err,
}
return
}
for _, bucket := range listAllMyBucketsResult.Buckets.Bucket {
ch <- bucket
}
}
// listBucketRequest wrapper creates a new listBuckets request.
func (a API) listBucketsRequest() (*Request, error) {
// List buckets is directly on the endpoint URL.
targetURL := a.endpointURL
targetURL.Path = "/"
// Instantiate a new request.
req, err := newRequest("GET", targetURL, requestMetadata{
credentials: a.credentials,
userAgent: a.userAgent,
bucketRegion: "us-east-1",
contentTransport: a.httpTransport,
})
if err != nil {
return nil, err
}
return req, nil
return listAllMyBucketsResult.Buckets.Bucket, nil
}
// ListObjects - (List Objects) - List some objects or all recursively.
@ -126,124 +73,75 @@ func (a API) listBucketsRequest() (*Request, error) {
// fmt.Println(message)
// }
//
func (a API) ListObjects(bucketName string, objectPrefix string, recursive bool) <-chan ObjectStat {
ch := make(chan ObjectStat, 1000)
go a.listObjectsInRoutine(bucketName, objectPrefix, recursive, ch)
return ch
}
// listObjectsRecursive lists all objects recursively matching a prefix.
func (a API) listObjectsRecursive(bucketName, objectPrefix string, ch chan<- ObjectStat) {
var objectMarker string
for {
result, err := a.listObjects(bucketName, objectPrefix, objectMarker, "", 1000)
if err != nil {
ch <- ObjectStat{
Err: err,
}
return
}
for _, object := range result.Contents {
ch <- object
objectMarker = object.Key
}
if !result.IsTruncated {
break
}
func (c Client) ListObjects(bucketName string, objectPrefix string, recursive bool) <-chan ObjectStat {
// Allocate new list objects channel.
objectStatCh := make(chan ObjectStat, 1000)
// Default listing is delimited at "/"
delimiter := "/"
if recursive {
// If recursive we do not delimit.
delimiter = ""
}
}
// listObjectsNonRecursive lists objects delimited with "/" matching a prefix.
func (a API) listObjectsNonRecursive(bucketName, objectPrefix string, ch chan<- ObjectStat) {
// Non recursive delimit with "/".
var objectMarker string
for {
result, err := a.listObjects(bucketName, objectPrefix, objectMarker, "/", 1000)
if err != nil {
ch <- ObjectStat{
Err: err,
}
return
}
objectMarker = result.NextMarker
for _, object := range result.Contents {
ch <- object
}
for _, obj := range result.CommonPrefixes {
object := ObjectStat{}
object.Key = obj.Prefix
object.Size = 0
ch <- object
}
if !result.IsTruncated {
break
}
}
}
// listObjectsInRoutine goroutine based iterator for listObjects.
func (a API) listObjectsInRoutine(bucketName, objectPrefix string, recursive bool, ch chan<- ObjectStat) {
defer close(ch)
// Validate bucket name.
if err := isValidBucketName(bucketName); err != nil {
ch <- ObjectStat{
defer close(objectStatCh)
objectStatCh <- ObjectStat{
Err: err,
}
return
return objectStatCh
}
// Validate incoming object prefix.
if err := isValidObjectPrefix(objectPrefix); err != nil {
ch <- ObjectStat{
defer close(objectStatCh)
objectStatCh <- ObjectStat{
Err: err,
}
return
}
// Recursive do not delimit.
if recursive {
a.listObjectsRecursive(bucketName, objectPrefix, ch)
return
}
a.listObjectsNonRecursive(bucketName, objectPrefix, ch)
return
}
// listObjectsRequest wrapper creates a new listObjects request.
func (a API) listObjectsRequest(bucketName, objectPrefix, objectMarker, delimiter string, maxkeys int) (*Request, error) {
// Get resources properly escaped and lined up before
// using them in http request.
urlValues := make(url.Values)
// Set object prefix.
urlValues.Set("prefix", urlEncodePath(objectPrefix))
// Set object marker.
urlValues.Set("marker", urlEncodePath(objectMarker))
// Set delimiter.
urlValues.Set("delimiter", delimiter)
// Set max keys.
urlValues.Set("max-keys", fmt.Sprintf("%d", maxkeys))
// Get target url.
targetURL, err := getTargetURL(a.endpointURL, bucketName, "", urlValues)
if err != nil {
return nil, err
return objectStatCh
}
// get bucket region.
region, err := a.getRegion(bucketName)
if err != nil {
return nil, err
}
// Initiate list objects goroutine here.
go func(objectStatCh chan<- ObjectStat) {
defer close(objectStatCh)
// Save marker for next request.
var marker string
for {
// Get list of objects a maximum of 1000 per request.
result, err := c.listObjects(bucketName, objectPrefix, marker, delimiter, 1000)
if err != nil {
objectStatCh <- ObjectStat{
Err: err,
}
return
}
// Initialize a new request.
req, err := newRequest("GET", targetURL, requestMetadata{
credentials: a.credentials,
userAgent: a.userAgent,
bucketRegion: region,
contentTransport: a.httpTransport,
})
if err != nil {
return nil, err
}
return req, nil
// If contents are available loop through and send over channel.
for _, object := range result.Contents {
objectStatCh <- object
// Save the marker.
marker = object.Key
}
// Send all common prefixes if any.
// NOTE: prefixes are only present if the request is delimited.
for _, obj := range result.CommonPrefixes {
object := ObjectStat{}
object.Key = obj.Prefix
object.Size = 0
objectStatCh <- object
}
// If next marker present, save it for next request.
if result.NextMarker != "" {
marker = result.NextMarker
}
// Listing ends result is not truncated, return right here.
if !result.IsTruncated {
return
}
}
}(objectStatCh)
return objectStatCh
}
/// Bucket Read Operations.
@ -257,33 +155,52 @@ func (a API) listObjectsRequest(bucketName, objectPrefix, objectMarker, delimite
// ?delimiter - A delimiter is a character you use to group keys.
// ?prefix - Limits the response to keys that begin with the specified prefix.
// ?max-keys - Sets the maximum number of keys returned in the response body.
func (a API) listObjects(bucketName, objectPrefix, objectMarker, delimiter string, maxkeys int) (listBucketResult, error) {
func (c Client) listObjects(bucketName, objectPrefix, objectMarker, delimiter string, maxkeys int) (listBucketResult, error) {
// Validate bucket name.
if err := isValidBucketName(bucketName); err != nil {
return listBucketResult{}, err
}
// Validate object prefix.
if err := isValidObjectPrefix(objectPrefix); err != nil {
return listBucketResult{}, err
}
req, err := a.listObjectsRequest(bucketName, objectPrefix, objectMarker, delimiter, maxkeys)
// Get resources properly escaped and lined up before
// using them in http request.
urlValues := make(url.Values)
// Set object prefix.
urlValues.Set("prefix", urlEncodePath(objectPrefix))
// Set object marker.
urlValues.Set("marker", urlEncodePath(objectMarker))
// Set delimiter.
urlValues.Set("delimiter", delimiter)
// Set max keys.
urlValues.Set("max-keys", fmt.Sprintf("%d", maxkeys))
// Initialize a new request.
req, err := c.newRequest("GET", requestMetadata{
bucketName: bucketName,
queryValues: urlValues,
})
if err != nil {
return listBucketResult{}, err
}
resp, err := req.Do()
// Execute list buckets.
resp, err := c.httpClient.Do(req)
defer closeResponse(resp)
if err != nil {
return listBucketResult{}, err
}
if resp != nil {
if resp.StatusCode != http.StatusOK {
return listBucketResult{}, BodyToErrorResponse(resp.Body)
return listBucketResult{}, HTTPRespToErrorResponse(resp, bucketName, "")
}
}
// Decode listBuckets XML.
listBucketResult := listBucketResult{}
err = xmlDecoder(resp.Body, &listBucketResult)
if err != nil {
return listBucketResult, err
}
// close body while returning, along with any error.
return listBucketResult, nil
}
@ -303,113 +220,99 @@ func (a API) listObjects(bucketName, objectPrefix, objectMarker, delimiter strin
// fmt.Println(message)
// }
//
func (a API) ListIncompleteUploads(bucketName, objectPrefix string, recursive bool) <-chan ObjectMultipartStat {
return a.listIncompleteUploads(bucketName, objectPrefix, recursive)
func (c Client) ListIncompleteUploads(bucketName, objectPrefix string, recursive bool) <-chan ObjectMultipartStat {
// Turn on size aggregation of individual parts.
isAggregateSize := true
return c.listIncompleteUploads(bucketName, objectPrefix, recursive, isAggregateSize)
}
// listIncompleteUploads lists all incomplete uploads.
func (a API) listIncompleteUploads(bucketName, objectName string, recursive bool) <-chan ObjectMultipartStat {
ch := make(chan ObjectMultipartStat, 1000)
go a.listIncompleteUploadsInRoutine(bucketName, objectName, recursive, ch)
return ch
}
// listIncompleteUploadsRecursive list incomplete uploads matching a prefix recursively.
func (a API) listIncompleteUploadsRecursive(bucketName, objectPrefix string, ch chan<- ObjectMultipartStat) {
var objectMarker string
var uploadIDMarker string
for {
result, err := a.listMultipartUploads(bucketName, objectMarker, uploadIDMarker, objectPrefix, "", 1000)
if err != nil {
ch <- ObjectMultipartStat{
Err: err,
}
return
}
for _, objectSt := range result.Uploads {
// NOTE: getTotalMultipartSize can make listing incomplete uploads slower.
objectSt.Size, err = a.getTotalMultipartSize(bucketName, objectSt.Key, objectSt.UploadID)
if err != nil {
ch <- ObjectMultipartStat{
Err: err,
}
}
ch <- objectSt
objectMarker = result.NextKeyMarker
uploadIDMarker = result.NextUploadIDMarker
}
if !result.IsTruncated {
break
}
func (c Client) listIncompleteUploads(bucketName, objectPrefix string, recursive, aggregateSize bool) <-chan ObjectMultipartStat {
// Allocate channel for multipart uploads.
objectMultipartStatCh := make(chan ObjectMultipartStat, 1000)
// Delimiter is set to "/" by default.
delimiter := "/"
if recursive {
// If recursive do not delimit.
delimiter = ""
}
return
}
// listIncompleteUploadsNonRecursive list incomplete uploads delimited at "/" matching a prefix.
func (a API) listIncompleteUploadsNonRecursive(bucketName, objectPrefix string, ch chan<- ObjectMultipartStat) {
// Non recursive with "/" delimiter.
var objectMarker string
var uploadIDMarker string
for {
result, err := a.listMultipartUploads(bucketName, objectMarker, uploadIDMarker, objectPrefix, "/", 1000)
if err != nil {
ch <- ObjectMultipartStat{
Err: err,
}
return
}
objectMarker = result.NextKeyMarker
uploadIDMarker = result.NextUploadIDMarker
for _, objectSt := range result.Uploads {
objectSt.Size, err = a.getTotalMultipartSize(bucketName, objectSt.Key, objectSt.UploadID)
if err != nil {
ch <- ObjectMultipartStat{
Err: err,
}
}
ch <- objectSt
}
for _, obj := range result.CommonPrefixes {
object := ObjectMultipartStat{}
object.Key = obj.Prefix
object.Size = 0
ch <- object
}
if !result.IsTruncated {
break
}
}
}
// listIncompleteUploadsInRoutine goroutine based iterator for listing all incomplete uploads.
func (a API) listIncompleteUploadsInRoutine(bucketName, objectPrefix string, recursive bool, ch chan<- ObjectMultipartStat) {
defer close(ch)
// Validate incoming bucket name.
// Validate bucket name.
if err := isValidBucketName(bucketName); err != nil {
ch <- ObjectMultipartStat{
defer close(objectMultipartStatCh)
objectMultipartStatCh <- ObjectMultipartStat{
Err: err,
}
return
return objectMultipartStatCh
}
// Validate incoming object prefix.
if err := isValidObjectPrefix(objectPrefix); err != nil {
ch <- ObjectMultipartStat{
defer close(objectMultipartStatCh)
objectMultipartStatCh <- ObjectMultipartStat{
Err: err,
}
return
return objectMultipartStatCh
}
// Recursive with no delimiter.
if recursive {
a.listIncompleteUploadsRecursive(bucketName, objectPrefix, ch)
return
}
a.listIncompleteUploadsNonRecursive(bucketName, objectPrefix, ch)
return
go func(objectMultipartStatCh chan<- ObjectMultipartStat) {
defer close(objectMultipartStatCh)
// object and upload ID marker for future requests.
var objectMarker string
var uploadIDMarker string
for {
// list all multipart uploads.
result, err := c.listMultipartUploads(bucketName, objectMarker, uploadIDMarker, objectPrefix, delimiter, 1000)
if err != nil {
objectMultipartStatCh <- ObjectMultipartStat{
Err: err,
}
return
}
// Save objectMarker and uploadIDMarker for next request.
objectMarker = result.NextKeyMarker
uploadIDMarker = result.NextUploadIDMarker
// Send all multipart uploads.
for _, obj := range result.Uploads {
// Calculate total size of the uploaded parts if 'aggregateSize' is enabled.
if aggregateSize {
// Get total multipart size.
obj.Size, err = c.getTotalMultipartSize(bucketName, obj.Key, obj.UploadID)
if err != nil {
objectMultipartStatCh <- ObjectMultipartStat{
Err: err,
}
}
}
objectMultipartStatCh <- obj
}
// Send all common prefixes if any.
// NOTE: prefixes are only present if the request is delimited.
for _, obj := range result.CommonPrefixes {
object := ObjectMultipartStat{}
object.Key = obj.Prefix
object.Size = 0
objectMultipartStatCh <- object
}
// Listing ends if result not truncated, return right here.
if !result.IsTruncated {
return
}
}
}(objectMultipartStatCh)
// return.
return objectMultipartStatCh
}
// listMultipartUploadsRequest wrapper creates a new listMultipartUploads request.
func (a API) listMultipartUploadsRequest(bucketName, keyMarker, uploadIDMarker,
prefix, delimiter string, maxUploads int) (*Request, error) {
// listMultipartUploads - (List Multipart Uploads).
// - Lists some or all (up to 1000) in-progress multipart uploads in a bucket.
//
// You can use the request parameters as selection criteria to return a subset of the uploads in a bucket.
// request paramters. :-
// ---------
// ?key-marker - Specifies the multipart upload after which listing should begin.
// ?upload-id-marker - Together with key-marker specifies the multipart upload after which listing should begin.
// ?delimiter - A delimiter is a character you use to group keys.
// ?prefix - Limits the response to keys that begin with the specified prefix.
// ?max-uploads - Sets the maximum number of multipart uploads returned in the response body.
func (c Client) listMultipartUploads(bucketName, keyMarker, uploadIDMarker, prefix, delimiter string, maxUploads int) (listMultipartUploadsResult, error) {
// Get resources properly escaped and lined up before using them in http request.
urlValues := make(url.Values)
// Set uploads.
@ -425,55 +328,26 @@ func (a API) listMultipartUploadsRequest(bucketName, keyMarker, uploadIDMarker,
// Set max-uploads.
urlValues.Set("max-uploads", fmt.Sprintf("%d", maxUploads))
// get targetURL.
targetURL, err := getTargetURL(a.endpointURL, bucketName, "", urlValues)
if err != nil {
return nil, err
}
// get bucket region.
region, err := a.getRegion(bucketName)
if err != nil {
return nil, err
}
// Instantiate a new request.
return newRequest("GET", targetURL, requestMetadata{
credentials: a.credentials,
userAgent: a.userAgent,
bucketRegion: region,
contentTransport: a.httpTransport,
req, err := c.newRequest("GET", requestMetadata{
bucketName: bucketName,
queryValues: urlValues,
})
}
// listMultipartUploads - (List Multipart Uploads).
// - Lists some or all (up to 1000) in-progress multipart uploads in a bucket.
//
// You can use the request parameters as selection criteria to return a subset of the uploads in a bucket.
// request paramters. :-
// ---------
// ?key-marker - Specifies the multipart upload after which listing should begin.
// ?upload-id-marker - Together with key-marker specifies the multipart upload after which listing should begin.
// ?delimiter - A delimiter is a character you use to group keys.
// ?prefix - Limits the response to keys that begin with the specified prefix.
// ?max-uploads - Sets the maximum number of multipart uploads returned in the response body.
func (a API) listMultipartUploads(bucketName, keyMarker,
uploadIDMarker, prefix, delimiter string, maxUploads int) (listMultipartUploadsResult, error) {
req, err := a.listMultipartUploadsRequest(bucketName,
keyMarker, uploadIDMarker, prefix, delimiter, maxUploads)
if err != nil {
return listMultipartUploadsResult{}, err
}
resp, err := req.Do()
// Execute list multipart uploads request.
resp, err := c.httpClient.Do(req)
defer closeResponse(resp)
if err != nil {
return listMultipartUploadsResult{}, err
}
if resp != nil {
if resp.StatusCode != http.StatusOK {
return listMultipartUploadsResult{}, BodyToErrorResponse(resp.Body)
return listMultipartUploadsResult{}, HTTPRespToErrorResponse(resp, bucketName, "")
}
}
// Decode response body.
listMultipartUploadsResult := listMultipartUploadsResult{}
err = xmlDecoder(resp.Body, &listMultipartUploadsResult)
if err != nil {
@ -483,92 +357,83 @@ func (a API) listMultipartUploads(bucketName, keyMarker,
}
// listObjectPartsRecursive list all object parts recursively.
func (a API) listObjectPartsRecursive(bucketName, objectName, uploadID string) <-chan objectPartMetadata {
func (c Client) listObjectPartsRecursive(bucketName, objectName, uploadID string, doneCh <-chan bool) <-chan objectPartMetadata {
// Allocate new list parts channel.
objectPartCh := make(chan objectPartMetadata, 1000)
go a.listObjectPartsRecursiveInRoutine(bucketName, objectName, uploadID, objectPartCh)
// Initiate list parts goroutine.
go func(objectPartCh chan<- objectPartMetadata, doneCh <-chan bool) {
defer close(objectPartCh)
// part number marker for the next request if IsTruncated is set.
var nextPartNumberMarker int
for {
// Get list of uploaded parts a maximum of 1000 per request.
listObjPartsResult, err := c.listObjectParts(bucketName, objectName, uploadID, nextPartNumberMarker, 1000)
if err != nil {
objectPartCh <- objectPartMetadata{
Err: err,
}
return
}
// Loop through all object parts and send over the channel.
// Additionally wait on done channel to return the routine.
for _, uploadedObjectPart := range listObjPartsResult.ObjectParts {
select {
// If done channel return here.
case <-doneCh:
return
// Send uploaded parts here.
case objectPartCh <- uploadedObjectPart:
}
}
// Keep part number marker, for the next iteration.
nextPartNumberMarker = listObjPartsResult.NextPartNumberMarker
// Listing ends result is not truncated, return right here.
if !listObjPartsResult.IsTruncated {
return
}
}
}(objectPartCh, doneCh)
// Return the channel here.
return objectPartCh
}
// listObjectPartsRecursiveInRoutine gorountine based iterator for listing all object parts.
func (a API) listObjectPartsRecursiveInRoutine(bucketName, objectName, uploadID string, ch chan<- objectPartMetadata) {
defer close(ch)
listObjPartsResult, err := a.listObjectParts(bucketName, objectName, uploadID, 0, 1000)
if err != nil {
ch <- objectPartMetadata{
Err: err,
// findUploadID lists all incomplete uploads and finds the uploadID of the matching object name.
func (c Client) findUploadID(bucketName, objectName string) (string, error) {
// Make list incomplete uploads recursive.
isRecursive := true
// Turn off size aggregation of individual parts, in this request.
isAggregateSize := false
for mpUpload := range c.listIncompleteUploads(bucketName, objectName, isRecursive, isAggregateSize) {
if mpUpload.Err != nil {
return "", mpUpload.Err
}
return
}
for _, uploadedObjectPart := range listObjPartsResult.ObjectParts {
ch <- uploadedObjectPart
}
// listObject parts.
for {
if !listObjPartsResult.IsTruncated {
break
}
nextPartNumberMarker := listObjPartsResult.NextPartNumberMarker
listObjPartsResult, err = a.listObjectParts(bucketName, objectName, uploadID, nextPartNumberMarker, 1000)
if err != nil {
ch <- objectPartMetadata{
Err: err,
}
return
}
for _, uploadedObjectPart := range listObjPartsResult.ObjectParts {
ch <- uploadedObjectPart
// if object name found, return the upload id.
if objectName == mpUpload.Key {
return mpUpload.UploadID, nil
}
}
// No upload id was found, return success and empty upload id.
return "", nil
}
// getTotalMultipartSize - calculate total uploaded size for the a given multipart object.
func (a API) getTotalMultipartSize(bucketName, objectName, uploadID string) (int64, error) {
func (c Client) getTotalMultipartSize(bucketName, objectName, uploadID string) (int64, error) {
var size int64
// Allocate a new done channel.
doneCh := make(chan bool, 1)
defer close(doneCh)
// Iterate over all parts and aggregate the size.
for part := range a.listObjectPartsRecursive(bucketName, objectName, uploadID) {
for part := range c.listObjectPartsRecursive(bucketName, objectName, uploadID, doneCh) {
if part.Err != nil {
return 0, part.Err
}
size += part.Size
}
// Done channel is not used here since, it is not necessary.
return size, nil
}
// listObjectPartsRequest wrapper creates a new ListObjectParts request.
func (a API) listObjectPartsRequest(bucketName, objectName, uploadID string, partNumberMarker, maxParts int) (*Request, error) {
// Get resources properly escaped and lined up before using them in http request.
urlValues := make(url.Values)
// Set part number marker.
urlValues.Set("part-number-marker", fmt.Sprintf("%d", partNumberMarker))
// Set upload id.
urlValues.Set("uploadId", uploadID)
// Set max parts.
urlValues.Set("max-parts", fmt.Sprintf("%d", maxParts))
// get targetURL.
targetURL, err := getTargetURL(a.endpointURL, bucketName, objectName, urlValues)
if err != nil {
return nil, err
}
// get bucket region.
region, err := a.getRegion(bucketName)
if err != nil {
return nil, err
}
req, err := newRequest("GET", targetURL, requestMetadata{
credentials: a.credentials,
userAgent: a.userAgent,
bucketRegion: region,
contentTransport: a.httpTransport,
})
if err != nil {
return nil, err
}
return req, nil
}
// listObjectParts (List Parts)
// - lists some or all (up to 1000) parts that have been uploaded for a specific multipart upload
//
@ -576,21 +441,36 @@ func (a API) listObjectPartsRequest(bucketName, objectName, uploadID string, par
// request paramters :-
// ---------
// ?part-number-marker - Specifies the part after which listing should begin.
func (a API) listObjectParts(bucketName, objectName, uploadID string, partNumberMarker, maxParts int) (listObjectPartsResult, error) {
req, err := a.listObjectPartsRequest(bucketName, objectName, uploadID, partNumberMarker, maxParts)
func (c Client) listObjectParts(bucketName, objectName, uploadID string, partNumberMarker, maxParts int) (listObjectPartsResult, error) {
// Get resources properly escaped and lined up before using them in http request.
urlValues := make(url.Values)
// Set part number marker.
urlValues.Set("part-number-marker", fmt.Sprintf("%d", partNumberMarker))
// Set upload id.
urlValues.Set("uploadId", uploadID)
// Set max parts.
urlValues.Set("max-parts", fmt.Sprintf("%d", maxParts))
req, err := c.newRequest("GET", requestMetadata{
bucketName: bucketName,
objectName: objectName,
queryValues: urlValues,
})
if err != nil {
return listObjectPartsResult{}, err
}
resp, err := req.Do()
// Exectue list object parts.
resp, err := c.httpClient.Do(req)
defer closeResponse(resp)
if err != nil {
return listObjectPartsResult{}, err
}
if resp != nil {
if resp.StatusCode != http.StatusOK {
return listObjectPartsResult{}, BodyToErrorResponse(resp.Body)
return listObjectPartsResult{}, HTTPRespToErrorResponse(resp, bucketName, objectName)
}
}
// Decode list object parts XML.
listObjectPartsResult := listObjectPartsResult{}
err = xmlDecoder(resp.Body, &listObjectPartsResult)
if err != nil {

View File

@ -1,105 +1,87 @@
/*
* Minio Go Library for Amazon S3 Compatible Cloud Storage (C) 2015 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package minio
import (
"errors"
"fmt"
"net/url"
"time"
)
// PresignedGetObject returns a presigned URL to access an object without credentials.
// Expires maximum is 7days - ie. 604800 and minimum is 1.
func (a API) PresignedGetObject(bucketName, objectName string, expires time.Duration) (string, error) {
func (c Client) PresignedGetObject(bucketName, objectName string, expires time.Duration) (string, error) {
// Input validation.
if err := isValidBucketName(bucketName); err != nil {
return "", err
}
if err := isValidObjectName(objectName); err != nil {
return "", err
}
if err := isValidExpiry(expires); err != nil {
return "", err
}
expireSeconds := int64(expires / time.Second)
return a.presignedGetObject(bucketName, objectName, expireSeconds, 0, 0)
}
// presignedGetObject - generate presigned get object URL.
func (a API) presignedGetObject(bucketName, objectName string, expires, offset, length int64) (string, error) {
// get targetURL.
targetURL, err := getTargetURL(a.endpointURL, bucketName, objectName, url.Values{})
if err != nil {
return "", err
}
// get bucket region.
region, err := a.getRegion(bucketName)
if err != nil {
return "", err
}
// Instantiate a new request.
req, err := newRequest("GET", targetURL, requestMetadata{
credentials: a.credentials,
expires: expires,
userAgent: a.userAgent,
bucketRegion: region,
contentTransport: a.httpTransport,
// Since expires is set newRequest will presign the request.
req, err := c.newRequest("GET", requestMetadata{
presignURL: true,
bucketName: bucketName,
objectName: objectName,
expires: expireSeconds,
})
if err != nil {
return "", err
}
// Set ranges if length and offset are valid.
if length > 0 && offset >= 0 {
req.Set("Range", fmt.Sprintf("bytes=%d-%d", offset, offset+length-1))
} else if offset > 0 && length == 0 {
req.Set("Range", fmt.Sprintf("bytes=%d-", offset))
} else if length > 0 && offset == 0 {
req.Set("Range", fmt.Sprintf("bytes=-%d", length))
}
if req.credentials.Signature.isV2() {
return req.PreSignV2()
}
return req.PreSignV4()
return req.URL.String(), nil
}
// PresignedPutObject returns a presigned URL to upload an object without credentials.
// Expires maximum is 7days - ie. 604800 and minimum is 1.
func (a API) PresignedPutObject(bucketName, objectName string, expires time.Duration) (string, error) {
func (c Client) PresignedPutObject(bucketName, objectName string, expires time.Duration) (string, error) {
// Input validation.
if err := isValidBucketName(bucketName); err != nil {
return "", err
}
if err := isValidObjectName(objectName); err != nil {
return "", err
}
if err := isValidExpiry(expires); err != nil {
return "", err
}
expireSeconds := int64(expires / time.Second)
return a.presignedPutObject(bucketName, objectName, expireSeconds)
}
// presignedPutObject - generate presigned PUT url.
func (a API) presignedPutObject(bucketName, objectName string, expires int64) (string, error) {
// get targetURL.
targetURL, err := getTargetURL(a.endpointURL, bucketName, objectName, url.Values{})
if err != nil {
return "", err
}
// get bucket region.
region, err := a.getRegion(bucketName)
if err != nil {
return "", err
}
// Instantiate a new request.
req, err := newRequest("PUT", targetURL, requestMetadata{
credentials: a.credentials,
expires: expires,
userAgent: a.userAgent,
bucketRegion: region,
contentTransport: a.httpTransport,
// Since expires is set newRequest will presign the request.
req, err := c.newRequest("PUT", requestMetadata{
presignURL: true,
bucketName: bucketName,
objectName: objectName,
expires: expireSeconds,
})
if err != nil {
return "", err
}
if req.credentials.Signature.isV2() {
return req.PreSignV2()
}
return req.PreSignV4()
return req.URL.String(), nil
}
// PresignedPostPolicy returns POST form data to upload an object at a location.
func (a API) PresignedPostPolicy(p *PostPolicy) (map[string]string, error) {
func (c Client) PresignedPostPolicy(p *PostPolicy) (map[string]string, error) {
// Validate input arguments.
if p.expiration.IsZero() {
return nil, errors.New("Expiration time must be specified")
}
@ -109,69 +91,57 @@ func (a API) PresignedPostPolicy(p *PostPolicy) (map[string]string, error) {
if _, ok := p.formData["bucket"]; !ok {
return nil, errors.New("bucket name must be specified")
}
return a.presignedPostPolicy(p)
}
// presignedPostPolicy - generate post form data.
func (a API) presignedPostPolicy(p *PostPolicy) (map[string]string, error) {
// get targetURL.
targetURL, err := getTargetURL(a.endpointURL, p.formData["bucket"], "", url.Values{})
if err != nil {
return nil, err
}
// get bucket region.
region, err := a.getRegion(p.formData["bucket"])
if err != nil {
return nil, err
}
// Instantiate a new request.
req, err := newRequest("POST", targetURL, requestMetadata{
credentials: a.credentials,
userAgent: a.userAgent,
bucketRegion: region,
contentTransport: a.httpTransport,
})
bucketName := p.formData["bucket"]
// Fetch the location.
location, err := c.getBucketLocation(bucketName)
if err != nil {
return nil, err
}
// Keep time.
t := time.Now().UTC()
if req.credentials.Signature.isV2() {
if c.signature.isV2() {
policyBase64 := p.base64()
p.formData["policy"] = policyBase64
// for all other regions set this value to be 'AWSAccessKeyId'.
if isGoogleEndpoint(a.endpointURL) {
p.formData["GoogleAccessId"] = req.credentials.AccessKeyID
// For Google endpoint set this value to be 'GoogleAccessId'.
if isGoogleEndpoint(c.endpointURL) {
p.formData["GoogleAccessId"] = c.accessKeyID
} else {
p.formData["AWSAccessKeyId"] = req.credentials.AccessKeyID
// For all other endpoints set this value to be 'AWSAccessKeyId'.
p.formData["AWSAccessKeyId"] = c.accessKeyID
}
p.formData["signature"] = req.PostPresignSignatureV2(policyBase64)
// Sign the policy.
p.formData["signature"] = PostPresignSignatureV2(policyBase64, c.secretAccessKey)
return p.formData, nil
}
credential := getCredential(req.credentials.AccessKeyID, req.bucketRegion, t)
// Add date policy.
p.addNewPolicy(policyCondition{
matchType: "eq",
condition: "$x-amz-date",
value: t.Format(iso8601DateFormat),
})
// Add algorithm policy.
p.addNewPolicy(policyCondition{
matchType: "eq",
condition: "$x-amz-algorithm",
value: authHeader,
value: signV4Algorithm,
})
// Add a credential policy.
credential := getCredential(c.accessKeyID, location, t)
p.addNewPolicy(policyCondition{
matchType: "eq",
condition: "$x-amz-credential",
value: credential,
})
// get base64 encoded policy.
policyBase64 := p.base64()
// Fill in the form data.
p.formData["policy"] = policyBase64
p.formData["x-amz-algorithm"] = authHeader
p.formData["x-amz-algorithm"] = signV4Algorithm
p.formData["x-amz-credential"] = credential
p.formData["x-amz-date"] = t.Format(iso8601DateFormat)
p.formData["x-amz-signature"] = req.PostPresignSignatureV4(policyBase64, t)
p.formData["x-amz-signature"] = PostPresignSignatureV4(policyBase64, t, c.secretAccessKey, location)
return p.formData, nil
}

View File

@ -1,7 +1,24 @@
/*
* Minio Go Library for Amazon S3 Compatible Cloud Storage (C) 2015 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package minio
import (
"bytes"
"encoding/hex"
"encoding/xml"
"io/ioutil"
"net/http"
@ -24,7 +41,13 @@ import (
//
// For Amazon S3 for more supported regions - http://docs.aws.amazon.com/general/latest/gr/rande.html
// For Google Cloud Storage for more supported regions - https://cloud.google.com/storage/docs/bucket-locations
func (a API) MakeBucket(bucketName string, acl BucketACL, region string) error {
func (c Client) MakeBucket(bucketName string, acl BucketACL, location string) error {
// Validate if request is made on anonymous requests.
if c.anonymous {
return ErrInvalidArgument("Make bucket cannot be issued with anonymous credentials.")
}
// Validate the input arguments.
if err := isValidBucketName(bucketName); err != nil {
return err
}
@ -32,13 +55,14 @@ func (a API) MakeBucket(bucketName string, acl BucketACL, region string) error {
return ErrInvalidArgument("Unrecognized ACL " + acl.String())
}
req, err := a.makeBucketRequest(bucketName, string(acl), region)
// Instantiate the request.
req, err := c.makeBucketRequest(bucketName, acl, location)
if err != nil {
return err
}
// Initiate the request.
resp, err := req.Do()
// Execute the request.
resp, err := c.httpClient.Do(req)
defer closeResponse(resp)
if err != nil {
return err
@ -46,62 +70,77 @@ func (a API) MakeBucket(bucketName string, acl BucketACL, region string) error {
if resp != nil {
if resp.StatusCode != http.StatusOK {
return BodyToErrorResponse(resp.Body)
return HTTPRespToErrorResponse(resp, bucketName, "")
}
}
// Return.
return nil
}
// makeBucketRequest constructs request for makeBucket.
func (a API) makeBucketRequest(bucketName, acl, region string) (*Request, error) {
// get target URL.
targetURL, err := getTargetURL(a.endpointURL, bucketName, "", url.Values{})
func (c Client) makeBucketRequest(bucketName string, acl BucketACL, location string) (*http.Request, error) {
// Validate input arguments.
if err := isValidBucketName(bucketName); err != nil {
return nil, err
}
if !acl.isValidBucketACL() {
return nil, ErrInvalidArgument("Unrecognized ACL " + acl.String())
}
// Set get bucket location always as path style.
targetURL := c.endpointURL
targetURL.Path = "/" + bucketName
// get a new HTTP request for the method.
req, err := http.NewRequest("PUT", targetURL.String(), nil)
if err != nil {
return nil, err
}
// Initialize request metadata.
var reqMetadata requestMetadata
reqMetadata = requestMetadata{
userAgent: a.userAgent,
credentials: a.credentials,
bucketRegion: "us-east-1",
contentTransport: a.httpTransport,
// by default bucket acl is set to private.
req.Header.Set("x-amz-acl", "private")
if acl != "" {
req.Header.Set("x-amz-acl", string(acl))
}
// if region is 'us-east-1' no need to apply location constraint on the bucket.
if region == "us-east-1" {
region = ""
// set UserAgent for the request.
c.setUserAgent(req)
// if location is 'us-east-1' no need to apply location constraint on the bucket.
if location == "us-east-1" {
location = ""
}
// If region is set use to create bucket location config.
if region != "" {
// set sha256 sum for signature calculation only with signature version '4'.
if c.signature.isV4() || c.signature.isLatest() {
req.Header.Set("X-Amz-Content-Sha256", hex.EncodeToString(sum256([]byte{})))
}
// If location is set use to create bucket location config.
if location != "" {
createBucketConfig := new(createBucketConfiguration)
createBucketConfig.Location = region
createBucketConfig.Location = location
var createBucketConfigBytes []byte
createBucketConfigBytes, err = xml.Marshal(createBucketConfig)
if err != nil {
return nil, err
}
createBucketConfigBuffer := bytes.NewBuffer(createBucketConfigBytes)
reqMetadata.contentBody = ioutil.NopCloser(createBucketConfigBuffer)
reqMetadata.contentLength = int64(createBucketConfigBuffer.Len())
reqMetadata.contentSha256Bytes = sum256(createBucketConfigBuffer.Bytes())
req.Body = ioutil.NopCloser(createBucketConfigBuffer)
req.ContentLength = int64(createBucketConfigBuffer.Len())
if c.signature.isV4() || c.signature.isLatest() {
req.Header.Set("X-Amz-Content-Sha256", hex.EncodeToString(sum256(createBucketConfigBuffer.Bytes())))
}
}
// Initialize new request.
req, err := newRequest("PUT", targetURL, reqMetadata)
if err != nil {
return nil, err
}
// by default bucket acl is set to private.
req.Set("x-amz-acl", "private")
if acl != "" {
req.Set("x-amz-acl", acl)
// Sign the request.
if c.signature.isV4() || c.signature.isLatest() {
req = SignV4(*req, c.accessKeyID, c.secretAccessKey, "us-east-1")
} else if c.signature.isV2() {
req = SignV2(*req, c.accessKeyID, c.secretAccessKey)
}
// Return signed request.
return req, nil
}
@ -113,7 +152,8 @@ func (a API) makeBucketRequest(bucketName, acl, region string) (*Request, error)
// public-read - owner gets full access, all others get read access.
// public-read-write - owner gets full access, all others get full access too.
// authenticated-read - owner gets full access, authenticated users get read access.
func (a API) SetBucketACL(bucketName string, acl BucketACL) error {
func (c Client) SetBucketACL(bucketName string, acl BucketACL) error {
// Input validation.
if err := isValidBucketName(bucketName); err != nil {
return err
}
@ -121,63 +161,42 @@ func (a API) SetBucketACL(bucketName string, acl BucketACL) error {
return ErrInvalidArgument("Unrecognized ACL " + acl.String())
}
// Initialize a new request.
req, err := a.setBucketACLRequest(bucketName, string(acl))
// Set acl query.
urlValues := make(url.Values)
urlValues.Set("acl", "")
// Add misc headers.
customHeader := make(http.Header)
if acl != "" {
customHeader.Set("x-amz-acl", acl.String())
} else {
customHeader.Set("x-amz-acl", "private")
}
// Instantiate a new request.
req, err := c.newRequest("PUT", requestMetadata{
bucketName: bucketName,
queryValues: urlValues,
customHeader: customHeader,
})
if err != nil {
return err
}
// Initiate the request.
resp, err := req.Do()
resp, err := c.httpClient.Do(req)
defer closeResponse(resp)
if err != nil {
return err
}
if resp != nil {
// if error return.
if resp.StatusCode != http.StatusOK {
return BodyToErrorResponse(resp.Body)
return HTTPRespToErrorResponse(resp, bucketName, "")
}
}
// return
return nil
}
// setBucketRequestACL constructs request for SetBucketACL.
func (a API) setBucketACLRequest(bucketName, acl string) (*Request, error) {
// Set acl query.
urlValues := make(url.Values)
urlValues.Set("acl", "")
// get target URL.
targetURL, err := getTargetURL(a.endpointURL, bucketName, "", urlValues)
if err != nil {
return nil, err
}
// get bucket region.
region, err := a.getRegion(bucketName)
if err != nil {
return nil, err
}
// Instantiate a new request.
req, err := newRequest("PUT", targetURL, requestMetadata{
credentials: a.credentials,
userAgent: a.userAgent,
bucketRegion: region,
contentTransport: a.httpTransport,
})
if err != nil {
return nil, err
}
// Set relevant acl.
if acl != "" {
req.Set("x-amz-acl", acl)
} else {
req.Set("x-amz-acl", "private")
}
// Return.
return req, nil
}

View File

@ -67,7 +67,7 @@ func (a completedParts) Less(i, j int) bool { return a[i].PartNumber < a[j].Part
//
// NOTE: For anonymous requests Amazon S3 doesn't allow multipart upload,
// so we fall back to single PUT operation.
func (a API) PutObject(bucketName, objectName string, data io.ReadSeeker, size int64, contentType string) (int64, error) {
func (c Client) PutObject(bucketName, objectName string, data io.ReadSeeker, size int64, contentType string) (int64, error) {
// Input validation.
if err := isValidBucketName(bucketName); err != nil {
return 0, err
@ -76,7 +76,7 @@ func (a API) PutObject(bucketName, objectName string, data io.ReadSeeker, size i
return 0, err
}
// NOTE: S3 doesn't allow anonymous multipart requests.
if isAmazonEndpoint(a.endpointURL) && isAnonymousCredentials(*a.credentials) {
if isAmazonEndpoint(c.endpointURL) && c.anonymous {
if size <= -1 {
return 0, ErrorResponse{
Code: "NotImplemented",
@ -86,11 +86,11 @@ func (a API) PutObject(bucketName, objectName string, data io.ReadSeeker, size i
}
}
// Do not compute MD5 for anonymous requests to Amazon S3. Uploads upto 5GB in size.
return a.putAnonymous(bucketName, objectName, data, size, contentType)
return c.putAnonymous(bucketName, objectName, data, size, contentType)
}
// NOTE: Google Cloud Storage multipart Put is not compatible with Amazon S3 APIs.
// Current implementation will only upload a maximum of 5GB to Google Cloud Storage servers.
if isGoogleEndpoint(a.endpointURL) {
if isGoogleEndpoint(c.endpointURL) {
if size <= -1 {
return 0, ErrorResponse{
Code: "NotImplemented",
@ -100,19 +100,26 @@ func (a API) PutObject(bucketName, objectName string, data io.ReadSeeker, size i
}
}
// Do not compute MD5 for Google Cloud Storage. Uploads upto 5GB in size.
return a.putNoChecksum(bucketName, objectName, data, size, contentType)
return c.putNoChecksum(bucketName, objectName, data, size, contentType)
}
// Large file upload is initiated for uploads for input data size
// if its greater than 5MB or data size is negative.
if size >= minimumPartSize || size < 0 {
return a.putLargeObject(bucketName, objectName, data, size, contentType)
return c.putLargeObject(bucketName, objectName, data, size, contentType)
}
return a.putSmallObject(bucketName, objectName, data, size, contentType)
return c.putSmallObject(bucketName, objectName, data, size, contentType)
}
// putNoChecksum special function used Google Cloud Storage. This special function
// is used for Google Cloud Storage since Google's multipart API is not S3 compatible.
func (a API) putNoChecksum(bucketName, objectName string, data io.ReadSeeker, size int64, contentType string) (int64, error) {
func (c Client) putNoChecksum(bucketName, objectName string, data io.ReadSeeker, size int64, contentType string) (int64, error) {
// Input validation.
if err := isValidBucketName(bucketName); err != nil {
return 0, err
}
if err := isValidObjectName(objectName); err != nil {
return 0, err
}
if size > maxPartSize {
return 0, ErrEntityTooLarge(size, bucketName, objectName)
}
@ -124,7 +131,8 @@ func (a API) putNoChecksum(bucketName, objectName string, data io.ReadSeeker, si
Size: size,
ContentType: contentType,
}
if _, err := a.putObject(bucketName, objectName, putObjMetadata); err != nil {
// Execute put object.
if _, err := c.putObject(bucketName, objectName, putObjMetadata); err != nil {
return 0, err
}
return size, nil
@ -133,12 +141,27 @@ func (a API) putNoChecksum(bucketName, objectName string, data io.ReadSeeker, si
// putAnonymous is a special function for uploading content as anonymous request.
// This special function is necessary since Amazon S3 doesn't allow anonymous
// multipart uploads.
func (a API) putAnonymous(bucketName, objectName string, data io.ReadSeeker, size int64, contentType string) (int64, error) {
return a.putNoChecksum(bucketName, objectName, data, size, contentType)
func (c Client) putAnonymous(bucketName, objectName string, data io.ReadSeeker, size int64, contentType string) (int64, error) {
// Input validation.
if err := isValidBucketName(bucketName); err != nil {
return 0, err
}
if err := isValidObjectName(objectName); err != nil {
return 0, err
}
return c.putNoChecksum(bucketName, objectName, data, size, contentType)
}
// putSmallObject uploads files smaller than 5 mega bytes.
func (a API) putSmallObject(bucketName, objectName string, data io.ReadSeeker, size int64, contentType string) (int64, error) {
func (c Client) putSmallObject(bucketName, objectName string, data io.ReadSeeker, size int64, contentType string) (int64, error) {
// Input validation.
if err := isValidBucketName(bucketName); err != nil {
return 0, err
}
if err := isValidObjectName(objectName); err != nil {
return 0, err
}
// Read input data fully into buffer.
dataBytes, err := ioutil.ReadAll(data)
if err != nil {
return 0, err
@ -146,6 +169,7 @@ func (a API) putSmallObject(bucketName, objectName string, data io.ReadSeeker, s
if int64(len(dataBytes)) != size {
return 0, ErrUnexpectedEOF(int64(len(dataBytes)), size, bucketName, objectName)
}
// Construct a new PUT object metadata.
putObjMetadata := putObjectMetadata{
MD5Sum: sumMD5(dataBytes),
Sha256Sum: sum256(dataBytes),
@ -154,38 +178,49 @@ func (a API) putSmallObject(bucketName, objectName string, data io.ReadSeeker, s
ContentType: contentType,
}
// Single part use case, use putObject directly.
if _, err := a.putObject(bucketName, objectName, putObjMetadata); err != nil {
if _, err := c.putObject(bucketName, objectName, putObjMetadata); err != nil {
return 0, err
}
return size, nil
}
// putLargeObject uploads files bigger than 5 mega bytes.
func (a API) putLargeObject(bucketName, objectName string, data io.ReadSeeker, size int64, contentType string) (int64, error) {
var uploadID string
isRecursive := true
for mpUpload := range a.listIncompleteUploads(bucketName, objectName, isRecursive) {
if mpUpload.Err != nil {
return 0, mpUpload.Err
}
if mpUpload.Key == objectName {
uploadID = mpUpload.UploadID
break
}
func (c Client) putLargeObject(bucketName, objectName string, data io.ReadSeeker, size int64, contentType string) (int64, error) {
// Input validation.
if err := isValidBucketName(bucketName); err != nil {
return 0, err
}
if err := isValidObjectName(objectName); err != nil {
return 0, err
}
// Find upload id before upload.
uploadID, err := c.findUploadID(bucketName, objectName)
if err != nil {
return 0, err
}
if uploadID == "" {
initMultipartUploadResult, err := a.initiateMultipartUpload(bucketName, objectName, contentType)
// Initiate multipart upload.
initMultipartUploadResult, err := c.initiateMultipartUpload(bucketName, objectName, contentType)
if err != nil {
return 0, err
}
// Save the new upload id.
uploadID = initMultipartUploadResult.UploadID
}
// Initiate multipart upload.
return a.putParts(bucketName, objectName, uploadID, data, size)
return c.putParts(bucketName, objectName, uploadID, data, size)
}
// putParts - fully managed multipart uploader, resumes where its left off at `uploadID`
func (a API) putParts(bucketName, objectName, uploadID string, data io.ReadSeeker, size int64) (int64, error) {
func (c Client) putParts(bucketName, objectName, uploadID string, data io.ReadSeeker, size int64) (int64, error) {
// Input validation.
if err := isValidBucketName(bucketName); err != nil {
return 0, err
}
if err := isValidObjectName(objectName); err != nil {
return 0, err
}
// Cleanup any previously left stale files, as the function exits.
defer cleanupStaleTempfiles("multiparts$")
@ -198,13 +233,17 @@ func (a API) putParts(bucketName, objectName, uploadID string, data io.ReadSeeke
// Starting part number. Always part '1'.
partNumber := 1
completeMultipartUpload := completeMultipartUpload{}
for objPart := range a.listObjectPartsRecursive(bucketName, objectName, uploadID) {
doneCh := make(chan bool, 1)
defer close(doneCh)
for objPart := range c.listObjectPartsRecursive(bucketName, objectName, uploadID, doneCh) {
if objPart.Err != nil {
return 0, objPart.Err
}
// Verify if there is a hole i.e one of the parts is missing
// Break and start uploading that part.
if partNumber != objPart.PartNumber {
// Close listObjectParts channel.
doneCh <- true
break
}
var completedPart completePart
@ -250,8 +289,8 @@ func (a API) putParts(bucketName, objectName, uploadID string, data io.ReadSeeke
}
var enableSha256Sum bool
// if signature V4 - enable Sha256 calculation for individual parts.
if a.credentials.Signature.isV4() {
// Enable Sha256 calculation if signature version '4'.
if c.signature.isV4() {
enableSha256Sum = true
}
@ -262,6 +301,7 @@ func (a API) putParts(bucketName, objectName, uploadID string, data io.ReadSeeke
// Account for all parts uploaded simultaneousy.
wg.Add(1)
part.Number = partNumber
// Initiate the part upload goroutine.
go func(mpQueueCh <-chan struct{}, part partMetadata, wg *sync.WaitGroup, uploadedPartsCh chan<- uploadedPart) {
defer wg.Done()
defer func() {
@ -274,7 +314,8 @@ func (a API) putParts(bucketName, objectName, uploadID string, data io.ReadSeeke
}
return
}
complPart, err := a.uploadPart(bucketName, objectName, uploadID, part)
// execute upload part.
complPart, err := c.uploadPart(bucketName, objectName, uploadID, part)
if err != nil {
uploadedPartsCh <- uploadedPart{
Error: err,
@ -300,7 +341,7 @@ func (a API) putParts(bucketName, objectName, uploadID string, data io.ReadSeeke
}
// Save successfully uploaded size.
totalWritten += part.Size
// Save successfully uploaded part metadata.
// Save successfully uploaded part metadatc.
completeMultipartUpload.Parts = append(completeMultipartUpload.Parts, uploadedPrt.Part)
}
partNumber++
@ -313,68 +354,58 @@ func (a API) putParts(bucketName, objectName, uploadID string, data io.ReadSeeke
return totalWritten, ErrUnexpectedEOF(totalWritten, size, bucketName, objectName)
}
}
// sort all completed parts.
sort.Sort(completedParts(completeMultipartUpload.Parts))
_, err := a.completeMultipartUpload(bucketName, objectName, uploadID, completeMultipartUpload)
_, err := c.completeMultipartUpload(bucketName, objectName, uploadID, completeMultipartUpload)
if err != nil {
return totalWritten, err
}
return totalWritten, nil
}
// putObjectRequest wrapper creates a new PutObject request.
func (a API) putObjectRequest(bucketName, objectName string, putObjMetadata putObjectMetadata) (*Request, error) {
targetURL, err := getTargetURL(a.endpointURL, bucketName, objectName, url.Values{})
if err != nil {
return nil, err
// putObject - add an object to a bucket.
// NOTE: You must have WRITE permissions on a bucket to add an object to it.
func (c Client) putObject(bucketName, objectName string, putObjMetadata putObjectMetadata) (ObjectStat, error) {
// Input validation.
if err := isValidBucketName(bucketName); err != nil {
return ObjectStat{}, err
}
if err := isValidObjectName(objectName); err != nil {
return ObjectStat{}, err
}
if strings.TrimSpace(putObjMetadata.ContentType) == "" {
putObjMetadata.ContentType = "application/octet-stream"
}
// get bucket region.
region, err := a.getRegion(bucketName)
if err != nil {
return nil, err
}
// Set headers.
putObjMetadataHeader := make(http.Header)
putObjMetadataHeader.Set("Content-Type", putObjMetadata.ContentType)
customHeader := make(http.Header)
customHeader.Set("Content-Type", putObjMetadata.ContentType)
// Populate request metadata.
reqMetadata := requestMetadata{
credentials: a.credentials,
userAgent: a.userAgent,
bucketRegion: region,
bucketName: bucketName,
objectName: objectName,
customHeader: customHeader,
contentBody: putObjMetadata.ReadCloser,
contentLength: putObjMetadata.Size,
contentHeader: putObjMetadataHeader,
contentTransport: a.httpTransport,
contentSha256Bytes: putObjMetadata.Sha256Sum,
contentMD5Bytes: putObjMetadata.MD5Sum,
}
req, err := newRequest("PUT", targetURL, reqMetadata)
if err != nil {
return nil, err
}
return req, nil
}
// putObject - add an object to a bucket.
// NOTE: You must have WRITE permissions on a bucket to add an object to it.
func (a API) putObject(bucketName, objectName string, putObjMetadata putObjectMetadata) (ObjectStat, error) {
req, err := a.putObjectRequest(bucketName, objectName, putObjMetadata)
// Initiate new request.
req, err := c.newRequest("PUT", reqMetadata)
if err != nil {
return ObjectStat{}, err
}
resp, err := req.Do()
// Execute the request.
resp, err := c.httpClient.Do(req)
defer closeResponse(resp)
if err != nil {
return ObjectStat{}, err
}
if resp != nil {
if resp.StatusCode != http.StatusOK {
return ObjectStat{}, BodyToErrorResponse(resp.Body)
return ObjectStat{}, HTTPRespToErrorResponse(resp, bucketName, objectName)
}
}
var metadata ObjectStat
@ -385,57 +416,52 @@ func (a API) putObject(bucketName, objectName string, putObjMetadata putObjectMe
return metadata, nil
}
// initiateMultipartRequest wrapper creates a new initiateMultiPart request.
func (a API) initiateMultipartRequest(bucketName, objectName, contentType string) (*Request, error) {
// initiateMultipartUpload initiates a multipart upload and returns an upload ID.
func (c Client) initiateMultipartUpload(bucketName, objectName, contentType string) (initiateMultipartUploadResult, error) {
// Input validation.
if err := isValidBucketName(bucketName); err != nil {
return initiateMultipartUploadResult{}, err
}
if err := isValidObjectName(objectName); err != nil {
return initiateMultipartUploadResult{}, err
}
// Initialize url queries.
urlValues := make(url.Values)
urlValues.Set("uploads", "")
// get targetURL.
targetURL, err := getTargetURL(a.endpointURL, bucketName, objectName, urlValues)
if err != nil {
return nil, err
}
// get bucket region.
region, err := a.getRegion(bucketName)
if err != nil {
return nil, err
}
if contentType == "" {
contentType = "application/octet-stream"
}
// set ContentType header.
multipartHeader := make(http.Header)
multipartHeader.Set("Content-Type", contentType)
customHeader := make(http.Header)
customHeader.Set("Content-Type", contentType)
reqMetadata := requestMetadata{
credentials: a.credentials,
userAgent: a.userAgent,
bucketRegion: region,
contentHeader: multipartHeader,
contentTransport: a.httpTransport,
bucketName: bucketName,
objectName: objectName,
queryValues: urlValues,
customHeader: customHeader,
}
return newRequest("POST", targetURL, reqMetadata)
}
// initiateMultipartUpload initiates a multipart upload and returns an upload ID.
func (a API) initiateMultipartUpload(bucketName, objectName, contentType string) (initiateMultipartUploadResult, error) {
req, err := a.initiateMultipartRequest(bucketName, objectName, contentType)
// Instantiate the request.
req, err := c.newRequest("POST", reqMetadata)
if err != nil {
return initiateMultipartUploadResult{}, err
}
resp, err := req.Do()
// Execute the request.
resp, err := c.httpClient.Do(req)
defer closeResponse(resp)
if err != nil {
return initiateMultipartUploadResult{}, err
}
if resp != nil {
if resp.StatusCode != http.StatusOK {
return initiateMultipartUploadResult{}, BodyToErrorResponse(resp.Body)
return initiateMultipartUploadResult{}, HTTPRespToErrorResponse(resp, bucketName, objectName)
}
}
// Decode xml initiate multipart.
initiateMultipartUploadResult := initiateMultipartUploadResult{}
err = xmlDecoder(resp.Body, &initiateMultipartUploadResult)
if err != nil {
@ -444,8 +470,16 @@ func (a API) initiateMultipartUpload(bucketName, objectName, contentType string)
return initiateMultipartUploadResult, nil
}
// uploadPartRequest wrapper creates a new UploadPart request.
func (a API) uploadPartRequest(bucketName, objectName, uploadID string, uploadingPart partMetadata) (*Request, error) {
// uploadPart uploads a part in a multipart upload.
func (c Client) uploadPart(bucketName, objectName, uploadID string, uploadingPart partMetadata) (completePart, error) {
// Input validation.
if err := isValidBucketName(bucketName); err != nil {
return completePart{}, err
}
if err := isValidObjectName(objectName); err != nil {
return completePart{}, err
}
// Get resources properly escaped and lined up before using them in http request.
urlValues := make(url.Values)
// Set part number.
@ -453,115 +487,88 @@ func (a API) uploadPartRequest(bucketName, objectName, uploadID string, uploadin
// Set upload id.
urlValues.Set("uploadId", uploadID)
// get targetURL.
targetURL, err := getTargetURL(a.endpointURL, bucketName, objectName, urlValues)
if err != nil {
return nil, err
}
// get bucket region.
region, err := a.getRegion(bucketName)
if err != nil {
return nil, err
}
reqMetadata := requestMetadata{
credentials: a.credentials,
userAgent: a.userAgent,
bucketRegion: region,
bucketName: bucketName,
objectName: objectName,
queryValues: urlValues,
contentBody: uploadingPart.ReadCloser,
contentLength: uploadingPart.Size,
contentTransport: a.httpTransport,
contentSha256Bytes: uploadingPart.Sha256Sum,
contentMD5Bytes: uploadingPart.MD5Sum,
}
req, err := newRequest("PUT", targetURL, reqMetadata)
if err != nil {
return nil, err
}
return req, nil
}
// uploadPart uploads a part in a multipart upload.
func (a API) uploadPart(bucketName, objectName, uploadID string, uploadingPart partMetadata) (completePart, error) {
req, err := a.uploadPartRequest(bucketName, objectName, uploadID, uploadingPart)
// Instantiate a request.
req, err := c.newRequest("PUT", reqMetadata)
if err != nil {
return completePart{}, err
}
// Initiate the request.
resp, err := req.Do()
// Execute the request.
resp, err := c.httpClient.Do(req)
defer closeResponse(resp)
if err != nil {
return completePart{}, err
}
if resp != nil {
if resp.StatusCode != http.StatusOK {
return completePart{}, BodyToErrorResponse(resp.Body)
return completePart{}, HTTPRespToErrorResponse(resp, bucketName, objectName)
}
}
cPart := completePart{}
cPart.PartNumber = uploadingPart.Number
cPart.ETag = resp.Header.Get("ETag")
return cPart, nil
// Once successfully uploaded, return completed part.
cmplPart := completePart{}
cmplPart.PartNumber = uploadingPart.Number
cmplPart.ETag = resp.Header.Get("ETag")
return cmplPart, nil
}
// completeMultipartUploadRequest wrapper creates a new CompleteMultipartUpload request.
func (a API) completeMultipartUploadRequest(bucketName, objectName, uploadID string,
complete completeMultipartUpload) (*Request, error) {
// completeMultipartUpload completes a multipart upload by assembling previously uploaded parts.
func (c Client) completeMultipartUpload(bucketName, objectName, uploadID string, complete completeMultipartUpload) (completeMultipartUploadResult, error) {
// Input validation.
if err := isValidBucketName(bucketName); err != nil {
return completeMultipartUploadResult{}, err
}
if err := isValidObjectName(objectName); err != nil {
return completeMultipartUploadResult{}, err
}
// Initialize url queries.
urlValues := make(url.Values)
urlValues.Set("uploadId", uploadID)
// get targetURL.
targetURL, err := getTargetURL(a.endpointURL, bucketName, objectName, urlValues)
if err != nil {
return nil, err
}
// Marshal complete multipart body.
completeMultipartUploadBytes, err := xml.Marshal(complete)
if err != nil {
return nil, err
}
// get bucket region.
region, err := a.getRegion(bucketName)
if err != nil {
return nil, err
}
completeMultipartUploadBuffer := bytes.NewBuffer(completeMultipartUploadBytes)
reqMetadata := requestMetadata{
credentials: a.credentials,
userAgent: a.userAgent,
bucketRegion: region,
contentBody: ioutil.NopCloser(completeMultipartUploadBuffer),
contentLength: int64(completeMultipartUploadBuffer.Len()),
contentTransport: a.httpTransport,
contentSha256Bytes: sum256(completeMultipartUploadBuffer.Bytes()),
}
req, err := newRequest("POST", targetURL, reqMetadata)
if err != nil {
return nil, err
}
return req, nil
}
// completeMultipartUpload completes a multipart upload by assembling previously uploaded parts.
func (a API) completeMultipartUpload(bucketName, objectName, uploadID string,
c completeMultipartUpload) (completeMultipartUploadResult, error) {
req, err := a.completeMultipartUploadRequest(bucketName, objectName, uploadID, c)
if err != nil {
return completeMultipartUploadResult{}, err
}
resp, err := req.Do()
// Instantiate all the complete multipart buffer.
completeMultipartUploadBuffer := bytes.NewBuffer(completeMultipartUploadBytes)
reqMetadata := requestMetadata{
bucketName: bucketName,
objectName: objectName,
queryValues: urlValues,
contentBody: ioutil.NopCloser(completeMultipartUploadBuffer),
contentLength: int64(completeMultipartUploadBuffer.Len()),
contentSha256Bytes: sum256(completeMultipartUploadBuffer.Bytes()),
}
// Instantiate the request.
req, err := c.newRequest("POST", reqMetadata)
if err != nil {
return completeMultipartUploadResult{}, err
}
// Execute the request.
resp, err := c.httpClient.Do(req)
defer closeResponse(resp)
if err != nil {
return completeMultipartUploadResult{}, err
}
if resp != nil {
if resp.StatusCode != http.StatusOK {
return completeMultipartUploadResult{}, BodyToErrorResponse(resp.Body)
return completeMultipartUploadResult{}, HTTPRespToErrorResponse(resp, bucketName, objectName)
}
}
// If successful response, decode the body.
completeMultipartUploadResult := completeMultipartUploadResult{}
err = xmlDecoder(resp.Body, &completeMultipartUploadResult)
if err != nil {

View File

@ -1,3 +1,19 @@
/*
* Minio Go Library for Amazon S3 Compatible Cloud Storage (C) 2015 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package minio
import (
@ -9,100 +25,45 @@ import (
//
// All objects (including all object versions and delete markers).
// in the bucket must be deleted before successfully attempting this request.
func (a API) RemoveBucket(bucketName string) error {
func (c Client) RemoveBucket(bucketName string) error {
if err := isValidBucketName(bucketName); err != nil {
return err
}
req, err := a.removeBucketRequest(bucketName)
req, err := c.newRequest("DELETE", requestMetadata{
bucketName: bucketName,
})
if err != nil {
return err
}
resp, err := req.Do()
resp, err := c.httpClient.Do(req)
defer closeResponse(resp)
if err != nil {
return err
}
if resp != nil {
if resp.StatusCode != http.StatusNoContent {
var errorResponse ErrorResponse
switch resp.StatusCode {
case http.StatusNotFound:
errorResponse = ErrorResponse{
Code: "NoSuchBucket",
Message: "The specified bucket does not exist.",
BucketName: bucketName,
RequestID: resp.Header.Get("x-amz-request-id"),
HostID: resp.Header.Get("x-amz-id-2"),
AmzBucketRegion: resp.Header.Get("x-amz-bucket-region"),
}
case http.StatusForbidden:
errorResponse = ErrorResponse{
Code: "AccessDenied",
Message: "Access Denied.",
BucketName: bucketName,
RequestID: resp.Header.Get("x-amz-request-id"),
HostID: resp.Header.Get("x-amz-id-2"),
AmzBucketRegion: resp.Header.Get("x-amz-bucket-region"),
}
case http.StatusConflict:
errorResponse = ErrorResponse{
Code: "Conflict",
Message: "Bucket not empty.",
BucketName: bucketName,
RequestID: resp.Header.Get("x-amz-request-id"),
HostID: resp.Header.Get("x-amz-id-2"),
AmzBucketRegion: resp.Header.Get("x-amz-bucket-region"),
}
default:
errorResponse = ErrorResponse{
Code: resp.Status,
Message: resp.Status,
BucketName: bucketName,
RequestID: resp.Header.Get("x-amz-request-id"),
HostID: resp.Header.Get("x-amz-id-2"),
AmzBucketRegion: resp.Header.Get("x-amz-bucket-region"),
}
}
return errorResponse
return HTTPRespToErrorResponse(resp, bucketName, "")
}
}
return nil
}
// removeBucketRequest constructs a new request for RemoveBucket.
func (a API) removeBucketRequest(bucketName string) (*Request, error) {
targetURL, err := getTargetURL(a.endpointURL, bucketName, "", url.Values{})
if err != nil {
return nil, err
}
// get bucket region.
region, err := a.getRegion(bucketName)
if err != nil {
return nil, err
}
return newRequest("DELETE", targetURL, requestMetadata{
credentials: a.credentials,
userAgent: a.userAgent,
bucketRegion: region,
contentTransport: a.httpTransport,
})
}
// RemoveObject remove an object from a bucket.
func (a API) RemoveObject(bucketName, objectName string) error {
func (c Client) RemoveObject(bucketName, objectName string) error {
if err := isValidBucketName(bucketName); err != nil {
return err
}
if err := isValidObjectName(objectName); err != nil {
return err
}
req, err := a.removeObjectRequest(bucketName, objectName)
req, err := c.newRequest("DELETE", requestMetadata{
bucketName: bucketName,
objectName: objectName,
})
if err != nil {
return err
}
resp, err := req.Do()
resp, err := c.httpClient.Do(req)
defer closeResponse(resp)
if err != nil {
return err
@ -113,105 +74,67 @@ func (a API) RemoveObject(bucketName, objectName string) error {
return nil
}
// removeObjectRequest constructs a request for RemoveObject.
func (a API) removeObjectRequest(bucketName, objectName string) (*Request, error) {
// get targetURL.
targetURL, err := getTargetURL(a.endpointURL, bucketName, objectName, url.Values{})
if err != nil {
return nil, err
}
// get bucket region.
region, err := a.getRegion(bucketName)
if err != nil {
return nil, err
}
// Instantiate a new request.
req, err := newRequest("DELETE", targetURL, requestMetadata{
credentials: a.credentials,
userAgent: a.userAgent,
bucketRegion: region,
contentTransport: a.httpTransport,
})
if err != nil {
return nil, err
}
return req, nil
}
// RemoveIncompleteUpload aborts an partially uploaded object.
// Requires explicit authentication, no anonymous requests are allowed for multipart API.
func (a API) RemoveIncompleteUpload(bucketName, objectName string) <-chan error {
errorCh := make(chan error)
go a.removeIncompleteUploadInRoutine(bucketName, objectName, errorCh)
return errorCh
}
// removeIncompleteUploadInRoutine iterates over all incomplete uploads
// and removes only input object name.
func (a API) removeIncompleteUploadInRoutine(bucketName, objectName string, errorCh chan<- error) {
defer close(errorCh)
// Validate incoming bucket name.
func (c Client) RemoveIncompleteUpload(bucketName, objectName string) error {
// Validate input arguments.
if err := isValidBucketName(bucketName); err != nil {
errorCh <- err
return
return err
}
// Validate incoming object name.
if err := isValidObjectName(objectName); err != nil {
errorCh <- err
return
return err
}
// List all incomplete uploads recursively.
for mpUpload := range a.listIncompleteUploads(bucketName, objectName, true) {
if objectName == mpUpload.Key {
err := a.abortMultipartUpload(bucketName, mpUpload.Key, mpUpload.UploadID)
errorCh := make(chan error)
go func(errorCh chan<- error) {
defer close(errorCh)
// Find multipart upload id of the object.
uploadID, err := c.findUploadID(bucketName, objectName)
if err != nil {
errorCh <- err
return
}
if uploadID != "" {
// If uploadID is not an empty string, initiate the request.
err := c.abortMultipartUpload(bucketName, objectName, uploadID)
if err != nil {
errorCh <- err
return
}
return
}
}(errorCh)
err, ok := <-errorCh
if ok && err != nil {
return err
}
return nil
}
// abortMultipartUploadRequest wrapper creates a new AbortMultipartUpload request.
func (a API) abortMultipartUploadRequest(bucketName, objectName, uploadID string) (*Request, error) {
// abortMultipartUpload aborts a multipart upload for the given uploadID, all parts are deleted.
func (c Client) abortMultipartUpload(bucketName, objectName, uploadID string) error {
// Validate input arguments.
if err := isValidBucketName(bucketName); err != nil {
return err
}
if err := isValidObjectName(objectName); err != nil {
return err
}
// Initialize url queries.
urlValues := make(url.Values)
urlValues.Set("uploadId", uploadID)
// get targetURL.
targetURL, err := getTargetURL(a.endpointURL, bucketName, objectName, urlValues)
if err != nil {
return nil, err
}
// get bucket region.
region, err := a.getRegion(bucketName)
if err != nil {
return nil, err
}
req, err := newRequest("DELETE", targetURL, requestMetadata{
credentials: a.credentials,
userAgent: a.userAgent,
bucketRegion: region,
contentTransport: a.httpTransport,
// Instantiate a new DELETE request.
req, err := c.newRequest("DELETE", requestMetadata{
bucketName: bucketName,
objectName: objectName,
queryValues: urlValues,
})
if err != nil {
return nil, err
}
return req, nil
}
// abortMultipartUpload aborts a multipart upload for the given uploadID, all parts are deleted.
func (a API) abortMultipartUpload(bucketName, objectName, uploadID string) error {
req, err := a.abortMultipartUploadRequest(bucketName, objectName, uploadID)
if err != nil {
return err
}
resp, err := req.Do()
// execute the request.
resp, err := c.httpClient.Do(req)
defer closeResponse(resp)
if err != nil {
return err
@ -222,6 +145,7 @@ func (a API) abortMultipartUpload(bucketName, objectName, uploadID string) error
var errorResponse ErrorResponse
switch resp.StatusCode {
case http.StatusNotFound:
// This is needed specifically for Abort and it cannot be converged.
errorResponse = ErrorResponse{
Code: "NoSuchUpload",
Message: "The specified multipart upload does not exist.",
@ -231,26 +155,8 @@ func (a API) abortMultipartUpload(bucketName, objectName, uploadID string) error
HostID: resp.Header.Get("x-amz-id-2"),
AmzBucketRegion: resp.Header.Get("x-amz-bucket-region"),
}
case http.StatusForbidden:
errorResponse = ErrorResponse{
Code: "AccessDenied",
Message: "Access Denied.",
BucketName: bucketName,
Key: objectName,
RequestID: resp.Header.Get("x-amz-request-id"),
HostID: resp.Header.Get("x-amz-id-2"),
AmzBucketRegion: resp.Header.Get("x-amz-bucket-region"),
}
default:
errorResponse = ErrorResponse{
Code: resp.Status,
Message: "Unknown error, please report this at https://github.com/minio/minio-go-legacy/issues.",
BucketName: bucketName,
Key: objectName,
RequestID: resp.Header.Get("x-amz-request-id"),
HostID: resp.Header.Get("x-amz-id-2"),
AmzBucketRegion: resp.Header.Get("x-amz-bucket-region"),
}
return HTTPRespToErrorResponse(resp, bucketName, objectName)
}
return errorResponse
}

View File

@ -1,141 +1,76 @@
/*
* Minio Go Library for Amazon S3 Compatible Cloud Storage (C) 2015 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package minio
import (
"net/http"
"net/url"
"strconv"
"strings"
"time"
)
// BucketExists verify if bucket exists and you have permission to access it.
func (a API) BucketExists(bucketName string) error {
func (c Client) BucketExists(bucketName string) error {
if err := isValidBucketName(bucketName); err != nil {
return err
}
req, err := a.bucketExistsRequest(bucketName)
req, err := c.newRequest("HEAD", requestMetadata{
bucketName: bucketName,
})
if err != nil {
return err
}
resp, err := req.Do()
resp, err := c.httpClient.Do(req)
defer closeResponse(resp)
if err != nil {
return err
}
if resp != nil {
if resp.StatusCode != http.StatusOK {
// Head has no response body, handle it.
var errorResponse ErrorResponse
switch resp.StatusCode {
case http.StatusNotFound:
errorResponse = ErrorResponse{
Code: "NoSuchBucket",
Message: "The specified bucket does not exist.",
BucketName: bucketName,
RequestID: resp.Header.Get("x-amz-request-id"),
HostID: resp.Header.Get("x-amz-id-2"),
AmzBucketRegion: resp.Header.Get("x-amz-bucket-region"),
}
case http.StatusForbidden:
errorResponse = ErrorResponse{
Code: "AccessDenied",
Message: "Access Denied.",
BucketName: bucketName,
RequestID: resp.Header.Get("x-amz-request-id"),
HostID: resp.Header.Get("x-amz-id-2"),
AmzBucketRegion: resp.Header.Get("x-amz-bucket-region"),
}
default:
errorResponse = ErrorResponse{
Code: resp.Status,
Message: resp.Status,
BucketName: bucketName,
RequestID: resp.Header.Get("x-amz-request-id"),
HostID: resp.Header.Get("x-amz-id-2"),
AmzBucketRegion: resp.Header.Get("x-amz-bucket-region"),
}
}
return errorResponse
return HTTPRespToErrorResponse(resp, bucketName, "")
}
}
return nil
}
// bucketExistsRequest constructs a new request for BucketExists.
func (a API) bucketExistsRequest(bucketName string) (*Request, error) {
targetURL, err := getTargetURL(a.endpointURL, bucketName, "", url.Values{})
if err != nil {
return nil, err
}
// get bucket region.
region, err := a.getRegion(bucketName)
if err != nil {
return nil, err
}
return newRequest("HEAD", targetURL, requestMetadata{
credentials: a.credentials,
userAgent: a.userAgent,
bucketRegion: region,
contentTransport: a.httpTransport,
})
}
// StatObject verifies if object exists and you have permission to access.
func (a API) StatObject(bucketName, objectName string) (ObjectStat, error) {
func (c Client) StatObject(bucketName, objectName string) (ObjectStat, error) {
if err := isValidBucketName(bucketName); err != nil {
return ObjectStat{}, err
}
if err := isValidObjectName(objectName); err != nil {
return ObjectStat{}, err
}
req, err := a.statObjectRequest(bucketName, objectName)
// Instantiate a new request.
req, err := c.newRequest("HEAD", requestMetadata{
bucketName: bucketName,
objectName: objectName,
})
if err != nil {
return ObjectStat{}, err
}
resp, err := req.Do()
resp, err := c.httpClient.Do(req)
defer closeResponse(resp)
if err != nil {
return ObjectStat{}, err
}
if resp != nil {
if resp.StatusCode != http.StatusOK {
var errorResponse ErrorResponse
switch resp.StatusCode {
case http.StatusNotFound:
errorResponse = ErrorResponse{
Code: "NoSuchKey",
Message: "The specified key does not exist.",
BucketName: bucketName,
Key: objectName,
RequestID: resp.Header.Get("x-amz-request-id"),
HostID: resp.Header.Get("x-amz-id-2"),
AmzBucketRegion: resp.Header.Get("x-amz-bucket-region"),
}
case http.StatusForbidden:
errorResponse = ErrorResponse{
Code: "AccessDenied",
Message: "Access Denied.",
BucketName: bucketName,
Key: objectName,
RequestID: resp.Header.Get("x-amz-request-id"),
HostID: resp.Header.Get("x-amz-id-2"),
AmzBucketRegion: resp.Header.Get("x-amz-bucket-region"),
}
default:
errorResponse = ErrorResponse{
Code: resp.Status,
Message: resp.Status,
BucketName: bucketName,
Key: objectName,
RequestID: resp.Header.Get("x-amz-request-id"),
HostID: resp.Header.Get("x-amz-id-2"),
AmzBucketRegion: resp.Header.Get("x-amz-bucket-region"),
}
}
return ObjectStat{}, errorResponse
return ObjectStat{}, HTTPRespToErrorResponse(resp, bucketName, objectName)
}
}
md5sum := strings.Trim(resp.Header.Get("ETag"), "\"") // trim off the odd double quotes
@ -143,7 +78,7 @@ func (a API) StatObject(bucketName, objectName string) (ObjectStat, error) {
if err != nil {
return ObjectStat{}, ErrorResponse{
Code: "InternalError",
Message: "Content-Length is invalid, please report this issue at https://github.com/minio/minio-go/issues.",
Message: "Content-Length is invalid. " + reportIssue,
BucketName: bucketName,
Key: objectName,
RequestID: resp.Header.Get("x-amz-request-id"),
@ -155,7 +90,7 @@ func (a API) StatObject(bucketName, objectName string) (ObjectStat, error) {
if err != nil {
return ObjectStat{}, ErrorResponse{
Code: "InternalError",
Message: "Last-Modified time format is invalid, please report this issue at https://github.com/minio/minio-go/issues.",
Message: "Last-Modified time format is invalid. " + reportIssue,
BucketName: bucketName,
Key: objectName,
RequestID: resp.Header.Get("x-amz-request-id"),
@ -176,32 +111,3 @@ func (a API) StatObject(bucketName, objectName string) (ObjectStat, error) {
objectStat.ContentType = contentType
return objectStat, nil
}
// statObjectRequest wrapper creates a new headObject request.
func (a API) statObjectRequest(bucketName, objectName string) (*Request, error) {
// get targetURL.
targetURL, err := getTargetURL(a.endpointURL, bucketName, objectName, url.Values{})
if err != nil {
return nil, err
}
// get bucket region.
region, err := a.getRegion(bucketName)
if err != nil {
return nil, err
}
// Instantiate a new request.
req, err := newRequest("HEAD", targetURL, requestMetadata{
credentials: a.credentials,
userAgent: a.userAgent,
bucketRegion: region,
contentTransport: a.httpTransport,
})
if err != nil {
return nil, err
}
// Return new request.
return req, nil
}

324
api.go
View File

@ -17,125 +17,130 @@
package minio
import (
"encoding/base64"
"encoding/hex"
"io"
"net/http"
"net/url"
"runtime"
"time"
)
// API implements Amazon S3 compatible methods.
type API struct {
// Client implements Amazon S3 compatible methods.
type Client struct {
/// Standard options.
accessKeyID string // AccessKeyID required for authorized requests.
secretAccessKey string // SecretAccessKey required for authorized requests.
signature SignatureType // Choose a signature type if necessary.
anonymous bool // Set to 'true' if Client has no access and secret keys.
// User supplied.
userAgent string
credentials *clientCredentials
appInfo struct {
appName string
appVersion string
}
endpointURL *url.URL
// This http transport is usually needed for debugging OR to add your own
// custom TLS certificates on the client transport, for custom CA's and
// certs which are not part of standard certificate authority.
httpTransport http.RoundTripper
// Needs allocation.
bucketRgnC *bucketRegionCache
httpClient *http.Client
bucketLocCache *bucketLocationCache
}
// Global constants.
const (
libraryName = "minio-go"
libraryVersion = "0.2.5"
)
// User Agent should always following the below style.
// Please open an issue to discuss any new changes here.
//
// Minio (OS; ARCH) LIB/VER APP/VER
const (
libraryUserAgentPrefix = "Minio (" + runtime.GOOS + "; " + runtime.GOARCH + ") "
libraryUserAgent = libraryUserAgentPrefix + libraryName + "/" + libraryVersion
)
// NewV2 - instantiate minio client with Amazon S3 signature version '2' compatiblity.
func NewV2(endpoint string, accessKeyID, secretAccessKey string, inSecure bool) (API, error) {
// construct endpoint.
endpointURL, err := getEndpointURL(endpoint, inSecure)
func NewV2(endpoint string, accessKeyID, secretAccessKey string, insecure bool) (CloudStorageClient, error) {
clnt, err := privateNew(endpoint, accessKeyID, secretAccessKey, insecure)
if err != nil {
return API{}, err
return nil, err
}
// create a new client Config.
credentials := &clientCredentials{}
credentials.AccessKeyID = accessKeyID
credentials.SecretAccessKey = secretAccessKey
credentials.Signature = SignatureV2
// instantiate new API.
api := API{
// Save for lower level calls.
userAgent: libraryUserAgent,
credentials: credentials,
endpointURL: endpointURL,
// Allocate.
bucketRgnC: newBucketRegionCache(),
}
return api, nil
// Set to use signature version '2'.
clnt.signature = SignatureV2
return clnt, nil
}
// NewV4 - instantiate minio client with Amazon S3 signature version '4' compatibility.
func NewV4(endpoint string, accessKeyID, secretAccessKey string, inSecure bool) (API, error) {
// construct endpoint.
endpointURL, err := getEndpointURL(endpoint, inSecure)
func NewV4(endpoint string, accessKeyID, secretAccessKey string, insecure bool) (CloudStorageClient, error) {
clnt, err := privateNew(endpoint, accessKeyID, secretAccessKey, insecure)
if err != nil {
return API{}, err
return nil, err
}
// create a new client Config.
credentials := &clientCredentials{}
credentials.AccessKeyID = accessKeyID
credentials.SecretAccessKey = secretAccessKey
credentials.Signature = SignatureV4
// instantiate new API.
api := API{
// Save for lower level calls.
userAgent: libraryUserAgent,
credentials: credentials,
endpointURL: endpointURL,
// Allocate.
bucketRgnC: newBucketRegionCache(),
}
return api, nil
// Set to use signature version '4'.
clnt.signature = SignatureV4
return clnt, nil
}
// New - instantiate minio client API, adds automatic verification of signature.
func New(endpoint string, accessKeyID, secretAccessKey string, inSecure bool) (API, error) {
// construct endpoint.
endpointURL, err := getEndpointURL(endpoint, inSecure)
// New - instantiate minio client Client, adds automatic verification of signature.
func New(endpoint string, accessKeyID, secretAccessKey string, insecure bool) (CloudStorageClient, error) {
clnt, err := privateNew(endpoint, accessKeyID, secretAccessKey, insecure)
if err != nil {
return API{}, err
return nil, err
}
// create a new client Config.
credentials := &clientCredentials{}
credentials.AccessKeyID = accessKeyID
credentials.SecretAccessKey = secretAccessKey
// Google cloud storage should be set to signature V2, force it if not.
if isGoogleEndpoint(endpointURL) {
credentials.Signature = SignatureV2
if isGoogleEndpoint(clnt.endpointURL) {
clnt.signature = SignatureV2
}
// If Amazon S3 set to signature v2.
if isAmazonEndpoint(endpointURL) {
credentials.Signature = SignatureV4
if isAmazonEndpoint(clnt.endpointURL) {
clnt.signature = SignatureV4
}
return clnt, nil
}
func privateNew(endpoint, accessKeyID, secretAccessKey string, insecure bool) (*Client, error) {
// construct endpoint.
endpointURL, err := getEndpointURL(endpoint, insecure)
if err != nil {
return nil, err
}
// instantiate new API.
api := API{
// Save for lower level calls.
userAgent: libraryUserAgent,
credentials: credentials,
endpointURL: endpointURL,
// Allocate.
bucketRgnC: newBucketRegionCache(),
// instantiate new Client.
clnt := new(Client)
clnt.accessKeyID = accessKeyID
clnt.secretAccessKey = secretAccessKey
if clnt.accessKeyID == "" || clnt.secretAccessKey == "" {
clnt.anonymous = true
}
return api, nil
// Save endpoint URL, user agent for future uses.
clnt.endpointURL = endpointURL
// Instantiate http client and bucket location cache.
clnt.httpClient = &http.Client{}
clnt.bucketLocCache = newBucketLocationCache()
// Return.
return clnt, nil
}
// SetAppInfo - add application details to user agent.
func (a *API) SetAppInfo(appName string, appVersion string) {
func (c *Client) SetAppInfo(appName string, appVersion string) {
// if app name and version is not set, we do not a new user agent.
if appName != "" && appVersion != "" {
appUserAgent := appName + "/" + appVersion
a.userAgent = libraryUserAgent + " " + appUserAgent
c.appInfo = struct {
appName string
appVersion string
}{}
c.appInfo.appName = appName
c.appInfo.appVersion = appVersion
}
}
// SetCustomTransport - set new custom transport.
func (a *API) SetCustomTransport(customHTTPTransport http.RoundTripper) {
func (c *Client) SetCustomTransport(customHTTPTransport http.RoundTripper) {
// Set this to override default transport ``http.DefaultTransport``.
//
// This transport is usually needed for debugging OR to add your own
@ -149,11 +154,160 @@ func (a *API) SetCustomTransport(customHTTPTransport http.RoundTripper) {
// }
// api.SetTransport(tr)
//
a.httpTransport = customHTTPTransport
if c.httpClient != nil {
c.httpClient.Transport = customHTTPTransport
}
}
// CloudStorageAPI - Cloud Storage API interface.
type CloudStorageAPI interface {
// requestMetadata - is container for all the values to make a request.
type requestMetadata struct {
// If set newRequest presigns the URL.
presignURL bool
// User supplied.
bucketName string
objectName string
queryValues url.Values
customHeader http.Header
expires int64
// Generated by our internal code.
contentBody io.ReadCloser
contentLength int64
contentSha256Bytes []byte
contentMD5Bytes []byte
}
func (c Client) newRequest(method string, metadata requestMetadata) (*http.Request, error) {
// If no method is supplied default to 'POST'.
if method == "" {
method = "POST"
}
// construct a new target URL.
targetURL, err := c.makeTargetURL(metadata.bucketName, metadata.objectName, metadata.queryValues)
if err != nil {
return nil, err
}
// get a new HTTP request for the method.
req, err := http.NewRequest(method, targetURL.String(), nil)
if err != nil {
return nil, err
}
// Gather location only if bucketName is present.
location := "us-east-1" // Default all other requests to "us-east-1".
if metadata.bucketName != "" {
location, err = c.getBucketLocation(metadata.bucketName)
if err != nil {
return nil, err
}
}
// If presigned request, return quickly.
if metadata.expires != 0 {
if c.anonymous {
return nil, ErrInvalidArgument("Requests cannot be presigned with anonymous credentials.")
}
if c.signature.isV2() {
// Presign URL with signature v2.
req = PreSignV2(*req, c.accessKeyID, c.secretAccessKey, metadata.expires)
} else {
// Presign URL with signature v4.
req = PreSignV4(*req, c.accessKeyID, c.secretAccessKey, location, metadata.expires)
}
return req, nil
}
// Set content body if available.
if metadata.contentBody != nil {
req.Body = metadata.contentBody
}
// set UserAgent for the request.
c.setUserAgent(req)
// Set all headers.
for k, v := range metadata.customHeader {
req.Header.Set(k, v[0])
}
// set incoming content-length.
if metadata.contentLength > 0 {
req.ContentLength = metadata.contentLength
}
// Set sha256 sum only for non anonymous credentials.
if !c.anonymous {
// set sha256 sum for signature calculation only with signature version '4'.
if c.signature.isV4() || c.signature.isLatest() {
req.Header.Set("X-Amz-Content-Sha256", hex.EncodeToString(sum256([]byte{})))
if metadata.contentSha256Bytes != nil {
req.Header.Set("X-Amz-Content-Sha256", hex.EncodeToString(metadata.contentSha256Bytes))
}
}
}
// set md5Sum for content protection.
if metadata.contentMD5Bytes != nil {
req.Header.Set("Content-MD5", base64.StdEncoding.EncodeToString(metadata.contentMD5Bytes))
}
// Sign the request if not anonymous.
if !c.anonymous {
if c.signature.isV2() {
// Add signature version '2' authorization header.
req = SignV2(*req, c.accessKeyID, c.secretAccessKey)
} else if c.signature.isV4() || c.signature.isLatest() {
// Add signature version '4' authorization header.
req = SignV4(*req, c.accessKeyID, c.secretAccessKey, location)
}
}
// return request.
return req, nil
}
func (c Client) setUserAgent(req *http.Request) {
req.Header.Set("User-Agent", libraryUserAgent)
if c.appInfo.appName != "" && c.appInfo.appVersion != "" {
req.Header.Set("User-Agent", libraryUserAgent+" "+c.appInfo.appName+"/"+c.appInfo.appVersion)
}
}
func (c Client) makeTargetURL(bucketName, objectName string, queryValues url.Values) (*url.URL, error) {
urlStr := c.endpointURL.Scheme + "://" + c.endpointURL.Host + "/"
// Make URL only if bucketName is available, otherwise use the endpoint URL.
if bucketName != "" {
// If endpoint supports virtual host style use that always.
// Currently only S3 and Google Cloud Storage would support this.
if isVirtualHostSupported(c.endpointURL) {
urlStr = c.endpointURL.Scheme + "://" + bucketName + "." + c.endpointURL.Host + "/"
if objectName != "" {
urlStr = urlStr + urlEncodePath(objectName)
}
} else {
// If not fall back to using path style.
urlStr = urlStr + bucketName
if objectName != "" {
urlStr = urlStr + "/" + urlEncodePath(objectName)
}
}
}
// If there are any query values, add them to the end.
if len(queryValues) > 0 {
urlStr = urlStr + "?" + queryValues.Encode()
}
u, err := url.Parse(urlStr)
if err != nil {
return nil, err
}
return u, nil
}
// CloudStorageClient - Cloud Storage Client interface.
type CloudStorageClient interface {
// Bucket Read/Write/Stat operations.
MakeBucket(bucket string, cannedACL BucketACL, location string) error
BucketExists(bucket string) error
@ -161,7 +315,7 @@ type CloudStorageAPI interface {
SetBucketACL(bucket string, cannedACL BucketACL) error
GetBucketACL(bucket string) (BucketACL, error)
ListBuckets() <-chan BucketStat
ListBuckets() ([]BucketStat, error)
ListObjects(bucket, prefix string, recursive bool) <-chan ObjectStat
ListIncompleteUploads(bucket, prefix string, recursive bool) <-chan ObjectMultipartStat
@ -171,7 +325,7 @@ type CloudStorageAPI interface {
PutObject(bucket, object string, data io.ReadSeeker, size int64, contentType string) (int64, error)
StatObject(bucket, object string) (ObjectStat, error)
RemoveObject(bucket, object string) error
RemoveIncompleteUpload(bucket, object string) <-chan error
RemoveIncompleteUpload(bucket, object string) error
// Presigned operations.
PresignedGetObject(bucket, object string, expires time.Duration) (string, error)

View File

@ -1,6 +1,8 @@
package minio_test
import (
"bytes"
"io/ioutil"
"math/rand"
"testing"
"time"
@ -33,29 +35,32 @@ func randString(n int, src rand.Source) string {
}
func TestFunctional(t *testing.T) {
a, err := minio.New("play.minio.io:9002",
c, err := minio.New("play.minio.io:9002",
"Q3AM3UQ867SPQQA43P2F", "zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG", false)
if err != nil {
t.Fatal("Error:", err)
}
// Set user agent.
c.SetAppInfo("Test", "0.1.0")
bucketName := randString(60, rand.NewSource(time.Now().UnixNano()))
err = a.MakeBucket(bucketName, "private", "us-east-1")
err = c.MakeBucket(bucketName, "private", "us-east-1")
if err != nil {
t.Fatal("Error:", err, bucketName)
}
err = a.BucketExists(bucketName)
err = c.BucketExists(bucketName)
if err != nil {
t.Fatal("Error:", err, bucketName)
}
err = a.SetBucketACL(bucketName, "public-read-write")
err = c.SetBucketACL(bucketName, "public-read-write")
if err != nil {
t.Fatal("Error:", err)
}
acl, err := a.GetBucketACL(bucketName)
acl, err := c.GetBucketACL(bucketName)
if err != nil {
t.Fatal("Error:", err)
}
@ -63,18 +68,46 @@ func TestFunctional(t *testing.T) {
t.Fatal("Error:", acl)
}
for b := range a.ListBuckets() {
if b.Err != nil {
t.Fatal("Error:", b.Err)
}
}
err = a.RemoveBucket(bucketName)
_, err = c.ListBuckets()
if err != nil {
t.Fatal("Error:", err)
}
err = a.RemoveBucket("bucket1")
objectName := bucketName + "Minio"
readSeeker := bytes.NewReader([]byte("Hello World!"))
n, err := c.PutObject(bucketName, objectName, readSeeker, int64(readSeeker.Len()), "")
if err != nil {
t.Fatal("Error: ", err)
}
if n != int64(len([]byte("Hello World!"))) {
t.Fatal("Error: bad length ", n, readSeeker.Len())
}
newReadSeeker, err := c.GetObject(bucketName, objectName)
if err != nil {
t.Fatal("Error: ", err)
}
newReadBytes, err := ioutil.ReadAll(newReadSeeker)
if err != nil {
t.Fatal("Error: ", err)
}
if !bytes.Equal(newReadBytes, []byte("Hello World!")) {
t.Fatal("Error: bytes invalid.")
}
err = c.RemoveObject(bucketName, objectName)
if err != nil {
t.Fatal("Error: ", err)
}
err = c.RemoveBucket(bucketName)
if err != nil {
t.Fatal("Error:", err)
}
err = c.RemoveBucket("bucket1")
if err == nil {
t.Fatal("Error:")
}

View File

@ -1,173 +0,0 @@
/*
* Minio Go Library for Amazon S3 Compatible Cloud Storage (C) 2015 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package minio_test
import (
"bytes"
"io"
"net/http"
"strconv"
"time"
)
// bucketHandler is an http.Handler that verifies bucket
// responses and validates incoming requests.
type bucketHandler struct {
resource string
}
func (h bucketHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
path := r.URL.Path
switch {
case r.Method == "GET":
switch {
case path == "/":
response := []byte("<?xml version=\"1.0\" encoding=\"UTF-8\"?><ListAllMyBucketsResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\"><Buckets><Bucket><Name>bucket</Name><CreationDate>2015-05-20T23:05:09.230Z</CreationDate></Bucket></Buckets><Owner><ID>minio</ID><DisplayName>minio</DisplayName></Owner></ListAllMyBucketsResult>")
w.Header().Set("Content-Length", strconv.Itoa(len(response)))
w.Write(response)
case path == h.resource:
_, ok := r.URL.Query()["acl"]
if ok {
response := []byte("<?xml version=\"1.0\" encoding=\"UTF-8\"?><AccessControlPolicy><Owner><ID>75aa57f09aa0c8caeab4f8c24e99d10f8e7faeebf76c078efc7c6caea54ba06a</ID><DisplayName>CustomersName@amazon.com</DisplayName></Owner><AccessControlList><Grant><Grantee xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:type=\"CanonicalUser\"><ID>75aa57f09aa0c8caeab4f8c24e99d10f8e7faeebf76c078efc7c6caea54ba06a</ID><DisplayName>CustomersName@amazon.com</DisplayName></Grantee><Permission>FULL_CONTROL</Permission></Grant></AccessControlList></AccessControlPolicy>")
w.Header().Set("Content-Length", strconv.Itoa(len(response)))
w.Write(response)
return
}
fallthrough
case path == h.resource:
response := []byte("<?xml version=\"1.0\" encoding=\"UTF-8\"?><ListBucketResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\"><Contents><ETag>\"259d04a13802ae09c7e41be50ccc6baa\"</ETag><Key>object</Key><LastModified>2015-05-21T18:24:21.097Z</LastModified><Size>22061</Size><Owner><ID>minio</ID><DisplayName>minio</DisplayName></Owner><StorageClass>STANDARD</StorageClass></Contents><Delimiter></Delimiter><EncodingType></EncodingType><IsTruncated>false</IsTruncated><Marker></Marker><MaxKeys>1000</MaxKeys><Name>testbucket</Name><NextMarker></NextMarker><Prefix></Prefix></ListBucketResult>")
w.Header().Set("Content-Length", strconv.Itoa(len(response)))
w.Write(response)
}
case r.Method == "PUT":
switch {
case path == h.resource:
_, ok := r.URL.Query()["acl"]
if ok {
switch r.Header.Get("x-amz-acl") {
case "public-read-write":
fallthrough
case "public-read":
fallthrough
case "private":
fallthrough
case "authenticated-read":
w.WriteHeader(http.StatusOK)
return
default:
w.WriteHeader(http.StatusNotImplemented)
return
}
}
w.WriteHeader(http.StatusOK)
default:
w.WriteHeader(http.StatusBadRequest)
}
case r.Method == "HEAD":
switch {
case path == h.resource:
w.WriteHeader(http.StatusOK)
default:
w.WriteHeader(http.StatusForbidden)
}
case r.Method == "DELETE":
switch {
case path != h.resource:
w.WriteHeader(http.StatusNotFound)
default:
h.resource = ""
w.WriteHeader(http.StatusNoContent)
}
}
}
// objectHandler is an http.Handler that verifies object responses
// and validates incoming requests.
type objectHandler struct {
resource string
data []byte
}
func (h objectHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == "PUT":
length, err := strconv.Atoi(r.Header.Get("Content-Length"))
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
var buffer bytes.Buffer
_, err = io.CopyN(&buffer, r.Body, int64(length))
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
if !bytes.Equal(h.data, buffer.Bytes()) {
w.WriteHeader(http.StatusInternalServerError)
return
}
w.Header().Set("ETag", "\"9af2f8218b150c351ad802c6f3d66abe\"")
w.WriteHeader(http.StatusOK)
case r.Method == "HEAD":
if r.URL.Path != h.resource {
w.WriteHeader(http.StatusNotFound)
return
}
w.Header().Set("Content-Length", strconv.Itoa(len(h.data)))
w.Header().Set("Last-Modified", time.Now().UTC().Format(http.TimeFormat))
w.Header().Set("ETag", "\"9af2f8218b150c351ad802c6f3d66abe\"")
w.WriteHeader(http.StatusOK)
case r.Method == "POST":
_, ok := r.URL.Query()["uploads"]
if ok {
response := []byte("<?xml version=\"1.0\" encoding=\"UTF-8\"?><InitiateMultipartUploadResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\"><Bucket>example-bucket</Bucket><Key>object</Key><UploadId>XXBsb2FkIElEIGZvciBlbHZpbmcncyVcdS1tb3ZpZS5tMnRzEEEwbG9hZA</UploadId></InitiateMultipartUploadResult>")
w.Header().Set("Content-Length", strconv.Itoa(len(response)))
w.Write(response)
return
}
case r.Method == "GET":
_, ok := r.URL.Query()["uploadId"]
if ok {
uploadID := r.URL.Query().Get("uploadId")
if uploadID != "XXBsb2FkIElEIGZvciBlbHZpbmcncyVcdS1tb3ZpZS5tMnRzEEEwbG9hZA" {
w.WriteHeader(http.StatusNotFound)
return
}
response := []byte("<?xml version=\"1.0\" encoding=\"UTF-8\"?><ListPartsResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\"><Bucket>example-bucket</Bucket><Key>example-object</Key><UploadId>XXBsb2FkIElEIGZvciBlbHZpbmcncyVcdS1tb3ZpZS5tMnRzEEEwbG9hZA</UploadId><Initiator><ID>arn:aws:iam::111122223333:user/some-user-11116a31-17b5-4fb7-9df5-b288870f11xx</ID><DisplayName>umat-user-11116a31-17b5-4fb7-9df5-b288870f11xx</DisplayName></Initiator><Owner><ID>75aa57f09aa0c8caeab4f8c24e99d10f8e7faeebf76c078efc7c6caea54ba06a</ID><DisplayName>someName</DisplayName></Owner><StorageClass>STANDARD</StorageClass><PartNumberMarker>1</PartNumberMarker><NextPartNumberMarker>3</NextPartNumberMarker><MaxParts>2</MaxParts><IsTruncated>true</IsTruncated><Part><PartNumber>2</PartNumber><LastModified>2010-11-10T20:48:34.000Z</LastModified><ETag>\"7778aef83f66abc1fa1e8477f296d394\"</ETag><Size>10485760</Size></Part><Part><PartNumber>3</PartNumber><LastModified>2010-11-10T20:48:33.000Z</LastModified><ETag>\"aaaa18db4cc2f85cedef654fccc4a4x8\"</ETag><Size>10485760</Size></Part></ListPartsResult>")
w.Header().Set("Content-Length", strconv.Itoa(len(response)))
w.Write(response)
return
}
if r.URL.Path != h.resource {
w.WriteHeader(http.StatusNotFound)
return
}
w.Header().Set("Content-Length", strconv.Itoa(len(h.data)))
w.Header().Set("Last-Modified", time.Now().UTC().Format(http.TimeFormat))
w.Header().Set("ETag", "\"9af2f8218b150c351ad802c6f3d66abe\"")
w.WriteHeader(http.StatusOK)
io.Copy(w, bytes.NewReader(h.data))
case r.Method == "DELETE":
if r.URL.Path != h.resource {
w.WriteHeader(http.StatusNotFound)
return
}
h.resource = ""
h.data = nil
w.WriteHeader(http.StatusNoContent)
}
}

View File

@ -22,19 +22,19 @@ import (
)
func TestSignature(t *testing.T) {
credentials := clientCredentials{}
if !credentials.Signature.isLatest() {
clnt := Client{}
if !clnt.signature.isLatest() {
t.Fatal("Error")
}
credentials.Signature = SignatureV2
if !credentials.Signature.isV2() {
clnt.signature = SignatureV2
if !clnt.signature.isV2() {
t.Fatal("Error")
}
if credentials.Signature.isV4() {
if clnt.signature.isV4() {
t.Fatal("Error")
}
credentials.Signature = SignatureV4
if !credentials.Signature.isV4() {
clnt.signature = SignatureV4
if !clnt.signature.isV4() {
t.Fatal("Error")
}
}

View File

@ -1,294 +0,0 @@
/*
* Minio Go Library for Amazon S3 Compatible Cloud Storage (C) 2015 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package minio_test
import (
"bytes"
"io"
"net/http/httptest"
"net/url"
"testing"
"time"
"github.com/minio/minio-go"
)
func TestBucketOperations(t *testing.T) {
bucket := bucketHandler(bucketHandler{
resource: "/bucket",
})
server := httptest.NewServer(bucket)
defer server.Close()
u, err := url.Parse(server.URL)
if err != nil {
t.Fatal("Error:", err)
}
a, err := minio.New(u.Host, "", "", true)
if err != nil {
t.Fatal("Error:", err)
}
err = a.MakeBucket("bucket", "private", "")
if err != nil {
t.Fatal("Error:", err)
}
err = a.BucketExists("bucket")
if err != nil {
t.Fatal("Error:", err)
}
err = a.BucketExists("bucket1")
if err == nil {
t.Fatal("Error:", err)
}
if err.Error() != "Access Denied." {
t.Fatal("Error")
}
err = a.SetBucketACL("bucket", "public-read-write")
if err != nil {
t.Fatal("Error:", err)
}
acl, err := a.GetBucketACL("bucket")
if err != nil {
t.Fatal("Error:", err)
}
if acl != minio.BucketACL("private") {
t.Fatal("Error")
}
for b := range a.ListBuckets() {
if b.Err != nil {
t.Fatal("Error:", b.Err.Error())
}
if b.Name != "bucket" {
t.Fatal("Error")
}
}
for o := range a.ListObjects("bucket", "", true) {
if o.Err != nil {
t.Fatal("Error:", o.Err.Error())
}
if o.Key != "object" {
t.Fatal("Error")
}
}
err = a.RemoveBucket("bucket")
if err != nil {
t.Fatal("Error:", err)
}
err = a.RemoveBucket("bucket1")
if err == nil {
t.Fatal("Error")
}
if err.Error() != "The specified bucket does not exist." {
t.Fatal("Error:", err)
}
}
func TestBucketOperationsFail(t *testing.T) {
bucket := bucketHandler(bucketHandler{
resource: "/bucket",
})
server := httptest.NewServer(bucket)
defer server.Close()
u, err := url.Parse(server.URL)
if err != nil {
t.Fatal("Error:", err)
}
a, err := minio.New(u.Host, "", "", true)
if err != nil {
t.Fatal("Error:", err)
}
err = a.MakeBucket("bucket$$$", "private", "")
if err == nil {
t.Fatal("Error:", err)
}
err = a.BucketExists("bucket.")
if err == nil {
t.Fatal("Error:", err)
}
err = a.SetBucketACL("bucket-.", "public-read-write")
if err == nil {
t.Fatal("Error")
}
_, err = a.GetBucketACL("bucket??")
if err == nil {
t.Fatal("Error:", err)
}
for o := range a.ListObjects("bucket??", "", true) {
if o.Err == nil {
t.Fatal("Error:", o.Err.Error())
}
}
err = a.RemoveBucket("bucket??")
if err == nil {
t.Fatal("Error")
}
if err.Error() != "Bucket name contains invalid characters." {
t.Fatal("Error:", err)
}
}
func TestObjectOperations(t *testing.T) {
object := objectHandler(objectHandler{
resource: "/bucket/object",
data: []byte("Hello, World"),
})
server := httptest.NewServer(object)
defer server.Close()
u, err := url.Parse(server.URL)
if err != nil {
t.Fatal("Error:", err)
}
a, err := minio.New(u.Host, "", "", true)
if err != nil {
t.Fatal("Error:", err)
}
data := []byte("Hello, World")
n, err := a.PutObject("bucket", "object", bytes.NewReader(data), int64(len(data)), "")
if err != nil {
t.Fatal("Error:", err)
}
if n != int64(len(data)) {
t.Fatal("Error")
}
metadata, err := a.StatObject("bucket", "object")
if err != nil {
t.Fatal("Error:", err)
}
if metadata.Key != "object" {
t.Fatal("Error")
}
if metadata.ETag != "9af2f8218b150c351ad802c6f3d66abe" {
t.Fatal("Error")
}
reader, err := a.GetObject("bucket", "object")
if err != nil {
t.Fatal("Error:", err)
}
var buffer bytes.Buffer
_, err = io.Copy(&buffer, reader)
if err != nil {
t.Fatal("Error:", err)
}
if !bytes.Equal(buffer.Bytes(), data) {
t.Fatal("Error")
}
err = a.RemoveObject("bucket", "object")
if err != nil {
t.Fatal("Error:", err)
}
}
func TestPresignedURL(t *testing.T) {
object := objectHandler(objectHandler{
resource: "/bucket/object",
data: []byte("Hello, World"),
})
server := httptest.NewServer(object)
defer server.Close()
u, err := url.Parse(server.URL)
if err != nil {
t.Fatal("Error:", err)
}
a, err := minio.New(u.Host, "", "", true)
if err != nil {
t.Fatal("Error:", err)
}
// should error out for invalid access keys.
_, err = a.PresignedGetObject("bucket", "object", time.Duration(1000)*time.Second)
if err == nil {
t.Fatal("Error:", err)
}
a, err = minio.New(u.Host, "accessKey", "secretKey", true)
if err != nil {
t.Fatal("Error:", err)
}
url, err := a.PresignedGetObject("bucket", "object", time.Duration(1000)*time.Second)
if err != nil {
t.Fatal("Error:", err)
}
if url == "" {
t.Fatal("Error")
}
_, err = a.PresignedGetObject("bucket", "object", time.Duration(0)*time.Second)
if err == nil {
t.Fatal("Error")
}
_, err = a.PresignedGetObject("bucket", "object", time.Duration(604801)*time.Second)
if err == nil {
t.Fatal("Error")
}
}
func TestErrorResponse(t *testing.T) {
errorResponse := []byte("<?xml version=\"1.0\" encoding=\"UTF-8\"?><Error><Code>AccessDenied</Code><Message>Access Denied</Message><BucketName>mybucket</BucketName><Key>myphoto.jpg</Key><RequestId>F19772218238A85A</RequestId><HostId>GuWkjyviSiGHizehqpmsD1ndz5NClSP19DOT+s2mv7gXGQ8/X1lhbDGiIJEXpGFD</HostId></Error>")
errorReader := bytes.NewReader(errorResponse)
err := minio.BodyToErrorResponse(errorReader)
if err == nil {
t.Fatal("Error")
}
if err.Error() != "Access Denied" {
t.Fatal("Error:", err)
}
resp := minio.ToErrorResponse(err)
if resp.Code != "AccessDenied" {
t.Fatal("Error:", resp)
}
if resp.RequestID != "F19772218238A85A" {
t.Fatal("Error:", resp.RequestID)
}
if resp.Message != "Access Denied" {
t.Fatal("Error:", resp.Message)
}
if resp.BucketName != "mybucket" {
t.Fatal("Error:", resp.BucketName)
}
if resp.Key != "myphoto.jpg" {
t.Fatal("Error:", resp.Key)
}
if resp.HostID != "GuWkjyviSiGHizehqpmsD1ndz5NClSP19DOT+s2mv7gXGQ8/X1lhbDGiIJEXpGFD" {
t.Fatal("Error:", resp.HostID)
}
if resp.ToXML() == "" {
t.Fatal("Error")
}
if resp.ToJSON() == "" {
t.Fatal("Error")
}
}

View File

@ -17,123 +17,79 @@
package minio
import (
"encoding/hex"
"net/http"
"net/url"
"path/filepath"
"sync"
)
// bucketRegionCache provides simple mechansim to hold bucket regions in memory.
type bucketRegionCache struct {
// bucketLocationCache provides simple mechansim to hold bucket locations in memory.
type bucketLocationCache struct {
// Mutex is used for handling the concurrent
// read/write requests for cache
sync.RWMutex
// items holds the cached regions.
// items holds the cached bucket locations.
items map[string]string
}
// newBucketRegionCache provides a new bucket region cache to be used
// newBucketLocationCache provides a new bucket location cache to be used
// internally with the client object.
func newBucketRegionCache() *bucketRegionCache {
return &bucketRegionCache{
func newBucketLocationCache() *bucketLocationCache {
return &bucketLocationCache{
items: make(map[string]string),
}
}
// Get returns a value of a given key if it exists
func (r *bucketRegionCache) Get(bucketName string) (region string, ok bool) {
func (r *bucketLocationCache) Get(bucketName string) (location string, ok bool) {
r.RLock()
defer r.RUnlock()
region, ok = r.items[bucketName]
location, ok = r.items[bucketName]
return
}
// Set will persist a value to the cache
func (r *bucketRegionCache) Set(bucketName string, region string) {
func (r *bucketLocationCache) Set(bucketName string, location string) {
r.Lock()
defer r.Unlock()
r.items[bucketName] = region
r.items[bucketName] = location
}
// Delete deletes a bucket name.
func (r *bucketRegionCache) Delete(bucketName string) {
func (r *bucketLocationCache) Delete(bucketName string) {
r.Lock()
defer r.Unlock()
delete(r.items, bucketName)
}
// getRegion - get region for the bucketName from region map cache.
func (a API) getRegion(bucketName string) (string, error) {
// If signature version '2', no need to fetch bucket location.
if a.credentials.Signature.isV2() {
// getBucketLocation - get location for the bucketName from location map cache.
func (c Client) getBucketLocation(bucketName string) (string, error) {
// For anonymous requests, default to "us-east-1" and let other calls
// move forward.
if c.anonymous {
return "us-east-1", nil
}
// If signature version '4' and latest and endpoint is not Amazon.
// Return 'us-east-1'
if a.credentials.Signature.isV4() || a.credentials.Signature.isLatest() {
if !isAmazonEndpoint(a.endpointURL) {
return "us-east-1", nil
}
}
if region, ok := a.bucketRgnC.Get(bucketName); ok {
return region, nil
if location, ok := c.bucketLocCache.Get(bucketName); ok {
return location, nil
}
// get bucket location.
location, err := a.getBucketLocation(bucketName)
if err != nil {
return "", err
}
region := "us-east-1"
// location is region in context of S3 API.
if location != "" {
region = location
}
a.bucketRgnC.Set(bucketName, region)
return region, nil
}
// getBucketLocationRequest wrapper creates a new getBucketLocation request.
func (a API) getBucketLocationRequest(bucketName string) (*Request, error) {
// Set location query.
urlValues := make(url.Values)
urlValues.Set("location", "")
// Set get bucket location always as path style.
targetURL := a.endpointURL
targetURL.Path = filepath.Join(bucketName, "")
targetURL.RawQuery = urlValues.Encode()
// Instantiate a new request.
req, err := newRequest("GET", targetURL, requestMetadata{
bucketRegion: "us-east-1",
credentials: a.credentials,
contentTransport: a.httpTransport,
})
if err != nil {
return nil, err
}
return req, nil
}
// getBucketLocation uses location subresource to return a bucket's region.
func (a API) getBucketLocation(bucketName string) (string, error) {
// Initialize a new request.
req, err := a.getBucketLocationRequest(bucketName)
req, err := c.getBucketLocationRequest(bucketName)
if err != nil {
return "", err
}
// Initiate the request.
resp, err := req.Do()
resp, err := c.httpClient.Do(req)
defer closeResponse(resp)
if err != nil {
return "", err
}
if resp != nil {
if resp.StatusCode != http.StatusOK {
return "", BodyToErrorResponse(resp.Body)
return "", HTTPRespToErrorResponse(resp, bucketName, "")
}
}
@ -144,16 +100,54 @@ func (a API) getBucketLocation(bucketName string) (string, error) {
return "", err
}
location := locationConstraint
// location is empty will be 'us-east-1'.
if locationConstraint == "" {
return "us-east-1", nil
if location == "" {
location = "us-east-1"
}
// location can be 'EU' convert it to meaningful 'eu-west-1'.
if locationConstraint == "EU" {
return "eu-west-1", nil
if location == "EU" {
location = "eu-west-1"
}
// return location.
return locationConstraint, nil
// Save the location into cache.
c.bucketLocCache.Set(bucketName, location)
// Return.
return location, nil
}
// getBucketLocationRequest wrapper creates a new getBucketLocation request.
func (c Client) getBucketLocationRequest(bucketName string) (*http.Request, error) {
// Set location query.
urlValues := make(url.Values)
urlValues.Set("location", "")
// Set get bucket location always as path style.
targetURL := c.endpointURL
targetURL.Path = filepath.Join(bucketName, "")
targetURL.RawQuery = urlValues.Encode()
// get a new HTTP request for the method.
req, err := http.NewRequest("GET", targetURL.String(), nil)
if err != nil {
return nil, err
}
// set UserAgent for the request.
c.setUserAgent(req)
// set sha256 sum for signature calculation only with signature version '4'.
if c.signature.isV4() || c.signature.isLatest() {
req.Header.Set("X-Amz-Content-Sha256", hex.EncodeToString(sum256([]byte{})))
}
// Sign the request.
if c.signature.isV4() || c.signature.isLatest() {
req = SignV4(*req, c.accessKeyID, c.secretAccessKey, "us-east-1")
} else if c.signature.isV2() {
req = SignV2(*req, c.accessKeyID, c.secretAccessKey)
}
return req, nil
}

View File

@ -35,10 +35,11 @@ func main() {
log.Fatalln(err)
}
for bucket := range s3Client.ListBuckets() {
if bucket.Err != nil {
log.Fatalln(bucket.Err)
}
buckets, err := s3Client.ListBuckets()
if err != nil {
log.Fatalln(err)
}
for _, bucket := range buckets {
log.Println(bucket)
}
}

View File

@ -50,5 +50,5 @@ func main() {
fmt.Printf("-F %s=%s ", k, v)
}
fmt.Printf("-F file=@/etc/bashrc ")
fmt.Printf(config.Endpoint + "/bucket-name\n")
fmt.Printf("https://play.minio.io:9002/bucket-name\n")
}

View File

@ -36,7 +36,7 @@ func main() {
if err != nil {
log.Fatalln(err)
}
reader, err := s3Client.GetObject("bucke-name", "objectName")
reader, err := s3Client.GetObject("bucket-name", "objectName")
if err != nil {
log.Fatalln(err)
}

View File

@ -35,10 +35,11 @@ func main() {
log.Fatalln(err)
}
for bucket := range s3Client.ListBuckets() {
if bucket.Err != nil {
log.Fatalln(bucket.Err)
}
buckets, err := s3Client.ListBuckets()
if err != nil {
log.Fatalln(err)
}
for _, bucket := range buckets {
log.Println(bucket)
}
}

View File

@ -49,6 +49,6 @@ func main() {
for k, v := range m {
fmt.Printf("-F %s=%s ", k, v)
}
fmt.Printf("-F file=@/etc/bashrc ")
fmt.Printf(config.Endpoint + "/bucket-name\n")
fmt.Printf("-F file=@/etc/bash.bashrc ")
fmt.Printf("https://bucket-name.s3.amazonaws.com\n")
}

View File

@ -1,26 +0,0 @@
package minio
import "runtime"
// clientCredentials - main configuration struct used for credentials.
type clientCredentials struct {
/// Standard options.
AccessKeyID string // AccessKeyID required for authorized requests.
SecretAccessKey string // SecretAccessKey required for authorized requests.
Signature SignatureType // choose a signature type if necessary.
}
// Global constants.
const (
libraryName = "minio-go"
libraryVersion = "0.2.5"
)
// User Agent should always following the below style.
// Please open an issue to discuss any new changes here.
//
// Minio (OS; ARCH) LIB/VER APP/VER
const (
libraryUserAgentPrefix = "Minio (" + runtime.GOOS + "; " + runtime.GOARCH + ") "
libraryUserAgent = libraryUserAgentPrefix + libraryName + "/" + libraryVersion
)

View File

@ -21,7 +21,6 @@ import (
"crypto/hmac"
"crypto/sha1"
"encoding/base64"
"errors"
"fmt"
"net/http"
"net/url"
@ -31,6 +30,11 @@ import (
"time"
)
// signature and API related constants.
const (
signV2Algorithm = "AWS"
)
// Encode input URL path to URL encoded path.
func encodeURL2Path(u *url.URL) (path string) {
// Encode URL path.
@ -52,49 +56,51 @@ func encodeURL2Path(u *url.URL) (path string) {
// PreSignV2 - presign the request in following style.
// https://${S3_BUCKET}.s3.amazonaws.com/${S3_OBJECT}?AWSAccessKeyId=${S3_ACCESS_KEY}&Expires=${TIMESTAMP}&Signature=${SIGNATURE}
func (r *Request) PreSignV2() (string, error) {
// if config is anonymous then presigning cannot be achieved, throw an error.
if isAnonymousCredentials(*r.credentials) {
return "", errors.New("Presigning cannot be achieved with anonymous credentials")
func PreSignV2(req http.Request, accessKeyID, secretAccessKey string, expires int64) *http.Request {
// presign is a noop for anonymous credentials.
if accessKeyID == "" || secretAccessKey == "" {
return nil
}
d := time.Now().UTC()
// Add date if not present
if date := r.Get("Date"); date == "" {
r.Set("Date", d.Format(http.TimeFormat))
if date := req.Header.Get("Date"); date == "" {
req.Header.Set("Date", d.Format(http.TimeFormat))
}
// Get encoded URL path.
path := encodeURL2Path(r.req.URL)
path := encodeURL2Path(req.URL)
// Find epoch expires when the request will expire.
epochExpires := d.Unix() + r.expires
epochExpires := d.Unix() + expires
// get string to sign.
stringToSign := fmt.Sprintf("%s\n\n\n%d\n%s", r.req.Method, epochExpires, path)
hm := hmac.New(sha1.New, []byte(r.credentials.SecretAccessKey))
stringToSign := fmt.Sprintf("%s\n\n\n%d\n%s", req.Method, epochExpires, path)
hm := hmac.New(sha1.New, []byte(secretAccessKey))
hm.Write([]byte(stringToSign))
// calculate signature.
signature := base64.StdEncoding.EncodeToString(hm.Sum(nil))
query := r.req.URL.Query()
query := req.URL.Query()
// Handle specially for Google Cloud Storage.
if strings.Contains(r.req.URL.Host, ".storage.googleapis.com") {
query.Set("GoogleAccessId", r.credentials.AccessKeyID)
if strings.Contains(req.URL.Host, ".storage.googleapis.com") {
query.Set("GoogleAccessId", accessKeyID)
} else {
query.Set("AWSAccessKeyId", r.credentials.AccessKeyID)
query.Set("AWSAccessKeyId", accessKeyID)
}
// Fill in Expires and Signature for presigned query.
query.Set("Expires", strconv.FormatInt(epochExpires, 10))
query.Set("Signature", signature)
r.req.URL.RawQuery = query.Encode()
return r.req.URL.String(), nil
// Encode query and save.
req.URL.RawQuery = query.Encode()
return &req
}
// PostPresignSignatureV2 - presigned signature for PostPolicy request
func (r *Request) PostPresignSignatureV2(policyBase64 string) string {
hm := hmac.New(sha1.New, []byte(r.credentials.SecretAccessKey))
func PostPresignSignatureV2(policyBase64, secretAccessKey string) string {
hm := hmac.New(sha1.New, []byte(secretAccessKey))
hm.Write([]byte(policyBase64))
signature := base64.StdEncoding.EncodeToString(hm.Sum(nil))
return signature
@ -117,29 +123,31 @@ func (r *Request) PostPresignSignatureV2(policyBase64 string) string {
// CanonicalizedProtocolHeaders = <described below>
// SignV2 sign the request before Do() (AWS Signature Version 2).
func (r *Request) SignV2() {
func SignV2(req http.Request, accessKeyID, secretAccessKey string) *http.Request {
// Initial time.
d := time.Now().UTC()
// Add date if not present.
if date := r.Get("Date"); date == "" {
r.Set("Date", d.Format(http.TimeFormat))
if date := req.Header.Get("Date"); date == "" {
req.Header.Set("Date", d.Format(http.TimeFormat))
}
// Calculate HMAC for secretAccessKey.
stringToSign := r.getStringToSignV2()
hm := hmac.New(sha1.New, []byte(r.credentials.SecretAccessKey))
stringToSign := getStringToSignV2(req)
hm := hmac.New(sha1.New, []byte(secretAccessKey))
hm.Write([]byte(stringToSign))
// Prepare auth header.
authHeader := new(bytes.Buffer)
authHeader.WriteString(fmt.Sprintf("AWS %s:", r.credentials.AccessKeyID))
authHeader.WriteString(fmt.Sprintf("%s %s:", signV2Algorithm, accessKeyID))
encoder := base64.NewEncoder(base64.StdEncoding, authHeader)
encoder.Write(hm.Sum(nil))
encoder.Close()
// Set Authorization header.
r.req.Header.Set("Authorization", authHeader.String())
req.Header.Set("Authorization", authHeader.String())
return &req
}
// From the Amazon docs:
@ -150,34 +158,34 @@ func (r *Request) SignV2() {
// Date + "\n" +
// CanonicalizedProtocolHeaders +
// CanonicalizedResource;
func (r *Request) getStringToSignV2() string {
func getStringToSignV2(req http.Request) string {
buf := new(bytes.Buffer)
// write standard headers.
r.writeDefaultHeaders(buf)
writeDefaultHeaders(buf, req)
// write canonicalized protocol headers if any.
r.writeCanonicalizedHeaders(buf)
writeCanonicalizedHeaders(buf, req)
// write canonicalized Query resources if any.
r.writeCanonicalizedResource(buf)
writeCanonicalizedResource(buf, req)
return buf.String()
}
// writeDefaultHeader - write all default necessary headers
func (r *Request) writeDefaultHeaders(buf *bytes.Buffer) {
buf.WriteString(r.req.Method)
func writeDefaultHeaders(buf *bytes.Buffer, req http.Request) {
buf.WriteString(req.Method)
buf.WriteByte('\n')
buf.WriteString(r.req.Header.Get("Content-MD5"))
buf.WriteString(req.Header.Get("Content-MD5"))
buf.WriteByte('\n')
buf.WriteString(r.req.Header.Get("Content-Type"))
buf.WriteString(req.Header.Get("Content-Type"))
buf.WriteByte('\n')
buf.WriteString(r.req.Header.Get("Date"))
buf.WriteString(req.Header.Get("Date"))
buf.WriteByte('\n')
}
// writeCanonicalizedHeaders - write canonicalized headers.
func (r *Request) writeCanonicalizedHeaders(buf *bytes.Buffer) {
func writeCanonicalizedHeaders(buf *bytes.Buffer, req http.Request) {
var protoHeaders []string
vals := make(map[string][]string)
for k, vv := range r.req.Header {
for k, vv := range req.Header {
// all the AMZ and GOOG headers should be lowercase
lk := strings.ToLower(k)
if strings.HasPrefix(lk, "x-amz") {
@ -237,8 +245,9 @@ var resourceList = []string{
// CanonicalizedResource = [ "/" + Bucket ] +
// <HTTP-Request-URI, from the protocol name up to the query string> +
// [ sub-resource, if present. For example "?acl", "?location", "?logging", or "?torrent"];
func (r *Request) writeCanonicalizedResource(buf *bytes.Buffer) error {
requestURL := r.req.URL
func writeCanonicalizedResource(buf *bytes.Buffer, req http.Request) error {
requestURL := req.URL
// Get encoded URL path.
path := encodeURL2Path(requestURL)
buf.WriteString(path)

View File

@ -19,7 +19,6 @@ package minio
import (
"bytes"
"encoding/hex"
"errors"
"net/http"
"sort"
"strconv"
@ -29,7 +28,7 @@ import (
// signature and API related constants.
const (
authHeader = "AWS4-HMAC-SHA256"
signV4Algorithm = "AWS4-HMAC-SHA256"
iso8601DateFormat = "20060102T150405Z"
yyyymmdd = "20060102"
)
@ -70,10 +69,10 @@ var ignoredHeaders = map[string]bool{
}
// getSigningKey hmac seed to calculate final signature
func getSigningKey(secret, region string, t time.Time) []byte {
func getSigningKey(secret, loc string, t time.Time) []byte {
date := sumHMAC([]byte("AWS4"+secret), []byte(t.Format(yyyymmdd)))
regionbytes := sumHMAC(date, []byte(region))
service := sumHMAC(regionbytes, []byte("s3"))
location := sumHMAC(date, []byte(loc))
service := sumHMAC(location, []byte("s3"))
signingKey := sumHMAC(service, []byte("aws4_request"))
return signingKey
}
@ -84,10 +83,10 @@ func getSignature(signingKey []byte, stringToSign string) string {
}
// getScope generate a string of a specific date, an AWS region, and a service
func getScope(region string, t time.Time) string {
func getScope(location string, t time.Time) string {
scope := strings.Join([]string{
t.Format(yyyymmdd),
region,
location,
"s3",
"aws4_request",
}, "/")
@ -95,25 +94,26 @@ func getScope(region string, t time.Time) string {
}
// getCredential generate a credential string
func getCredential(accessKeyID, region string, t time.Time) string {
scope := getScope(region, t)
func getCredential(accessKeyID, location string, t time.Time) string {
scope := getScope(location, t)
return accessKeyID + "/" + scope
}
// getHashedPayload get the hexadecimal value of the SHA256 hash of the request payload
func (r *Request) getHashedPayload() string {
if r.expires != 0 {
return "UNSIGNED-PAYLOAD"
func getHashedPayload(req http.Request) string {
hashedPayload := req.Header.Get("X-Amz-Content-Sha256")
if hashedPayload == "" {
// Presign does not have a payload, use S3 recommended value.
hashedPayload = "UNSIGNED-PAYLOAD"
}
hashedPayload := r.req.Header.Get("X-Amz-Content-Sha256")
return hashedPayload
}
// getCanonicalHeaders generate a list of request headers for signature.
func (r *Request) getCanonicalHeaders() string {
func getCanonicalHeaders(req http.Request) string {
var headers []string
vals := make(map[string][]string)
for k, vv := range r.req.Header {
for k, vv := range req.Header {
if _, ok := ignoredHeaders[http.CanonicalHeaderKey(k)]; ok {
continue // ignored header
}
@ -129,7 +129,7 @@ func (r *Request) getCanonicalHeaders() string {
buf.WriteByte(':')
switch {
case k == "host":
buf.WriteString(r.req.URL.Host)
buf.WriteString(req.URL.Host)
fallthrough
default:
for idx, v := range vals[k] {
@ -146,9 +146,9 @@ func (r *Request) getCanonicalHeaders() string {
// getSignedHeaders generate all signed request headers.
// i.e alphabetically sorted, semicolon-separated list of lowercase request header names
func (r *Request) getSignedHeaders() string {
func getSignedHeaders(req http.Request) string {
var headers []string
for k := range r.req.Header {
for k := range req.Header {
if _, ok := ignoredHeaders[http.CanonicalHeaderKey(k)]; ok {
continue // ignored header
}
@ -169,95 +169,114 @@ func (r *Request) getSignedHeaders() string {
// <SignedHeaders>\n
// <HashedPayload>
//
func (r *Request) getCanonicalRequest() string {
r.req.URL.RawQuery = strings.Replace(r.req.URL.Query().Encode(), "+", "%20", -1)
func getCanonicalRequest(req http.Request) string {
req.URL.RawQuery = strings.Replace(req.URL.Query().Encode(), "+", "%20", -1)
canonicalRequest := strings.Join([]string{
r.req.Method,
urlEncodePath(r.req.URL.Path),
r.req.URL.RawQuery,
r.getCanonicalHeaders(),
r.getSignedHeaders(),
r.getHashedPayload(),
req.Method,
urlEncodePath(req.URL.Path),
req.URL.RawQuery,
getCanonicalHeaders(req),
getSignedHeaders(req),
getHashedPayload(req),
}, "\n")
return canonicalRequest
}
// getStringToSign a string based on selected query values.
func (r *Request) getStringToSignV4(canonicalRequest string, t time.Time) string {
stringToSign := authHeader + "\n" + t.Format(iso8601DateFormat) + "\n"
stringToSign = stringToSign + getScope(r.bucketRegion, t) + "\n"
func getStringToSignV4(t time.Time, location, canonicalRequest string) string {
stringToSign := signV4Algorithm + "\n" + t.Format(iso8601DateFormat) + "\n"
stringToSign = stringToSign + getScope(location, t) + "\n"
stringToSign = stringToSign + hex.EncodeToString(sum256([]byte(canonicalRequest)))
return stringToSign
}
// PreSignV4 presign the request, in accordance with
// http://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-query-string-auth.html.
func (r *Request) PreSignV4() (string, error) {
if isAnonymousCredentials(*r.credentials) {
return "", errors.New("presigning cannot be done with anonymous credentials")
func PreSignV4(req http.Request, accessKeyID, secretAccessKey, location string, expires int64) *http.Request {
// presign is a noop for anonymous credentials.
if accessKeyID == "" || secretAccessKey == "" {
return nil
}
// Initial time.
t := time.Now().UTC()
// get credential string.
credential := getCredential(r.credentials.AccessKeyID, r.bucketRegion, t)
// get hmac signing key.
signingKey := getSigningKey(r.credentials.SecretAccessKey, r.bucketRegion, t)
credential := getCredential(accessKeyID, location, t)
// Get all signed headers.
signedHeaders := r.getSignedHeaders()
signedHeaders := getSignedHeaders(req)
query := r.req.URL.Query()
query.Set("X-Amz-Algorithm", authHeader)
// set URL query.
query := req.URL.Query()
query.Set("X-Amz-Algorithm", signV4Algorithm)
query.Set("X-Amz-Date", t.Format(iso8601DateFormat))
query.Set("X-Amz-Expires", strconv.FormatInt(r.expires, 10))
query.Set("X-Amz-Expires", strconv.FormatInt(expires, 10))
query.Set("X-Amz-SignedHeaders", signedHeaders)
query.Set("X-Amz-Credential", credential)
r.req.URL.RawQuery = query.Encode()
req.URL.RawQuery = query.Encode()
// Get canonical request.
canonicalRequest := getCanonicalRequest(req)
// Get string to sign from canonical request.
stringToSign := r.getStringToSignV4(r.getCanonicalRequest(), t)
stringToSign := getStringToSignV4(t, location, canonicalRequest)
// get hmac signing key.
signingKey := getSigningKey(secretAccessKey, location, t)
// calculate signature.
signature := getSignature(signingKey, stringToSign)
r.req.URL.RawQuery += "&X-Amz-Signature=" + signature
// Add signature header to RawQuery.
req.URL.RawQuery += "&X-Amz-Signature=" + signature
return r.req.URL.String(), nil
return &req
}
// PostPresignSignatureV4 - presigned signature for PostPolicy requests.
func (r *Request) PostPresignSignatureV4(policyBase64 string, t time.Time) string {
signingkey := getSigningKey(r.credentials.SecretAccessKey, r.bucketRegion, t)
func PostPresignSignatureV4(policyBase64 string, t time.Time, secretAccessKey, location string) string {
signingkey := getSigningKey(secretAccessKey, location, t)
signature := getSignature(signingkey, policyBase64)
return signature
}
// SignV4 sign the request before Do(), in accordance with
// http://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-authenticating-requests.html.
func (r *Request) SignV4() {
func SignV4(req http.Request, accessKeyID, secretAccessKey, location string) *http.Request {
// Initial time.
t := time.Now().UTC()
// Set x-amz-date.
r.Set("X-Amz-Date", t.Format(iso8601DateFormat))
// Get all signed headers.
signedHeaders := r.getSignedHeaders()
// Set x-amz-date.
req.Header.Set("X-Amz-Date", t.Format(iso8601DateFormat))
// Get canonical request.
canonicalRequest := getCanonicalRequest(req)
// Get string to sign from canonical request.
stringToSign := r.getStringToSignV4(r.getCanonicalRequest(), t)
stringToSign := getStringToSignV4(t, location, canonicalRequest)
// get hmac signing key.
signingKey := getSigningKey(secretAccessKey, location, t)
// get credential string.
credential := getCredential(r.credentials.AccessKeyID, r.bucketRegion, t)
// get hmac signing key.
signingKey := getSigningKey(r.credentials.SecretAccessKey, r.bucketRegion, t)
credential := getCredential(accessKeyID, location, t)
// Get all signed headers.
signedHeaders := getSignedHeaders(req)
// calculate signature.
signature := getSignature(signingKey, stringToSign)
// if regular request, construct the final authorization header.
parts := []string{
authHeader + " Credential=" + credential,
signV4Algorithm + " Credential=" + credential,
"SignedHeaders=" + signedHeaders,
"Signature=" + signature,
}
// Set authorization header.
auth := strings.Join(parts, ", ")
r.Set("Authorization", auth)
req.Header.Set("Authorization", auth)
return &req
}

View File

@ -1,169 +0,0 @@
/*
* Minio Go Library for Amazon S3 Compatible Cloud Storage (C) 2015 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package minio
import (
"encoding/base64"
"encoding/hex"
"io"
"net/http"
"net/url"
)
// Request - a http request.
type Request struct {
req *http.Request
credentials *clientCredentials
transport http.RoundTripper
bucketRegion string
expires int64
}
// requestMetadata - is container for all the values to make a request.
type requestMetadata struct {
// User supplied.
expires int64
userAgent string
bucketRegion string
credentials *clientCredentials
contentTransport http.RoundTripper
contentHeader http.Header
// Generated by our internal code.
contentBody io.ReadCloser
contentLength int64
contentSha256Bytes []byte
contentMD5Bytes []byte
}
// getTargetURL - construct a encoded URL for the requests.
func getTargetURL(endpoint *url.URL, bucketName, objectName string, queryValues url.Values) (*url.URL, error) {
var urlStr string
// If endpoint supports virtual host style use that always.
// Currently only S3 and Google Cloud Storage would support this.
if isVirtualHostSupported(endpoint) {
urlStr = endpoint.Scheme + "://" + bucketName + "." + endpoint.Host
urlStr = urlStr + "/" + urlEncodePath(objectName)
} else {
// If not fall back to using path style.
urlStr = endpoint.Scheme + "://" + endpoint.Host + "/" + bucketName
if objectName != "" {
urlStr = urlStr + "/" + urlEncodePath(objectName)
}
}
// If there are any query values, add them to the end.
if len(queryValues) > 0 {
urlStr = urlStr + "?" + queryValues.Encode()
}
u, err := url.Parse(urlStr)
if err != nil {
return nil, err
}
return u, nil
}
// Do - start the request.
func (r *Request) Do() (resp *http.Response, err error) {
// if not an anonymous request, calculate relevant signature.
if !isAnonymousCredentials(*r.credentials) {
if r.credentials.Signature.isV2() {
// if signature version '2' requested, use that.
r.SignV2()
}
if r.credentials.Signature.isV4() || r.credentials.Signature.isLatest() {
// Not a presigned request, set behavior to default.
r.SignV4()
}
}
// Use custom transport if any.
transport := http.DefaultTransport
if r.transport != nil {
transport = r.transport
}
client := &http.Client{Transport: transport}
return client.Do(r.req)
}
// Set - set additional headers if any.
func (r *Request) Set(key, value string) {
r.req.Header.Set(key, value)
}
// Get - get header values.
func (r *Request) Get(key string) string {
return r.req.Header.Get(key)
}
// newRequest - provides a new instance of *Request*.
func newRequest(method string, targetURL *url.URL, metadata requestMetadata) (*Request, error) {
if method == "" {
method = "POST"
}
urlStr := targetURL.String()
// get a new HTTP request, for the requested method.
req, err := http.NewRequest(method, urlStr, nil)
if err != nil {
return nil, err
}
// Set content body if available.
if metadata.contentBody != nil {
req.Body = metadata.contentBody
}
// save for subsequent use.
r := new(Request)
r.req = req
r.credentials = metadata.credentials
r.bucketRegion = metadata.bucketRegion
r.transport = metadata.contentTransport
// If presigned request, return.
if metadata.expires != 0 {
r.expires = metadata.expires
return r, nil
}
// set UserAgent for the request.
r.Set("User-Agent", metadata.userAgent)
// Set all headers.
for k, v := range metadata.contentHeader {
r.Set(k, v[0])
}
// set incoming content-length.
if metadata.contentLength > 0 {
r.req.ContentLength = metadata.contentLength
}
// set sha256 sum for signature calculation only with signature version '4'.
if r.credentials.Signature.isV4() || r.credentials.Signature.isLatest() {
r.Set("X-Amz-Content-Sha256", hex.EncodeToString(sum256([]byte{})))
if metadata.contentSha256Bytes != nil {
r.Set("X-Amz-Content-Sha256", hex.EncodeToString(metadata.contentSha256Bytes))
}
}
// set md5Sum for content protection.
if metadata.contentMD5Bytes != nil {
r.Set("Content-MD5", base64.StdEncoding.EncodeToString(metadata.contentMD5Bytes))
}
// return request.
return r, nil
}

View File

@ -119,14 +119,6 @@ func closeResponse(resp *http.Response) {
}
}
// isAnonymousCredentials - True if config doesn't have access and secret keys.
func isAnonymousCredentials(c clientCredentials) bool {
if c.AccessKeyID != "" && c.SecretAccessKey != "" {
return false
}
return true
}
// isVirtualHostSupported - verify if host supports virtual hosted style.
// Currently only Amazon S3 and Google Cloud Storage would support this.
func isVirtualHostSupported(endpointURL *url.URL) bool {