diff --git a/MAINTAINERS.md b/MAINTAINERS.md
index 0e03854..6dbef62 100644
--- a/MAINTAINERS.md
+++ b/MAINTAINERS.md
@@ -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`.
diff --git a/README.md b/README.md
index e907bdd..747eecb 100644
--- a/README.md
+++ b/README.md
@@ -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)
diff --git a/api-definitions.go b/api-definitions.go
index deda622..7667645 100644
--- a/api-definitions.go
+++ b/api-definitions.go
@@ -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.
diff --git a/api-error-response.go b/api-error-response.go
index f1e590a..9672fc3 100644
--- a/api-error-response.go
+++ b/api-error-response.go
@@ -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
}
diff --git a/api-get.go b/api-get.go
index f810f5a..a86e26f 100644
--- a/api-get.go
+++ b/api-get.go
@@ -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"),
diff --git a/api-list.go b/api-list.go
index 0141aef..ab06f6d 100644
--- a/api-list.go
+++ b/api-list.go
@@ -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 {
diff --git a/api-presigned.go b/api-presigned.go
index 0d93d39..d466236 100644
--- a/api-presigned.go
+++ b/api-presigned.go
@@ -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
}
diff --git a/api-put-bucket.go b/api-put-bucket.go
index b0f00c7..65ffebf 100644
--- a/api-put-bucket.go
+++ b/api-put-bucket.go
@@ -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
-}
diff --git a/api-put-object.go b/api-put-object.go
index 57c09cd..4159544 100644
--- a/api-put-object.go
+++ b/api-put-object.go
@@ -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 {
diff --git a/api-remove.go b/api-remove.go
index 66aadf6..ee3c613 100644
--- a/api-remove.go
+++ b/api-remove.go
@@ -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
}
diff --git a/api-stat.go b/api-stat.go
index 1331b9d..29bd83f 100644
--- a/api-stat.go
+++ b/api-stat.go
@@ -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
-}
diff --git a/api.go b/api.go
index 1a2c2d9..293a406 100644
--- a/api.go
+++ b/api.go
@@ -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)
diff --git a/api_functional_test.go b/api_functional_test.go
index 70dd753..2351d62 100644
--- a/api_functional_test.go
+++ b/api_functional_test.go
@@ -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:")
}
diff --git a/api_handlers_test.go b/api_handlers_test.go
deleted file mode 100644
index dfd6fc6..0000000
--- a/api_handlers_test.go
+++ /dev/null
@@ -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("bucket2015-05-20T23:05:09.230Zminiominio")
- 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("75aa57f09aa0c8caeab4f8c24e99d10f8e7faeebf76c078efc7c6caea54ba06aCustomersName@amazon.com75aa57f09aa0c8caeab4f8c24e99d10f8e7faeebf76c078efc7c6caea54ba06aCustomersName@amazon.comFULL_CONTROL")
- w.Header().Set("Content-Length", strconv.Itoa(len(response)))
- w.Write(response)
- return
- }
- fallthrough
- case path == h.resource:
- response := []byte("\"259d04a13802ae09c7e41be50ccc6baa\"object2015-05-21T18:24:21.097Z22061miniominioSTANDARDfalse1000testbucket")
- 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("example-bucketobjectXXBsb2FkIElEIGZvciBlbHZpbmcncyVcdS1tb3ZpZS5tMnRzEEEwbG9hZA")
- 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("example-bucketexample-objectXXBsb2FkIElEIGZvciBlbHZpbmcncyVcdS1tb3ZpZS5tMnRzEEEwbG9hZAarn:aws:iam::111122223333:user/some-user-11116a31-17b5-4fb7-9df5-b288870f11xxumat-user-11116a31-17b5-4fb7-9df5-b288870f11xx75aa57f09aa0c8caeab4f8c24e99d10f8e7faeebf76c078efc7c6caea54ba06asomeNameSTANDARD132true22010-11-10T20:48:34.000Z\"7778aef83f66abc1fa1e8477f296d394\"1048576032010-11-10T20:48:33.000Z\"aaaa18db4cc2f85cedef654fccc4a4x8\"10485760")
- 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)
- }
-}
diff --git a/api_private_test.go b/api_private_test.go
index c8d4bcc..768b55c 100644
--- a/api_private_test.go
+++ b/api_private_test.go
@@ -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")
}
}
diff --git a/api_public_test.go b/api_public_test.go
deleted file mode 100644
index 7d52dd3..0000000
--- a/api_public_test.go
+++ /dev/null
@@ -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("AccessDenied
Access Deniedmybucketmyphoto.jpgF19772218238A85AGuWkjyviSiGHizehqpmsD1ndz5NClSP19DOT+s2mv7gXGQ8/X1lhbDGiIJEXpGFD")
- 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")
- }
-}
diff --git a/bucket-cache.go b/bucket-cache.go
index ec77c84..21ce872 100644
--- a/bucket-cache.go
+++ b/bucket-cache.go
@@ -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
}
diff --git a/examples/play/listbuckets.go b/examples/play/listbuckets.go
index f299df3..b5e505c 100644
--- a/examples/play/listbuckets.go
+++ b/examples/play/listbuckets.go
@@ -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)
}
}
diff --git a/examples/play/presignedpostpolicy.go b/examples/play/presignedpostpolicy.go
index 5e67f27..2d8fea7 100644
--- a/examples/play/presignedpostpolicy.go
+++ b/examples/play/presignedpostpolicy.go
@@ -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")
}
diff --git a/examples/s3/getobject.go b/examples/s3/getobject.go
index 996c157..ac4c28f 100644
--- a/examples/s3/getobject.go
+++ b/examples/s3/getobject.go
@@ -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)
}
diff --git a/examples/s3/listbuckets.go b/examples/s3/listbuckets.go
index 5d08d23..d0b0608 100644
--- a/examples/s3/listbuckets.go
+++ b/examples/s3/listbuckets.go
@@ -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)
}
}
diff --git a/examples/s3/presignedpostpolicy.go b/examples/s3/presignedpostpolicy.go
index 8bf68fc..b5fde57 100644
--- a/examples/s3/presignedpostpolicy.go
+++ b/examples/s3/presignedpostpolicy.go
@@ -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")
}
diff --git a/minio.go b/minio.go
deleted file mode 100644
index eeeec31..0000000
--- a/minio.go
+++ /dev/null
@@ -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
-)
diff --git a/request-signature-v2.go b/request-signature-v2.go
index 22f3ced..956b04f 100644
--- a/request-signature-v2.go
+++ b/request-signature-v2.go
@@ -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 =
// 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 ] +
// +
// [ 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)
diff --git a/request-signature-v4.go b/request-signature-v4.go
index 02d50ef..515d8ab 100644
--- a/request-signature-v4.go
+++ b/request-signature-v4.go
@@ -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 {
// \n
//
//
-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
}
diff --git a/request.go b/request.go
deleted file mode 100644
index 0459686..0000000
--- a/request.go
+++ /dev/null
@@ -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
-}
diff --git a/utils.go b/utils.go
index 3c1ed69..67ae4cf 100644
--- a/utils.go
+++ b/utils.go
@@ -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 {