Switch to using the dep tool and update all the dependencies
This commit is contained in:
+83
@@ -0,0 +1,83 @@
|
||||
package s3manager
|
||||
|
||||
import (
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"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"
|
||||
)
|
||||
|
||||
// GetBucketRegion will attempt to get the region for a bucket using the
|
||||
// regionHint to determine which AWS partition to perform the query on.
|
||||
//
|
||||
// The request will not be signed, and will not use your AWS credentials.
|
||||
//
|
||||
// A "NotFound" error code will be returned if the bucket does not exist in
|
||||
// the AWS partition the regionHint belongs to.
|
||||
//
|
||||
// For example to get the region of a bucket which exists in "eu-central-1"
|
||||
// you could provide a region hint of "us-west-2".
|
||||
//
|
||||
// sess := session.Must(session.NewSession())
|
||||
//
|
||||
// bucket := "my-bucket"
|
||||
// region, err := s3manager.GetBucketRegion(ctx, sess, bucket, "us-west-2")
|
||||
// if err != nil {
|
||||
// if aerr, ok := err.(awserr.Error); ok && aerr.Code() == "NotFound" {
|
||||
// fmt.Fprintf(os.Stderr, "unable to find bucket %s's region not found\n", bucket)
|
||||
// }
|
||||
// return err
|
||||
// }
|
||||
// fmt.Printf("Bucket %s is in %s region\n", bucket, region)
|
||||
//
|
||||
func GetBucketRegion(ctx aws.Context, c client.ConfigProvider, bucket, regionHint string, opts ...request.Option) (string, error) {
|
||||
svc := s3.New(c, &aws.Config{
|
||||
Region: aws.String(regionHint),
|
||||
})
|
||||
return GetBucketRegionWithClient(ctx, svc, bucket, opts...)
|
||||
}
|
||||
|
||||
const bucketRegionHeader = "X-Amz-Bucket-Region"
|
||||
|
||||
// GetBucketRegionWithClient is the same as GetBucketRegion with the exception
|
||||
// that it takes a S3 service client instead of a Session. The regionHint is
|
||||
// derived from the region the S3 service client was created in.
|
||||
//
|
||||
// See GetBucketRegion for more information.
|
||||
func GetBucketRegionWithClient(ctx aws.Context, svc s3iface.S3API, bucket string, opts ...request.Option) (string, error) {
|
||||
req, _ := svc.HeadBucketRequest(&s3.HeadBucketInput{
|
||||
Bucket: aws.String(bucket),
|
||||
})
|
||||
req.Config.S3ForcePathStyle = aws.Bool(true)
|
||||
req.Config.Credentials = credentials.AnonymousCredentials
|
||||
req.SetContext(ctx)
|
||||
|
||||
// Disable HTTP redirects to prevent an invalid 301 from eating the response
|
||||
// because Go's HTTP client will fail, and drop the response if an 301 is
|
||||
// received without a location header. S3 will return a 301 without the
|
||||
// location header for HeadObject API calls.
|
||||
req.DisableFollowRedirects = true
|
||||
|
||||
var bucketRegion string
|
||||
req.Handlers.Send.PushBack(func(r *request.Request) {
|
||||
bucketRegion = r.HTTPResponse.Header.Get(bucketRegionHeader)
|
||||
if len(bucketRegion) == 0 {
|
||||
return
|
||||
}
|
||||
r.HTTPResponse.StatusCode = 200
|
||||
r.HTTPResponse.Status = "OK"
|
||||
r.Error = nil
|
||||
})
|
||||
|
||||
req.ApplyOptions(opts...)
|
||||
|
||||
if err := req.Send(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
bucketRegion = s3.NormalizeBucketLocation(bucketRegion)
|
||||
|
||||
return bucketRegion, nil
|
||||
}
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
package s3manager
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/awserr"
|
||||
"github.com/aws/aws-sdk-go/awstesting/unit"
|
||||
"github.com/aws/aws-sdk-go/service/s3"
|
||||
)
|
||||
|
||||
func testSetupGetBucketRegionServer(region string, statusCode int, incHeader bool) *httptest.Server {
|
||||
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if incHeader {
|
||||
w.Header().Set(bucketRegionHeader, region)
|
||||
}
|
||||
w.WriteHeader(statusCode)
|
||||
}))
|
||||
}
|
||||
|
||||
var testGetBucketRegionCases = []struct {
|
||||
RespRegion string
|
||||
StatusCode int
|
||||
}{
|
||||
{"bucket-region", 301},
|
||||
{"bucket-region", 403},
|
||||
{"bucket-region", 200},
|
||||
}
|
||||
|
||||
func TestGetBucketRegion_Exists(t *testing.T) {
|
||||
for i, c := range testGetBucketRegionCases {
|
||||
server := testSetupGetBucketRegionServer(c.RespRegion, c.StatusCode, true)
|
||||
|
||||
sess := unit.Session.Copy()
|
||||
sess.Config.Endpoint = aws.String(server.URL)
|
||||
sess.Config.DisableSSL = aws.Bool(true)
|
||||
|
||||
ctx := aws.BackgroundContext()
|
||||
region, err := GetBucketRegion(ctx, sess, "bucket", "region")
|
||||
if err != nil {
|
||||
t.Fatalf("%d, expect no error, got %v", i, err)
|
||||
}
|
||||
if e, a := c.RespRegion, region; e != a {
|
||||
t.Errorf("%d, expect %q region, got %q", i, e, a)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetBucketRegion_NotExists(t *testing.T) {
|
||||
server := testSetupGetBucketRegionServer("ignore-region", 404, false)
|
||||
|
||||
sess := unit.Session.Copy()
|
||||
sess.Config.Endpoint = aws.String(server.URL)
|
||||
sess.Config.DisableSSL = aws.Bool(true)
|
||||
|
||||
ctx := aws.BackgroundContext()
|
||||
region, err := GetBucketRegion(ctx, sess, "bucket", "region")
|
||||
if err == nil {
|
||||
t.Fatalf("expect error, but did not get one")
|
||||
}
|
||||
aerr := err.(awserr.Error)
|
||||
if e, a := "NotFound", aerr.Code(); e != a {
|
||||
t.Errorf("expect %s error code, got %s", e, a)
|
||||
}
|
||||
if len(region) != 0 {
|
||||
t.Errorf("expect region not to be set, got %q", region)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetBucketRegionWithClient(t *testing.T) {
|
||||
for i, c := range testGetBucketRegionCases {
|
||||
server := testSetupGetBucketRegionServer(c.RespRegion, c.StatusCode, true)
|
||||
|
||||
svc := s3.New(unit.Session, &aws.Config{
|
||||
Region: aws.String("region"),
|
||||
Endpoint: aws.String(server.URL),
|
||||
DisableSSL: aws.Bool(true),
|
||||
})
|
||||
|
||||
ctx := aws.BackgroundContext()
|
||||
|
||||
region, err := GetBucketRegionWithClient(ctx, svc, "bucket")
|
||||
if err != nil {
|
||||
t.Fatalf("%d, expect no error, got %v", i, err)
|
||||
}
|
||||
if e, a := c.RespRegion, region; e != a {
|
||||
t.Errorf("%d, expect %q region, got %q", i, e, a)
|
||||
}
|
||||
}
|
||||
}
|
||||
+61
-23
@@ -40,6 +40,17 @@ type Downloader struct {
|
||||
|
||||
// An S3 client to use when performing downloads.
|
||||
S3 s3iface.S3API
|
||||
|
||||
// List of request options that will be passed down to individual API
|
||||
// operation requests made by the downloader.
|
||||
RequestOptions []request.Option
|
||||
}
|
||||
|
||||
// WithDownloaderRequestOptions appends to the Downloader's API request options.
|
||||
func WithDownloaderRequestOptions(opts ...request.Option) func(*Downloader) {
|
||||
return func(d *Downloader) {
|
||||
d.RequestOptions = append(d.RequestOptions, opts...)
|
||||
}
|
||||
}
|
||||
|
||||
// NewDownloader creates a new Downloader instance to downloads objects from
|
||||
@@ -50,7 +61,7 @@ type Downloader struct {
|
||||
//
|
||||
// Example:
|
||||
// // The session the S3 Downloader will use
|
||||
// sess, err := session.NewSession()
|
||||
// sess := session.Must(session.NewSession())
|
||||
//
|
||||
// // Create a downloader with the session and default options
|
||||
// downloader := s3manager.NewDownloader(sess)
|
||||
@@ -79,7 +90,7 @@ func NewDownloader(c client.ConfigProvider, options ...func(*Downloader)) *Downl
|
||||
//
|
||||
// Example:
|
||||
// // The session the S3 Downloader will use
|
||||
// sess, err := session.NewSession()
|
||||
// sess := session.Must(session.NewSession())
|
||||
//
|
||||
// // The S3 client the S3 Downloader will use
|
||||
// s3Svc := s3.new(sess)
|
||||
@@ -120,23 +131,47 @@ 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.
|
||||
func (d Downloader) Download(w io.WriterAt, input *s3.GetObjectInput, options ...func(*Downloader)) (n int64, err error) {
|
||||
impl := downloader{w: w, in: input, ctx: d}
|
||||
return d.DownloadWithContext(aws.BackgroundContext(), w, input, options...)
|
||||
}
|
||||
|
||||
// DownloadWithContext downloads an object in S3 and writes the payload into w
|
||||
// using concurrent GET requests.
|
||||
//
|
||||
// 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
|
||||
// DownloadWithContext may create sub-contexts for individual underlying
|
||||
// requests.
|
||||
//
|
||||
// Additional functional options can be provided to configure the individual
|
||||
// download. These options are copies of the Downloader instance Download is
|
||||
// called from. Modifying the options will not impact the original Downloader
|
||||
// instance. Use the WithDownloaderRequestOptions helper function to pass in request
|
||||
// options that will be applied to all API operations made with this downloader.
|
||||
//
|
||||
// The w io.WriterAt can be satisfied by an os.File to do multipart concurrent
|
||||
// downloads, or in memory []byte wrapper using aws.WriteAtBuffer.
|
||||
//
|
||||
// It is safe to call this method concurrently across goroutines.
|
||||
func (d Downloader) DownloadWithContext(ctx aws.Context, w io.WriterAt, input *s3.GetObjectInput, options ...func(*Downloader)) (n int64, err error) {
|
||||
impl := downloader{w: w, in: input, cfg: d, ctx: ctx}
|
||||
|
||||
for _, option := range options {
|
||||
option(&impl.ctx)
|
||||
option(&impl.cfg)
|
||||
}
|
||||
impl.cfg.RequestOptions = append(impl.cfg.RequestOptions, request.WithAppendUserAgent("S3Manager"))
|
||||
|
||||
if s, ok := d.S3.(maxRetrier); ok {
|
||||
impl.partBodyMaxRetries = s.MaxRetries()
|
||||
}
|
||||
|
||||
impl.totalBytes = -1
|
||||
if impl.ctx.Concurrency == 0 {
|
||||
impl.ctx.Concurrency = DefaultDownloadConcurrency
|
||||
if impl.cfg.Concurrency == 0 {
|
||||
impl.cfg.Concurrency = DefaultDownloadConcurrency
|
||||
}
|
||||
|
||||
if impl.ctx.PartSize == 0 {
|
||||
impl.ctx.PartSize = DefaultDownloadPartSize
|
||||
if impl.cfg.PartSize == 0 {
|
||||
impl.cfg.PartSize = DefaultDownloadPartSize
|
||||
}
|
||||
|
||||
return impl.download()
|
||||
@@ -144,7 +179,8 @@ func (d Downloader) Download(w io.WriterAt, input *s3.GetObjectInput, options ..
|
||||
|
||||
// downloader is the implementation structure used internally by Downloader.
|
||||
type downloader struct {
|
||||
ctx Downloader
|
||||
ctx aws.Context
|
||||
cfg Downloader
|
||||
|
||||
in *s3.GetObjectInput
|
||||
w io.WriterAt
|
||||
@@ -168,9 +204,9 @@ func (d *downloader) download() (n int64, err error) {
|
||||
|
||||
if total := d.getTotalBytes(); total >= 0 {
|
||||
// Spin up workers
|
||||
ch := make(chan dlchunk, d.ctx.Concurrency)
|
||||
ch := make(chan dlchunk, d.cfg.Concurrency)
|
||||
|
||||
for i := 0; i < d.ctx.Concurrency; i++ {
|
||||
for i := 0; i < d.cfg.Concurrency; i++ {
|
||||
d.wg.Add(1)
|
||||
go d.downloadPart(ch)
|
||||
}
|
||||
@@ -182,8 +218,8 @@ func (d *downloader) download() (n int64, err error) {
|
||||
}
|
||||
|
||||
// Queue the next range of bytes to read.
|
||||
ch <- dlchunk{w: d.w, start: d.pos, size: d.ctx.PartSize}
|
||||
d.pos += d.ctx.PartSize
|
||||
ch <- dlchunk{w: d.w, start: d.pos, size: d.cfg.PartSize}
|
||||
d.pos += d.cfg.PartSize
|
||||
}
|
||||
|
||||
// Wait for completion
|
||||
@@ -219,13 +255,17 @@ func (d *downloader) downloadPart(ch chan dlchunk) {
|
||||
defer d.wg.Done()
|
||||
for {
|
||||
chunk, ok := <-ch
|
||||
if !ok || d.getErr() != nil {
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
if d.getErr() != nil {
|
||||
// Drain the channel if there is an error, to prevent deadlocking
|
||||
// of download producer.
|
||||
continue
|
||||
}
|
||||
|
||||
if err := d.downloadChunk(chunk); err != nil {
|
||||
d.setErr(err)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -237,8 +277,8 @@ func (d *downloader) getChunk() {
|
||||
return
|
||||
}
|
||||
|
||||
chunk := dlchunk{w: d.w, start: d.pos, size: d.ctx.PartSize}
|
||||
d.pos += d.ctx.PartSize
|
||||
chunk := dlchunk{w: d.w, start: d.pos, size: d.cfg.PartSize}
|
||||
d.pos += d.cfg.PartSize
|
||||
|
||||
if err := d.downloadChunk(chunk); err != nil {
|
||||
d.setErr(err)
|
||||
@@ -257,10 +297,8 @@ func (d *downloader) downloadChunk(chunk dlchunk) error {
|
||||
var n int64
|
||||
var err error
|
||||
for retry := 0; retry <= d.partBodyMaxRetries; retry++ {
|
||||
req, resp := d.ctx.S3.GetObjectRequest(in)
|
||||
req.Handlers.Build.PushBack(request.MakeAddToUserAgentFreeFormHandler("S3Manager"))
|
||||
|
||||
err = req.Send()
|
||||
var resp *s3.GetObjectOutput
|
||||
resp, err = d.cfg.S3.GetObjectWithContext(d.ctx, in, d.cfg.RequestOptions...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -273,7 +311,7 @@ func (d *downloader) downloadChunk(chunk dlchunk) error {
|
||||
}
|
||||
|
||||
chunk.cur = 0
|
||||
logMessage(d.ctx.S3, aws.LogDebugWithRequestRetries,
|
||||
logMessage(d.cfg.S3, aws.LogDebugWithRequestRetries,
|
||||
fmt.Sprintf("DEBUG: object part body download interrupted %s, err, %v, retrying attempt %d",
|
||||
aws.StringValue(in.Key), err, retry))
|
||||
}
|
||||
@@ -320,7 +358,7 @@ func (d *downloader) setTotalBytes(resp *s3.GetObjectOutput) {
|
||||
}
|
||||
|
||||
if resp.ContentRange == nil {
|
||||
// ContentRange is nil when the full file contents is provied, and
|
||||
// ContentRange is nil when the full file contents is provided, and
|
||||
// is not chunked. Use ContentLength instead.
|
||||
if resp.ContentLength != nil {
|
||||
d.totalBytes = *resp.ContentLength
|
||||
|
||||
+515
@@ -0,0 +1,515 @@
|
||||
package s3manager_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/awserr"
|
||||
"github.com/aws/aws-sdk-go/aws/request"
|
||||
"github.com/aws/aws-sdk-go/awstesting"
|
||||
"github.com/aws/aws-sdk-go/awstesting/unit"
|
||||
"github.com/aws/aws-sdk-go/service/s3"
|
||||
"github.com/aws/aws-sdk-go/service/s3/s3manager"
|
||||
)
|
||||
|
||||
func dlLoggingSvc(data []byte) (*s3.S3, *[]string, *[]string) {
|
||||
var m sync.Mutex
|
||||
names := []string{}
|
||||
ranges := []string{}
|
||||
|
||||
svc := s3.New(unit.Session)
|
||||
svc.Handlers.Send.Clear()
|
||||
svc.Handlers.Send.PushBack(func(r *request.Request) {
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
|
||||
names = append(names, r.Operation.Name)
|
||||
ranges = append(ranges, *r.Params.(*s3.GetObjectInput).Range)
|
||||
|
||||
rerng := regexp.MustCompile(`bytes=(\d+)-(\d+)`)
|
||||
rng := rerng.FindStringSubmatch(r.HTTPRequest.Header.Get("Range"))
|
||||
start, _ := strconv.ParseInt(rng[1], 10, 64)
|
||||
fin, _ := strconv.ParseInt(rng[2], 10, 64)
|
||||
fin++
|
||||
|
||||
if fin > int64(len(data)) {
|
||||
fin = int64(len(data))
|
||||
}
|
||||
|
||||
bodyBytes := data[start:fin]
|
||||
r.HTTPResponse = &http.Response{
|
||||
StatusCode: 200,
|
||||
Body: ioutil.NopCloser(bytes.NewReader(bodyBytes)),
|
||||
Header: http.Header{},
|
||||
}
|
||||
r.HTTPResponse.Header.Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d",
|
||||
start, fin-1, len(data)))
|
||||
r.HTTPResponse.Header.Set("Content-Length", fmt.Sprintf("%d", len(bodyBytes)))
|
||||
})
|
||||
|
||||
return svc, &names, &ranges
|
||||
}
|
||||
|
||||
func dlLoggingSvcNoChunk(data []byte) (*s3.S3, *[]string) {
|
||||
var m sync.Mutex
|
||||
names := []string{}
|
||||
|
||||
svc := s3.New(unit.Session)
|
||||
svc.Handlers.Send.Clear()
|
||||
svc.Handlers.Send.PushBack(func(r *request.Request) {
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
|
||||
names = append(names, r.Operation.Name)
|
||||
|
||||
r.HTTPResponse = &http.Response{
|
||||
StatusCode: 200,
|
||||
Body: ioutil.NopCloser(bytes.NewReader(data[:])),
|
||||
Header: http.Header{},
|
||||
}
|
||||
r.HTTPResponse.Header.Set("Content-Length", fmt.Sprintf("%d", len(data)))
|
||||
})
|
||||
|
||||
return svc, &names
|
||||
}
|
||||
|
||||
func dlLoggingSvcNoContentRangeLength(data []byte, states []int) (*s3.S3, *[]string) {
|
||||
var m sync.Mutex
|
||||
names := []string{}
|
||||
var index int = 0
|
||||
|
||||
svc := s3.New(unit.Session)
|
||||
svc.Handlers.Send.Clear()
|
||||
svc.Handlers.Send.PushBack(func(r *request.Request) {
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
|
||||
names = append(names, r.Operation.Name)
|
||||
|
||||
r.HTTPResponse = &http.Response{
|
||||
StatusCode: states[index],
|
||||
Body: ioutil.NopCloser(bytes.NewReader(data[:])),
|
||||
Header: http.Header{},
|
||||
}
|
||||
index++
|
||||
})
|
||||
|
||||
return svc, &names
|
||||
}
|
||||
|
||||
func dlLoggingSvcContentRangeTotalAny(data []byte, states []int) (*s3.S3, *[]string) {
|
||||
var m sync.Mutex
|
||||
names := []string{}
|
||||
ranges := []string{}
|
||||
var index int = 0
|
||||
|
||||
svc := s3.New(unit.Session)
|
||||
svc.Handlers.Send.Clear()
|
||||
svc.Handlers.Send.PushBack(func(r *request.Request) {
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
|
||||
names = append(names, r.Operation.Name)
|
||||
ranges = append(ranges, *r.Params.(*s3.GetObjectInput).Range)
|
||||
|
||||
rerng := regexp.MustCompile(`bytes=(\d+)-(\d+)`)
|
||||
rng := rerng.FindStringSubmatch(r.HTTPRequest.Header.Get("Range"))
|
||||
start, _ := strconv.ParseInt(rng[1], 10, 64)
|
||||
fin, _ := strconv.ParseInt(rng[2], 10, 64)
|
||||
fin++
|
||||
|
||||
if fin >= int64(len(data)) {
|
||||
fin = int64(len(data))
|
||||
}
|
||||
|
||||
// Setting start and finish to 0 because this state of 1 is suppose to
|
||||
// be an error state of 416
|
||||
if index == len(states)-1 {
|
||||
start = 0
|
||||
fin = 0
|
||||
}
|
||||
|
||||
bodyBytes := data[start:fin]
|
||||
|
||||
r.HTTPResponse = &http.Response{
|
||||
StatusCode: states[index],
|
||||
Body: ioutil.NopCloser(bytes.NewReader(bodyBytes)),
|
||||
Header: http.Header{},
|
||||
}
|
||||
r.HTTPResponse.Header.Set("Content-Range", fmt.Sprintf("bytes %d-%d/*",
|
||||
start, fin-1))
|
||||
index++
|
||||
})
|
||||
|
||||
return svc, &names
|
||||
}
|
||||
|
||||
func dlLoggingSvcWithErrReader(cases []testErrReader) (*s3.S3, *[]string) {
|
||||
var m sync.Mutex
|
||||
names := []string{}
|
||||
var index int = 0
|
||||
|
||||
svc := s3.New(unit.Session, &aws.Config{
|
||||
MaxRetries: aws.Int(len(cases) - 1),
|
||||
})
|
||||
svc.Handlers.Send.Clear()
|
||||
svc.Handlers.Send.PushBack(func(r *request.Request) {
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
|
||||
names = append(names, r.Operation.Name)
|
||||
|
||||
c := cases[index]
|
||||
|
||||
r.HTTPResponse = &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: ioutil.NopCloser(&c),
|
||||
Header: http.Header{},
|
||||
}
|
||||
r.HTTPResponse.Header.Set("Content-Range",
|
||||
fmt.Sprintf("bytes %d-%d/%d", 0, c.Len-1, c.Len))
|
||||
r.HTTPResponse.Header.Set("Content-Length", fmt.Sprintf("%d", c.Len))
|
||||
index++
|
||||
})
|
||||
|
||||
return svc, &names
|
||||
}
|
||||
|
||||
func TestDownloadOrder(t *testing.T) {
|
||||
s, names, ranges := dlLoggingSvc(buf12MB)
|
||||
|
||||
d := s3manager.NewDownloaderWithClient(s, func(d *s3manager.Downloader) {
|
||||
d.Concurrency = 1
|
||||
})
|
||||
w := &aws.WriteAtBuffer{}
|
||||
n, err := d.Download(w, &s3.GetObjectInput{
|
||||
Bucket: aws.String("bucket"),
|
||||
Key: aws.String("key"),
|
||||
})
|
||||
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, int64(len(buf12MB)), n)
|
||||
assert.Equal(t, []string{"GetObject", "GetObject", "GetObject"}, *names)
|
||||
assert.Equal(t, []string{"bytes=0-5242879", "bytes=5242880-10485759", "bytes=10485760-15728639"}, *ranges)
|
||||
|
||||
count := 0
|
||||
for _, b := range w.Bytes() {
|
||||
count += int(b)
|
||||
}
|
||||
assert.Equal(t, 0, count)
|
||||
}
|
||||
|
||||
func TestDownloadZero(t *testing.T) {
|
||||
s, names, ranges := dlLoggingSvc([]byte{})
|
||||
|
||||
d := s3manager.NewDownloaderWithClient(s)
|
||||
w := &aws.WriteAtBuffer{}
|
||||
n, err := d.Download(w, &s3.GetObjectInput{
|
||||
Bucket: aws.String("bucket"),
|
||||
Key: aws.String("key"),
|
||||
})
|
||||
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, int64(0), n)
|
||||
assert.Equal(t, []string{"GetObject"}, *names)
|
||||
assert.Equal(t, []string{"bytes=0-5242879"}, *ranges)
|
||||
}
|
||||
|
||||
func TestDownloadSetPartSize(t *testing.T) {
|
||||
s, names, ranges := dlLoggingSvc([]byte{1, 2, 3})
|
||||
|
||||
d := s3manager.NewDownloaderWithClient(s, func(d *s3manager.Downloader) {
|
||||
d.Concurrency = 1
|
||||
d.PartSize = 1
|
||||
})
|
||||
w := &aws.WriteAtBuffer{}
|
||||
n, err := d.Download(w, &s3.GetObjectInput{
|
||||
Bucket: aws.String("bucket"),
|
||||
Key: aws.String("key"),
|
||||
})
|
||||
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, int64(3), n)
|
||||
assert.Equal(t, []string{"GetObject", "GetObject", "GetObject"}, *names)
|
||||
assert.Equal(t, []string{"bytes=0-0", "bytes=1-1", "bytes=2-2"}, *ranges)
|
||||
assert.Equal(t, []byte{1, 2, 3}, w.Bytes())
|
||||
}
|
||||
|
||||
func TestDownloadError(t *testing.T) {
|
||||
s, names, _ := dlLoggingSvc([]byte{1, 2, 3})
|
||||
|
||||
num := 0
|
||||
s.Handlers.Send.PushBack(func(r *request.Request) {
|
||||
num++
|
||||
if num > 1 {
|
||||
r.HTTPResponse.StatusCode = 400
|
||||
r.HTTPResponse.Body = ioutil.NopCloser(bytes.NewReader([]byte{}))
|
||||
}
|
||||
})
|
||||
|
||||
d := s3manager.NewDownloaderWithClient(s, func(d *s3manager.Downloader) {
|
||||
d.Concurrency = 1
|
||||
d.PartSize = 1
|
||||
})
|
||||
w := &aws.WriteAtBuffer{}
|
||||
n, err := d.Download(w, &s3.GetObjectInput{
|
||||
Bucket: aws.String("bucket"),
|
||||
Key: aws.String("key"),
|
||||
})
|
||||
|
||||
assert.NotNil(t, err)
|
||||
assert.Equal(t, int64(1), n)
|
||||
assert.Equal(t, []string{"GetObject", "GetObject"}, *names)
|
||||
assert.Equal(t, []byte{1}, w.Bytes())
|
||||
}
|
||||
|
||||
func TestDownloadNonChunk(t *testing.T) {
|
||||
s, names := dlLoggingSvcNoChunk(buf2MB)
|
||||
|
||||
d := s3manager.NewDownloaderWithClient(s, func(d *s3manager.Downloader) {
|
||||
d.Concurrency = 1
|
||||
})
|
||||
w := &aws.WriteAtBuffer{}
|
||||
n, err := d.Download(w, &s3.GetObjectInput{
|
||||
Bucket: aws.String("bucket"),
|
||||
Key: aws.String("key"),
|
||||
})
|
||||
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, int64(len(buf2MB)), n)
|
||||
assert.Equal(t, []string{"GetObject"}, *names)
|
||||
|
||||
count := 0
|
||||
for _, b := range w.Bytes() {
|
||||
count += int(b)
|
||||
}
|
||||
assert.Equal(t, 0, count)
|
||||
}
|
||||
|
||||
func TestDownloadNoContentRangeLength(t *testing.T) {
|
||||
s, names := dlLoggingSvcNoContentRangeLength(buf2MB, []int{200, 416})
|
||||
|
||||
d := s3manager.NewDownloaderWithClient(s, func(d *s3manager.Downloader) {
|
||||
d.Concurrency = 1
|
||||
})
|
||||
w := &aws.WriteAtBuffer{}
|
||||
n, err := d.Download(w, &s3.GetObjectInput{
|
||||
Bucket: aws.String("bucket"),
|
||||
Key: aws.String("key"),
|
||||
})
|
||||
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, int64(len(buf2MB)), n)
|
||||
assert.Equal(t, []string{"GetObject", "GetObject"}, *names)
|
||||
|
||||
count := 0
|
||||
for _, b := range w.Bytes() {
|
||||
count += int(b)
|
||||
}
|
||||
assert.Equal(t, 0, count)
|
||||
}
|
||||
|
||||
func TestDownloadContentRangeTotalAny(t *testing.T) {
|
||||
s, names := dlLoggingSvcContentRangeTotalAny(buf2MB, []int{200, 416})
|
||||
|
||||
d := s3manager.NewDownloaderWithClient(s, func(d *s3manager.Downloader) {
|
||||
d.Concurrency = 1
|
||||
})
|
||||
w := &aws.WriteAtBuffer{}
|
||||
n, err := d.Download(w, &s3.GetObjectInput{
|
||||
Bucket: aws.String("bucket"),
|
||||
Key: aws.String("key"),
|
||||
})
|
||||
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, int64(len(buf2MB)), n)
|
||||
assert.Equal(t, []string{"GetObject", "GetObject"}, *names)
|
||||
|
||||
count := 0
|
||||
for _, b := range w.Bytes() {
|
||||
count += int(b)
|
||||
}
|
||||
assert.Equal(t, 0, count)
|
||||
}
|
||||
|
||||
func TestDownloadPartBodyRetry_SuccessRetry(t *testing.T) {
|
||||
s, names := dlLoggingSvcWithErrReader([]testErrReader{
|
||||
{Buf: []byte("ab"), Len: 3, Err: io.ErrUnexpectedEOF},
|
||||
{Buf: []byte("123"), Len: 3, Err: io.EOF},
|
||||
})
|
||||
|
||||
d := s3manager.NewDownloaderWithClient(s, func(d *s3manager.Downloader) {
|
||||
d.Concurrency = 1
|
||||
})
|
||||
|
||||
w := &aws.WriteAtBuffer{}
|
||||
n, err := d.Download(w, &s3.GetObjectInput{
|
||||
Bucket: aws.String("bucket"),
|
||||
Key: aws.String("key"),
|
||||
})
|
||||
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, int64(3), n)
|
||||
assert.Equal(t, []string{"GetObject", "GetObject"}, *names)
|
||||
assert.Equal(t, []byte("123"), w.Bytes())
|
||||
}
|
||||
|
||||
func TestDownloadPartBodyRetry_SuccessNoRetry(t *testing.T) {
|
||||
s, names := dlLoggingSvcWithErrReader([]testErrReader{
|
||||
{Buf: []byte("abc"), Len: 3, Err: io.EOF},
|
||||
})
|
||||
|
||||
d := s3manager.NewDownloaderWithClient(s, func(d *s3manager.Downloader) {
|
||||
d.Concurrency = 1
|
||||
})
|
||||
|
||||
w := &aws.WriteAtBuffer{}
|
||||
n, err := d.Download(w, &s3.GetObjectInput{
|
||||
Bucket: aws.String("bucket"),
|
||||
Key: aws.String("key"),
|
||||
})
|
||||
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, int64(3), n)
|
||||
assert.Equal(t, []string{"GetObject"}, *names)
|
||||
assert.Equal(t, []byte("abc"), w.Bytes())
|
||||
}
|
||||
|
||||
func TestDownloadPartBodyRetry_FailRetry(t *testing.T) {
|
||||
s, names := dlLoggingSvcWithErrReader([]testErrReader{
|
||||
{Buf: []byte("ab"), Len: 3, Err: io.ErrUnexpectedEOF},
|
||||
})
|
||||
|
||||
d := s3manager.NewDownloaderWithClient(s, func(d *s3manager.Downloader) {
|
||||
d.Concurrency = 1
|
||||
})
|
||||
|
||||
w := &aws.WriteAtBuffer{}
|
||||
n, err := d.Download(w, &s3.GetObjectInput{
|
||||
Bucket: aws.String("bucket"),
|
||||
Key: aws.String("key"),
|
||||
})
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, int64(2), n)
|
||||
assert.Equal(t, []string{"GetObject"}, *names)
|
||||
assert.Equal(t, []byte("ab"), w.Bytes())
|
||||
}
|
||||
|
||||
func TestDownloadWithContextCanceled(t *testing.T) {
|
||||
d := s3manager.NewDownloader(unit.Session)
|
||||
|
||||
params := s3.GetObjectInput{
|
||||
Bucket: aws.String("Bucket"),
|
||||
Key: aws.String("Key"),
|
||||
}
|
||||
|
||||
ctx := &awstesting.FakeContext{DoneCh: make(chan struct{})}
|
||||
ctx.Error = fmt.Errorf("context canceled")
|
||||
close(ctx.DoneCh)
|
||||
|
||||
w := &aws.WriteAtBuffer{}
|
||||
|
||||
_, err := d.DownloadWithContext(ctx, w, ¶ms)
|
||||
if err == nil {
|
||||
t.Fatalf("expected error, did not get one")
|
||||
}
|
||||
aerr := err.(awserr.Error)
|
||||
if e, a := request.CanceledErrorCode, aerr.Code(); e != a {
|
||||
t.Errorf("expected error code %q, got %q", e, a)
|
||||
}
|
||||
if e, a := "canceled", aerr.Message(); !strings.Contains(a, e) {
|
||||
t.Errorf("expected error message to contain %q, but did not %q", e, a)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDownload_WithFailure(t *testing.T) {
|
||||
svc := s3.New(unit.Session)
|
||||
svc.Handlers.Send.Clear()
|
||||
|
||||
first := true
|
||||
svc.Handlers.Send.PushBack(func(r *request.Request) {
|
||||
if first {
|
||||
first = false
|
||||
body := bytes.NewReader(make([]byte, s3manager.DefaultDownloadPartSize))
|
||||
r.HTTPResponse = &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Status: http.StatusText(http.StatusOK),
|
||||
ContentLength: int64(body.Len()),
|
||||
Body: ioutil.NopCloser(body),
|
||||
Header: http.Header{},
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
// 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)
|
||||
})
|
||||
|
||||
start := time.Now()
|
||||
d := s3manager.NewDownloaderWithClient(svc, func(d *s3manager.Downloader) {
|
||||
d.Concurrency = 2
|
||||
})
|
||||
|
||||
w := &aws.WriteAtBuffer{}
|
||||
params := s3.GetObjectInput{
|
||||
Bucket: aws.String("Bucket"),
|
||||
Key: aws.String("Key"),
|
||||
}
|
||||
|
||||
// Expect this request to exit quickly after failure
|
||||
_, err := d.Download(w, ¶ms)
|
||||
if err == nil {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
type testErrReader struct {
|
||||
Buf []byte
|
||||
Err error
|
||||
Len int64
|
||||
|
||||
off int
|
||||
}
|
||||
|
||||
func (r *testErrReader) Read(p []byte) (int, error) {
|
||||
to := len(r.Buf) - r.off
|
||||
|
||||
n := copy(p, r.Buf[r.off:to])
|
||||
r.off += n
|
||||
|
||||
if n < len(p) {
|
||||
return n, r.Err
|
||||
|
||||
}
|
||||
|
||||
return n, nil
|
||||
}
|
||||
Generated
Vendored
+26
@@ -0,0 +1,26 @@
|
||||
// Package s3manageriface provides an interface for the s3manager package
|
||||
package s3manageriface
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/service/s3"
|
||||
"github.com/aws/aws-sdk-go/service/s3/s3manager"
|
||||
)
|
||||
|
||||
// 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)
|
||||
|
||||
// UploaderAPI is the interface type for s3manager.Uploader.
|
||||
type UploaderAPI interface {
|
||||
Upload(*s3manager.UploadInput, ...func(*s3manager.Uploader)) (*s3manager.UploadOutput, error)
|
||||
UploadWithContext(aws.Context, *s3manager.UploadInput, ...func(*s3manager.Uploader)) (*s3manager.UploadOutput, error)
|
||||
}
|
||||
|
||||
var _ UploaderAPI = (*s3manager.Uploader)(nil)
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
package s3manager_test
|
||||
|
||||
var buf12MB = make([]byte, 1024*1024*12)
|
||||
var buf2MB = make([]byte, 1024*1024*2)
|
||||
+88
-45
@@ -202,6 +202,13 @@ type UploadOutput struct {
|
||||
UploadID string
|
||||
}
|
||||
|
||||
// WithUploaderRequestOptions appends to the Uploader's API request options.
|
||||
func WithUploaderRequestOptions(opts ...request.Option) func(*Uploader) {
|
||||
return func(u *Uploader) {
|
||||
u.RequestOptions = append(u.RequestOptions, opts...)
|
||||
}
|
||||
}
|
||||
|
||||
// The Uploader structure that calls Upload(). It is safe to call Upload()
|
||||
// on this structure for multiple objects and across concurrent goroutines.
|
||||
// Mutating the Uploader's properties is not safe to be done concurrently.
|
||||
@@ -232,6 +239,10 @@ type Uploader struct {
|
||||
|
||||
// The client to use when uploading to S3.
|
||||
S3 s3iface.S3API
|
||||
|
||||
// List of request options that will be passed down to individual API
|
||||
// operation requests made by the uploader.
|
||||
RequestOptions []request.Option
|
||||
}
|
||||
|
||||
// NewUploader creates a new Uploader instance to upload objects to S3. Pass In
|
||||
@@ -241,7 +252,7 @@ type Uploader struct {
|
||||
//
|
||||
// Example:
|
||||
// // The session the S3 Uploader will use
|
||||
// sess, err := session.NewSession()
|
||||
// sess := session.Must(session.NewSession())
|
||||
//
|
||||
// // Create an uploader with the session and default options
|
||||
// uploader := s3manager.NewUploader(sess)
|
||||
@@ -272,7 +283,7 @@ func NewUploader(c client.ConfigProvider, options ...func(*Uploader)) *Uploader
|
||||
//
|
||||
// Example:
|
||||
// // The session the S3 Uploader will use
|
||||
// sess, err := session.NewSession()
|
||||
// sess := session.Must(session.NewSession())
|
||||
//
|
||||
// // S3 service client the Upload manager will use.
|
||||
// s3Svc := s3.New(sess)
|
||||
@@ -308,6 +319,9 @@ func NewUploaderWithClient(svc s3iface.S3API, options ...func(*Uploader)) *Uploa
|
||||
// upload. These options are copies of the Uploader instance Upload is called from.
|
||||
// Modifying the options will not impact the original Uploader instance.
|
||||
//
|
||||
// Use the WithUploaderRequestOptions helper function to pass in request
|
||||
// options that will be applied to all API operations made with this uploader.
|
||||
//
|
||||
// It is safe to call this method concurrently across goroutines.
|
||||
//
|
||||
// Example:
|
||||
@@ -327,18 +341,42 @@ func NewUploaderWithClient(svc s3iface.S3API, options ...func(*Uploader)) *Uploa
|
||||
// u.LeavePartsOnError = true // Don't delete the parts if the upload fails.
|
||||
// })
|
||||
func (u Uploader) Upload(input *UploadInput, options ...func(*Uploader)) (*UploadOutput, error) {
|
||||
i := uploader{in: input, ctx: u}
|
||||
return u.UploadWithContext(aws.BackgroundContext(), input, options...)
|
||||
}
|
||||
|
||||
for _, option := range options {
|
||||
option(&i.ctx)
|
||||
// UploadWithContext uploads an object to S3, intelligently buffering large
|
||||
// files into smaller chunks and sending them in parallel across multiple
|
||||
// goroutines. You can configure the buffer size and concurrency through the
|
||||
// Uploader's parameters.
|
||||
//
|
||||
// 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
|
||||
// UploadWithContext may create sub-contexts for individual underlying requests.
|
||||
//
|
||||
// Additional functional options can be provided to configure the individual
|
||||
// upload. These options are copies of the Uploader instance Upload is called from.
|
||||
// Modifying the options will not impact the original Uploader instance.
|
||||
//
|
||||
// Use the WithUploaderRequestOptions helper function to pass in request
|
||||
// options that will be applied to all API operations made with this uploader.
|
||||
//
|
||||
// It is safe to call this method concurrently across goroutines.
|
||||
func (u Uploader) UploadWithContext(ctx aws.Context, input *UploadInput, opts ...func(*Uploader)) (*UploadOutput, error) {
|
||||
i := uploader{in: input, cfg: u, ctx: ctx}
|
||||
|
||||
for _, opt := range opts {
|
||||
opt(&i.cfg)
|
||||
}
|
||||
i.cfg.RequestOptions = append(i.cfg.RequestOptions, request.WithAppendUserAgent("S3Manager"))
|
||||
|
||||
return i.upload()
|
||||
}
|
||||
|
||||
// internal structure to manage an upload to S3.
|
||||
type uploader struct {
|
||||
ctx Uploader
|
||||
ctx aws.Context
|
||||
cfg Uploader
|
||||
|
||||
in *UploadInput
|
||||
|
||||
@@ -351,7 +389,7 @@ type uploader struct {
|
||||
func (u *uploader) upload() (*UploadOutput, error) {
|
||||
u.init()
|
||||
|
||||
if u.ctx.PartSize < MinUploadPartSize {
|
||||
if u.cfg.PartSize < MinUploadPartSize {
|
||||
msg := fmt.Sprintf("part size must be at least %d bytes", MinUploadPartSize)
|
||||
return nil, awserr.New("ConfigError", msg, nil)
|
||||
}
|
||||
@@ -370,11 +408,11 @@ func (u *uploader) upload() (*UploadOutput, error) {
|
||||
|
||||
// init will initialize all default options.
|
||||
func (u *uploader) init() {
|
||||
if u.ctx.Concurrency == 0 {
|
||||
u.ctx.Concurrency = DefaultUploadConcurrency
|
||||
if u.cfg.Concurrency == 0 {
|
||||
u.cfg.Concurrency = DefaultUploadConcurrency
|
||||
}
|
||||
if u.ctx.PartSize == 0 {
|
||||
u.ctx.PartSize = DefaultUploadPartSize
|
||||
if u.cfg.PartSize == 0 {
|
||||
u.cfg.PartSize = DefaultUploadPartSize
|
||||
}
|
||||
|
||||
// Try to get the total size for some optimizations
|
||||
@@ -399,10 +437,10 @@ func (u *uploader) initSize() {
|
||||
|
||||
// Try to adjust partSize if it is too small and account for
|
||||
// integer division truncation.
|
||||
if u.totalSize/u.ctx.PartSize >= int64(u.ctx.MaxUploadParts) {
|
||||
if u.totalSize/u.cfg.PartSize >= int64(u.cfg.MaxUploadParts) {
|
||||
// Add one to the part size to account for remainders
|
||||
// during the size calculation. e.g odd number of bytes.
|
||||
u.ctx.PartSize = (u.totalSize / int64(u.ctx.MaxUploadParts)) + 1
|
||||
u.cfg.PartSize = (u.totalSize / int64(u.cfg.MaxUploadParts)) + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -420,11 +458,11 @@ func (u *uploader) nextReader() (io.ReadSeeker, int, error) {
|
||||
case readerAtSeeker:
|
||||
var err error
|
||||
|
||||
n := u.ctx.PartSize
|
||||
n := u.cfg.PartSize
|
||||
if u.totalSize >= 0 {
|
||||
bytesLeft := u.totalSize - u.readerPos
|
||||
|
||||
if bytesLeft <= u.ctx.PartSize {
|
||||
if bytesLeft <= u.cfg.PartSize {
|
||||
err = io.EOF
|
||||
n = bytesLeft
|
||||
}
|
||||
@@ -436,7 +474,7 @@ func (u *uploader) nextReader() (io.ReadSeeker, int, error) {
|
||||
return reader, int(n), err
|
||||
|
||||
default:
|
||||
part := make([]byte, u.ctx.PartSize)
|
||||
part := make([]byte, u.cfg.PartSize)
|
||||
n, err := readFillBuf(r, part)
|
||||
u.readerPos += int64(n)
|
||||
|
||||
@@ -462,8 +500,11 @@ func (u *uploader) singlePart(buf io.ReadSeeker) (*UploadOutput, error) {
|
||||
awsutil.Copy(params, u.in)
|
||||
params.Body = buf
|
||||
|
||||
req, out := u.ctx.S3.PutObjectRequest(params)
|
||||
req.Handlers.Build.PushBack(request.MakeAddToUserAgentFreeFormHandler("S3Manager"))
|
||||
// Need to use request form because URL generated in request is
|
||||
// used in return.
|
||||
req, out := u.cfg.S3.PutObjectRequest(params)
|
||||
req.SetContext(u.ctx)
|
||||
req.ApplyOptions(u.cfg.RequestOptions...)
|
||||
if err := req.Send(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -506,16 +547,15 @@ func (u *multiuploader) upload(firstBuf io.ReadSeeker) (*UploadOutput, error) {
|
||||
awsutil.Copy(params, u.in)
|
||||
|
||||
// Create the multipart
|
||||
req, resp := u.ctx.S3.CreateMultipartUploadRequest(params)
|
||||
req.Handlers.Build.PushBack(request.MakeAddToUserAgentFreeFormHandler("S3Manager"))
|
||||
if err := req.Send(); err != nil {
|
||||
resp, err := u.cfg.S3.CreateMultipartUploadWithContext(u.ctx, params, u.cfg.RequestOptions...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
u.uploadID = *resp.UploadId
|
||||
|
||||
// Create the workers
|
||||
ch := make(chan chunk, u.ctx.Concurrency)
|
||||
for i := 0; i < u.ctx.Concurrency; i++ {
|
||||
ch := make(chan chunk, u.cfg.Concurrency)
|
||||
for i := 0; i < u.cfg.Concurrency; i++ {
|
||||
u.wg.Add(1)
|
||||
go u.readChunk(ch)
|
||||
}
|
||||
@@ -525,15 +565,14 @@ func (u *multiuploader) upload(firstBuf io.ReadSeeker) (*UploadOutput, error) {
|
||||
ch <- chunk{buf: firstBuf, num: num}
|
||||
|
||||
// Read and queue the rest of the parts
|
||||
var err error
|
||||
for u.geterr() == nil && err == nil {
|
||||
num++
|
||||
// This upload exceeded maximum number of supported parts, error now.
|
||||
if num > int64(u.ctx.MaxUploadParts) || num > int64(MaxUploadParts) {
|
||||
if num > int64(u.cfg.MaxUploadParts) || num > int64(MaxUploadParts) {
|
||||
var msg string
|
||||
if num > int64(u.ctx.MaxUploadParts) {
|
||||
if num > int64(u.cfg.MaxUploadParts) {
|
||||
msg = fmt.Sprintf("exceeded total allowed configured MaxUploadParts (%d). Adjust PartSize to fit in this limit",
|
||||
u.ctx.MaxUploadParts)
|
||||
u.cfg.MaxUploadParts)
|
||||
} else {
|
||||
msg = fmt.Sprintf("exceeded total allowed S3 limit MaxUploadParts (%d). Adjust PartSize to fit in this limit",
|
||||
MaxUploadParts)
|
||||
@@ -607,15 +646,17 @@ func (u *multiuploader) readChunk(ch chan chunk) {
|
||||
// send performs an UploadPart request and keeps track of the completed
|
||||
// part information.
|
||||
func (u *multiuploader) send(c chunk) error {
|
||||
req, resp := u.ctx.S3.UploadPartRequest(&s3.UploadPartInput{
|
||||
Bucket: u.in.Bucket,
|
||||
Key: u.in.Key,
|
||||
Body: c.buf,
|
||||
UploadId: &u.uploadID,
|
||||
PartNumber: &c.num,
|
||||
})
|
||||
req.Handlers.Build.PushBack(request.MakeAddToUserAgentFreeFormHandler("S3Manager"))
|
||||
if err := req.Send(); err != nil {
|
||||
params := &s3.UploadPartInput{
|
||||
Bucket: u.in.Bucket,
|
||||
Key: u.in.Key,
|
||||
Body: c.buf,
|
||||
UploadId: &u.uploadID,
|
||||
SSECustomerAlgorithm: u.in.SSECustomerAlgorithm,
|
||||
SSECustomerKey: u.in.SSECustomerKey,
|
||||
PartNumber: &c.num,
|
||||
}
|
||||
resp, err := u.cfg.S3.UploadPartWithContext(u.ctx, params, u.cfg.RequestOptions...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -647,17 +688,19 @@ func (u *multiuploader) seterr(e error) {
|
||||
|
||||
// fail will abort the multipart unless LeavePartsOnError is set to true.
|
||||
func (u *multiuploader) fail() {
|
||||
if u.ctx.LeavePartsOnError {
|
||||
if u.cfg.LeavePartsOnError {
|
||||
return
|
||||
}
|
||||
|
||||
req, _ := u.ctx.S3.AbortMultipartUploadRequest(&s3.AbortMultipartUploadInput{
|
||||
params := &s3.AbortMultipartUploadInput{
|
||||
Bucket: u.in.Bucket,
|
||||
Key: u.in.Key,
|
||||
UploadId: &u.uploadID,
|
||||
})
|
||||
req.Handlers.Build.PushBack(request.MakeAddToUserAgentFreeFormHandler("S3Manager"))
|
||||
req.Send()
|
||||
}
|
||||
_, err := u.cfg.S3.AbortMultipartUploadWithContext(u.ctx, params, u.cfg.RequestOptions...)
|
||||
if err != nil {
|
||||
logMessage(u.cfg.S3, aws.LogDebug, fmt.Sprintf("failed to abort multipart upload, %v", err))
|
||||
}
|
||||
}
|
||||
|
||||
// complete successfully completes a multipart upload and returns the response.
|
||||
@@ -670,14 +713,14 @@ func (u *multiuploader) complete() *s3.CompleteMultipartUploadOutput {
|
||||
// Parts must be sorted in PartNumber order.
|
||||
sort.Sort(u.parts)
|
||||
|
||||
req, resp := u.ctx.S3.CompleteMultipartUploadRequest(&s3.CompleteMultipartUploadInput{
|
||||
params := &s3.CompleteMultipartUploadInput{
|
||||
Bucket: u.in.Bucket,
|
||||
Key: u.in.Key,
|
||||
UploadId: &u.uploadID,
|
||||
MultipartUpload: &s3.CompletedMultipartUpload{Parts: u.parts},
|
||||
})
|
||||
req.Handlers.Build.PushBack(request.MakeAddToUserAgentFreeFormHandler("S3Manager"))
|
||||
if err := req.Send(); err != nil {
|
||||
}
|
||||
resp, err := u.cfg.S3.CompleteMultipartUploadWithContext(u.ctx, params, u.cfg.RequestOptions...)
|
||||
if err != nil {
|
||||
u.seterr(err)
|
||||
u.fail()
|
||||
}
|
||||
|
||||
+755
@@ -0,0 +1,755 @@
|
||||
package s3manager_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"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/request"
|
||||
"github.com/aws/aws-sdk-go/awstesting"
|
||||
"github.com/aws/aws-sdk-go/awstesting/unit"
|
||||
"github.com/aws/aws-sdk-go/service/s3"
|
||||
"github.com/aws/aws-sdk-go/service/s3/s3manager"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var emptyList = []string{}
|
||||
|
||||
func val(i interface{}, s string) interface{} {
|
||||
v, err := awsutil.ValuesAtPath(i, s)
|
||||
if err != nil || len(v) == 0 {
|
||||
return nil
|
||||
}
|
||||
if _, ok := v[0].(io.Reader); ok {
|
||||
return v[0]
|
||||
}
|
||||
|
||||
if rv := reflect.ValueOf(v[0]); rv.Kind() == reflect.Ptr {
|
||||
return rv.Elem().Interface()
|
||||
}
|
||||
|
||||
return v[0]
|
||||
}
|
||||
|
||||
func contains(src []string, s string) bool {
|
||||
for _, v := range src {
|
||||
if s == v {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func loggingSvc(ignoreOps []string) (*s3.S3, *[]string, *[]interface{}) {
|
||||
var m sync.Mutex
|
||||
partNum := 0
|
||||
names := []string{}
|
||||
params := []interface{}{}
|
||||
svc := s3.New(unit.Session)
|
||||
svc.Handlers.Unmarshal.Clear()
|
||||
svc.Handlers.UnmarshalMeta.Clear()
|
||||
svc.Handlers.UnmarshalError.Clear()
|
||||
svc.Handlers.Send.Clear()
|
||||
svc.Handlers.Send.PushBack(func(r *request.Request) {
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
|
||||
if !contains(ignoreOps, r.Operation.Name) {
|
||||
names = append(names, r.Operation.Name)
|
||||
params = append(params, r.Params)
|
||||
}
|
||||
|
||||
r.HTTPResponse = &http.Response{
|
||||
StatusCode: 200,
|
||||
Body: ioutil.NopCloser(bytes.NewReader([]byte{})),
|
||||
}
|
||||
|
||||
switch data := r.Data.(type) {
|
||||
case *s3.CreateMultipartUploadOutput:
|
||||
data.UploadId = aws.String("UPLOAD-ID")
|
||||
case *s3.UploadPartOutput:
|
||||
partNum++
|
||||
data.ETag = aws.String(fmt.Sprintf("ETAG%d", partNum))
|
||||
case *s3.CompleteMultipartUploadOutput:
|
||||
data.Location = aws.String("https://location")
|
||||
data.VersionId = aws.String("VERSION-ID")
|
||||
case *s3.PutObjectOutput:
|
||||
data.VersionId = aws.String("VERSION-ID")
|
||||
}
|
||||
})
|
||||
|
||||
return svc, &names, ¶ms
|
||||
}
|
||||
|
||||
func buflen(i interface{}) int {
|
||||
r := i.(io.Reader)
|
||||
b, _ := ioutil.ReadAll(r)
|
||||
return len(b)
|
||||
}
|
||||
|
||||
func TestUploadOrderMulti(t *testing.T) {
|
||||
s, ops, args := loggingSvc(emptyList)
|
||||
u := s3manager.NewUploaderWithClient(s)
|
||||
|
||||
resp, err := u.Upload(&s3manager.UploadInput{
|
||||
Bucket: aws.String("Bucket"),
|
||||
Key: aws.String("Key"),
|
||||
Body: bytes.NewReader(buf12MB),
|
||||
ServerSideEncryption: aws.String("aws:kms"),
|
||||
SSEKMSKeyId: aws.String("KmsId"),
|
||||
ContentType: aws.String("content/type"),
|
||||
})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []string{"CreateMultipartUpload", "UploadPart", "UploadPart", "UploadPart", "CompleteMultipartUpload"}, *ops)
|
||||
assert.Equal(t, "https://location", resp.Location)
|
||||
assert.Equal(t, "UPLOAD-ID", resp.UploadID)
|
||||
assert.Equal(t, aws.String("VERSION-ID"), resp.VersionID)
|
||||
|
||||
// Validate input values
|
||||
|
||||
// UploadPart
|
||||
assert.Equal(t, "UPLOAD-ID", val((*args)[1], "UploadId"))
|
||||
assert.Equal(t, "UPLOAD-ID", val((*args)[2], "UploadId"))
|
||||
assert.Equal(t, "UPLOAD-ID", val((*args)[3], "UploadId"))
|
||||
|
||||
// CompleteMultipartUpload
|
||||
assert.Equal(t, "UPLOAD-ID", val((*args)[4], "UploadId"))
|
||||
assert.Equal(t, int64(1), val((*args)[4], "MultipartUpload.Parts[0].PartNumber"))
|
||||
assert.Equal(t, int64(2), val((*args)[4], "MultipartUpload.Parts[1].PartNumber"))
|
||||
assert.Equal(t, int64(3), val((*args)[4], "MultipartUpload.Parts[2].PartNumber"))
|
||||
assert.Regexp(t, `^ETAG\d+$`, val((*args)[4], "MultipartUpload.Parts[0].ETag"))
|
||||
assert.Regexp(t, `^ETAG\d+$`, val((*args)[4], "MultipartUpload.Parts[1].ETag"))
|
||||
assert.Regexp(t, `^ETAG\d+$`, val((*args)[4], "MultipartUpload.Parts[2].ETag"))
|
||||
|
||||
// Custom headers
|
||||
assert.Equal(t, "aws:kms", val((*args)[0], "ServerSideEncryption"))
|
||||
assert.Equal(t, "KmsId", val((*args)[0], "SSEKMSKeyId"))
|
||||
assert.Equal(t, "content/type", val((*args)[0], "ContentType"))
|
||||
}
|
||||
|
||||
func TestUploadOrderMultiDifferentPartSize(t *testing.T) {
|
||||
s, ops, args := loggingSvc(emptyList)
|
||||
mgr := s3manager.NewUploaderWithClient(s, func(u *s3manager.Uploader) {
|
||||
u.PartSize = 1024 * 1024 * 7
|
||||
u.Concurrency = 1
|
||||
})
|
||||
_, err := mgr.Upload(&s3manager.UploadInput{
|
||||
Bucket: aws.String("Bucket"),
|
||||
Key: aws.String("Key"),
|
||||
Body: bytes.NewReader(buf12MB),
|
||||
})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []string{"CreateMultipartUpload", "UploadPart", "UploadPart", "CompleteMultipartUpload"}, *ops)
|
||||
|
||||
// Part lengths
|
||||
assert.Equal(t, 1024*1024*7, buflen(val((*args)[1], "Body")))
|
||||
assert.Equal(t, 1024*1024*5, buflen(val((*args)[2], "Body")))
|
||||
}
|
||||
|
||||
func TestUploadIncreasePartSize(t *testing.T) {
|
||||
s, ops, args := loggingSvc(emptyList)
|
||||
mgr := s3manager.NewUploaderWithClient(s, func(u *s3manager.Uploader) {
|
||||
u.Concurrency = 1
|
||||
u.MaxUploadParts = 2
|
||||
})
|
||||
_, err := mgr.Upload(&s3manager.UploadInput{
|
||||
Bucket: aws.String("Bucket"),
|
||||
Key: aws.String("Key"),
|
||||
Body: bytes.NewReader(buf12MB),
|
||||
})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(s3manager.DefaultDownloadPartSize), mgr.PartSize)
|
||||
assert.Equal(t, []string{"CreateMultipartUpload", "UploadPart", "UploadPart", "CompleteMultipartUpload"}, *ops)
|
||||
|
||||
// Part lengths
|
||||
assert.Equal(t, (1024*1024*6)+1, buflen(val((*args)[1], "Body")))
|
||||
assert.Equal(t, (1024*1024*6)-1, buflen(val((*args)[2], "Body")))
|
||||
}
|
||||
|
||||
func TestUploadFailIfPartSizeTooSmall(t *testing.T) {
|
||||
mgr := s3manager.NewUploader(unit.Session, func(u *s3manager.Uploader) {
|
||||
u.PartSize = 5
|
||||
})
|
||||
resp, err := mgr.Upload(&s3manager.UploadInput{
|
||||
Bucket: aws.String("Bucket"),
|
||||
Key: aws.String("Key"),
|
||||
Body: bytes.NewReader(buf12MB),
|
||||
})
|
||||
|
||||
assert.Nil(t, resp)
|
||||
assert.NotNil(t, err)
|
||||
|
||||
aerr := err.(awserr.Error)
|
||||
assert.Equal(t, "ConfigError", aerr.Code())
|
||||
assert.Contains(t, aerr.Message(), "part size must be at least")
|
||||
}
|
||||
|
||||
func TestUploadOrderSingle(t *testing.T) {
|
||||
s, ops, args := loggingSvc(emptyList)
|
||||
mgr := s3manager.NewUploaderWithClient(s)
|
||||
resp, err := mgr.Upload(&s3manager.UploadInput{
|
||||
Bucket: aws.String("Bucket"),
|
||||
Key: aws.String("Key"),
|
||||
Body: bytes.NewReader(buf2MB),
|
||||
ServerSideEncryption: aws.String("aws:kms"),
|
||||
SSEKMSKeyId: aws.String("KmsId"),
|
||||
ContentType: aws.String("content/type"),
|
||||
})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []string{"PutObject"}, *ops)
|
||||
assert.NotEqual(t, "", resp.Location)
|
||||
assert.Equal(t, aws.String("VERSION-ID"), resp.VersionID)
|
||||
assert.Equal(t, "", resp.UploadID)
|
||||
assert.Equal(t, "aws:kms", val((*args)[0], "ServerSideEncryption"))
|
||||
assert.Equal(t, "KmsId", val((*args)[0], "SSEKMSKeyId"))
|
||||
assert.Equal(t, "content/type", val((*args)[0], "ContentType"))
|
||||
}
|
||||
|
||||
func TestUploadOrderSingleFailure(t *testing.T) {
|
||||
s, ops, _ := loggingSvc(emptyList)
|
||||
s.Handlers.Send.PushBack(func(r *request.Request) {
|
||||
r.HTTPResponse.StatusCode = 400
|
||||
})
|
||||
mgr := s3manager.NewUploaderWithClient(s)
|
||||
resp, err := mgr.Upload(&s3manager.UploadInput{
|
||||
Bucket: aws.String("Bucket"),
|
||||
Key: aws.String("Key"),
|
||||
Body: bytes.NewReader(buf2MB),
|
||||
})
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, []string{"PutObject"}, *ops)
|
||||
assert.Nil(t, resp)
|
||||
}
|
||||
|
||||
func TestUploadOrderZero(t *testing.T) {
|
||||
s, ops, args := loggingSvc(emptyList)
|
||||
mgr := s3manager.NewUploaderWithClient(s)
|
||||
resp, err := mgr.Upload(&s3manager.UploadInput{
|
||||
Bucket: aws.String("Bucket"),
|
||||
Key: aws.String("Key"),
|
||||
Body: bytes.NewReader(make([]byte, 0)),
|
||||
})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []string{"PutObject"}, *ops)
|
||||
assert.NotEqual(t, "", resp.Location)
|
||||
assert.Equal(t, "", resp.UploadID)
|
||||
assert.Equal(t, 0, buflen(val((*args)[0], "Body")))
|
||||
}
|
||||
|
||||
func TestUploadOrderMultiFailure(t *testing.T) {
|
||||
s, ops, _ := loggingSvc(emptyList)
|
||||
s.Handlers.Send.PushBack(func(r *request.Request) {
|
||||
switch t := r.Data.(type) {
|
||||
case *s3.UploadPartOutput:
|
||||
if *t.ETag == "ETAG2" {
|
||||
r.HTTPResponse.StatusCode = 400
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
mgr := s3manager.NewUploaderWithClient(s, func(u *s3manager.Uploader) {
|
||||
u.Concurrency = 1
|
||||
})
|
||||
_, err := mgr.Upload(&s3manager.UploadInput{
|
||||
Bucket: aws.String("Bucket"),
|
||||
Key: aws.String("Key"),
|
||||
Body: bytes.NewReader(buf12MB),
|
||||
})
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, []string{"CreateMultipartUpload", "UploadPart", "UploadPart", "AbortMultipartUpload"}, *ops)
|
||||
}
|
||||
|
||||
func TestUploadOrderMultiFailureOnComplete(t *testing.T) {
|
||||
s, ops, _ := loggingSvc(emptyList)
|
||||
s.Handlers.Send.PushBack(func(r *request.Request) {
|
||||
switch r.Data.(type) {
|
||||
case *s3.CompleteMultipartUploadOutput:
|
||||
r.HTTPResponse.StatusCode = 400
|
||||
}
|
||||
})
|
||||
|
||||
mgr := s3manager.NewUploaderWithClient(s, func(u *s3manager.Uploader) {
|
||||
u.Concurrency = 1
|
||||
})
|
||||
_, err := mgr.Upload(&s3manager.UploadInput{
|
||||
Bucket: aws.String("Bucket"),
|
||||
Key: aws.String("Key"),
|
||||
Body: bytes.NewReader(buf12MB),
|
||||
})
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, []string{"CreateMultipartUpload", "UploadPart", "UploadPart",
|
||||
"UploadPart", "CompleteMultipartUpload", "AbortMultipartUpload"}, *ops)
|
||||
}
|
||||
|
||||
func TestUploadOrderMultiFailureOnCreate(t *testing.T) {
|
||||
s, ops, _ := loggingSvc(emptyList)
|
||||
s.Handlers.Send.PushBack(func(r *request.Request) {
|
||||
switch r.Data.(type) {
|
||||
case *s3.CreateMultipartUploadOutput:
|
||||
r.HTTPResponse.StatusCode = 400
|
||||
}
|
||||
})
|
||||
|
||||
mgr := s3manager.NewUploaderWithClient(s)
|
||||
_, err := mgr.Upload(&s3manager.UploadInput{
|
||||
Bucket: aws.String("Bucket"),
|
||||
Key: aws.String("Key"),
|
||||
Body: bytes.NewReader(make([]byte, 1024*1024*12)),
|
||||
})
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, []string{"CreateMultipartUpload"}, *ops)
|
||||
}
|
||||
|
||||
func TestUploadOrderMultiFailureLeaveParts(t *testing.T) {
|
||||
s, ops, _ := loggingSvc(emptyList)
|
||||
s.Handlers.Send.PushBack(func(r *request.Request) {
|
||||
switch data := r.Data.(type) {
|
||||
case *s3.UploadPartOutput:
|
||||
if *data.ETag == "ETAG2" {
|
||||
r.HTTPResponse.StatusCode = 400
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
mgr := s3manager.NewUploaderWithClient(s, func(u *s3manager.Uploader) {
|
||||
u.Concurrency = 1
|
||||
u.LeavePartsOnError = true
|
||||
})
|
||||
_, err := mgr.Upload(&s3manager.UploadInput{
|
||||
Bucket: aws.String("Bucket"),
|
||||
Key: aws.String("Key"),
|
||||
Body: bytes.NewReader(make([]byte, 1024*1024*12)),
|
||||
})
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, []string{"CreateMultipartUpload", "UploadPart", "UploadPart"}, *ops)
|
||||
}
|
||||
|
||||
type failreader struct {
|
||||
times int
|
||||
failCount int
|
||||
}
|
||||
|
||||
func (f *failreader) Read(b []byte) (int, error) {
|
||||
f.failCount++
|
||||
if f.failCount >= f.times {
|
||||
return 0, fmt.Errorf("random failure")
|
||||
}
|
||||
return len(b), nil
|
||||
}
|
||||
|
||||
func TestUploadOrderReadFail1(t *testing.T) {
|
||||
s, ops, _ := loggingSvc(emptyList)
|
||||
mgr := s3manager.NewUploaderWithClient(s)
|
||||
_, err := mgr.Upload(&s3manager.UploadInput{
|
||||
Bucket: aws.String("Bucket"),
|
||||
Key: aws.String("Key"),
|
||||
Body: &failreader{times: 1},
|
||||
})
|
||||
|
||||
assert.Equal(t, "ReadRequestBody", err.(awserr.Error).Code())
|
||||
assert.EqualError(t, err.(awserr.Error).OrigErr(), "random failure")
|
||||
assert.Equal(t, []string{}, *ops)
|
||||
}
|
||||
|
||||
func TestUploadOrderReadFail2(t *testing.T) {
|
||||
s, ops, _ := loggingSvc([]string{"UploadPart"})
|
||||
mgr := s3manager.NewUploaderWithClient(s, func(u *s3manager.Uploader) {
|
||||
u.Concurrency = 1
|
||||
})
|
||||
_, err := mgr.Upload(&s3manager.UploadInput{
|
||||
Bucket: aws.String("Bucket"),
|
||||
Key: aws.String("Key"),
|
||||
Body: &failreader{times: 2},
|
||||
})
|
||||
|
||||
assert.Equal(t, "MultipartUpload", err.(awserr.Error).Code())
|
||||
assert.Equal(t, "ReadRequestBody", err.(awserr.Error).OrigErr().(awserr.Error).Code())
|
||||
assert.Contains(t, err.(awserr.Error).OrigErr().Error(), "random failure")
|
||||
assert.Equal(t, []string{"CreateMultipartUpload", "AbortMultipartUpload"}, *ops)
|
||||
}
|
||||
|
||||
type sizedReader struct {
|
||||
size int
|
||||
cur int
|
||||
err error
|
||||
}
|
||||
|
||||
func (s *sizedReader) Read(p []byte) (n int, err error) {
|
||||
if s.cur >= s.size {
|
||||
if s.err == nil {
|
||||
s.err = io.EOF
|
||||
}
|
||||
return 0, s.err
|
||||
}
|
||||
|
||||
n = len(p)
|
||||
s.cur += len(p)
|
||||
if s.cur > s.size {
|
||||
n -= s.cur - s.size
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func TestUploadOrderMultiBufferedReader(t *testing.T) {
|
||||
s, ops, args := loggingSvc(emptyList)
|
||||
mgr := s3manager.NewUploaderWithClient(s)
|
||||
_, err := mgr.Upload(&s3manager.UploadInput{
|
||||
Bucket: aws.String("Bucket"),
|
||||
Key: aws.String("Key"),
|
||||
Body: &sizedReader{size: 1024 * 1024 * 12},
|
||||
})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []string{"CreateMultipartUpload", "UploadPart", "UploadPart", "UploadPart", "CompleteMultipartUpload"}, *ops)
|
||||
|
||||
// Part lengths
|
||||
parts := []int{
|
||||
buflen(val((*args)[1], "Body")),
|
||||
buflen(val((*args)[2], "Body")),
|
||||
buflen(val((*args)[3], "Body")),
|
||||
}
|
||||
sort.Ints(parts)
|
||||
assert.Equal(t, []int{1024 * 1024 * 2, 1024 * 1024 * 5, 1024 * 1024 * 5}, parts)
|
||||
}
|
||||
|
||||
func TestUploadOrderMultiBufferedReaderPartial(t *testing.T) {
|
||||
s, ops, args := loggingSvc(emptyList)
|
||||
mgr := s3manager.NewUploaderWithClient(s)
|
||||
_, err := mgr.Upload(&s3manager.UploadInput{
|
||||
Bucket: aws.String("Bucket"),
|
||||
Key: aws.String("Key"),
|
||||
Body: &sizedReader{size: 1024 * 1024 * 12, err: io.EOF},
|
||||
})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []string{"CreateMultipartUpload", "UploadPart", "UploadPart", "UploadPart", "CompleteMultipartUpload"}, *ops)
|
||||
|
||||
// Part lengths
|
||||
parts := []int{
|
||||
buflen(val((*args)[1], "Body")),
|
||||
buflen(val((*args)[2], "Body")),
|
||||
buflen(val((*args)[3], "Body")),
|
||||
}
|
||||
sort.Ints(parts)
|
||||
assert.Equal(t, []int{1024 * 1024 * 2, 1024 * 1024 * 5, 1024 * 1024 * 5}, parts)
|
||||
}
|
||||
|
||||
// TestUploadOrderMultiBufferedReaderEOF tests the edge case where the
|
||||
// file size is the same as part size.
|
||||
func TestUploadOrderMultiBufferedReaderEOF(t *testing.T) {
|
||||
s, ops, args := loggingSvc(emptyList)
|
||||
mgr := s3manager.NewUploaderWithClient(s)
|
||||
_, err := mgr.Upload(&s3manager.UploadInput{
|
||||
Bucket: aws.String("Bucket"),
|
||||
Key: aws.String("Key"),
|
||||
Body: &sizedReader{size: 1024 * 1024 * 10, err: io.EOF},
|
||||
})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []string{"CreateMultipartUpload", "UploadPart", "UploadPart", "CompleteMultipartUpload"}, *ops)
|
||||
|
||||
// Part lengths
|
||||
parts := []int{
|
||||
buflen(val((*args)[1], "Body")),
|
||||
buflen(val((*args)[2], "Body")),
|
||||
}
|
||||
sort.Ints(parts)
|
||||
assert.Equal(t, []int{1024 * 1024 * 5, 1024 * 1024 * 5}, parts)
|
||||
}
|
||||
|
||||
func TestUploadOrderMultiBufferedReaderExceedTotalParts(t *testing.T) {
|
||||
s, ops, _ := loggingSvc([]string{"UploadPart"})
|
||||
mgr := s3manager.NewUploaderWithClient(s, func(u *s3manager.Uploader) {
|
||||
u.Concurrency = 1
|
||||
u.MaxUploadParts = 2
|
||||
})
|
||||
resp, err := mgr.Upload(&s3manager.UploadInput{
|
||||
Bucket: aws.String("Bucket"),
|
||||
Key: aws.String("Key"),
|
||||
Body: &sizedReader{size: 1024 * 1024 * 12},
|
||||
})
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, resp)
|
||||
assert.Equal(t, []string{"CreateMultipartUpload", "AbortMultipartUpload"}, *ops)
|
||||
|
||||
aerr := err.(awserr.Error)
|
||||
assert.Equal(t, "MultipartUpload", aerr.Code())
|
||||
assert.Equal(t, "TotalPartsExceeded", aerr.OrigErr().(awserr.Error).Code())
|
||||
assert.Contains(t, aerr.Error(), "configured MaxUploadParts (2)")
|
||||
}
|
||||
|
||||
func TestUploadOrderSingleBufferedReader(t *testing.T) {
|
||||
s, ops, _ := loggingSvc(emptyList)
|
||||
mgr := s3manager.NewUploaderWithClient(s)
|
||||
resp, err := mgr.Upload(&s3manager.UploadInput{
|
||||
Bucket: aws.String("Bucket"),
|
||||
Key: aws.String("Key"),
|
||||
Body: &sizedReader{size: 1024 * 1024 * 2},
|
||||
})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []string{"PutObject"}, *ops)
|
||||
assert.NotEqual(t, "", resp.Location)
|
||||
assert.Equal(t, "", resp.UploadID)
|
||||
}
|
||||
|
||||
func TestUploadZeroLenObject(t *testing.T) {
|
||||
requestMade := false
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
requestMade = true
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
mgr := s3manager.NewUploaderWithClient(s3.New(unit.Session, &aws.Config{
|
||||
Endpoint: aws.String(server.URL),
|
||||
}))
|
||||
resp, err := mgr.Upload(&s3manager.UploadInput{
|
||||
Bucket: aws.String("Bucket"),
|
||||
Key: aws.String("Key"),
|
||||
Body: strings.NewReader(""),
|
||||
})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, requestMade)
|
||||
assert.NotEqual(t, "", resp.Location)
|
||||
assert.Equal(t, "", resp.UploadID)
|
||||
}
|
||||
|
||||
func TestUploadInputS3PutObjectInputPairity(t *testing.T) {
|
||||
matchings := compareStructType(reflect.TypeOf(s3.PutObjectInput{}),
|
||||
reflect.TypeOf(s3manager.UploadInput{}))
|
||||
aOnly := []string{}
|
||||
bOnly := []string{}
|
||||
|
||||
for k, c := range matchings {
|
||||
if c == 1 && k != "ContentLength" {
|
||||
aOnly = append(aOnly, k)
|
||||
} else if c == 2 {
|
||||
bOnly = append(bOnly, k)
|
||||
}
|
||||
}
|
||||
assert.Empty(t, aOnly, "s3.PutObjectInput")
|
||||
assert.Empty(t, bOnly, "s3Manager.UploadInput")
|
||||
}
|
||||
|
||||
type testIncompleteReader struct {
|
||||
Buf []byte
|
||||
Count int
|
||||
}
|
||||
|
||||
func (r *testIncompleteReader) Read(p []byte) (n int, err error) {
|
||||
if r.Count < 0 {
|
||||
return 0, io.ErrUnexpectedEOF
|
||||
}
|
||||
|
||||
r.Count--
|
||||
return copy(p, r.Buf), nil
|
||||
}
|
||||
|
||||
func TestUploadUnexpectedEOF(t *testing.T) {
|
||||
s, ops, args := loggingSvc(emptyList)
|
||||
mgr := s3manager.NewUploaderWithClient(s, func(u *s3manager.Uploader) {
|
||||
u.Concurrency = 1
|
||||
})
|
||||
_, err := mgr.Upload(&s3manager.UploadInput{
|
||||
Bucket: aws.String("Bucket"),
|
||||
Key: aws.String("Key"),
|
||||
Body: &testIncompleteReader{
|
||||
Buf: make([]byte, 1024*1024*5),
|
||||
Count: 1,
|
||||
},
|
||||
})
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "CreateMultipartUpload", (*ops)[0])
|
||||
assert.Equal(t, "UploadPart", (*ops)[1])
|
||||
assert.Equal(t, "AbortMultipartUpload", (*ops)[len(*ops)-1])
|
||||
|
||||
// Part lengths
|
||||
assert.Equal(t, 1024*1024*5, buflen(val((*args)[1], "Body")))
|
||||
}
|
||||
|
||||
func compareStructType(a, b reflect.Type) map[string]int {
|
||||
if a.Kind() != reflect.Struct || b.Kind() != reflect.Struct {
|
||||
panic(fmt.Sprintf("types must both be structs, got %v and %v", a.Kind(), b.Kind()))
|
||||
}
|
||||
|
||||
aFields := enumFields(a)
|
||||
bFields := enumFields(b)
|
||||
|
||||
matchings := map[string]int{}
|
||||
|
||||
for i := 0; i < len(aFields) || i < len(bFields); i++ {
|
||||
if i < len(aFields) {
|
||||
c := matchings[aFields[i].Name]
|
||||
matchings[aFields[i].Name] = c + 1
|
||||
}
|
||||
if i < len(bFields) {
|
||||
c := matchings[bFields[i].Name]
|
||||
matchings[bFields[i].Name] = c + 2
|
||||
}
|
||||
}
|
||||
|
||||
return matchings
|
||||
}
|
||||
|
||||
func enumFields(v reflect.Type) []reflect.StructField {
|
||||
fields := []reflect.StructField{}
|
||||
|
||||
for i := 0; i < v.NumField(); i++ {
|
||||
field := v.Field(i)
|
||||
// Ignoreing anon fields
|
||||
if field.PkgPath != "" {
|
||||
// Ignore unexported fields
|
||||
continue
|
||||
}
|
||||
|
||||
fields = append(fields, field)
|
||||
}
|
||||
|
||||
return fields
|
||||
}
|
||||
|
||||
type fooReaderAt struct{}
|
||||
|
||||
func (r *fooReaderAt) Read(p []byte) (n int, err error) {
|
||||
return 12, io.EOF
|
||||
}
|
||||
|
||||
func (r *fooReaderAt) ReadAt(p []byte, off int64) (n int, err error) {
|
||||
return 12, io.EOF
|
||||
}
|
||||
|
||||
func TestReaderAt(t *testing.T) {
|
||||
svc := s3.New(unit.Session)
|
||||
svc.Handlers.Unmarshal.Clear()
|
||||
svc.Handlers.UnmarshalMeta.Clear()
|
||||
svc.Handlers.UnmarshalError.Clear()
|
||||
svc.Handlers.Send.Clear()
|
||||
|
||||
contentLen := ""
|
||||
svc.Handlers.Send.PushBack(func(r *request.Request) {
|
||||
contentLen = r.HTTPRequest.Header.Get("Content-Length")
|
||||
r.HTTPResponse = &http.Response{
|
||||
StatusCode: 200,
|
||||
Body: ioutil.NopCloser(bytes.NewReader([]byte{})),
|
||||
}
|
||||
})
|
||||
|
||||
mgr := s3manager.NewUploaderWithClient(svc, func(u *s3manager.Uploader) {
|
||||
u.Concurrency = 1
|
||||
})
|
||||
|
||||
_, err := mgr.Upload(&s3manager.UploadInput{
|
||||
Bucket: aws.String("Bucket"),
|
||||
Key: aws.String("Key"),
|
||||
Body: &fooReaderAt{},
|
||||
})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, contentLen, "12")
|
||||
}
|
||||
|
||||
func TestSSE(t *testing.T) {
|
||||
svc := s3.New(unit.Session)
|
||||
svc.Handlers.Unmarshal.Clear()
|
||||
svc.Handlers.UnmarshalMeta.Clear()
|
||||
svc.Handlers.UnmarshalError.Clear()
|
||||
svc.Handlers.ValidateResponse.Clear()
|
||||
svc.Handlers.Send.Clear()
|
||||
partNum := 0
|
||||
mutex := &sync.Mutex{}
|
||||
|
||||
svc.Handlers.Send.PushBack(func(r *request.Request) {
|
||||
mutex.Lock()
|
||||
defer mutex.Unlock()
|
||||
r.HTTPResponse = &http.Response{
|
||||
StatusCode: 200,
|
||||
Body: ioutil.NopCloser(bytes.NewReader([]byte{})),
|
||||
}
|
||||
switch data := r.Data.(type) {
|
||||
case *s3.CreateMultipartUploadOutput:
|
||||
data.UploadId = aws.String("UPLOAD-ID")
|
||||
case *s3.UploadPartOutput:
|
||||
input := r.Params.(*s3.UploadPartInput)
|
||||
if input.SSECustomerAlgorithm == nil {
|
||||
t.Fatal("SSECustomerAlgoritm should not be nil")
|
||||
}
|
||||
if input.SSECustomerKey == nil {
|
||||
t.Fatal("SSECustomerKey should not be nil")
|
||||
}
|
||||
partNum++
|
||||
data.ETag = aws.String(fmt.Sprintf("ETAG%d", partNum))
|
||||
case *s3.CompleteMultipartUploadOutput:
|
||||
data.Location = aws.String("https://location")
|
||||
data.VersionId = aws.String("VERSION-ID")
|
||||
case *s3.PutObjectOutput:
|
||||
data.VersionId = aws.String("VERSION-ID")
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
mgr := s3manager.NewUploaderWithClient(svc, func(u *s3manager.Uploader) {
|
||||
u.Concurrency = 5
|
||||
})
|
||||
|
||||
_, err := mgr.Upload(&s3manager.UploadInput{
|
||||
Bucket: aws.String("Bucket"),
|
||||
Key: aws.String("Key"),
|
||||
SSECustomerAlgorithm: aws.String("AES256"),
|
||||
SSECustomerKey: aws.String("foo"),
|
||||
Body: bytes.NewBuffer(make([]byte, 1024*1024*10)),
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
t.Fatal("Expected no error, but received" + err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestUploadWithContextCanceled(t *testing.T) {
|
||||
u := s3manager.NewUploader(unit.Session)
|
||||
|
||||
params := s3manager.UploadInput{
|
||||
Bucket: aws.String("Bucket"),
|
||||
Key: aws.String("Key"),
|
||||
Body: bytes.NewReader(make([]byte, 0)),
|
||||
}
|
||||
|
||||
ctx := &awstesting.FakeContext{DoneCh: make(chan struct{})}
|
||||
ctx.Error = fmt.Errorf("context canceled")
|
||||
close(ctx.DoneCh)
|
||||
|
||||
_, err := u.UploadWithContext(ctx, ¶ms)
|
||||
if err == nil {
|
||||
t.Fatalf("expected error, did not get one")
|
||||
}
|
||||
aerr := err.(awserr.Error)
|
||||
if e, a := request.CanceledErrorCode, aerr.Code(); e != a {
|
||||
t.Errorf("expected error code %q, got %q", e, a)
|
||||
}
|
||||
if e, a := "canceled", aerr.Message(); !strings.Contains(a, e) {
|
||||
t.Errorf("expected error message to contain %q, but did not %q", e, a)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user