mirror of
https://github.com/aptly-dev/aptly.git
synced 2026-05-30 04:20:53 +00:00
Update Go AWS SDK to the latest version
This commit is contained in:
committed by
Andrey Smirnov
parent
d08be990ef
commit
94a72b23ff
+8
-3
@@ -273,7 +273,7 @@ type DeleteObjectsIterator struct {
|
||||
inc bool
|
||||
}
|
||||
|
||||
// Next will increment the default iterator's index and and ensure that there
|
||||
// Next will increment the default iterator's index and ensure that there
|
||||
// is another object to iterator to.
|
||||
func (iter *DeleteObjectsIterator) Next() bool {
|
||||
if iter.inc {
|
||||
@@ -338,6 +338,11 @@ func (d *BatchDelete) Delete(ctx aws.Context, iter BatchDeleteIterator) error {
|
||||
}
|
||||
}
|
||||
|
||||
// iter.Next() could return false (above) plus populate iter.Err()
|
||||
if iter.Err() != nil {
|
||||
errs = append(errs, newError(iter.Err(), nil, nil))
|
||||
}
|
||||
|
||||
if input != nil && len(input.Delete.Objects) > 0 {
|
||||
if err := deleteBatch(ctx, d, input, objects); err != nil {
|
||||
errs = append(errs, err...)
|
||||
@@ -453,7 +458,7 @@ type DownloadObjectsIterator struct {
|
||||
inc bool
|
||||
}
|
||||
|
||||
// Next will increment the default iterator's index and and ensure that there
|
||||
// Next will increment the default iterator's index and ensure that there
|
||||
// is another object to iterator to.
|
||||
func (batcher *DownloadObjectsIterator) Next() bool {
|
||||
if batcher.inc {
|
||||
@@ -492,7 +497,7 @@ type UploadObjectsIterator struct {
|
||||
inc bool
|
||||
}
|
||||
|
||||
// Next will increment the default iterator's index and and ensure that there
|
||||
// Next will increment the default iterator's index and ensure that there
|
||||
// is another object to iterator to.
|
||||
func (batcher *UploadObjectsIterator) Next() bool {
|
||||
if batcher.inc {
|
||||
|
||||
+15
-5
@@ -39,6 +39,8 @@ type Downloader struct {
|
||||
// The number of goroutines to spin up in parallel when sending parts.
|
||||
// If this is set to zero, the DefaultDownloadConcurrency value will be used.
|
||||
//
|
||||
// Concurrency of 1 will download the parts sequentially.
|
||||
//
|
||||
// Concurrency is ignored if the Range input parameter is provided.
|
||||
Concurrency int
|
||||
|
||||
@@ -124,7 +126,8 @@ type maxRetrier interface {
|
||||
}
|
||||
|
||||
// Download downloads an object in S3 and writes the payload into w using
|
||||
// concurrent GET requests.
|
||||
// concurrent GET requests. The n int64 returned is the size of the object downloaded
|
||||
// in bytes.
|
||||
//
|
||||
// Additional functional options can be provided to configure the individual
|
||||
// download. These options are copies of the Downloader instance Download is called from.
|
||||
@@ -135,6 +138,9 @@ type maxRetrier interface {
|
||||
// The w io.WriterAt can be satisfied by an os.File to do multipart concurrent
|
||||
// downloads, or in memory []byte wrapper using aws.WriteAtBuffer.
|
||||
//
|
||||
// Specifying a Downloader.Concurrency of 1 will cause the Downloader to
|
||||
// download the parts from S3 sequentially.
|
||||
//
|
||||
// If the GetObjectInput's Range value is provided that will cause the downloader
|
||||
// to perform a single GetObjectInput request for that object's range. This will
|
||||
// caused the part size, and concurrency configurations to be ignored.
|
||||
@@ -143,11 +149,12 @@ func (d Downloader) Download(w io.WriterAt, input *s3.GetObjectInput, options ..
|
||||
}
|
||||
|
||||
// DownloadWithContext downloads an object in S3 and writes the payload into w
|
||||
// using concurrent GET requests.
|
||||
// using concurrent GET requests. The n int64 returned is the size of the object downloaded
|
||||
// in bytes.
|
||||
//
|
||||
// DownloadWithContext is the same as Download with the additional support for
|
||||
// Context input parameters. The Context must not be nil. A nil Context will
|
||||
// cause a panic. Use the Context to add deadlining, timeouts, ect. The
|
||||
// cause a panic. Use the Context to add deadlining, timeouts, etc. The
|
||||
// DownloadWithContext may create sub-contexts for individual underlying
|
||||
// requests.
|
||||
//
|
||||
@@ -160,6 +167,9 @@ func (d Downloader) Download(w io.WriterAt, input *s3.GetObjectInput, options ..
|
||||
// The w io.WriterAt can be satisfied by an os.File to do multipart concurrent
|
||||
// downloads, or in memory []byte wrapper using aws.WriteAtBuffer.
|
||||
//
|
||||
// Specifying a Downloader.Concurrency of 1 will cause the Downloader to
|
||||
// download the parts from S3 sequentially.
|
||||
//
|
||||
// It is safe to call this method concurrently across goroutines.
|
||||
//
|
||||
// If the GetObjectInput's Range value is provided that will cause the downloader
|
||||
@@ -207,14 +217,14 @@ func (d Downloader) DownloadWithContext(ctx aws.Context, w io.WriterAt, input *s
|
||||
//
|
||||
// objects := []s3manager.BatchDownloadObject {
|
||||
// {
|
||||
// Input: &s3.GetObjectInput {
|
||||
// Object: &s3.GetObjectInput {
|
||||
// Bucket: aws.String("bucket"),
|
||||
// Key: aws.String("foo"),
|
||||
// },
|
||||
// Writer: fooFile,
|
||||
// },
|
||||
// {
|
||||
// Input: &s3.GetObjectInput {
|
||||
// Object: &s3.GetObjectInput {
|
||||
// Bucket: aws.String("bucket"),
|
||||
// Key: aws.String("bar"),
|
||||
// },
|
||||
|
||||
+26
-19
@@ -11,6 +11,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -582,10 +583,22 @@ func TestDownload_WithFailure(t *testing.T) {
|
||||
svc := s3.New(unit.Session)
|
||||
svc.Handlers.Send.Clear()
|
||||
|
||||
first := true
|
||||
reqCount := int64(0)
|
||||
startingByte := 0
|
||||
svc.Handlers.Send.PushBack(func(r *request.Request) {
|
||||
if first {
|
||||
first = false
|
||||
switch atomic.LoadInt64(&reqCount) {
|
||||
case 1:
|
||||
// Give a chance for the multipart chunks to be queued up
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
r.HTTPResponse = &http.Response{
|
||||
Header: http.Header{},
|
||||
Body: ioutil.NopCloser(&bytes.Buffer{}),
|
||||
}
|
||||
r.Error = awserr.New("ConnectionError", "some connection error", nil)
|
||||
r.Retryable = aws.Bool(false)
|
||||
|
||||
default:
|
||||
body := bytes.NewReader(make([]byte, s3manager.DefaultDownloadPartSize))
|
||||
r.HTTPResponse = &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
@@ -596,22 +609,18 @@ func TestDownload_WithFailure(t *testing.T) {
|
||||
}
|
||||
r.HTTPResponse.Header.Set("Content-Length", strconv.Itoa(body.Len()))
|
||||
r.HTTPResponse.Header.Set("Content-Range",
|
||||
fmt.Sprintf("bytes 0-%d/%d", body.Len()-1, body.Len()*10))
|
||||
return
|
||||
fmt.Sprintf("bytes %d-%d/%d", startingByte, body.Len()-1, body.Len()*10))
|
||||
|
||||
startingByte += body.Len()
|
||||
if reqCount > 0 {
|
||||
// sleep here to ensure context switching between goroutines
|
||||
time.Sleep(25 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
// Give a chance for the multipart chunks to be queued up
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
r.HTTPResponse = &http.Response{
|
||||
Header: http.Header{},
|
||||
Body: ioutil.NopCloser(&bytes.Buffer{}),
|
||||
}
|
||||
r.Error = awserr.New("ConnectionError", "some connection error", nil)
|
||||
r.Retryable = aws.Bool(false)
|
||||
atomic.AddInt64(&reqCount, 1)
|
||||
})
|
||||
|
||||
start := time.Now()
|
||||
d := s3manager.NewDownloaderWithClient(svc, func(d *s3manager.Downloader) {
|
||||
d.Concurrency = 2
|
||||
})
|
||||
@@ -628,10 +637,8 @@ func TestDownload_WithFailure(t *testing.T) {
|
||||
t.Fatalf("expect error, got none")
|
||||
}
|
||||
|
||||
limit := start.Add(5 * time.Second)
|
||||
dur := time.Now().Sub(start)
|
||||
if time.Now().After(limit) {
|
||||
t.Errorf("expect time to be less than %v, took %v", limit, dur)
|
||||
if atomic.LoadInt64(&reqCount) > 3 {
|
||||
t.Errorf("expect no more than 3 requests, but received %d", reqCount)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Generated
Vendored
+26
@@ -0,0 +1,26 @@
|
||||
// +build integration
|
||||
|
||||
package s3manager_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/service/s3/s3manager"
|
||||
)
|
||||
|
||||
func TestGetBucketRegion(t *testing.T) {
|
||||
expectRegion := aws.StringValue(integSess.Config.Region)
|
||||
|
||||
ctx := aws.BackgroundContext()
|
||||
region, err := s3manager.GetBucketRegion(ctx, integSess,
|
||||
aws.StringValue(bucketName), expectRegion)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("expect no error, got %v", err)
|
||||
}
|
||||
|
||||
if e, a := expectRegion, region; e != a {
|
||||
t.Errorf("expect %s bucket region, got %s", e, a)
|
||||
}
|
||||
}
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
// +build integration
|
||||
|
||||
package s3manager_test
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
"github.com/aws/aws-sdk-go/awstesting/integration"
|
||||
"github.com/aws/aws-sdk-go/awstesting/integration/s3integ"
|
||||
"github.com/aws/aws-sdk-go/service/s3"
|
||||
"github.com/aws/aws-sdk-go/service/s3/s3manager"
|
||||
)
|
||||
|
||||
func init() {
|
||||
integSess = integration.SessionWithDefaultRegion("us-west-2")
|
||||
}
|
||||
|
||||
var integSess *session.Session
|
||||
var bucketName *string
|
||||
var svc *s3.S3
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
svc = s3.New(integSess)
|
||||
bucketName = aws.String(s3integ.GenerateBucketName())
|
||||
if err := s3integ.SetupTest(svc, *bucketName); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
var result int
|
||||
defer func() {
|
||||
if err := s3integ.CleanupTest(svc, *bucketName); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
}
|
||||
if r := recover(); r != nil {
|
||||
fmt.Fprintln(os.Stderr, "S3 integrationt tests paniced,", r)
|
||||
result = 1
|
||||
}
|
||||
os.Exit(result)
|
||||
}()
|
||||
|
||||
result = m.Run()
|
||||
}
|
||||
|
||||
type dlwriter struct {
|
||||
buf []byte
|
||||
}
|
||||
|
||||
func newDLWriter(size int) *dlwriter {
|
||||
return &dlwriter{buf: make([]byte, size)}
|
||||
}
|
||||
|
||||
func (d dlwriter) WriteAt(p []byte, pos int64) (n int, err error) {
|
||||
if pos > int64(len(d.buf)) {
|
||||
return 0, io.EOF
|
||||
}
|
||||
|
||||
written := 0
|
||||
for i, b := range p {
|
||||
if i >= len(d.buf) {
|
||||
break
|
||||
}
|
||||
d.buf[pos+int64(i)] = b
|
||||
written++
|
||||
}
|
||||
return written, nil
|
||||
}
|
||||
|
||||
func validate(t *testing.T, key string, md5value string) {
|
||||
mgr := s3manager.NewDownloader(integSess)
|
||||
params := &s3.GetObjectInput{Bucket: bucketName, Key: &key}
|
||||
|
||||
w := newDLWriter(1024 * 1024 * 20)
|
||||
n, err := mgr.Download(w, params)
|
||||
if err != nil {
|
||||
t.Fatalf("expect no error, got %v", err)
|
||||
}
|
||||
if e, a := md5value, fmt.Sprintf("%x", md5.Sum(w.buf[0:n])); e != a {
|
||||
t.Errorf("expect %s md5 value, got %s", e, a)
|
||||
}
|
||||
}
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
// +build integration
|
||||
|
||||
package s3manager_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/md5"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws/awserr"
|
||||
"github.com/aws/aws-sdk-go/aws/request"
|
||||
"github.com/aws/aws-sdk-go/service/s3"
|
||||
"github.com/aws/aws-sdk-go/service/s3/s3manager"
|
||||
)
|
||||
|
||||
var integBuf12MB = make([]byte, 1024*1024*12)
|
||||
var integMD512MB = fmt.Sprintf("%x", md5.Sum(integBuf12MB))
|
||||
|
||||
func TestUploadConcurrently(t *testing.T) {
|
||||
key := "12mb-1"
|
||||
mgr := s3manager.NewUploader(integSess)
|
||||
out, err := mgr.Upload(&s3manager.UploadInput{
|
||||
Bucket: bucketName,
|
||||
Key: &key,
|
||||
Body: bytes.NewReader(integBuf12MB),
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("expect no error, got %v", err)
|
||||
}
|
||||
if len(out.UploadID) == 0 {
|
||||
t.Errorf("expect upload ID but was empty")
|
||||
}
|
||||
|
||||
re := regexp.MustCompile(`^https?://.+/` + key + `$`)
|
||||
if e, a := re.String(), out.Location; !re.MatchString(a) {
|
||||
t.Errorf("expect %s to match URL regexp %q, did not", e, a)
|
||||
}
|
||||
|
||||
validate(t, key, integMD512MB)
|
||||
}
|
||||
|
||||
func TestUploadFailCleanup(t *testing.T) {
|
||||
// Break checksum on 2nd part so it fails
|
||||
part := 0
|
||||
svc.Handlers.Build.PushBack(func(r *request.Request) {
|
||||
if r.Operation.Name == "UploadPart" {
|
||||
if part == 1 {
|
||||
r.HTTPRequest.Header.Set("X-Amz-Content-Sha256", "000")
|
||||
}
|
||||
part++
|
||||
}
|
||||
})
|
||||
|
||||
key := "12mb-leave"
|
||||
mgr := s3manager.NewUploaderWithClient(svc, func(u *s3manager.Uploader) {
|
||||
u.LeavePartsOnError = false
|
||||
})
|
||||
_, err := mgr.Upload(&s3manager.UploadInput{
|
||||
Bucket: bucketName,
|
||||
Key: &key,
|
||||
Body: bytes.NewReader(integBuf12MB),
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatalf("expect error, but did not get one")
|
||||
}
|
||||
|
||||
aerr := err.(awserr.Error)
|
||||
if e, a := "MissingRegion", aerr.Code(); strings.Contains(a, e) {
|
||||
t.Errorf("expect %q to not be in error code %q", e, a)
|
||||
}
|
||||
|
||||
uploadID := ""
|
||||
merr := err.(s3manager.MultiUploadFailure)
|
||||
if uploadID = merr.UploadID(); len(uploadID) == 0 {
|
||||
t.Errorf("expect upload ID to not be empty, but was")
|
||||
}
|
||||
|
||||
_, err = svc.ListParts(&s3.ListPartsInput{
|
||||
Bucket: bucketName, Key: &key, UploadId: &uploadID,
|
||||
})
|
||||
if err == nil {
|
||||
t.Errorf("expect error for list parts, but got none")
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+22
-2
@@ -9,13 +9,21 @@ import (
|
||||
"github.com/aws/aws-sdk-go/service/s3/s3manager"
|
||||
)
|
||||
|
||||
var _ DownloaderAPI = (*s3manager.Downloader)(nil)
|
||||
|
||||
// DownloaderAPI is the interface type for s3manager.Downloader.
|
||||
type DownloaderAPI interface {
|
||||
Download(io.WriterAt, *s3.GetObjectInput, ...func(*s3manager.Downloader)) (int64, error)
|
||||
DownloadWithContext(aws.Context, io.WriterAt, *s3.GetObjectInput, ...func(*s3manager.Downloader)) (int64, error)
|
||||
}
|
||||
|
||||
var _ DownloaderAPI = (*s3manager.Downloader)(nil)
|
||||
// DownloadWithIterator is the interface type for the contained method of the same name.
|
||||
type DownloadWithIterator interface {
|
||||
DownloadWithIterator(aws.Context, s3manager.BatchDownloadIterator, ...func(*s3manager.Downloader)) error
|
||||
}
|
||||
|
||||
var _ UploaderAPI = (*s3manager.Uploader)(nil)
|
||||
var _ UploadWithIterator = (*s3manager.Uploader)(nil)
|
||||
|
||||
// UploaderAPI is the interface type for s3manager.Uploader.
|
||||
type UploaderAPI interface {
|
||||
@@ -23,4 +31,16 @@ type UploaderAPI interface {
|
||||
UploadWithContext(aws.Context, *s3manager.UploadInput, ...func(*s3manager.Uploader)) (*s3manager.UploadOutput, error)
|
||||
}
|
||||
|
||||
var _ UploaderAPI = (*s3manager.Uploader)(nil)
|
||||
// UploadWithIterator is the interface for uploading objects to S3 using the S3
|
||||
// upload manager.
|
||||
type UploadWithIterator interface {
|
||||
UploadWithIterator(aws.Context, s3manager.BatchUploadIterator, ...func(*s3manager.Uploader)) error
|
||||
}
|
||||
|
||||
var _ BatchDelete = (*s3manager.BatchDelete)(nil)
|
||||
|
||||
// BatchDelete is the interface type for batch deleting objects from S3 using
|
||||
// the S3 manager. (separated for user to compose).
|
||||
type BatchDelete interface {
|
||||
Delete(aws.Context, s3manager.BatchDeleteIterator) error
|
||||
}
|
||||
|
||||
+41
-115
@@ -6,12 +6,12 @@ import (
|
||||
"io"
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/awserr"
|
||||
"github.com/aws/aws-sdk-go/aws/awsutil"
|
||||
"github.com/aws/aws-sdk-go/aws/client"
|
||||
"github.com/aws/aws-sdk-go/aws/credentials"
|
||||
"github.com/aws/aws-sdk-go/aws/request"
|
||||
"github.com/aws/aws-sdk-go/service/s3"
|
||||
"github.com/aws/aws-sdk-go/service/s3/s3iface"
|
||||
@@ -96,100 +96,6 @@ func (m multiUploadError) UploadID() string {
|
||||
return m.uploadID
|
||||
}
|
||||
|
||||
// UploadInput contains all input for upload requests to Amazon S3.
|
||||
type UploadInput struct {
|
||||
// The canned ACL to apply to the object.
|
||||
ACL *string `location:"header" locationName:"x-amz-acl" type:"string"`
|
||||
|
||||
Bucket *string `location:"uri" locationName:"Bucket" type:"string" required:"true"`
|
||||
|
||||
// Specifies caching behavior along the request/reply chain.
|
||||
CacheControl *string `location:"header" locationName:"Cache-Control" type:"string"`
|
||||
|
||||
// Specifies presentational information for the object.
|
||||
ContentDisposition *string `location:"header" locationName:"Content-Disposition" type:"string"`
|
||||
|
||||
// Specifies what content encodings have been applied to the object and thus
|
||||
// what decoding mechanisms must be applied to obtain the media-type referenced
|
||||
// by the Content-Type header field.
|
||||
ContentEncoding *string `location:"header" locationName:"Content-Encoding" type:"string"`
|
||||
|
||||
// The language the content is in.
|
||||
ContentLanguage *string `location:"header" locationName:"Content-Language" type:"string"`
|
||||
|
||||
// The base64-encoded 128-bit MD5 digest of the part data.
|
||||
ContentMD5 *string `location:"header" locationName:"Content-MD5" type:"string"`
|
||||
|
||||
// A standard MIME type describing the format of the object data.
|
||||
ContentType *string `location:"header" locationName:"Content-Type" type:"string"`
|
||||
|
||||
// The date and time at which the object is no longer cacheable.
|
||||
Expires *time.Time `location:"header" locationName:"Expires" type:"timestamp" timestampFormat:"rfc822"`
|
||||
|
||||
// Gives the grantee READ, READ_ACP, and WRITE_ACP permissions on the object.
|
||||
GrantFullControl *string `location:"header" locationName:"x-amz-grant-full-control" type:"string"`
|
||||
|
||||
// Allows grantee to read the object data and its metadata.
|
||||
GrantRead *string `location:"header" locationName:"x-amz-grant-read" type:"string"`
|
||||
|
||||
// Allows grantee to read the object ACL.
|
||||
GrantReadACP *string `location:"header" locationName:"x-amz-grant-read-acp" type:"string"`
|
||||
|
||||
// Allows grantee to write the ACL for the applicable object.
|
||||
GrantWriteACP *string `location:"header" locationName:"x-amz-grant-write-acp" type:"string"`
|
||||
|
||||
Key *string `location:"uri" locationName:"Key" type:"string" required:"true"`
|
||||
|
||||
// A map of metadata to store with the object in S3.
|
||||
Metadata map[string]*string `location:"headers" locationName:"x-amz-meta-" type:"map"`
|
||||
|
||||
// Confirms that the requester knows that she or he will be charged for the
|
||||
// request. Bucket owners need not specify this parameter in their requests.
|
||||
// Documentation on downloading objects from requester pays buckets can be found
|
||||
// at http://docs.aws.amazon.com/AmazonS3/latest/dev/ObjectsinRequesterPaysBuckets.html
|
||||
RequestPayer *string `location:"header" locationName:"x-amz-request-payer" type:"string"`
|
||||
|
||||
// Specifies the algorithm to use to when encrypting the object (e.g., AES256,
|
||||
// aws:kms).
|
||||
SSECustomerAlgorithm *string `location:"header" locationName:"x-amz-server-side-encryption-customer-algorithm" type:"string"`
|
||||
|
||||
// Specifies the customer-provided encryption key for Amazon S3 to use in encrypting
|
||||
// data. This value is used to store the object and then it is discarded; Amazon
|
||||
// does not store the encryption key. The key must be appropriate for use with
|
||||
// the algorithm specified in the x-amz-server-side-encryption-customer-algorithm
|
||||
// header.
|
||||
SSECustomerKey *string `location:"header" locationName:"x-amz-server-side-encryption-customer-key" type:"string"`
|
||||
|
||||
// Specifies the 128-bit MD5 digest of the encryption key according to RFC 1321.
|
||||
// Amazon S3 uses this header for a message integrity check to ensure the encryption
|
||||
// key was transmitted without error.
|
||||
SSECustomerKeyMD5 *string `location:"header" locationName:"x-amz-server-side-encryption-customer-key-MD5" type:"string"`
|
||||
|
||||
// Specifies the AWS KMS key ID to use for object encryption. All GET and PUT
|
||||
// requests for an object protected by AWS KMS will fail if not made via SSL
|
||||
// or using SigV4. Documentation on configuring any of the officially supported
|
||||
// AWS SDKs and CLI can be found at http://docs.aws.amazon.com/AmazonS3/latest/dev/UsingAWSSDK.html#specify-signature-version
|
||||
SSEKMSKeyId *string `location:"header" locationName:"x-amz-server-side-encryption-aws-kms-key-id" type:"string"`
|
||||
|
||||
// The Server-side encryption algorithm used when storing this object in S3
|
||||
// (e.g., AES256, aws:kms).
|
||||
ServerSideEncryption *string `location:"header" locationName:"x-amz-server-side-encryption" type:"string"`
|
||||
|
||||
// The type of storage to use for the object. Defaults to 'STANDARD'.
|
||||
StorageClass *string `location:"header" locationName:"x-amz-storage-class" type:"string"`
|
||||
|
||||
// The tag-set for the object. The tag-set must be encoded as URL Query parameters
|
||||
Tagging *string `location:"header" locationName:"x-amz-tagging" type:"string"`
|
||||
|
||||
// If the bucket is configured as a website, redirects requests for this object
|
||||
// to another object in the same bucket or to an external URL. Amazon S3 stores
|
||||
// the value of this header in the object metadata.
|
||||
WebsiteRedirectLocation *string `location:"header" locationName:"x-amz-website-redirect-location" type:"string"`
|
||||
|
||||
// The readable body payload to send to S3.
|
||||
Body io.Reader
|
||||
}
|
||||
|
||||
// UploadOutput represents a response from the Upload() call.
|
||||
type UploadOutput struct {
|
||||
// The URL where the object was uploaded to.
|
||||
@@ -239,8 +145,15 @@ type Uploader struct {
|
||||
// MaxUploadParts is the max number of parts which will be uploaded to S3.
|
||||
// Will be used to calculate the partsize of the object to be uploaded.
|
||||
// E.g: 5GB file, with MaxUploadParts set to 100, will upload the file
|
||||
// as 100, 50MB parts.
|
||||
// With a limited of s3.MaxUploadParts (10,000 parts).
|
||||
// as 100, 50MB parts. With a limited of s3.MaxUploadParts (10,000 parts).
|
||||
//
|
||||
// MaxUploadParts must not be used to limit the total number of bytes uploaded.
|
||||
// Use a type like to io.LimitReader (https://golang.org/pkg/io/#LimitedReader)
|
||||
// instead. An io.LimitReader is helpful when uploading an unbounded reader
|
||||
// to S3, and you know its maximum size. Otherwise the reader's io.EOF returned
|
||||
// error must be used to signal end of stream.
|
||||
//
|
||||
// Defaults to package const's MaxUploadParts value.
|
||||
MaxUploadParts int
|
||||
|
||||
// The client to use when uploading to S3.
|
||||
@@ -357,7 +270,7 @@ func (u Uploader) Upload(input *UploadInput, options ...func(*Uploader)) (*Uploa
|
||||
//
|
||||
// UploadWithContext is the same as Upload with the additional support for
|
||||
// Context input parameters. The Context must not be nil. A nil Context will
|
||||
// cause a panic. Use the context to add deadlining, timeouts, ect. The
|
||||
// cause a panic. Use the context to add deadlining, timeouts, etc. The
|
||||
// UploadWithContext may create sub-contexts for individual underlying requests.
|
||||
//
|
||||
// Additional functional options can be provided to configure the individual
|
||||
@@ -395,7 +308,7 @@ func (u Uploader) UploadWithContext(ctx aws.Context, input *UploadInput, opts ..
|
||||
// },
|
||||
// }
|
||||
//
|
||||
// iter := &s3managee.UploadObjectsIterator{Objects: objects}
|
||||
// iter := &s3manager.UploadObjectsIterator{Objects: objects}
|
||||
// if err := svc.UploadWithIterator(aws.BackgroundContext(), iter); err != nil {
|
||||
// return err
|
||||
// }
|
||||
@@ -477,6 +390,9 @@ func (u *uploader) init() {
|
||||
if u.cfg.PartSize == 0 {
|
||||
u.cfg.PartSize = DefaultUploadPartSize
|
||||
}
|
||||
if u.cfg.MaxUploadParts == 0 {
|
||||
u.cfg.MaxUploadParts = MaxUploadParts
|
||||
}
|
||||
|
||||
u.bufferPool = sync.Pool{
|
||||
New: func() interface{} { return make([]byte, u.cfg.PartSize) },
|
||||
@@ -631,21 +547,6 @@ func (u *multiuploader) upload(firstBuf io.ReadSeeker, firstPart []byte) (*Uploa
|
||||
|
||||
// Read and queue the rest of the parts
|
||||
for u.geterr() == nil && err == nil {
|
||||
num++
|
||||
// This upload exceeded maximum number of supported parts, error now.
|
||||
if num > int64(u.cfg.MaxUploadParts) || num > int64(MaxUploadParts) {
|
||||
var msg string
|
||||
if num > int64(u.cfg.MaxUploadParts) {
|
||||
msg = fmt.Sprintf("exceeded total allowed configured MaxUploadParts (%d). Adjust PartSize to fit in this limit",
|
||||
u.cfg.MaxUploadParts)
|
||||
} else {
|
||||
msg = fmt.Sprintf("exceeded total allowed S3 limit MaxUploadParts (%d). Adjust PartSize to fit in this limit",
|
||||
MaxUploadParts)
|
||||
}
|
||||
u.seterr(awserr.New("TotalPartsExceeded", msg, nil))
|
||||
break
|
||||
}
|
||||
|
||||
var reader io.ReadSeeker
|
||||
var nextChunkLen int
|
||||
var part []byte
|
||||
@@ -666,6 +567,21 @@ func (u *multiuploader) upload(firstBuf io.ReadSeeker, firstPart []byte) (*Uploa
|
||||
break
|
||||
}
|
||||
|
||||
num++
|
||||
// This upload exceeded maximum number of supported parts, error now.
|
||||
if num > int64(u.cfg.MaxUploadParts) || num > int64(MaxUploadParts) {
|
||||
var msg string
|
||||
if num > int64(u.cfg.MaxUploadParts) {
|
||||
msg = fmt.Sprintf("exceeded total allowed configured MaxUploadParts (%d). Adjust PartSize to fit in this limit",
|
||||
u.cfg.MaxUploadParts)
|
||||
} else {
|
||||
msg = fmt.Sprintf("exceeded total allowed S3 limit MaxUploadParts (%d). Adjust PartSize to fit in this limit",
|
||||
MaxUploadParts)
|
||||
}
|
||||
u.seterr(awserr.New("TotalPartsExceeded", msg, nil))
|
||||
break
|
||||
}
|
||||
|
||||
ch <- chunk{buf: reader, part: part, num: num}
|
||||
}
|
||||
|
||||
@@ -683,8 +599,18 @@ func (u *multiuploader) upload(firstBuf io.ReadSeeker, firstPart []byte) (*Uploa
|
||||
uploadID: u.uploadID,
|
||||
}
|
||||
}
|
||||
|
||||
// Create a presigned URL of the S3 Get Object in order to have parity with
|
||||
// single part upload.
|
||||
getReq, _ := u.cfg.S3.GetObjectRequest(&s3.GetObjectInput{
|
||||
Bucket: u.in.Bucket,
|
||||
Key: u.in.Key,
|
||||
})
|
||||
getReq.Config.Credentials = credentials.AnonymousCredentials
|
||||
uploadLocation, _, _ := getReq.PresignRequest(1)
|
||||
|
||||
return &UploadOutput{
|
||||
Location: aws.StringValue(complete.Location),
|
||||
Location: uploadLocation,
|
||||
VersionID: complete.VersionId,
|
||||
UploadID: u.uploadID,
|
||||
}, nil
|
||||
|
||||
+129
@@ -0,0 +1,129 @@
|
||||
// Code generated by private/model/cli/gen-api/main.go. DO NOT EDIT.
|
||||
|
||||
package s3manager
|
||||
|
||||
import (
|
||||
"io"
|
||||
"time"
|
||||
)
|
||||
|
||||
// UploadInput provides the input parameters for uploading a stream or buffer
|
||||
// to an object in an Amazon S3 bucket. This type is similar to the s3
|
||||
// package's PutObjectInput with the exception that the Body member is an
|
||||
// io.Reader instead of an io.ReadSeeker.
|
||||
type UploadInput struct {
|
||||
_ struct{} `type:"structure" payload:"Body"`
|
||||
|
||||
// The canned ACL to apply to the object.
|
||||
ACL *string `location:"header" locationName:"x-amz-acl" type:"string" enum:"ObjectCannedACL"`
|
||||
|
||||
// The readable body payload to send to S3.
|
||||
Body io.Reader
|
||||
|
||||
// Name of the bucket to which the PUT operation was initiated.
|
||||
//
|
||||
// Bucket is a required field
|
||||
Bucket *string `location:"uri" locationName:"Bucket" type:"string" required:"true"`
|
||||
|
||||
// Specifies caching behavior along the request/reply chain.
|
||||
CacheControl *string `location:"header" locationName:"Cache-Control" type:"string"`
|
||||
|
||||
// Specifies presentational information for the object.
|
||||
ContentDisposition *string `location:"header" locationName:"Content-Disposition" type:"string"`
|
||||
|
||||
// Specifies what content encodings have been applied to the object and thus
|
||||
// what decoding mechanisms must be applied to obtain the media-type referenced
|
||||
// by the Content-Type header field.
|
||||
ContentEncoding *string `location:"header" locationName:"Content-Encoding" type:"string"`
|
||||
|
||||
// The language the content is in.
|
||||
ContentLanguage *string `location:"header" locationName:"Content-Language" type:"string"`
|
||||
|
||||
// The base64-encoded 128-bit MD5 digest of the part data. This parameter is
|
||||
// auto-populated when using the command from the CLI. This parameted is required
|
||||
// if object lock parameters are specified.
|
||||
ContentMD5 *string `location:"header" locationName:"Content-MD5" type:"string"`
|
||||
|
||||
// A standard MIME type describing the format of the object data.
|
||||
ContentType *string `location:"header" locationName:"Content-Type" type:"string"`
|
||||
|
||||
// The date and time at which the object is no longer cacheable.
|
||||
Expires *time.Time `location:"header" locationName:"Expires" type:"timestamp"`
|
||||
|
||||
// Gives the grantee READ, READ_ACP, and WRITE_ACP permissions on the object.
|
||||
GrantFullControl *string `location:"header" locationName:"x-amz-grant-full-control" type:"string"`
|
||||
|
||||
// Allows grantee to read the object data and its metadata.
|
||||
GrantRead *string `location:"header" locationName:"x-amz-grant-read" type:"string"`
|
||||
|
||||
// Allows grantee to read the object ACL.
|
||||
GrantReadACP *string `location:"header" locationName:"x-amz-grant-read-acp" type:"string"`
|
||||
|
||||
// Allows grantee to write the ACL for the applicable object.
|
||||
GrantWriteACP *string `location:"header" locationName:"x-amz-grant-write-acp" type:"string"`
|
||||
|
||||
// Object key for which the PUT operation was initiated.
|
||||
//
|
||||
// Key is a required field
|
||||
Key *string `location:"uri" locationName:"Key" min:"1" type:"string" required:"true"`
|
||||
|
||||
// A map of metadata to store with the object in S3.
|
||||
Metadata map[string]*string `location:"headers" locationName:"x-amz-meta-" type:"map"`
|
||||
|
||||
// The Legal Hold status that you want to apply to the specified object.
|
||||
ObjectLockLegalHoldStatus *string `location:"header" locationName:"x-amz-object-lock-legal-hold" type:"string" enum:"ObjectLockLegalHoldStatus"`
|
||||
|
||||
// The object lock mode that you want to apply to this object.
|
||||
ObjectLockMode *string `location:"header" locationName:"x-amz-object-lock-mode" type:"string" enum:"ObjectLockMode"`
|
||||
|
||||
// The date and time when you want this object's object lock to expire.
|
||||
ObjectLockRetainUntilDate *time.Time `location:"header" locationName:"x-amz-object-lock-retain-until-date" type:"timestamp" timestampFormat:"iso8601"`
|
||||
|
||||
// Confirms that the requester knows that she or he will be charged for the
|
||||
// request. Bucket owners need not specify this parameter in their requests.
|
||||
// Documentation on downloading objects from requester pays buckets can be found
|
||||
// at http://docs.aws.amazon.com/AmazonS3/latest/dev/ObjectsinRequesterPaysBuckets.html
|
||||
RequestPayer *string `location:"header" locationName:"x-amz-request-payer" type:"string" enum:"RequestPayer"`
|
||||
|
||||
// Specifies the algorithm to use to when encrypting the object (e.g., AES256).
|
||||
SSECustomerAlgorithm *string `location:"header" locationName:"x-amz-server-side-encryption-customer-algorithm" type:"string"`
|
||||
|
||||
// Specifies the customer-provided encryption key for Amazon S3 to use in encrypting
|
||||
// data. This value is used to store the object and then it is discarded; Amazon
|
||||
// does not store the encryption key. The key must be appropriate for use with
|
||||
// the algorithm specified in the x-amz-server-side-encryption-customer-algorithm
|
||||
// header.
|
||||
SSECustomerKey *string `marshal-as:"blob" location:"header" locationName:"x-amz-server-side-encryption-customer-key" type:"string" sensitive:"true"`
|
||||
|
||||
// Specifies the 128-bit MD5 digest of the encryption key according to RFC 1321.
|
||||
// Amazon S3 uses this header for a message integrity check to ensure the encryption
|
||||
// key was transmitted without error.
|
||||
SSECustomerKeyMD5 *string `location:"header" locationName:"x-amz-server-side-encryption-customer-key-MD5" type:"string"`
|
||||
|
||||
// Specifies the AWS KMS Encryption Context to use for object encryption. The
|
||||
// value of this header is a base64-encoded UTF-8 string holding JSON with the
|
||||
// encryption context key-value pairs.
|
||||
SSEKMSEncryptionContext *string `location:"header" locationName:"x-amz-server-side-encryption-context" type:"string" sensitive:"true"`
|
||||
|
||||
// Specifies the AWS KMS key ID to use for object encryption. All GET and PUT
|
||||
// requests for an object protected by AWS KMS will fail if not made via SSL
|
||||
// or using SigV4. Documentation on configuring any of the officially supported
|
||||
// AWS SDKs and CLI can be found at http://docs.aws.amazon.com/AmazonS3/latest/dev/UsingAWSSDK.html#specify-signature-version
|
||||
SSEKMSKeyId *string `location:"header" locationName:"x-amz-server-side-encryption-aws-kms-key-id" type:"string" sensitive:"true"`
|
||||
|
||||
// The Server-side encryption algorithm used when storing this object in S3
|
||||
// (e.g., AES256, aws:kms).
|
||||
ServerSideEncryption *string `location:"header" locationName:"x-amz-server-side-encryption" type:"string" enum:"ServerSideEncryption"`
|
||||
|
||||
// The type of storage to use for the object. Defaults to 'STANDARD'.
|
||||
StorageClass *string `location:"header" locationName:"x-amz-storage-class" type:"string" enum:"StorageClass"`
|
||||
|
||||
// The tag-set for the object. The tag-set must be encoded as URL Query parameters.
|
||||
// (For example, "Key1=Value1")
|
||||
Tagging *string `location:"header" locationName:"x-amz-tagging" type:"string"`
|
||||
|
||||
// If the bucket is configured as a website, redirects requests for this object
|
||||
// to another object in the same bucket or to an external URL. Amazon S3 stores
|
||||
// the value of this header in the object metadata.
|
||||
WebsiteRedirectLocation *string `location:"header" locationName:"x-amz-website-redirect-location" type:"string"`
|
||||
}
|
||||
+334
-32
@@ -1,3 +1,5 @@
|
||||
// +build go1.8
|
||||
|
||||
package s3manager_test
|
||||
|
||||
import (
|
||||
@@ -7,12 +9,15 @@ import (
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/awserr"
|
||||
@@ -104,7 +109,7 @@ func TestUploadOrderMulti(t *testing.T) {
|
||||
|
||||
resp, err := u.Upload(&s3manager.UploadInput{
|
||||
Bucket: aws.String("Bucket"),
|
||||
Key: aws.String("Key"),
|
||||
Key: aws.String("Key - value"),
|
||||
Body: bytes.NewReader(buf12MB),
|
||||
ServerSideEncryption: aws.String("aws:kms"),
|
||||
SSEKMSKeyId: aws.String("KmsId"),
|
||||
@@ -120,8 +125,8 @@ func TestUploadOrderMulti(t *testing.T) {
|
||||
t.Errorf("Expected %v, but received %v", expected, *ops)
|
||||
}
|
||||
|
||||
if "https://location" != resp.Location {
|
||||
t.Errorf("Expected %q, but received %q", "https://location", resp.Location)
|
||||
if e, a := `https://s3.mock-region.amazonaws.com/Bucket/Key%20-%20value`, resp.Location; e != a {
|
||||
t.Errorf("Expected %q, but received %q", e, a)
|
||||
}
|
||||
|
||||
if "UPLOAD-ID" != resp.UploadID {
|
||||
@@ -129,7 +134,7 @@ func TestUploadOrderMulti(t *testing.T) {
|
||||
}
|
||||
|
||||
if "VERSION-ID" != *resp.VersionID {
|
||||
t.Errorf("Expected %q, but received %q", "VERSION-ID", resp.VersionID)
|
||||
t.Errorf("Expected %q, but received %q", "VERSION-ID", *resp.VersionID)
|
||||
}
|
||||
|
||||
// Validate input values
|
||||
@@ -268,12 +273,12 @@ func TestUploadFailIfPartSizeTooSmall(t *testing.T) {
|
||||
}
|
||||
|
||||
aerr := err.(awserr.Error)
|
||||
if "ConfigError" != aerr.Code() {
|
||||
t.Errorf("Expected %q, but received %q", "ConfigError", aerr.Code())
|
||||
if e, a := "ConfigError", aerr.Code(); e != a {
|
||||
t.Errorf("Expected %q, but received %q", e, a)
|
||||
}
|
||||
|
||||
if strings.Contains("part size must be at least", aerr.Message()) {
|
||||
t.Errorf("Expected string to contain %q, but received %q", "part size must be at least", aerr.Message())
|
||||
if e, a := "part size must be at least", aerr.Message(); !strings.Contains(a, e) {
|
||||
t.Errorf("expect %v to be in %v", e, a)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -282,7 +287,7 @@ func TestUploadOrderSingle(t *testing.T) {
|
||||
mgr := s3manager.NewUploaderWithClient(s)
|
||||
resp, err := mgr.Upload(&s3manager.UploadInput{
|
||||
Bucket: aws.String("Bucket"),
|
||||
Key: aws.String("Key"),
|
||||
Key: aws.String("Key - value"),
|
||||
Body: bytes.NewReader(buf2MB),
|
||||
ServerSideEncryption: aws.String("aws:kms"),
|
||||
SSEKMSKeyId: aws.String("KmsId"),
|
||||
@@ -297,12 +302,12 @@ func TestUploadOrderSingle(t *testing.T) {
|
||||
t.Errorf("Expected %v, but received %v", vals, *ops)
|
||||
}
|
||||
|
||||
if len(resp.Location) == 0 {
|
||||
t.Error("Expected Location to not be empty")
|
||||
if e, a := `https://s3.mock-region.amazonaws.com/Bucket/Key%20-%20value`, resp.Location; e != a {
|
||||
t.Errorf("Expected %q, but received %q", e, a)
|
||||
}
|
||||
|
||||
if e := "VERSION-ID"; e != *resp.VersionID {
|
||||
t.Errorf("Expected %q, but received %q", e, resp.VersionID)
|
||||
t.Errorf("Expected %q, but received %q", e, *resp.VersionID)
|
||||
}
|
||||
|
||||
if len(resp.UploadID) > 0 {
|
||||
@@ -785,53 +790,45 @@ func TestUploadInputS3PutObjectInputPairity(t *testing.T) {
|
||||
}
|
||||
|
||||
type testIncompleteReader struct {
|
||||
Buf []byte
|
||||
Count int
|
||||
Size int64
|
||||
read int64
|
||||
}
|
||||
|
||||
func (r *testIncompleteReader) Read(p []byte) (n int, err error) {
|
||||
if r.Count < 0 {
|
||||
return 0, io.ErrUnexpectedEOF
|
||||
r.read += int64(len(p))
|
||||
if r.read >= r.Size {
|
||||
return int(r.read - r.Size), io.ErrUnexpectedEOF
|
||||
}
|
||||
|
||||
r.Count--
|
||||
return copy(p, r.Buf), nil
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func TestUploadUnexpectedEOF(t *testing.T) {
|
||||
s, ops, args := loggingSvc(emptyList)
|
||||
s, ops, _ := loggingSvc(emptyList)
|
||||
mgr := s3manager.NewUploaderWithClient(s, func(u *s3manager.Uploader) {
|
||||
u.Concurrency = 1
|
||||
u.PartSize = s3manager.MinUploadPartSize
|
||||
})
|
||||
_, err := mgr.Upload(&s3manager.UploadInput{
|
||||
Bucket: aws.String("Bucket"),
|
||||
Key: aws.String("Key"),
|
||||
Body: &testIncompleteReader{
|
||||
Buf: make([]byte, 1024*1024*5),
|
||||
Count: 1,
|
||||
Size: int64(s3manager.MinUploadPartSize + 1),
|
||||
},
|
||||
})
|
||||
|
||||
if err == nil {
|
||||
t.Error("Expected error, but received none")
|
||||
}
|
||||
|
||||
// Ensure upload started.
|
||||
if e, a := "CreateMultipartUpload", (*ops)[0]; e != a {
|
||||
t.Errorf("Expected %q, but received %q", e, a)
|
||||
}
|
||||
|
||||
if e, a := "UploadPart", (*ops)[1]; e != a {
|
||||
t.Errorf("Expected %q, but received %q", e, a)
|
||||
}
|
||||
|
||||
// Part may or may not be sent because of timing of sending parts and
|
||||
// reading next part in upload manager. Just check for the last abort.
|
||||
if e, a := "AbortMultipartUpload", (*ops)[len(*ops)-1]; e != a {
|
||||
t.Errorf("Expected %q, but received %q", e, a)
|
||||
}
|
||||
|
||||
// Part lengths
|
||||
if e, a := 1024*1024*5, buflen(val((*args)[1], "Body")); e != a {
|
||||
t.Errorf("Expected %d, but received %d", e, a)
|
||||
}
|
||||
}
|
||||
|
||||
func compareStructType(a, b reflect.Type) map[string]int {
|
||||
@@ -1001,3 +998,308 @@ func TestUploadWithContextCanceled(t *testing.T) {
|
||||
t.Errorf("expected error message to contain %q, but did not %q", e, a)
|
||||
}
|
||||
}
|
||||
|
||||
// S3 Uploader incorrectly fails an upload if the content being uploaded
|
||||
// has a size of MinPartSize * MaxUploadParts.
|
||||
// Github: aws/aws-sdk-go#2557
|
||||
func TestUploadMaxPartsEOF(t *testing.T) {
|
||||
s, ops, _ := loggingSvc(emptyList)
|
||||
mgr := s3manager.NewUploaderWithClient(s, func(u *s3manager.Uploader) {
|
||||
u.Concurrency = 1
|
||||
u.PartSize = s3manager.DefaultUploadPartSize
|
||||
u.MaxUploadParts = 2
|
||||
})
|
||||
f := bytes.NewReader(make([]byte, int(mgr.PartSize)*mgr.MaxUploadParts))
|
||||
|
||||
r1 := io.NewSectionReader(f, 0, s3manager.DefaultUploadPartSize)
|
||||
r2 := io.NewSectionReader(f, s3manager.DefaultUploadPartSize, 2*s3manager.DefaultUploadPartSize)
|
||||
body := io.MultiReader(r1, r2)
|
||||
|
||||
_, err := mgr.Upload(&s3manager.UploadInput{
|
||||
Bucket: aws.String("Bucket"),
|
||||
Key: aws.String("Key"),
|
||||
Body: body,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("expect no error, got %v", err)
|
||||
}
|
||||
|
||||
expectOps := []string{
|
||||
"CreateMultipartUpload",
|
||||
"UploadPart",
|
||||
"UploadPart",
|
||||
"CompleteMultipartUpload",
|
||||
}
|
||||
if e, a := expectOps, *ops; !reflect.DeepEqual(e, a) {
|
||||
t.Errorf("expect %v ops, got %v", e, a)
|
||||
}
|
||||
}
|
||||
|
||||
func createTempFile(t *testing.T, size int64) (*os.File, func(*testing.T), error) {
|
||||
file, err := ioutil.TempFile(os.TempDir(), aws.SDKName+t.Name())
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
filename := file.Name()
|
||||
if err := file.Truncate(size); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return file,
|
||||
func(t *testing.T) {
|
||||
if err := file.Close(); err != nil {
|
||||
t.Errorf("failed to close temp file, %s, %v", filename, err)
|
||||
}
|
||||
if err := os.Remove(filename); err != nil {
|
||||
t.Errorf("failed to remove temp file, %s, %v", filename, err)
|
||||
}
|
||||
},
|
||||
nil
|
||||
}
|
||||
|
||||
func buildFailHandlers(tb testing.TB, parts, retry int) []http.Handler {
|
||||
handlers := make([]http.Handler, parts)
|
||||
for i := 0; i < len(handlers); i++ {
|
||||
handlers[i] = &failPartHandler{
|
||||
tb: tb,
|
||||
failsRemaining: retry,
|
||||
successHandler: successPartHandler{tb: tb},
|
||||
}
|
||||
}
|
||||
|
||||
return handlers
|
||||
}
|
||||
|
||||
func TestUploadRetry(t *testing.T) {
|
||||
const numParts, retries = 3, 10
|
||||
|
||||
testFile, testFileCleanup, err := createTempFile(t, s3manager.DefaultUploadPartSize*numParts)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create test file, %v", err)
|
||||
}
|
||||
defer testFileCleanup(t)
|
||||
|
||||
cases := map[string]struct {
|
||||
Body io.Reader
|
||||
PartHandlers func(testing.TB) []http.Handler
|
||||
}{
|
||||
"bytes.Buffer": {
|
||||
Body: bytes.NewBuffer(make([]byte, s3manager.DefaultUploadPartSize*numParts)),
|
||||
PartHandlers: func(tb testing.TB) []http.Handler {
|
||||
return buildFailHandlers(tb, numParts, retries)
|
||||
},
|
||||
},
|
||||
"bytes.Reader": {
|
||||
Body: bytes.NewReader(make([]byte, s3manager.DefaultUploadPartSize*numParts)),
|
||||
PartHandlers: func(tb testing.TB) []http.Handler {
|
||||
return buildFailHandlers(tb, numParts, retries)
|
||||
},
|
||||
},
|
||||
"os.File": {
|
||||
Body: testFile,
|
||||
PartHandlers: func(tb testing.TB) []http.Handler {
|
||||
return buildFailHandlers(tb, numParts, retries)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for name, c := range cases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
mux := newMockS3UploadServer(t, c.PartHandlers(t))
|
||||
server := httptest.NewServer(mux)
|
||||
defer server.Close()
|
||||
|
||||
sess := unit.Session.Copy(&aws.Config{
|
||||
Endpoint: aws.String(server.URL),
|
||||
S3ForcePathStyle: aws.Bool(true),
|
||||
DisableSSL: aws.Bool(true),
|
||||
Logger: t,
|
||||
MaxRetries: aws.Int(retries + 1),
|
||||
SleepDelay: func(time.Duration) {},
|
||||
|
||||
LogLevel: aws.LogLevel(
|
||||
aws.LogDebugWithRequestErrors | aws.LogDebugWithRequestRetries,
|
||||
),
|
||||
//Credentials: credentials.AnonymousCredentials,
|
||||
})
|
||||
|
||||
uploader := s3manager.NewUploader(sess, func(u *s3manager.Uploader) {
|
||||
// u.Concurrency = 1
|
||||
})
|
||||
_, err := uploader.Upload(&s3manager.UploadInput{
|
||||
Bucket: aws.String("bucket"),
|
||||
Key: aws.String("key"),
|
||||
Body: c.Body,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("expect no error, got %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type mockS3UploadServer struct {
|
||||
*http.ServeMux
|
||||
|
||||
tb testing.TB
|
||||
partHandler []http.Handler
|
||||
}
|
||||
|
||||
func newMockS3UploadServer(tb testing.TB, partHandler []http.Handler) *mockS3UploadServer {
|
||||
s := &mockS3UploadServer{
|
||||
ServeMux: http.NewServeMux(),
|
||||
partHandler: partHandler,
|
||||
tb: tb,
|
||||
}
|
||||
|
||||
s.HandleFunc("/", s.handleRequest)
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func (s mockS3UploadServer) handleRequest(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
|
||||
_, hasUploads := r.URL.Query()["uploads"]
|
||||
|
||||
switch {
|
||||
case r.Method == "POST" && hasUploads:
|
||||
// CreateMultipartUpload
|
||||
w.Header().Set("Content-Length", strconv.Itoa(len(createUploadResp)))
|
||||
w.Write([]byte(createUploadResp))
|
||||
|
||||
case r.Method == "PUT":
|
||||
// UploadPart
|
||||
partNumStr := r.URL.Query().Get("partNumber")
|
||||
id, err := strconv.Atoi(partNumStr)
|
||||
if err != nil {
|
||||
failRequest(w, 400, "BadRequest",
|
||||
fmt.Sprintf("unable to parse partNumber, %q, %v",
|
||||
partNumStr, err))
|
||||
return
|
||||
}
|
||||
id--
|
||||
if id < 0 || id >= len(s.partHandler) {
|
||||
failRequest(w, 400, "BadRequest",
|
||||
fmt.Sprintf("invalid partNumber %v", id))
|
||||
return
|
||||
}
|
||||
s.partHandler[id].ServeHTTP(w, r)
|
||||
|
||||
case r.Method == "POST":
|
||||
// CompleteMultipartUpload
|
||||
w.Header().Set("Content-Length", strconv.Itoa(len(completeUploadResp)))
|
||||
w.Write([]byte(completeUploadResp))
|
||||
|
||||
case r.Method == "DELETE":
|
||||
// AbortMultipartUpload
|
||||
w.Header().Set("Content-Length", strconv.Itoa(len(abortUploadResp)))
|
||||
w.WriteHeader(200)
|
||||
w.Write([]byte(abortUploadResp))
|
||||
|
||||
default:
|
||||
failRequest(w, 400, "BadRequest",
|
||||
fmt.Sprintf("invalid request %v %v", r.Method, r.URL))
|
||||
}
|
||||
}
|
||||
|
||||
func failRequest(w http.ResponseWriter, status int, code, msg string) {
|
||||
msg = fmt.Sprintf(baseRequestErrorResp, code, msg)
|
||||
w.Header().Set("Content-Length", strconv.Itoa(len(msg)))
|
||||
w.WriteHeader(status)
|
||||
w.Write([]byte(msg))
|
||||
}
|
||||
|
||||
type successPartHandler struct {
|
||||
tb testing.TB
|
||||
}
|
||||
|
||||
func (h successPartHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
|
||||
n, err := io.Copy(ioutil.Discard, r.Body)
|
||||
if err != nil {
|
||||
failRequest(w, 400, "BadRequest",
|
||||
fmt.Sprintf("failed to read body, %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
contLenStr := r.Header.Get("Content-Length")
|
||||
expectLen, err := strconv.ParseInt(contLenStr, 10, 64)
|
||||
if err != nil {
|
||||
h.tb.Logf("expect content-length, got %q, %v", contLenStr, err)
|
||||
failRequest(w, 400, "BadRequest",
|
||||
fmt.Sprintf("unable to get content-length %v", err))
|
||||
return
|
||||
}
|
||||
if e, a := expectLen, n; e != a {
|
||||
h.tb.Logf("expect %v read, got %v", e, a)
|
||||
failRequest(w, 400, "BadRequest",
|
||||
fmt.Sprintf(
|
||||
"content-length and body do not match, %v, %v", e, a))
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Length", strconv.Itoa(len(uploadPartResp)))
|
||||
w.Write([]byte(uploadPartResp))
|
||||
}
|
||||
|
||||
type failPartHandler struct {
|
||||
tb testing.TB
|
||||
|
||||
failsRemaining int
|
||||
successHandler http.Handler
|
||||
}
|
||||
|
||||
func (h *failPartHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
|
||||
if h.failsRemaining == 0 && h.successHandler != nil {
|
||||
h.successHandler.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
io.Copy(ioutil.Discard, r.Body)
|
||||
|
||||
failRequest(w, 500, "InternalException",
|
||||
fmt.Sprintf("mock error, partNumber %v", r.URL.Query().Get("partNumber")))
|
||||
|
||||
h.failsRemaining--
|
||||
}
|
||||
|
||||
const createUploadResp = `
|
||||
<CreateMultipartUploadResponse>
|
||||
<Bucket>bucket</Bucket>
|
||||
<Key>key</Key>
|
||||
<UploadId>abc123</UploadId>
|
||||
</CreateMultipartUploadResponse>
|
||||
`
|
||||
const uploadPartResp = `
|
||||
<UploadPartResponse>
|
||||
<ETag>key</ETag>
|
||||
</UploadPartResponse>
|
||||
`
|
||||
const baseRequestErrorResp = `
|
||||
<Error>
|
||||
<Code>%s</Code>
|
||||
<Message>%s</Message>
|
||||
<RequestId>request-id</RequestId>
|
||||
<HostId>host-id</HostId>
|
||||
</Error>
|
||||
`
|
||||
const completeUploadResp = `
|
||||
<CompleteMultipartUploadResponse>
|
||||
<Bucket>bucket</Bucket>
|
||||
<Key>key</Key>
|
||||
<ETag>key</ETag>
|
||||
<Location>https://bucket.us-west-2.amazonaws.com/key</Location>
|
||||
<UploadId>abc123</UploadId>
|
||||
</CompleteMultipartUploadResponse>
|
||||
`
|
||||
|
||||
const abortUploadResp = `
|
||||
<AbortMultipartUploadResponse>
|
||||
</AbortMultipartUploadResponse>
|
||||
`
|
||||
|
||||
Reference in New Issue
Block a user