2015-02-11 02:14:23 +00:00
// Package s3 provides a storagedriver.StorageDriver implementation to
// store blobs in Amazon S3 cloud storage.
//
2016-01-22 02:17:53 +00:00
// This package leverages the official aws client library for interfacing with
// S3.
2015-02-11 02:14:23 +00:00
//
2016-01-22 02:17:53 +00:00
// Because S3 is a key, value store the Stat call does not support last modification
2015-02-11 02:14:23 +00:00
// time for directories (directories are an abstraction for key, value stores)
//
2016-01-22 02:17:53 +00:00
// Keep in mind that S3 guarantees only read-after-write consistency for new
// objects, but no read-after-update or list-after-write consistency.
2015-02-11 02:14:23 +00:00
package s3
import (
"bytes"
"fmt"
"io"
"io/ioutil"
"net/http"
2015-04-22 21:31:34 +00:00
"reflect"
2015-02-11 02:14:23 +00:00
"strconv"
"strings"
"time"
2016-01-22 02:17:53 +00:00
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/credentials/ec2rolecreds"
"github.com/aws/aws-sdk-go/aws/ec2metadata"
"github.com/aws/aws-sdk-go/aws/request"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/s3"
2015-04-27 22:58:58 +00:00
"github.com/docker/distribution/context"
2016-01-21 00:40:58 +00:00
"github.com/docker/distribution/registry/client/transport"
2015-02-11 02:14:23 +00:00
storagedriver "github.com/docker/distribution/registry/storage/driver"
"github.com/docker/distribution/registry/storage/driver/base"
"github.com/docker/distribution/registry/storage/driver/factory"
)
2016-01-22 02:17:53 +00:00
const driverName = "s3aws"
2015-02-11 02:14:23 +00:00
// minChunkSize defines the minimum multipart upload chunk size
// S3 API requires multipart upload chunks to be at least 5MB
const minChunkSize = 5 << 20
const defaultChunkSize = 2 * minChunkSize
// listMax is the largest amount of objects you can request from S3 in a list call
const listMax = 1000
2016-01-22 02:17:53 +00:00
// validRegions maps known s3 region identifiers to region descriptors
var validRegions = map [ string ] struct { } { }
2015-02-11 02:14:23 +00:00
//DriverParameters A struct that encapsulates all of the driver parameters after all values have been set
type DriverParameters struct {
2016-03-05 18:46:44 +00:00
AccessKey string
SecretKey string
Bucket string
Region string
RegionEndpoint string
Encrypt bool
2016-03-10 00:52:59 +00:00
KeyID string
2016-03-05 18:46:44 +00:00
Secure bool
ChunkSize int64
RootDirectory string
StorageClass string
UserAgent string
2015-02-11 02:14:23 +00:00
}
func init ( ) {
2016-01-22 02:17:53 +00:00
for _ , region := range [ ] string {
"us-east-1" ,
"us-west-1" ,
"us-west-2" ,
"eu-west-1" ,
"eu-central-1" ,
"ap-southeast-1" ,
"ap-southeast-2" ,
"ap-northeast-1" ,
"ap-northeast-2" ,
"sa-east-1" ,
} {
validRegions [ region ] = struct { } { }
}
// Register this as the default s3 driver in addition to s3aws
factory . Register ( "s3" , & s3DriverFactory { } )
2015-02-11 02:14:23 +00:00
factory . Register ( driverName , & s3DriverFactory { } )
}
// s3DriverFactory implements the factory.StorageDriverFactory interface
type s3DriverFactory struct { }
func ( factory * s3DriverFactory ) Create ( parameters map [ string ] interface { } ) ( storagedriver . StorageDriver , error ) {
return FromParameters ( parameters )
}
type driver struct {
S3 * s3 . S3
2016-01-22 02:17:53 +00:00
Bucket string
2015-02-11 02:14:23 +00:00
ChunkSize int64
Encrypt bool
2016-03-10 00:52:59 +00:00
KeyID string
2015-02-11 02:14:23 +00:00
RootDirectory string
2016-01-22 02:17:53 +00:00
StorageClass string
2015-02-11 02:14:23 +00:00
}
type baseEmbed struct {
base . Base
}
// Driver is a storagedriver.StorageDriver implementation backed by Amazon S3
// Objects are stored at absolute keys in the provided bucket.
type Driver struct {
baseEmbed
}
// FromParameters constructs a new Driver with a given parameters map
// Required parameters:
// - accesskey
// - secretkey
// - region
// - bucket
// - encrypt
func FromParameters ( parameters map [ string ] interface { } ) ( * Driver , error ) {
// Providing no values for these is valid in case the user is authenticating
// with an IAM on an ec2 instance (in which case the instance credentials will
// be summoned when GetAuth is called)
2016-03-07 19:50:46 +00:00
accessKey := parameters [ "accesskey" ]
if accessKey == nil {
2015-02-11 02:14:23 +00:00
accessKey = ""
}
2016-03-07 19:50:46 +00:00
secretKey := parameters [ "secretkey" ]
if secretKey == nil {
2015-02-11 02:14:23 +00:00
secretKey = ""
}
2016-01-22 02:17:53 +00:00
regionName , ok := parameters [ "region" ]
2016-03-07 19:50:46 +00:00
if regionName == nil || fmt . Sprint ( regionName ) == "" {
2015-02-11 02:14:23 +00:00
return nil , fmt . Errorf ( "No region parameter provided" )
}
2016-01-22 02:17:53 +00:00
region := fmt . Sprint ( regionName )
_ , ok = validRegions [ region ]
if ! ok {
2015-02-11 02:14:23 +00:00
return nil , fmt . Errorf ( "Invalid region provided: %v" , region )
}
2016-03-07 19:50:46 +00:00
bucket := parameters [ "bucket" ]
if bucket == nil || fmt . Sprint ( bucket ) == "" {
2015-02-11 02:14:23 +00:00
return nil , fmt . Errorf ( "No bucket parameter provided" )
}
2016-03-05 18:46:44 +00:00
regionEndpoint := parameters [ "regionendpoint" ]
if regionEndpoint == nil {
regionEndpoint = ""
}
2015-02-11 02:14:23 +00:00
encryptBool := false
2016-03-07 19:50:46 +00:00
encrypt := parameters [ "encrypt" ]
switch encrypt := encrypt . ( type ) {
case string :
b , err := strconv . ParseBool ( encrypt )
if err != nil {
2015-02-11 02:14:23 +00:00
return nil , fmt . Errorf ( "The encrypt parameter should be a boolean" )
}
2016-03-07 19:50:46 +00:00
encryptBool = b
case bool :
encryptBool = encrypt
case nil :
// do nothing
default :
return nil , fmt . Errorf ( "The encrypt parameter should be a boolean" )
2015-02-11 02:14:23 +00:00
}
secureBool := true
2016-03-07 19:50:46 +00:00
secure := parameters [ "secure" ]
switch secure := secure . ( type ) {
case string :
b , err := strconv . ParseBool ( secure )
if err != nil {
2015-02-11 02:14:23 +00:00
return nil , fmt . Errorf ( "The secure parameter should be a boolean" )
}
2016-03-07 19:50:46 +00:00
secureBool = b
case bool :
secureBool = secure
case nil :
// do nothing
default :
return nil , fmt . Errorf ( "The secure parameter should be a boolean" )
2015-02-11 02:14:23 +00:00
}
2016-03-10 00:52:59 +00:00
keyID := parameters [ "keyid" ]
if keyID == nil {
keyID = ""
}
2015-02-11 02:14:23 +00:00
chunkSize := int64 ( defaultChunkSize )
2016-03-07 19:50:46 +00:00
chunkSizeParam := parameters [ "chunksize" ]
switch v := chunkSizeParam . ( type ) {
case string :
vv , err := strconv . ParseInt ( v , 0 , 64 )
if err != nil {
return nil , fmt . Errorf ( "chunksize parameter must be an integer, %v invalid" , chunkSizeParam )
2015-04-22 21:31:34 +00:00
}
2016-03-07 19:50:46 +00:00
chunkSize = vv
case int64 :
chunkSize = v
case int , uint , int32 , uint32 , uint64 :
chunkSize = reflect . ValueOf ( v ) . Convert ( reflect . TypeOf ( chunkSize ) ) . Int ( )
case nil :
// do nothing
default :
return nil , fmt . Errorf ( "invalid value for chunksize: %#v" , chunkSizeParam )
}
2015-04-22 21:31:34 +00:00
2016-03-07 19:50:46 +00:00
if chunkSize < minChunkSize {
return nil , fmt . Errorf ( "The chunksize %#v parameter should be a number that is larger than or equal to %d" , chunkSize , minChunkSize )
2015-02-11 02:14:23 +00:00
}
2016-03-07 19:50:46 +00:00
rootDirectory := parameters [ "rootdirectory" ]
if rootDirectory == nil {
2015-02-11 02:14:23 +00:00
rootDirectory = ""
}
2016-01-22 02:17:53 +00:00
storageClass := s3 . StorageClassStandard
2016-03-07 19:50:46 +00:00
storageClassParam := parameters [ "storageclass" ]
if storageClassParam != nil {
2016-01-28 23:48:49 +00:00
storageClassString , ok := storageClassParam . ( string )
if ! ok {
2016-01-22 02:17:53 +00:00
return nil , fmt . Errorf ( "The storageclass parameter must be one of %v, %v invalid" , [ ] string { s3 . StorageClassStandard , s3 . StorageClassReducedRedundancy } , storageClassParam )
2016-01-28 23:48:49 +00:00
}
// All valid storage class parameters are UPPERCASE, so be a bit more flexible here
2016-01-22 02:17:53 +00:00
storageClassString = strings . ToUpper ( storageClassString )
if storageClassString != s3 . StorageClassStandard && storageClassString != s3 . StorageClassReducedRedundancy {
return nil , fmt . Errorf ( "The storageclass parameter must be one of %v, %v invalid" , [ ] string { s3 . StorageClassStandard , s3 . StorageClassReducedRedundancy } , storageClassParam )
2016-01-28 23:48:49 +00:00
}
2016-01-22 02:17:53 +00:00
storageClass = storageClassString
2016-01-28 23:48:49 +00:00
}
2016-03-07 19:50:46 +00:00
userAgent := parameters [ "useragent" ]
if userAgent == nil {
2016-01-21 00:40:58 +00:00
userAgent = ""
}
2015-02-11 02:14:23 +00:00
params := DriverParameters {
fmt . Sprint ( accessKey ) ,
fmt . Sprint ( secretKey ) ,
fmt . Sprint ( bucket ) ,
region ,
2016-03-05 18:46:44 +00:00
fmt . Sprint ( regionEndpoint ) ,
2015-02-11 02:14:23 +00:00
encryptBool ,
2016-03-10 00:52:59 +00:00
fmt . Sprint ( keyID ) ,
2015-02-11 02:14:23 +00:00
secureBool ,
chunkSize ,
fmt . Sprint ( rootDirectory ) ,
2016-01-28 23:48:49 +00:00
storageClass ,
2016-01-21 00:40:58 +00:00
fmt . Sprint ( userAgent ) ,
2015-02-11 02:14:23 +00:00
}
return New ( params )
}
// New constructs a new Driver with the given AWS credentials, region, encryption flag, and
// bucketName
func New ( params DriverParameters ) ( * Driver , error ) {
2016-01-22 02:17:53 +00:00
awsConfig := aws . NewConfig ( )
2016-03-05 18:46:44 +00:00
var creds * credentials . Credentials
if params . RegionEndpoint == "" {
creds = credentials . NewChainCredentials ( [ ] credentials . Provider {
& credentials . StaticProvider {
Value : credentials . Value {
AccessKeyID : params . AccessKey ,
SecretAccessKey : params . SecretKey ,
} ,
} ,
& credentials . EnvProvider { } ,
& credentials . SharedCredentialsProvider { } ,
& ec2rolecreds . EC2RoleProvider { Client : ec2metadata . New ( session . New ( ) ) } ,
} )
} else {
creds = credentials . NewChainCredentials ( [ ] credentials . Provider {
& credentials . StaticProvider {
Value : credentials . Value {
AccessKeyID : params . AccessKey ,
SecretAccessKey : params . SecretKey ,
} ,
2016-01-22 02:17:53 +00:00
} ,
2016-03-05 18:46:44 +00:00
& credentials . EnvProvider { } ,
} )
awsConfig . WithS3ForcePathStyle ( true )
awsConfig . WithEndpoint ( params . RegionEndpoint )
}
2015-02-11 02:14:23 +00:00
2016-01-22 02:17:53 +00:00
awsConfig . WithCredentials ( creds )
awsConfig . WithRegion ( params . Region )
awsConfig . WithDisableSSL ( ! params . Secure )
2016-01-21 00:40:58 +00:00
if params . UserAgent != "" {
2016-01-22 02:17:53 +00:00
awsConfig . WithHTTPClient ( & http . Client {
Transport : transport . NewTransport ( http . DefaultTransport , transport . NewHeaderRequestModifier ( http . Header { http . CanonicalHeaderKey ( "User-Agent" ) : [ ] string { params . UserAgent } } ) ) ,
} )
2016-01-21 00:40:58 +00:00
}
2015-02-11 02:14:23 +00:00
2016-01-22 02:17:53 +00:00
s3obj := s3 . New ( session . New ( awsConfig ) )
2016-01-21 00:40:58 +00:00
2015-02-11 02:14:23 +00:00
// TODO Currently multipart uploads have no timestamps, so this would be unwise
// if you initiated a new s3driver while another one is running on the same bucket.
// multis, _, err := bucket.ListMulti("", "")
// if err != nil {
// return nil, err
// }
// for _, multi := range multis {
// err := multi.Abort()
// //TODO appropriate to do this error checking?
// if err != nil {
// return nil, err
// }
// }
d := & driver {
S3 : s3obj ,
2016-01-22 02:17:53 +00:00
Bucket : params . Bucket ,
2015-02-11 02:14:23 +00:00
ChunkSize : params . ChunkSize ,
Encrypt : params . Encrypt ,
2016-03-10 00:52:59 +00:00
KeyID : params . KeyID ,
2015-02-11 02:14:23 +00:00
RootDirectory : params . RootDirectory ,
2016-01-28 23:48:49 +00:00
StorageClass : params . StorageClass ,
2015-02-11 02:14:23 +00:00
}
return & Driver {
baseEmbed : baseEmbed {
Base : base . Base {
StorageDriver : d ,
} ,
} ,
} , nil
}
// Implement the storagedriver.StorageDriver interface
2015-04-23 00:30:01 +00:00
func ( d * driver ) Name ( ) string {
return driverName
}
2015-02-11 02:14:23 +00:00
// GetContent retrieves the content stored at "path" as a []byte.
2015-04-27 22:58:58 +00:00
func ( d * driver ) GetContent ( ctx context . Context , path string ) ( [ ] byte , error ) {
2016-02-08 22:29:21 +00:00
reader , err := d . Reader ( ctx , path , 0 )
2015-02-11 02:14:23 +00:00
if err != nil {
2016-01-22 02:17:53 +00:00
return nil , err
2015-02-11 02:14:23 +00:00
}
2016-01-22 02:17:53 +00:00
return ioutil . ReadAll ( reader )
2015-02-11 02:14:23 +00:00
}
// PutContent stores the []byte content at a location designated by "path".
2015-04-27 22:58:58 +00:00
func ( d * driver ) PutContent ( ctx context . Context , path string , contents [ ] byte ) error {
2016-01-22 02:17:53 +00:00
_ , err := d . S3 . PutObject ( & s3 . PutObjectInput {
Bucket : aws . String ( d . Bucket ) ,
Key : aws . String ( d . s3Path ( path ) ) ,
ContentType : d . getContentType ( ) ,
ACL : d . getACL ( ) ,
ServerSideEncryption : d . getEncryptionMode ( ) ,
2016-03-10 00:52:59 +00:00
SSEKMSKeyId : d . getSSEKMSKeyID ( ) ,
2016-01-22 02:17:53 +00:00
StorageClass : d . getStorageClass ( ) ,
Body : bytes . NewReader ( contents ) ,
} )
return parseError ( path , err )
2015-02-11 02:14:23 +00:00
}
2016-02-08 22:29:21 +00:00
// Reader retrieves an io.ReadCloser for the content stored at "path" with a
2015-02-11 02:14:23 +00:00
// given byte offset.
2016-02-08 22:29:21 +00:00
func ( d * driver ) Reader ( ctx context . Context , path string , offset int64 ) ( io . ReadCloser , error ) {
2016-01-22 02:17:53 +00:00
resp , err := d . S3 . GetObject ( & s3 . GetObjectInput {
Bucket : aws . String ( d . Bucket ) ,
Key : aws . String ( d . s3Path ( path ) ) ,
Range : aws . String ( "bytes=" + strconv . FormatInt ( offset , 10 ) + "-" ) ,
} )
2015-02-11 02:14:23 +00:00
if err != nil {
2016-01-22 02:17:53 +00:00
if s3Err , ok := err . ( awserr . Error ) ; ok && s3Err . Code ( ) == "InvalidRange" {
2015-02-11 02:14:23 +00:00
return ioutil . NopCloser ( bytes . NewReader ( nil ) ) , nil
}
return nil , parseError ( path , err )
}
return resp . Body , nil
}
2016-02-08 22:29:21 +00:00
// Writer returns a FileWriter which will store the content written to it
// at the location designated by "path" after the call to Commit.
func ( d * driver ) Writer ( ctx context . Context , path string , append bool ) ( storagedriver . FileWriter , error ) {
key := d . s3Path ( path )
if ! append {
// TODO (brianbland): cancel other uploads at this path
resp , err := d . S3 . CreateMultipartUpload ( & s3 . CreateMultipartUploadInput {
Bucket : aws . String ( d . Bucket ) ,
Key : aws . String ( key ) ,
ContentType : d . getContentType ( ) ,
ACL : d . getACL ( ) ,
ServerSideEncryption : d . getEncryptionMode ( ) ,
2016-03-10 00:52:59 +00:00
SSEKMSKeyId : d . getSSEKMSKeyID ( ) ,
2016-02-08 22:29:21 +00:00
StorageClass : d . getStorageClass ( ) ,
} )
2015-02-11 02:14:23 +00:00
if err != nil {
2016-02-08 22:29:21 +00:00
return nil , err
2015-02-11 02:14:23 +00:00
}
2016-02-08 22:29:21 +00:00
return d . newWriter ( key , * resp . UploadId , nil ) , nil
2015-02-11 02:14:23 +00:00
}
2016-02-08 22:29:21 +00:00
resp , err := d . S3 . ListMultipartUploads ( & s3 . ListMultipartUploadsInput {
Bucket : aws . String ( d . Bucket ) ,
Prefix : aws . String ( key ) ,
} )
if err != nil {
return nil , parseError ( path , err )
2015-02-11 02:14:23 +00:00
}
2016-02-08 22:29:21 +00:00
for _ , multi := range resp . Uploads {
if key != * multi . Key {
continue
}
resp , err := d . S3 . ListParts ( & s3 . ListPartsInput {
Bucket : aws . String ( d . Bucket ) ,
Key : aws . String ( key ) ,
UploadId : multi . UploadId ,
2016-01-22 02:17:53 +00:00
} )
2015-02-11 02:14:23 +00:00
if err != nil {
2016-02-08 22:29:21 +00:00
return nil , parseError ( path , err )
2015-02-11 02:14:23 +00:00
}
2016-02-08 22:29:21 +00:00
var multiSize int64
for _ , part := range resp . Parts {
multiSize += * part . Size
2015-02-11 02:14:23 +00:00
}
2016-02-08 22:29:21 +00:00
return d . newWriter ( key , * multi . UploadId , resp . Parts ) , nil
2015-02-11 02:14:23 +00:00
}
2016-02-08 22:29:21 +00:00
return nil , storagedriver . PathNotFoundError { Path : path }
2015-02-11 02:14:23 +00:00
}
// Stat retrieves the FileInfo for the given path, including the current size
// in bytes and the creation time.
2015-04-27 22:58:58 +00:00
func ( d * driver ) Stat ( ctx context . Context , path string ) ( storagedriver . FileInfo , error ) {
2016-01-22 02:17:53 +00:00
resp , err := d . S3 . ListObjects ( & s3 . ListObjectsInput {
Bucket : aws . String ( d . Bucket ) ,
Prefix : aws . String ( d . s3Path ( path ) ) ,
MaxKeys : aws . Int64 ( 1 ) ,
} )
2015-02-11 02:14:23 +00:00
if err != nil {
return nil , err
}
fi := storagedriver . FileInfoFields {
Path : path ,
}
2016-01-22 02:17:53 +00:00
if len ( resp . Contents ) == 1 {
if * resp . Contents [ 0 ] . Key != d . s3Path ( path ) {
2015-02-11 02:14:23 +00:00
fi . IsDir = true
} else {
fi . IsDir = false
2016-01-22 02:17:53 +00:00
fi . Size = * resp . Contents [ 0 ] . Size
fi . ModTime = * resp . Contents [ 0 ] . LastModified
2015-02-11 02:14:23 +00:00
}
2016-01-22 02:17:53 +00:00
} else if len ( resp . CommonPrefixes ) == 1 {
2015-02-11 02:14:23 +00:00
fi . IsDir = true
} else {
return nil , storagedriver . PathNotFoundError { Path : path }
}
return storagedriver . FileInfoInternal { FileInfoFields : fi } , nil
}
// List returns a list of the objects that are direct descendants of the given path.
2015-12-08 19:02:40 +00:00
func ( d * driver ) List ( ctx context . Context , opath string ) ( [ ] string , error ) {
path := opath
2015-02-11 02:14:23 +00:00
if path != "/" && path [ len ( path ) - 1 ] != '/' {
path = path + "/"
}
2015-02-20 00:31:34 +00:00
// This is to cover for the cases when the rootDirectory of the driver is either "" or "/".
// In those cases, there is no root prefix to replace and we must actually add a "/" to all
// results in order to keep them as valid paths as recognized by storagedriver.PathRegexp
prefix := ""
if d . s3Path ( "" ) == "" {
prefix = "/"
}
2016-01-22 02:17:53 +00:00
resp , err := d . S3 . ListObjects ( & s3 . ListObjectsInput {
Bucket : aws . String ( d . Bucket ) ,
Prefix : aws . String ( d . s3Path ( path ) ) ,
Delimiter : aws . String ( "/" ) ,
MaxKeys : aws . Int64 ( listMax ) ,
} )
2015-02-11 02:14:23 +00:00
if err != nil {
2015-12-08 19:02:40 +00:00
return nil , parseError ( opath , err )
2015-11-24 22:23:12 +00:00
}
2015-02-11 02:14:23 +00:00
files := [ ] string { }
directories := [ ] string { }
for {
2016-01-22 02:17:53 +00:00
for _ , key := range resp . Contents {
files = append ( files , strings . Replace ( * key . Key , d . s3Path ( "" ) , prefix , 1 ) )
2015-02-11 02:14:23 +00:00
}
2016-01-22 02:17:53 +00:00
for _ , commonPrefix := range resp . CommonPrefixes {
commonPrefix := * commonPrefix . Prefix
2015-02-20 00:31:34 +00:00
directories = append ( directories , strings . Replace ( commonPrefix [ 0 : len ( commonPrefix ) - 1 ] , d . s3Path ( "" ) , prefix , 1 ) )
2015-02-11 02:14:23 +00:00
}
2016-01-22 02:17:53 +00:00
if * resp . IsTruncated {
resp , err = d . S3 . ListObjects ( & s3 . ListObjectsInput {
Bucket : aws . String ( d . Bucket ) ,
Prefix : aws . String ( d . s3Path ( path ) ) ,
Delimiter : aws . String ( "/" ) ,
MaxKeys : aws . Int64 ( listMax ) ,
Marker : resp . NextMarker ,
} )
2015-02-11 02:14:23 +00:00
if err != nil {
return nil , err
}
} else {
break
}
}
2015-12-08 19:02:40 +00:00
if opath != "/" {
if len ( files ) == 0 && len ( directories ) == 0 {
// Treat empty response as missing directory, since we don't actually
// have directories in s3.
return nil , storagedriver . PathNotFoundError { Path : opath }
}
}
2015-02-11 02:14:23 +00:00
return append ( files , directories ... ) , nil
}
// Move moves an object stored at sourcePath to destPath, removing the original
// object.
2015-04-27 22:58:58 +00:00
func ( d * driver ) Move ( ctx context . Context , sourcePath string , destPath string ) error {
2015-02-11 02:14:23 +00:00
/* This is terrible, but aws doesn't have an actual move. */
2016-01-22 02:17:53 +00:00
_ , err := d . S3 . CopyObject ( & s3 . CopyObjectInput {
Bucket : aws . String ( d . Bucket ) ,
Key : aws . String ( d . s3Path ( destPath ) ) ,
ContentType : d . getContentType ( ) ,
ACL : d . getACL ( ) ,
ServerSideEncryption : d . getEncryptionMode ( ) ,
2016-03-10 00:52:59 +00:00
SSEKMSKeyId : d . getSSEKMSKeyID ( ) ,
2016-01-22 02:17:53 +00:00
StorageClass : d . getStorageClass ( ) ,
CopySource : aws . String ( d . Bucket + "/" + d . s3Path ( sourcePath ) ) ,
} )
2015-02-11 02:14:23 +00:00
if err != nil {
return parseError ( sourcePath , err )
}
2015-04-27 22:58:58 +00:00
return d . Delete ( ctx , sourcePath )
2015-02-11 02:14:23 +00:00
}
// Delete recursively deletes all objects stored at "path" and its subpaths.
2015-04-27 22:58:58 +00:00
func ( d * driver ) Delete ( ctx context . Context , path string ) error {
2016-01-22 02:17:53 +00:00
resp , err := d . S3 . ListObjects ( & s3 . ListObjectsInput {
Bucket : aws . String ( d . Bucket ) ,
Prefix : aws . String ( d . s3Path ( path ) ) ,
} )
if err != nil || len ( resp . Contents ) == 0 {
2015-02-11 02:14:23 +00:00
return storagedriver . PathNotFoundError { Path : path }
}
2016-01-22 02:17:53 +00:00
s3Objects := make ( [ ] * s3 . ObjectIdentifier , 0 , listMax )
2015-02-11 02:14:23 +00:00
2016-01-22 02:17:53 +00:00
for len ( resp . Contents ) > 0 {
for _ , key := range resp . Contents {
s3Objects = append ( s3Objects , & s3 . ObjectIdentifier {
Key : key . Key ,
} )
2015-02-11 02:14:23 +00:00
}
2016-01-22 02:17:53 +00:00
_ , err := d . S3 . DeleteObjects ( & s3 . DeleteObjectsInput {
Bucket : aws . String ( d . Bucket ) ,
Delete : & s3 . Delete {
Objects : s3Objects ,
Quiet : aws . Bool ( false ) ,
} ,
} )
2015-02-11 02:14:23 +00:00
if err != nil {
return nil
}
2016-01-22 02:17:53 +00:00
resp , err = d . S3 . ListObjects ( & s3 . ListObjectsInput {
Bucket : aws . String ( d . Bucket ) ,
Prefix : aws . String ( d . s3Path ( path ) ) ,
} )
2015-02-11 02:14:23 +00:00
if err != nil {
return err
}
}
return nil
}
// URLFor returns a URL which may be used to retrieve the content stored at the given path.
// May return an UnsupportedMethodErr in certain StorageDriver implementations.
2015-04-27 22:58:58 +00:00
func ( d * driver ) URLFor ( ctx context . Context , path string , options map [ string ] interface { } ) ( string , error ) {
2015-02-11 02:14:23 +00:00
methodString := "GET"
method , ok := options [ "method" ]
if ok {
methodString , ok = method . ( string )
if ! ok || ( methodString != "GET" && methodString != "HEAD" ) {
2015-11-02 21:23:53 +00:00
return "" , storagedriver . ErrUnsupportedMethod { }
2015-02-11 02:14:23 +00:00
}
}
2016-01-22 02:17:53 +00:00
expiresIn := 20 * time . Minute
2015-02-11 02:14:23 +00:00
expires , ok := options [ "expiry" ]
if ok {
et , ok := expires . ( time . Time )
if ok {
2016-01-22 02:17:53 +00:00
expiresIn = et . Sub ( time . Now ( ) )
2015-02-11 02:14:23 +00:00
}
}
2016-01-22 02:17:53 +00:00
var req * request . Request
switch methodString {
case "GET" :
req , _ = d . S3 . GetObjectRequest ( & s3 . GetObjectInput {
Bucket : aws . String ( d . Bucket ) ,
Key : aws . String ( d . s3Path ( path ) ) ,
} )
case "HEAD" :
req , _ = d . S3 . HeadObjectRequest ( & s3 . HeadObjectInput {
Bucket : aws . String ( d . Bucket ) ,
Key : aws . String ( d . s3Path ( path ) ) ,
} )
default :
panic ( "unreachable" )
}
return req . Presign ( expiresIn )
2015-02-11 02:14:23 +00:00
}
func ( d * driver ) s3Path ( path string ) string {
return strings . TrimLeft ( strings . TrimRight ( d . RootDirectory , "/" ) + path , "/" )
}
2015-04-06 23:23:31 +00:00
// S3BucketKey returns the s3 bucket key for the given storage driver path.
func ( d * Driver ) S3BucketKey ( path string ) string {
return d . StorageDriver . ( * driver ) . s3Path ( path )
}
2015-02-11 02:14:23 +00:00
func parseError ( path string , err error ) error {
2016-01-22 02:17:53 +00:00
if s3Err , ok := err . ( awserr . Error ) ; ok && s3Err . Code ( ) == "NoSuchKey" {
2015-02-11 02:14:23 +00:00
return storagedriver . PathNotFoundError { Path : path }
}
return err
}
2016-01-22 02:17:53 +00:00
func ( d * driver ) getEncryptionMode ( ) * string {
2016-03-10 00:52:59 +00:00
if ! d . Encrypt {
return nil
}
if d . KeyID == "" {
2016-01-22 02:17:53 +00:00
return aws . String ( "AES256" )
}
2016-03-10 00:52:59 +00:00
return aws . String ( "aws:kms" )
}
func ( d * driver ) getSSEKMSKeyID ( ) * string {
if d . KeyID != "" {
return aws . String ( d . KeyID )
}
2016-01-22 02:17:53 +00:00
return nil
2015-02-11 02:14:23 +00:00
}
2016-01-22 02:17:53 +00:00
func ( d * driver ) getContentType ( ) * string {
return aws . String ( "application/octet-stream" )
2015-02-11 02:14:23 +00:00
}
2016-01-22 02:17:53 +00:00
func ( d * driver ) getACL ( ) * string {
return aws . String ( "private" )
2015-02-11 02:14:23 +00:00
}
2016-01-22 02:17:53 +00:00
func ( d * driver ) getStorageClass ( ) * string {
return aws . String ( d . StorageClass )
2015-02-11 02:14:23 +00:00
}
2015-04-22 22:07:18 +00:00
2016-02-08 22:29:21 +00:00
// writer attempts to upload parts to S3 in a buffered fashion where the last
// part is at least as large as the chunksize, so the multipart upload could be
// cleanly resumed in the future. This is violated if Close is called after less
// than a full chunk is written.
type writer struct {
driver * driver
key string
uploadID string
parts [ ] * s3 . Part
size int64
readyPart [ ] byte
pendingPart [ ] byte
closed bool
committed bool
cancelled bool
2015-04-22 22:07:18 +00:00
}
2016-02-08 22:29:21 +00:00
func ( d * driver ) newWriter ( key , uploadID string , parts [ ] * s3 . Part ) storagedriver . FileWriter {
var size int64
for _ , part := range parts {
size += * part . Size
}
return & writer {
driver : d ,
key : key ,
uploadID : uploadID ,
parts : parts ,
size : size ,
}
}
func ( w * writer ) Write ( p [ ] byte ) ( int , error ) {
if w . closed {
return 0 , fmt . Errorf ( "already closed" )
} else if w . committed {
return 0 , fmt . Errorf ( "already committed" )
} else if w . cancelled {
return 0 , fmt . Errorf ( "already cancelled" )
}
// If the last written part is smaller than minChunkSize, we need to make a
// new multipart upload :sadface:
if len ( w . parts ) > 0 && int ( * w . parts [ len ( w . parts ) - 1 ] . Size ) < minChunkSize {
var completedParts [ ] * s3 . CompletedPart
for _ , part := range w . parts {
completedParts = append ( completedParts , & s3 . CompletedPart {
ETag : part . ETag ,
PartNumber : part . PartNumber ,
} )
}
_ , err := w . driver . S3 . CompleteMultipartUpload ( & s3 . CompleteMultipartUploadInput {
Bucket : aws . String ( w . driver . Bucket ) ,
Key : aws . String ( w . key ) ,
UploadId : aws . String ( w . uploadID ) ,
MultipartUpload : & s3 . CompletedMultipartUpload {
Parts : completedParts ,
} ,
} )
if err != nil {
w . driver . S3 . AbortMultipartUpload ( & s3 . AbortMultipartUploadInput {
Bucket : aws . String ( w . driver . Bucket ) ,
Key : aws . String ( w . key ) ,
UploadId : aws . String ( w . uploadID ) ,
} )
return 0 , err
}
resp , err := w . driver . S3 . CreateMultipartUpload ( & s3 . CreateMultipartUploadInput {
Bucket : aws . String ( w . driver . Bucket ) ,
Key : aws . String ( w . key ) ,
ContentType : w . driver . getContentType ( ) ,
ACL : w . driver . getACL ( ) ,
ServerSideEncryption : w . driver . getEncryptionMode ( ) ,
StorageClass : w . driver . getStorageClass ( ) ,
} )
if err != nil {
return 0 , err
}
w . uploadID = * resp . UploadId
// If the entire written file is smaller than minChunkSize, we need to make
// a new part from scratch :double sad face:
if w . size < minChunkSize {
resp , err := w . driver . S3 . GetObject ( & s3 . GetObjectInput {
Bucket : aws . String ( w . driver . Bucket ) ,
Key : aws . String ( w . key ) ,
} )
defer resp . Body . Close ( )
if err != nil {
return 0 , err
}
w . parts = nil
w . readyPart , err = ioutil . ReadAll ( resp . Body )
if err != nil {
return 0 , err
}
} else {
// Otherwise we can use the old file as the new first part
copyPartResp , err := w . driver . S3 . UploadPartCopy ( & s3 . UploadPartCopyInput {
Bucket : aws . String ( w . driver . Bucket ) ,
CopySource : aws . String ( w . driver . Bucket + "/" + w . key ) ,
Key : aws . String ( w . key ) ,
PartNumber : aws . Int64 ( 1 ) ,
UploadId : resp . UploadId ,
} )
if err != nil {
return 0 , err
}
w . parts = [ ] * s3 . Part {
{
ETag : copyPartResp . CopyPartResult . ETag ,
PartNumber : aws . Int64 ( 1 ) ,
Size : aws . Int64 ( w . size ) ,
} ,
}
}
}
var n int
for len ( p ) > 0 {
// If no parts are ready to write, fill up the first part
if neededBytes := int ( w . driver . ChunkSize ) - len ( w . readyPart ) ; neededBytes > 0 {
if len ( p ) >= neededBytes {
w . readyPart = append ( w . readyPart , p [ : neededBytes ] ... )
n += neededBytes
p = p [ neededBytes : ]
} else {
w . readyPart = append ( w . readyPart , p ... )
n += len ( p )
p = nil
}
}
if neededBytes := int ( w . driver . ChunkSize ) - len ( w . pendingPart ) ; neededBytes > 0 {
if len ( p ) >= neededBytes {
w . pendingPart = append ( w . pendingPart , p [ : neededBytes ] ... )
n += neededBytes
p = p [ neededBytes : ]
err := w . flushPart ( )
if err != nil {
w . size += int64 ( n )
return n , err
}
} else {
w . pendingPart = append ( w . pendingPart , p ... )
n += len ( p )
p = nil
}
}
}
w . size += int64 ( n )
return n , nil
}
func ( w * writer ) Size ( ) int64 {
return w . size
}
func ( w * writer ) Close ( ) error {
if w . closed {
return fmt . Errorf ( "already closed" )
}
w . closed = true
return w . flushPart ( )
}
func ( w * writer ) Cancel ( ) error {
if w . closed {
return fmt . Errorf ( "already closed" )
} else if w . committed {
return fmt . Errorf ( "already committed" )
}
w . cancelled = true
_ , err := w . driver . S3 . AbortMultipartUpload ( & s3 . AbortMultipartUploadInput {
Bucket : aws . String ( w . driver . Bucket ) ,
Key : aws . String ( w . key ) ,
UploadId : aws . String ( w . uploadID ) ,
} )
return err
}
func ( w * writer ) Commit ( ) error {
if w . closed {
return fmt . Errorf ( "already closed" )
} else if w . committed {
return fmt . Errorf ( "already committed" )
} else if w . cancelled {
return fmt . Errorf ( "already cancelled" )
}
err := w . flushPart ( )
if err != nil {
return err
}
w . committed = true
var completedParts [ ] * s3 . CompletedPart
for _ , part := range w . parts {
completedParts = append ( completedParts , & s3 . CompletedPart {
ETag : part . ETag ,
PartNumber : part . PartNumber ,
} )
}
_ , err = w . driver . S3 . CompleteMultipartUpload ( & s3 . CompleteMultipartUploadInput {
Bucket : aws . String ( w . driver . Bucket ) ,
Key : aws . String ( w . key ) ,
UploadId : aws . String ( w . uploadID ) ,
MultipartUpload : & s3 . CompletedMultipartUpload {
Parts : completedParts ,
} ,
} )
if err != nil {
w . driver . S3 . AbortMultipartUpload ( & s3 . AbortMultipartUploadInput {
Bucket : aws . String ( w . driver . Bucket ) ,
Key : aws . String ( w . key ) ,
UploadId : aws . String ( w . uploadID ) ,
} )
return err
}
return nil
}
// flushPart flushes buffers to write a part to S3.
// Only called by Write (with both buffers full) and Close/Commit (always)
func ( w * writer ) flushPart ( ) error {
if len ( w . readyPart ) == 0 && len ( w . pendingPart ) == 0 {
// nothing to write
return nil
}
if len ( w . pendingPart ) < int ( w . driver . ChunkSize ) {
// closing with a small pending part
// combine ready and pending to avoid writing a small part
w . readyPart = append ( w . readyPart , w . pendingPart ... )
w . pendingPart = nil
}
partNumber := aws . Int64 ( int64 ( len ( w . parts ) + 1 ) )
resp , err := w . driver . S3 . UploadPart ( & s3 . UploadPartInput {
Bucket : aws . String ( w . driver . Bucket ) ,
Key : aws . String ( w . key ) ,
PartNumber : partNumber ,
UploadId : aws . String ( w . uploadID ) ,
Body : bytes . NewReader ( w . readyPart ) ,
} )
if err != nil {
return err
}
w . parts = append ( w . parts , & s3 . Part {
ETag : resp . ETag ,
PartNumber : partNumber ,
Size : aws . Int64 ( int64 ( len ( w . readyPart ) ) ) ,
} )
w . readyPart = w . pendingPart
w . pendingPart = nil
return nil
2015-04-22 22:07:18 +00:00
}