clipper/backend/media/uploader.go

195 lines
4.6 KiB
Go
Raw Normal View History

2021-10-27 19:34:59 +00:00
package media
import (
"bytes"
"context"
2021-11-08 01:54:43 +00:00
"errors"
2021-10-27 19:34:59 +00:00
"fmt"
"io"
2021-10-27 19:34:59 +00:00
"log"
2021-11-08 01:54:43 +00:00
"sync"
2021-10-27 19:34:59 +00:00
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/aws/aws-sdk-go-v2/service/s3/types"
)
type multipartUploader struct {
s3 S3Client
2021-11-08 01:54:43 +00:00
}
type uploadResult struct {
completedPart types.CompletedPart
size int64
2021-10-27 19:34:59 +00:00
}
type readResult struct {
n int
err error
}
const (
targetPartSizeBytes = 5 * 1024 * 1024 // 5MB
bufferOverflowSize = 16_384 // 16Kb
)
func newMultipartUploader(s3Client S3Client) *multipartUploader {
return &multipartUploader{s3: s3Client}
}
// Upload uploads to an S3 bucket in 5MB parts. It buffers data internally
// until a part is ready to send over the network. Parts are sent as soon as
// they exceed the minimum part size of 5MB.
func (u *multipartUploader) Upload(ctx context.Context, r io.Reader, bucket, key, contentType string) (int64, error) {
var uploaded bool
2021-10-27 19:34:59 +00:00
input := s3.CreateMultipartUploadInput{
Bucket: aws.String(bucket),
Key: aws.String(key),
ContentType: aws.String(contentType),
}
output, err := u.s3.CreateMultipartUpload(ctx, &input)
2021-10-27 19:34:59 +00:00
if err != nil {
return 0, fmt.Errorf("error creating multipart upload: %v", err)
2021-10-27 19:34:59 +00:00
}
// abort the upload if possible, logging any errors, on exit.
defer func() {
if uploaded {
return
}
input := s3.AbortMultipartUploadInput{
Bucket: aws.String(bucket),
Key: aws.String(key),
UploadId: output.UploadId,
}
2021-10-27 19:34:59 +00:00
_, abortErr := u.s3.AbortMultipartUpload(ctx, &input)
if abortErr != nil {
log.Printf("error aborting upload: %v", abortErr)
} else {
log.Printf("aborted upload, key = %s", key)
}
}()
2021-10-27 19:34:59 +00:00
uploadResultChan := make(chan uploadResult)
uploadErrorChan := make(chan error, 1)
2021-10-27 19:34:59 +00:00
// uploadPart uploads an individual part.
uploadPart := func(wg *sync.WaitGroup, buf []byte, partNum int32) {
defer wg.Done()
2021-10-27 19:34:59 +00:00
partLen := int64(len(buf))
log.Printf("uploading part num = %d, len = %d", partNum, partLen)
2021-11-08 01:54:43 +00:00
input := s3.UploadPartInput{
Body: bytes.NewReader(buf),
Bucket: aws.String(bucket),
Key: aws.String(key),
PartNumber: partNum,
UploadId: output.UploadId,
ContentLength: partLen,
}
2021-10-27 19:34:59 +00:00
output, uploadErr := u.s3.UploadPart(ctx, &input)
if uploadErr != nil {
// TODO: retry on failure
uploadErrorChan <- uploadErr
return
}
2021-10-27 19:34:59 +00:00
log.Printf("uploaded part num = %d, etag = %s, bytes = %d", partNum, *output.ETag, partLen)
2021-10-27 19:34:59 +00:00
uploadResultChan <- uploadResult{
completedPart: types.CompletedPart{ETag: output.ETag, PartNumber: partNum},
size: partLen,
}
2021-11-08 01:54:43 +00:00
}
wgDone := make(chan struct{})
var wg sync.WaitGroup
wg.Add(1) // done when the reader returns EOF
go func() {
wg.Wait()
wgDone <- struct{}{}
}()
2021-11-08 01:54:43 +00:00
readChan := make(chan readResult)
buf := make([]byte, 32_768)
2021-11-08 01:54:43 +00:00
go func() {
for {
var rr readResult
rr.n, rr.err = r.Read(buf)
readChan <- rr
if rr.err != nil {
return
}
}
2021-11-08 01:54:43 +00:00
}()
var closing bool
currPart := bytes.NewBuffer(make([]byte, 0, targetPartSizeBytes+bufferOverflowSize))
partNum := int32(1)
results := make([]uploadResult, 0, 64)
2021-11-08 01:54:43 +00:00
outer:
for {
select {
case uploadResult := <-uploadResultChan:
results = append(results, uploadResult)
case uploadErr := <-uploadErrorChan:
return 0, fmt.Errorf("error while uploading part: %v", uploadErr)
case <-wgDone:
break outer
case <-ctx.Done():
return 0, ctx.Err()
case readResult := <-readChan:
if readResult.err == io.EOF {
wg.Done()
closing = true
} else if readResult.err != nil {
return 0, fmt.Errorf("reader error: %v", readResult.err)
2021-11-08 01:54:43 +00:00
}
_, _ = currPart.Write(buf[:readResult.n])
if closing || currPart.Len() >= targetPartSizeBytes {
part := make([]byte, currPart.Len())
copy(part, currPart.Bytes())
currPart.Truncate(0)
wg.Add(1)
go uploadPart(&wg, part, partNum)
partNum++
}
2021-11-08 01:54:43 +00:00
}
}
if len(results) == 0 {
2021-11-08 01:54:43 +00:00
return 0, errors.New("no parts available to upload")
2021-10-27 19:34:59 +00:00
}
completedParts := make([]types.CompletedPart, 0, 64)
var uploadedBytes int64
for _, result := range results {
completedParts = append(completedParts, result.completedPart)
uploadedBytes += result.size
}
completeInput := s3.CompleteMultipartUploadInput{
Bucket: aws.String(bucket),
Key: aws.String(key),
UploadId: output.UploadId,
2021-11-08 01:54:43 +00:00
MultipartUpload: &types.CompletedMultipartUpload{Parts: completedParts},
}
if _, err = u.s3.CompleteMultipartUpload(ctx, &completeInput); err != nil {
2021-11-08 01:54:43 +00:00
return 0, fmt.Errorf("error completing upload: %v", err)
}
log.Printf("completed upload, key = %s, bytesUploaded = %d", key, uploadedBytes)
uploaded = true
2021-11-08 01:54:43 +00:00
return uploadedBytes, nil
2021-10-27 19:34:59 +00:00
}