mirror of https://github.com/octoleo/restic.git
907 lines
27 KiB
Go
907 lines
27 KiB
Go
/*
|
|
* 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/base64"
|
|
"encoding/json"
|
|
"encoding/xml"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
separator = "/"
|
|
)
|
|
|
|
// apiCore container to hold unexported internal functions
|
|
type apiCore struct {
|
|
config *Config
|
|
}
|
|
|
|
// closeResp close non nil response with any response Body
|
|
func closeResp(resp *http.Response) {
|
|
if resp != nil && resp.Body != nil {
|
|
resp.Body.Close()
|
|
}
|
|
}
|
|
|
|
// putBucketRequest wrapper creates a new putBucket request
|
|
func (a apiCore) putBucketRequest(bucket, acl, location string) (*request, error) {
|
|
var r *request
|
|
var err error
|
|
op := &operation{
|
|
HTTPServer: a.config.Endpoint,
|
|
HTTPMethod: "PUT",
|
|
HTTPPath: separator + bucket,
|
|
}
|
|
var createBucketConfigBuffer *bytes.Reader
|
|
// If location is set use it and create proper bucket configuration
|
|
switch {
|
|
case location != "":
|
|
createBucketConfig := new(createBucketConfiguration)
|
|
createBucketConfig.Location = location
|
|
var createBucketConfigBytes []byte
|
|
switch {
|
|
case a.config.AcceptType == "application/xml":
|
|
createBucketConfigBytes, err = xml.Marshal(createBucketConfig)
|
|
case a.config.AcceptType == "application/json":
|
|
createBucketConfigBytes, err = json.Marshal(createBucketConfig)
|
|
default:
|
|
createBucketConfigBytes, err = xml.Marshal(createBucketConfig)
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
createBucketConfigBuffer = bytes.NewReader(createBucketConfigBytes)
|
|
}
|
|
switch {
|
|
case createBucketConfigBuffer == nil:
|
|
r, err = newRequest(op, a.config, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
default:
|
|
r, err = newRequest(op, a.config, createBucketConfigBuffer)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
r.req.ContentLength = int64(createBucketConfigBuffer.Len())
|
|
}
|
|
// by default bucket is private
|
|
switch {
|
|
case acl != "":
|
|
r.Set("x-amz-acl", acl)
|
|
default:
|
|
r.Set("x-amz-acl", "private")
|
|
}
|
|
|
|
return r, nil
|
|
}
|
|
|
|
/// Bucket Write Operations
|
|
|
|
// putBucket create a new bucket
|
|
//
|
|
// Requires valid AWS Access Key ID to authenticate requests
|
|
// Anonymous requests are never allowed to create buckets
|
|
//
|
|
// optional arguments are acl and location - by default all buckets are created
|
|
// with ``private`` acl and location set to US Standard if one wishes to set
|
|
// different ACLs and Location one can set them properly.
|
|
//
|
|
// ACL valid values
|
|
// ------------------
|
|
// private - owner gets full access [DEFAULT]
|
|
// 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
|
|
// ------------------
|
|
//
|
|
// Location valid values
|
|
// ------------------
|
|
// [ us-west-1 | us-west-2 | eu-west-1 | eu-central-1 | ap-southeast-1 | ap-northeast-1 | ap-southeast-2 | sa-east-1 ]
|
|
//
|
|
// Default - US standard
|
|
func (a apiCore) putBucket(bucket, acl, location string) error {
|
|
req, err := a.putBucketRequest(bucket, acl, location)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
resp, err := req.Do()
|
|
defer closeResp(resp)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if resp != nil {
|
|
if resp.StatusCode != http.StatusOK {
|
|
return BodyToErrorResponse(resp.Body, a.config.AcceptType)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// putBucketRequestACL wrapper creates a new putBucketACL request
|
|
func (a apiCore) putBucketACLRequest(bucket, acl string) (*request, error) {
|
|
op := &operation{
|
|
HTTPServer: a.config.Endpoint,
|
|
HTTPMethod: "PUT",
|
|
HTTPPath: separator + bucket + "?acl",
|
|
}
|
|
req, err := newRequest(op, a.config, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req.Set("x-amz-acl", acl)
|
|
return req, nil
|
|
}
|
|
|
|
// putBucketACL set the permissions on an existing bucket using Canned ACL's
|
|
func (a apiCore) putBucketACL(bucket, acl string) error {
|
|
req, err := a.putBucketACLRequest(bucket, acl)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
resp, err := req.Do()
|
|
defer closeResp(resp)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if resp != nil {
|
|
if resp.StatusCode != http.StatusOK {
|
|
return BodyToErrorResponse(resp.Body, a.config.AcceptType)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// getBucketACLRequest wrapper creates a new getBucketACL request
|
|
func (a apiCore) getBucketACLRequest(bucket string) (*request, error) {
|
|
op := &operation{
|
|
HTTPServer: a.config.Endpoint,
|
|
HTTPMethod: "GET",
|
|
HTTPPath: separator + bucket + "?acl",
|
|
}
|
|
req, err := newRequest(op, a.config, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return req, nil
|
|
}
|
|
|
|
// getBucketACL get the acl information on an existing bucket
|
|
func (a apiCore) getBucketACL(bucket string) (accessControlPolicy, error) {
|
|
req, err := a.getBucketACLRequest(bucket)
|
|
if err != nil {
|
|
return accessControlPolicy{}, err
|
|
}
|
|
resp, err := req.Do()
|
|
defer closeResp(resp)
|
|
if err != nil {
|
|
return accessControlPolicy{}, err
|
|
}
|
|
if resp != nil {
|
|
if resp.StatusCode != http.StatusOK {
|
|
return accessControlPolicy{}, BodyToErrorResponse(resp.Body, a.config.AcceptType)
|
|
}
|
|
}
|
|
policy := accessControlPolicy{}
|
|
err = acceptTypeDecoder(resp.Body, a.config.AcceptType, &policy)
|
|
if err != nil {
|
|
return accessControlPolicy{}, err
|
|
}
|
|
// In-case of google private bucket policy doesn't have any Grant list
|
|
if a.config.Region == "google" {
|
|
return policy, nil
|
|
}
|
|
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.",
|
|
Resource: separator + bucket,
|
|
RequestID: resp.Header.Get("x-amz-request-id"),
|
|
HostID: resp.Header.Get("x-amz-id-2"),
|
|
}
|
|
return accessControlPolicy{}, errorResponse
|
|
}
|
|
return policy, nil
|
|
}
|
|
|
|
// getBucketLocationRequest wrapper creates a new getBucketLocation request
|
|
func (a apiCore) getBucketLocationRequest(bucket string) (*request, error) {
|
|
op := &operation{
|
|
HTTPServer: a.config.Endpoint,
|
|
HTTPMethod: "GET",
|
|
HTTPPath: separator + bucket + "?location",
|
|
}
|
|
req, err := newRequest(op, a.config, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return req, nil
|
|
}
|
|
|
|
// getBucketLocation uses location subresource to return a bucket's region
|
|
func (a apiCore) getBucketLocation(bucket string) (string, error) {
|
|
req, err := a.getBucketLocationRequest(bucket)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
resp, err := req.Do()
|
|
defer closeResp(resp)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if resp != nil {
|
|
if resp.StatusCode != http.StatusOK {
|
|
return "", BodyToErrorResponse(resp.Body, a.config.AcceptType)
|
|
}
|
|
}
|
|
var locationConstraint string
|
|
err = acceptTypeDecoder(resp.Body, a.config.AcceptType, &locationConstraint)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return locationConstraint, nil
|
|
}
|
|
|
|
// listObjectsRequest wrapper creates a new listObjects request
|
|
func (a apiCore) listObjectsRequest(bucket, marker, prefix, delimiter string, maxkeys int) (*request, error) {
|
|
// resourceQuery - get resources properly escaped and lined up before using them in http request
|
|
resourceQuery := func() (*string, error) {
|
|
switch {
|
|
case marker != "":
|
|
marker = fmt.Sprintf("&marker=%s", getURLEncodedPath(marker))
|
|
fallthrough
|
|
case prefix != "":
|
|
prefix = fmt.Sprintf("&prefix=%s", getURLEncodedPath(prefix))
|
|
fallthrough
|
|
case delimiter != "":
|
|
delimiter = fmt.Sprintf("&delimiter=%s", delimiter)
|
|
}
|
|
query := fmt.Sprintf("?max-keys=%d", maxkeys) + marker + prefix + delimiter
|
|
return &query, nil
|
|
}
|
|
query, err := resourceQuery()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
op := &operation{
|
|
HTTPServer: a.config.Endpoint,
|
|
HTTPMethod: "GET",
|
|
HTTPPath: separator + bucket + *query,
|
|
}
|
|
r, err := newRequest(op, a.config, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return r, nil
|
|
}
|
|
|
|
/// Bucket Read Operations
|
|
|
|
// listObjects - (List Objects) - List some or all (up to 1000) of the objects in a bucket.
|
|
//
|
|
// You can use the request parameters as selection criteria to return a subset of the objects in a bucket.
|
|
// request paramters :-
|
|
// ---------
|
|
// ?marker - Specifies the key to start with when listing objects in a bucket.
|
|
// ?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 apiCore) listObjects(bucket, marker, prefix, delimiter string, maxkeys int) (listBucketResult, error) {
|
|
if err := invalidBucketError(bucket); err != nil {
|
|
return listBucketResult{}, err
|
|
}
|
|
req, err := a.listObjectsRequest(bucket, marker, prefix, delimiter, maxkeys)
|
|
if err != nil {
|
|
return listBucketResult{}, err
|
|
}
|
|
resp, err := req.Do()
|
|
defer closeResp(resp)
|
|
if err != nil {
|
|
return listBucketResult{}, err
|
|
}
|
|
if resp != nil {
|
|
if resp.StatusCode != http.StatusOK {
|
|
return listBucketResult{}, BodyToErrorResponse(resp.Body, a.config.AcceptType)
|
|
}
|
|
}
|
|
listBucketResult := listBucketResult{}
|
|
err = acceptTypeDecoder(resp.Body, a.config.AcceptType, &listBucketResult)
|
|
if err != nil {
|
|
return listBucketResult, err
|
|
}
|
|
// close body while returning, along with any error
|
|
return listBucketResult, nil
|
|
}
|
|
|
|
// headBucketRequest wrapper creates a new headBucket request
|
|
func (a apiCore) headBucketRequest(bucket string) (*request, error) {
|
|
op := &operation{
|
|
HTTPServer: a.config.Endpoint,
|
|
HTTPMethod: "HEAD",
|
|
HTTPPath: separator + bucket,
|
|
}
|
|
return newRequest(op, a.config, nil)
|
|
}
|
|
|
|
// headBucket useful to determine if a bucket exists and you have permission to access it.
|
|
func (a apiCore) headBucket(bucket string) error {
|
|
if err := invalidBucketError(bucket); err != nil {
|
|
return err
|
|
}
|
|
req, err := a.headBucketRequest(bucket)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
resp, err := req.Do()
|
|
defer closeResp(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.",
|
|
Resource: separator + bucket,
|
|
RequestID: resp.Header.Get("x-amz-request-id"),
|
|
HostID: resp.Header.Get("x-amz-id-2"),
|
|
}
|
|
case http.StatusForbidden:
|
|
errorResponse = ErrorResponse{
|
|
Code: "AccessDenied",
|
|
Message: "Access Denied.",
|
|
Resource: separator + bucket,
|
|
RequestID: resp.Header.Get("x-amz-request-id"),
|
|
HostID: resp.Header.Get("x-amz-id-2"),
|
|
}
|
|
default:
|
|
errorResponse = ErrorResponse{
|
|
Code: resp.Status,
|
|
Message: resp.Status,
|
|
Resource: separator + bucket,
|
|
RequestID: resp.Header.Get("x-amz-request-id"),
|
|
HostID: resp.Header.Get("x-amz-id-2"),
|
|
}
|
|
}
|
|
return errorResponse
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// deleteBucketRequest wrapper creates a new deleteBucket request
|
|
func (a apiCore) deleteBucketRequest(bucket string) (*request, error) {
|
|
op := &operation{
|
|
HTTPServer: a.config.Endpoint,
|
|
HTTPMethod: "DELETE",
|
|
HTTPPath: separator + bucket,
|
|
}
|
|
return newRequest(op, a.config, nil)
|
|
}
|
|
|
|
// deleteBucket deletes the bucket named in the URI
|
|
//
|
|
// NOTE: -
|
|
// All objects (including all object versions and delete markers)
|
|
// in the bucket must be deleted before successfully attempting this request
|
|
func (a apiCore) deleteBucket(bucket string) error {
|
|
if err := invalidBucketError(bucket); err != nil {
|
|
return err
|
|
}
|
|
req, err := a.deleteBucketRequest(bucket)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
resp, err := req.Do()
|
|
defer closeResp(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.",
|
|
Resource: separator + bucket,
|
|
RequestID: resp.Header.Get("x-amz-request-id"),
|
|
HostID: resp.Header.Get("x-amz-id-2"),
|
|
}
|
|
case http.StatusForbidden:
|
|
errorResponse = ErrorResponse{
|
|
Code: "AccessDenied",
|
|
Message: "Access Denied.",
|
|
Resource: separator + bucket,
|
|
RequestID: resp.Header.Get("x-amz-request-id"),
|
|
HostID: resp.Header.Get("x-amz-id-2"),
|
|
}
|
|
case http.StatusConflict:
|
|
errorResponse = ErrorResponse{
|
|
Code: "Conflict",
|
|
Message: "Bucket not empty.",
|
|
Resource: separator + bucket,
|
|
RequestID: resp.Header.Get("x-amz-request-id"),
|
|
HostID: resp.Header.Get("x-amz-id-2"),
|
|
}
|
|
default:
|
|
errorResponse = ErrorResponse{
|
|
Code: resp.Status,
|
|
Message: resp.Status,
|
|
Resource: separator + bucket,
|
|
RequestID: resp.Header.Get("x-amz-request-id"),
|
|
HostID: resp.Header.Get("x-amz-id-2"),
|
|
}
|
|
}
|
|
return errorResponse
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
/// Object Read/Write/Stat Operations
|
|
|
|
func (a apiCore) putObjectUnAuthenticatedRequest(bucket, object, contentType string, size int64, body io.Reader) (*request, error) {
|
|
if strings.TrimSpace(contentType) == "" {
|
|
contentType = "application/octet-stream"
|
|
}
|
|
op := &operation{
|
|
HTTPServer: a.config.Endpoint,
|
|
HTTPMethod: "PUT",
|
|
HTTPPath: separator + bucket + separator + object,
|
|
}
|
|
r, err := newUnauthenticatedRequest(op, a.config, body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// Content-MD5 is not set consciously
|
|
r.Set("Content-Type", contentType)
|
|
r.req.ContentLength = size
|
|
return r, nil
|
|
}
|
|
|
|
// putObjectUnAuthenticated - add an object to a bucket
|
|
// NOTE: You must have WRITE permissions on a bucket to add an object to it.
|
|
func (a apiCore) putObjectUnAuthenticated(bucket, object, contentType string, size int64, body io.Reader) (ObjectStat, error) {
|
|
req, err := a.putObjectUnAuthenticatedRequest(bucket, object, contentType, size, body)
|
|
if err != nil {
|
|
return ObjectStat{}, err
|
|
}
|
|
resp, err := req.Do()
|
|
defer closeResp(resp)
|
|
if err != nil {
|
|
return ObjectStat{}, err
|
|
}
|
|
if resp != nil {
|
|
if resp.StatusCode != http.StatusOK {
|
|
return ObjectStat{}, BodyToErrorResponse(resp.Body, a.config.AcceptType)
|
|
}
|
|
}
|
|
var metadata ObjectStat
|
|
metadata.ETag = strings.Trim(resp.Header.Get("ETag"), "\"") // trim off the odd double quotes
|
|
return metadata, nil
|
|
}
|
|
|
|
// putObjectRequest wrapper creates a new PutObject request
|
|
func (a apiCore) putObjectRequest(bucket, object, contentType string, md5SumBytes []byte, size int64, body io.ReadSeeker) (*request, error) {
|
|
if strings.TrimSpace(contentType) == "" {
|
|
contentType = "application/octet-stream"
|
|
}
|
|
op := &operation{
|
|
HTTPServer: a.config.Endpoint,
|
|
HTTPMethod: "PUT",
|
|
HTTPPath: separator + bucket + separator + object,
|
|
}
|
|
r, err := newRequest(op, a.config, body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// set Content-MD5 as base64 encoded md5
|
|
if md5SumBytes != nil {
|
|
r.Set("Content-MD5", base64.StdEncoding.EncodeToString(md5SumBytes))
|
|
}
|
|
r.Set("Content-Type", contentType)
|
|
r.req.ContentLength = size
|
|
return r, 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 apiCore) putObject(bucket, object, contentType string, md5SumBytes []byte, size int64, body io.ReadSeeker) (ObjectStat, error) {
|
|
req, err := a.putObjectRequest(bucket, object, contentType, md5SumBytes, size, body)
|
|
if err != nil {
|
|
return ObjectStat{}, err
|
|
}
|
|
resp, err := req.Do()
|
|
defer closeResp(resp)
|
|
if err != nil {
|
|
return ObjectStat{}, err
|
|
}
|
|
if resp != nil {
|
|
if resp.StatusCode != http.StatusOK {
|
|
return ObjectStat{}, BodyToErrorResponse(resp.Body, a.config.AcceptType)
|
|
}
|
|
}
|
|
var metadata ObjectStat
|
|
metadata.ETag = strings.Trim(resp.Header.Get("ETag"), "\"") // trim off the odd double quotes
|
|
return metadata, nil
|
|
}
|
|
|
|
func (a apiCore) presignedPostPolicy(p *PostPolicy) map[string]string {
|
|
t := time.Now().UTC()
|
|
r := new(request)
|
|
r.config = a.config
|
|
if r.config.Signature.isV2() {
|
|
policyBase64 := p.base64()
|
|
p.formData["policy"] = policyBase64
|
|
p.formData["AWSAccessKeyId"] = r.config.AccessKeyID
|
|
p.formData["signature"] = r.PostPresignSignatureV2(policyBase64)
|
|
return p.formData
|
|
}
|
|
credential := getCredential(r.config.AccessKeyID, r.config.Region, t)
|
|
p.addNewPolicy(policy{"eq", "$x-amz-date", t.Format(iso8601DateFormat)})
|
|
p.addNewPolicy(policy{"eq", "$x-amz-algorithm", authHeader})
|
|
p.addNewPolicy(policy{"eq", "$x-amz-credential", credential})
|
|
|
|
policyBase64 := p.base64()
|
|
p.formData["policy"] = policyBase64
|
|
p.formData["x-amz-algorithm"] = authHeader
|
|
p.formData["x-amz-credential"] = credential
|
|
p.formData["x-amz-date"] = t.Format(iso8601DateFormat)
|
|
p.formData["x-amz-signature"] = r.PostPresignSignatureV4(policyBase64, t)
|
|
return p.formData
|
|
}
|
|
|
|
func (a apiCore) presignedPutObject(bucket, object string, expires int64) (string, error) {
|
|
op := &operation{
|
|
HTTPServer: a.config.Endpoint,
|
|
HTTPMethod: "PUT",
|
|
HTTPPath: separator + bucket + separator + object,
|
|
}
|
|
r, err := newPresignedRequest(op, a.config, expires)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if r.config.Signature.isV2() {
|
|
return r.PreSignV2()
|
|
}
|
|
return r.PreSignV4()
|
|
}
|
|
|
|
func (a apiCore) presignedGetObjectRequest(bucket, object string, expires, offset, length int64) (*request, error) {
|
|
op := &operation{
|
|
HTTPServer: a.config.Endpoint,
|
|
HTTPMethod: "GET",
|
|
HTTPPath: separator + bucket + separator + object,
|
|
}
|
|
r, err := newPresignedRequest(op, a.config, expires)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
switch {
|
|
case length > 0 && offset > 0:
|
|
r.Set("Range", fmt.Sprintf("bytes=%d-%d", offset, offset+length-1))
|
|
case offset > 0 && length == 0:
|
|
r.Set("Range", fmt.Sprintf("bytes=%d-", offset))
|
|
case length > 0 && offset == 0:
|
|
r.Set("Range", fmt.Sprintf("bytes=-%d", length))
|
|
}
|
|
return r, nil
|
|
}
|
|
|
|
func (a apiCore) presignedGetObject(bucket, object string, expires, offset, length int64) (string, error) {
|
|
if err := invalidArgumentError(object); err != nil {
|
|
return "", err
|
|
}
|
|
r, err := a.presignedGetObjectRequest(bucket, object, expires, offset, length)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if r.config.Signature.isV2() {
|
|
return r.PreSignV2()
|
|
}
|
|
return r.PreSignV4()
|
|
}
|
|
|
|
// getObjectRequest wrapper creates a new getObject request
|
|
func (a apiCore) getObjectRequest(bucket, object string, offset, length int64) (*request, error) {
|
|
op := &operation{
|
|
HTTPServer: a.config.Endpoint,
|
|
HTTPMethod: "GET",
|
|
HTTPPath: separator + bucket + separator + object,
|
|
}
|
|
r, err := newRequest(op, a.config, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
switch {
|
|
case length > 0 && offset >= 0:
|
|
r.Set("Range", fmt.Sprintf("bytes=%d-%d", offset, offset+length-1))
|
|
case offset > 0 && length == 0:
|
|
r.Set("Range", fmt.Sprintf("bytes=%d-", offset))
|
|
// The final length bytes
|
|
case length < 0 && offset == 0:
|
|
r.Set("Range", fmt.Sprintf("bytes=%d", length))
|
|
}
|
|
return r, nil
|
|
}
|
|
|
|
// getObject - retrieve object from Object Storage
|
|
//
|
|
// Additionally this function also takes range arguments to download the specified
|
|
// range bytes of an object. 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 apiCore) getObject(bucket, object string, offset, length int64) (io.ReadCloser, ObjectStat, error) {
|
|
if err := invalidArgumentError(object); err != nil {
|
|
return nil, ObjectStat{}, err
|
|
}
|
|
req, err := a.getObjectRequest(bucket, object, offset, length)
|
|
if err != nil {
|
|
return nil, ObjectStat{}, err
|
|
}
|
|
resp, err := req.Do()
|
|
if err != nil {
|
|
return nil, ObjectStat{}, err
|
|
}
|
|
if resp != nil {
|
|
switch resp.StatusCode {
|
|
case http.StatusOK:
|
|
case http.StatusPartialContent:
|
|
default:
|
|
return nil, ObjectStat{}, BodyToErrorResponse(resp.Body, a.config.AcceptType)
|
|
}
|
|
}
|
|
md5sum := strings.Trim(resp.Header.Get("ETag"), "\"") // trim off the odd double quotes
|
|
date, err := time.Parse(http.TimeFormat, resp.Header.Get("Last-Modified"))
|
|
if err != nil {
|
|
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.",
|
|
RequestID: resp.Header.Get("x-amz-request-id"),
|
|
HostID: resp.Header.Get("x-amz-id-2"),
|
|
}
|
|
}
|
|
contentType := strings.TrimSpace(resp.Header.Get("Content-Type"))
|
|
if contentType == "" {
|
|
contentType = "application/octet-stream"
|
|
}
|
|
var objectstat ObjectStat
|
|
objectstat.ETag = md5sum
|
|
objectstat.Key = object
|
|
objectstat.Size = resp.ContentLength
|
|
objectstat.LastModified = date
|
|
objectstat.ContentType = contentType
|
|
|
|
// do not close body here, caller will close
|
|
return resp.Body, objectstat, nil
|
|
}
|
|
|
|
// deleteObjectRequest wrapper creates a new deleteObject request
|
|
func (a apiCore) deleteObjectRequest(bucket, object string) (*request, error) {
|
|
op := &operation{
|
|
HTTPServer: a.config.Endpoint,
|
|
HTTPMethod: "DELETE",
|
|
HTTPPath: separator + bucket + separator + object,
|
|
}
|
|
return newRequest(op, a.config, nil)
|
|
}
|
|
|
|
// deleteObject deletes a given object from a bucket
|
|
func (a apiCore) deleteObject(bucket, object string) error {
|
|
if err := invalidBucketError(bucket); err != nil {
|
|
return err
|
|
}
|
|
if err := invalidArgumentError(object); err != nil {
|
|
return err
|
|
}
|
|
req, err := a.deleteObjectRequest(bucket, object)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
resp, err := req.Do()
|
|
defer closeResp(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: "NoSuchKey",
|
|
Message: "The specified key does not exist.",
|
|
Resource: separator + bucket + separator + object,
|
|
RequestID: resp.Header.Get("x-amz-request-id"),
|
|
HostID: resp.Header.Get("x-amz-id-2"),
|
|
}
|
|
case http.StatusForbidden:
|
|
errorResponse = ErrorResponse{
|
|
Code: "AccessDenied",
|
|
Message: "Access Denied.",
|
|
Resource: separator + bucket + separator + object,
|
|
RequestID: resp.Header.Get("x-amz-request-id"),
|
|
HostID: resp.Header.Get("x-amz-id-2"),
|
|
}
|
|
default:
|
|
errorResponse = ErrorResponse{
|
|
Code: resp.Status,
|
|
Message: resp.Status,
|
|
Resource: separator + bucket + separator + object,
|
|
RequestID: resp.Header.Get("x-amz-request-id"),
|
|
HostID: resp.Header.Get("x-amz-id-2"),
|
|
}
|
|
}
|
|
return errorResponse
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// headObjectRequest wrapper creates a new headObject request
|
|
func (a apiCore) headObjectRequest(bucket, object string) (*request, error) {
|
|
op := &operation{
|
|
HTTPServer: a.config.Endpoint,
|
|
HTTPMethod: "HEAD",
|
|
HTTPPath: separator + bucket + separator + object,
|
|
}
|
|
return newRequest(op, a.config, nil)
|
|
}
|
|
|
|
// headObject retrieves metadata from an object without returning the object itself
|
|
func (a apiCore) headObject(bucket, object string) (ObjectStat, error) {
|
|
if err := invalidBucketError(bucket); err != nil {
|
|
return ObjectStat{}, err
|
|
}
|
|
if err := invalidArgumentError(object); err != nil {
|
|
return ObjectStat{}, err
|
|
}
|
|
req, err := a.headObjectRequest(bucket, object)
|
|
if err != nil {
|
|
return ObjectStat{}, err
|
|
}
|
|
resp, err := req.Do()
|
|
defer closeResp(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.",
|
|
Resource: separator + bucket + separator + object,
|
|
RequestID: resp.Header.Get("x-amz-request-id"),
|
|
HostID: resp.Header.Get("x-amz-id-2"),
|
|
}
|
|
case http.StatusForbidden:
|
|
errorResponse = ErrorResponse{
|
|
Code: "AccessDenied",
|
|
Message: "Access Denied.",
|
|
Resource: separator + bucket + separator + object,
|
|
RequestID: resp.Header.Get("x-amz-request-id"),
|
|
HostID: resp.Header.Get("x-amz-id-2"),
|
|
}
|
|
default:
|
|
errorResponse = ErrorResponse{
|
|
Code: resp.Status,
|
|
Message: resp.Status,
|
|
Resource: separator + bucket + separator + object,
|
|
RequestID: resp.Header.Get("x-amz-request-id"),
|
|
HostID: resp.Header.Get("x-amz-id-2"),
|
|
}
|
|
|
|
}
|
|
return ObjectStat{}, errorResponse
|
|
}
|
|
}
|
|
md5sum := strings.Trim(resp.Header.Get("ETag"), "\"") // trim off the odd double quotes
|
|
size, err := strconv.ParseInt(resp.Header.Get("Content-Length"), 10, 64)
|
|
if err != nil {
|
|
return ObjectStat{}, ErrorResponse{
|
|
Code: "InternalError",
|
|
Message: "Content-Length not recognized, please report this issue at https://github.com/minio/minio-go/issues.",
|
|
RequestID: resp.Header.Get("x-amz-request-id"),
|
|
HostID: resp.Header.Get("x-amz-id-2"),
|
|
}
|
|
}
|
|
date, err := time.Parse(http.TimeFormat, resp.Header.Get("Last-Modified"))
|
|
if err != nil {
|
|
return ObjectStat{}, ErrorResponse{
|
|
Code: "InternalError",
|
|
Message: "Last-Modified time format not recognized, please report this issue at https://github.com/minio/minio-go/issues.",
|
|
RequestID: resp.Header.Get("x-amz-request-id"),
|
|
HostID: resp.Header.Get("x-amz-id-2"),
|
|
}
|
|
}
|
|
contentType := strings.TrimSpace(resp.Header.Get("Content-Type"))
|
|
if contentType == "" {
|
|
contentType = "application/octet-stream"
|
|
}
|
|
|
|
var objectstat ObjectStat
|
|
objectstat.ETag = md5sum
|
|
objectstat.Key = object
|
|
objectstat.Size = size
|
|
objectstat.LastModified = date
|
|
objectstat.ContentType = contentType
|
|
return objectstat, nil
|
|
}
|
|
|
|
/// Service Operations
|
|
|
|
// listBucketRequest wrapper creates a new listBuckets request
|
|
func (a apiCore) listBucketsRequest() (*request, error) {
|
|
op := &operation{
|
|
HTTPServer: a.config.Endpoint,
|
|
HTTPMethod: "GET",
|
|
HTTPPath: separator,
|
|
}
|
|
return newRequest(op, a.config, nil)
|
|
}
|
|
|
|
// listBuckets list of all buckets owned by the authenticated sender of the request
|
|
func (a apiCore) listBuckets() (listAllMyBucketsResult, error) {
|
|
req, err := a.listBucketsRequest()
|
|
if err != nil {
|
|
return listAllMyBucketsResult{}, err
|
|
}
|
|
resp, err := req.Do()
|
|
defer closeResp(resp)
|
|
if err != nil {
|
|
return listAllMyBucketsResult{}, err
|
|
}
|
|
if resp != nil {
|
|
// for un-authenticated requests, amazon sends a redirect handle it
|
|
if resp.StatusCode == http.StatusTemporaryRedirect {
|
|
return listAllMyBucketsResult{}, 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"),
|
|
}
|
|
}
|
|
if resp.StatusCode != http.StatusOK {
|
|
return listAllMyBucketsResult{}, BodyToErrorResponse(resp.Body, a.config.AcceptType)
|
|
}
|
|
}
|
|
listAllMyBucketsResult := listAllMyBucketsResult{}
|
|
err = acceptTypeDecoder(resp.Body, a.config.AcceptType, &listAllMyBucketsResult)
|
|
if err != nil {
|
|
return listAllMyBucketsResult, err
|
|
}
|
|
return listAllMyBucketsResult, nil
|
|
}
|