github.com/Schaudge/grailbase@v0.0.0-20240223061707-44c758a471c0/s3util/s3copy.go (about) 1 package s3util 2 3 import ( 4 "context" 5 "fmt" 6 "net/url" 7 "strings" 8 "time" 9 10 "github.com/Schaudge/grailbase/errors" 11 "github.com/Schaudge/grailbase/retry" 12 "github.com/Schaudge/grailbase/traverse" 13 14 "github.com/aws/aws-sdk-go/aws" 15 "github.com/aws/aws-sdk-go/service/s3" 16 "github.com/aws/aws-sdk-go/service/s3/s3iface" 17 ) 18 19 const ( 20 // DefaultS3ObjectCopySizeLimit is the max size of object for a single PUT Object Copy request. 21 // As per AWS: https://docs.aws.amazon.com/AmazonS3/latest/API/RESTObjectCOPY.html 22 // the max size allowed is 5GB, but we use a smaller size here to speed up large file copies. 23 DefaultS3ObjectCopySizeLimit = 256 << 20 // 256MiB 24 25 // defaultS3MultipartCopyPartSize is the max size of each part when doing a multi-part copy. 26 // Note: Though we can do parts of size up to defaultS3ObjectCopySizeLimit, for large files 27 // using smaller size parts (concurrently) is much faster. 28 DefaultS3MultipartCopyPartSize = 128 << 20 // 128MiB 29 30 // s3MultipartCopyConcurrencyLimit is the number of concurrent parts to do during a multi-part copy. 31 s3MultipartCopyConcurrencyLimit = 100 32 33 defaultMaxRetries = 3 34 ) 35 36 var ( 37 // DefaultRetryPolicy is the default retry policy 38 DefaultRetryPolicy = retry.MaxRetries(retry.Jitter(retry.Backoff(1*time.Second, time.Minute, 2), 0.25), defaultMaxRetries) 39 ) 40 41 type Debugger interface { 42 Debugf(format string, args ...interface{}) 43 } 44 45 type noOpDebugger struct{} 46 47 func (d noOpDebugger) Debugf(format string, args ...interface{}) {} 48 49 // Copier supports operations to copy S3 objects (within or across buckets) 50 // by using S3 APIs that support the same (ie, without having to stream the data by reading and writing). 51 // 52 // Since AWS doesn't allow copying large files in a single operation, 53 // this will do a multi-part copy object in those cases. 54 // However, this behavior can also be controlled by setting appropriate values 55 // for S3ObjectCopySizeLimit and S3MultipartCopyPartSize. 56 57 type Copier struct { 58 client s3iface.S3API 59 retrier retry.Policy 60 61 // S3ObjectCopySizeLimit is the max size of object for a single PUT Object Copy request. 62 S3ObjectCopySizeLimit int64 63 // S3MultipartCopyPartSize is the max size of each part when doing a multi-part copy. 64 S3MultipartCopyPartSize int64 65 66 Debugger 67 } 68 69 func NewCopier(client s3iface.S3API) *Copier { 70 return NewCopierWithParams(client, DefaultRetryPolicy, DefaultS3ObjectCopySizeLimit, DefaultS3MultipartCopyPartSize, nil) 71 } 72 73 func NewCopierWithParams(client s3iface.S3API, retrier retry.Policy, s3ObjectCopySizeLimit int64, s3MultipartCopyPartSize int64, debugger Debugger) *Copier { 74 if debugger == nil { 75 debugger = noOpDebugger{} 76 } 77 return &Copier{ 78 client: client, 79 retrier: retrier, 80 S3ObjectCopySizeLimit: s3ObjectCopySizeLimit, 81 S3MultipartCopyPartSize: s3MultipartCopyPartSize, 82 Debugger: debugger, 83 } 84 } 85 86 // Copy copies the S3 object from srcUrl to dstUrl (both expected to be full S3 URLs) 87 // The size of the source object (srcSize) determines behavior (whether done as single or multi-part copy). 88 // 89 // dstMetadata must be set if the caller wishes to set the metadata on the dstUrl object. 90 // While the AWS API will copy the metadata over if done using CopyObject, but NOT when multi-part copy is done, 91 // this method requires that dstMetadata be always provided to remove ambiguity. 92 // So if metadata is desired on dstUrl object, *it must always be provided*. 93 func (c *Copier) Copy(ctx context.Context, srcUrl, dstUrl string, srcSize int64, dstMetadata map[string]*string) error { 94 copySrc := strings.TrimPrefix(srcUrl, "s3://") 95 dstBucket, dstKey, err := bucketKey(dstUrl) 96 if err != nil { 97 return err 98 } 99 if srcSize <= c.S3ObjectCopySizeLimit { 100 // Do single copy 101 input := &s3.CopyObjectInput{ 102 Bucket: aws.String(dstBucket), 103 Key: aws.String(dstKey), 104 CopySource: aws.String(copySrc), 105 Metadata: dstMetadata, 106 } 107 for retries := 0; ; retries++ { 108 _, err = c.client.CopyObjectWithContext(ctx, input) 109 err = CtxErr(ctx, err) 110 if err == nil { 111 break 112 } 113 severity := Severity(err) 114 if severity != errors.Temporary && severity != errors.Retriable { 115 break 116 } 117 c.Debugf("s3copy.Copy: attempt (%d): %s -> %s\n%v\n", retries, srcUrl, dstUrl, err) 118 if err = retry.Wait(ctx, c.retrier, retries); err != nil { 119 break 120 } 121 } 122 if err == nil { 123 c.Debugf("s3copy.Copy: done: %s -> %s", srcUrl, dstUrl) 124 } 125 return err 126 } 127 // Do a multi-part copy 128 numParts := (srcSize + c.S3MultipartCopyPartSize - 1) / c.S3MultipartCopyPartSize 129 input := &s3.CreateMultipartUploadInput{ 130 Bucket: aws.String(dstBucket), 131 Key: aws.String(dstKey), 132 Metadata: dstMetadata, 133 } 134 createOut, err := c.client.CreateMultipartUploadWithContext(ctx, input) 135 if err != nil { 136 return errors.E(fmt.Sprintf("CreateMultipartUpload: %s -> %s", srcUrl, dstUrl), err) 137 } 138 completedParts := make([]*s3.CompletedPart, numParts) 139 err = traverse.Limit(s3MultipartCopyConcurrencyLimit).Each(int(numParts), func(ti int) error { 140 i := int64(ti) 141 firstByte := i * c.S3MultipartCopyPartSize 142 lastByte := firstByte + c.S3MultipartCopyPartSize - 1 143 if lastByte >= srcSize { 144 lastByte = srcSize - 1 145 } 146 var partErr error 147 var uploadOut *s3.UploadPartCopyOutput 148 for retries := 0; ; retries++ { 149 uploadOut, partErr = c.client.UploadPartCopyWithContext(ctx, &s3.UploadPartCopyInput{ 150 Bucket: aws.String(dstBucket), 151 Key: aws.String(dstKey), 152 CopySource: aws.String(copySrc), 153 UploadId: createOut.UploadId, 154 PartNumber: aws.Int64(i + 1), 155 CopySourceRange: aws.String(fmt.Sprintf("bytes=%d-%d", firstByte, lastByte)), 156 }) 157 partErr = CtxErr(ctx, partErr) 158 if partErr == nil { 159 break 160 } 161 severity := Severity(partErr) 162 if severity != errors.Temporary && severity != errors.Retriable { 163 break 164 } 165 c.Debugf("s3copy.Copy: attempt (%d) (part %d/%d): %s -> %s\n%v\n", retries, i, numParts, srcUrl, dstUrl, partErr) 166 if partErr = retry.Wait(ctx, c.retrier, retries); partErr != nil { 167 break 168 } 169 } 170 if partErr == nil { 171 completedParts[i] = &s3.CompletedPart{ETag: uploadOut.CopyPartResult.ETag, PartNumber: aws.Int64(i + 1)} 172 c.Debugf("s3copy.Copy: done (part %d/%d): %s -> %s", i, numParts, srcUrl, dstUrl) 173 return nil 174 } 175 return errors.E(fmt.Sprintf("upload part copy (part %d/%d) %s -> %s", i, numParts, srcUrl, dstUrl), partErr) 176 }) 177 if err == nil { 178 // Complete the multi-part copy 179 for retries := 0; ; retries++ { 180 _, err = c.client.CompleteMultipartUploadWithContext(ctx, &s3.CompleteMultipartUploadInput{ 181 Bucket: aws.String(dstBucket), 182 Key: aws.String(dstKey), 183 UploadId: createOut.UploadId, 184 MultipartUpload: &s3.CompletedMultipartUpload{Parts: completedParts}, 185 }) 186 if err == nil || Severity(err) != errors.Temporary { 187 break 188 } 189 c.Debugf("s3copy.Copy complete upload: attempt (%d): %s -> %s\n%v\n", retries, srcUrl, dstUrl, err) 190 if err = retry.Wait(ctx, c.retrier, retries); err != nil { 191 break 192 } 193 } 194 if err == nil { 195 c.Debugf("s3copy.Copy: done (all %d parts): %s -> %s", numParts, srcUrl, dstUrl) 196 return nil 197 } 198 err = errors.E(fmt.Sprintf("complete multipart upload %s -> %s", srcUrl, dstUrl), Severity(err), err) 199 } 200 // Abort the multi-part copy 201 if _, er := c.client.AbortMultipartUploadWithContext(ctx, &s3.AbortMultipartUploadInput{ 202 Bucket: aws.String(dstBucket), 203 Key: aws.String(dstKey), 204 UploadId: createOut.UploadId, 205 }); er != nil { 206 err = fmt.Errorf("abort multipart copy %v (aborting due to original error: %v)", er, err) 207 } 208 return err 209 } 210 211 // bucketKey returns the bucket and key for the given S3 object url and error (if any). 212 func bucketKey(rawurl string) (string, string, error) { 213 u, err := url.Parse(rawurl) 214 if err != nil { 215 return "", "", errors.E(errors.Invalid, errors.Fatal, fmt.Sprintf("cannot determine bucket and key from rawurl %s", rawurl), err) 216 } 217 bucket := u.Host 218 return bucket, strings.TrimPrefix(rawurl, "s3://"+bucket+"/"), nil 219 }