Skip to main content

UploadPartCopy

UploadPartCopy fills one part of a multipart upload with data that is already stored - a whole object, or a byte range of one - so the bytes never have to travel through your client. The request has no body at all. A multipart upload starts with CreateMultipartUpload, takes its parts from UploadPartCopy and UploadPart in any mix, and is finished by CompleteMultipartUpload.

It is the way to copy an object too large for a single CopyObject, and to build a new object out of ranges of existing ones.

Good to know on STRG
  • A copy stays within one region: the source and the destination are both buckets in the endpoint's region.
  • One access key covers both ends of the copy. It holds Read objects as well as Write objects, and its buckets and object prefix apply to the source just as they do to the destination.
  • Parts other than the last are at least 5,000,000 bytes - a little under Amazon S3's 5 MiB, so a part size chosen for S3 works unchanged.
  • x-amz-copy-source and the x-amz-copy-source-* headers travel as headers: in a presigned URL's query string they get a 501 NotImplemented answer, so a client knows straight away.
  • x-amz-expected-bucket-owner and x-amz-source-expected-bucket-owner are welcome and have nothing to check: a key only ever reaches buckets of its own account.

When you'd use it​

  • Copying a very large object. A nightly job keeps a copy of a 20 GiB database backup in an archive bucket in the same region. An object that size is beyond what a single CopyObject is made for, so the job opens a multipart upload and copies the backup across in ranges, one part each, without downloading a byte.
  • Assembling one object from several. A video service records a long stream as separate segments and later joins them into one file: each segment becomes a part, copied in place, and CompleteMultipartUpload stitches them together.
  • Adding to an object that is already stored. A log archive grows by a few hundred MiB each day. Copying the existing archive as the first parts and uploading only the new data as the last part with UploadPart produces the longer archive, without the old data going up again.
  • Copying with the AWS CLI or an SDK's transfer helper. Above a size threshold, aws s3 cp between two buckets and boto3's managed copy() call UploadPartCopy for you, so it is often at work without being called by name.

Request​

PUT /videos/big-copy.mp4?partNumber=1&uploadId=UploadId HTTP/1.1
Host: my-bucket.s3.eu-central-1.strg.com
Authorization: AWS4-HMAC-SHA256 Credential=...
x-amz-date: 20260926T120000Z
x-amz-content-sha256: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
x-amz-copy-source: /my-bucket/videos/big.mp4
x-amz-copy-source-range: bytes=0-1073741823

Path-style, the same request is PUT /my-bucket/videos/big-copy.mp4?partNumber=1&uploadId=UploadId on s3.eu-central-1.strg.com - Addressing has both styles.

Every x-amz-* header, x-amz-copy-source included, is part of the signature; the AWS CLI and SDKs take care of that, and Authentication has the details.

Permissions​

A part copy reads one object and writes into an upload, so the access key needs both of these:

PermissionDashboard labelFor
objects:writeWrite objectsWriting the part into the upload
objects:readRead objectsReading the source object

For a key that reaches certain buckets or an object prefix, the destination bucket and key and the source bucket and key all fall within them; a copy reaching outside them is answered 403 AccessDenied. Access keys has the details.

URI parameters​

NameTypeRequiredDescription
BucketStringYesThe bucket of the multipart upload.
KeyStringYesThe key the multipart upload was started for.
partNumberIntegerYesThe part's number, 1 to 10,000. Any other value is answered 400 InvalidArgument.
uploadIdStringYesThe upload ID CreateMultipartUpload returned.

Any other query parameter gets a 501 NotImplemented answer. A specific version of the source is named inside x-amz-copy-source, never in the query string.

Request headers​

The headers below are every one S3 defines for UploadPartCopy, each with what STRG does with it.

NameTypeRequiredDescription
x-amz-copy-sourceStringYesThe source, as my-bucket/videos/big.mp4 (a leading / is optional), the key URL-encoded. Appending ?versionId=VersionId copies that version; without it, the current version is copied. The source bucket is in the endpoint's region, as the destination is. It travels as a header: in a presigned URL's query string it is answered 501 NotImplemented. A value that does not name a valid bucket and a non-empty key is answered 400 InvalidArgument.
x-amz-copy-source-rangeStringNoThe bytes to copy, as bytes=first-last - zero-based and inclusive, so bytes=0-9 is the first 10 bytes. Without it, the whole source object is copied.
x-amz-copy-source-if-matchStringNoCopies only if the source's ETag matches. STRG passes it along with the copy, and does not guarantee the outcome matches Amazon S3's.
x-amz-copy-source-if-none-matchStringNoCopies only if the source's ETag differs. STRG passes it along with the copy, and does not guarantee the outcome matches Amazon S3's.
x-amz-copy-source-if-modified-sinceTimestampNoCopies only if the source changed after this time. STRG passes it along with the copy, and does not guarantee the outcome matches Amazon S3's.
x-amz-copy-source-if-unmodified-sinceTimestampNoCopies only if the source has not changed since this time. STRG passes it along with the copy, and does not guarantee the outcome matches Amazon S3's.
x-amz-server-side-encryption-customer-algorithmStringNoAES256, when the upload is encrypted with your own key (SSE-C).
x-amz-server-side-encryption-customer-keyStringNoThe base64 key the upload is encrypted with - the same key CreateMultipartUpload was given.
x-amz-server-side-encryption-customer-key-MD5StringNoThe base64 MD5 digest of that key.
x-amz-copy-source-server-side-encryption-customer-algorithmStringNoAES256, when the source is encrypted with your own key.
x-amz-copy-source-server-side-encryption-customer-keyStringNoThe key the source was encrypted with.
x-amz-copy-source-server-side-encryption-customer-key-MD5StringNoThe base64 MD5 digest of that key.
x-amz-request-payerStringNoHas nothing to do: STRG has no Requester Pays buckets.
x-amz-expected-bucket-ownerStringNoWelcome, and has nothing to check: an access key only ever reaches its own account's buckets.
x-amz-source-expected-bucket-ownerStringNoWelcome, and has nothing to check, as above.

Request body​

None.

Part sizes​

A copied part follows the same size rules as every other part of a multipart upload:

Part numbers1 to 10,000
Minimum part size5,000,000 bytes, except the last part. A smaller one is answered 400 EntityTooSmall when the upload is completed
Maximum part size5 GiB

The limits are the same whether a part is copied or uploaded with UploadPart, and the parts of one upload can each be a different size. A source larger than 5 GiB is copied in ranges with x-amz-copy-source-range, one part each.

Unfinished uploads are billed

The parts of an upload that is never completed or aborted stay stored, copied parts included, and are billed as stored data. Each upload ends well with either CompleteMultipartUpload or AbortMultipartUpload, and a lifecycle rule that aborts incomplete multipart uploads catches the ones a crashed script leaves behind - Cleaning up abandoned multipart uploads shows one.

Response​

A successful copy answers 200 OK with a CopyPartResult document.

A 200 can still be an error

As on Amazon S3, a part copy that fails after it has started can answer 200 OK with an Error document in the body instead of CopyPartResult. Its code is one of InternalError, SlowDown, EntityTooSmall, InvalidPart, InvalidPartOrder or NoSuchUpload, and it comes without the x-amz-* response headers, since no part was stored. The AWS SDKs and the AWS CLI recognise this and raise an error; code calling the API directly reads the whole body and checks its root element.

Response headers​

NameDescription
x-amz-copy-source-version-idThe version ID of the source that was copied. Version IDs are opaque strings, best stored and sent back exactly as they are.
x-amz-server-side-encryptionAES256 when the upload is encrypted with STRG-managed keys.
x-amz-server-side-encryption-customer-algorithmAES256 when the upload is encrypted with your own key.
x-amz-server-side-encryption-customer-key-MD5The MD5 digest of your key, for SSE-C.
x-amz-request-idThe request ID, handy when contacting support.

x-amz-server-side-encryption-aws-kms-key-id, x-amz-server-side-encryption-bucket-key-enabled and x-amz-request-charged are not part of the answer: STRG has no SSE-KMS and no Requester Pays buckets.

Response body​

<?xml version="1.0" encoding="UTF-8"?>
<CopyPartResult>
<ETag>"85f30635602dc09bd85957a6e82a2c21"</ETag>
<LastModified>2026-09-26T12:00:00.000Z</LastModified>
</CopyPartResult>
ElementDescription
ETagThe part's entity tag. It goes, with the part number, into CompleteMultipartUpload.
LastModifiedWhen the part was copied.
ChecksumCRC32, ChecksumSHA256, ...The part's checksum, when the upload was started with a checksum algorithm (x-amz-checksum-algorithm on CreateMultipartUpload). STRG computes it for you from the copied bytes.

Errors​

These are the answers a part copy can get instead of 200 OK, and the situation behind each.

CodeHTTP statusWhen
NoSuchUpload404The upload ID does not exist, or the upload has already been completed or aborted.
NoSuchKey404The source object, or the version named by versionId, does not exist.
NoSuchBucket404The destination bucket does not exist, or it is in a region other than the endpoint's - including when your account stores nothing in the endpoint's region yet.
InvalidArgument400partNumber is not an integer from 1 to 10,000, or x-amz-copy-source does not name a valid bucket and key.
PreconditionFailed412An x-amz-copy-source-if-* condition is not met. STRG passes the conditions along with the copy, and does not guarantee this exact status and code for an unmet one.
NotImplemented501The request carries a query parameter this operation does not take, or x-amz-copy-source or an x-amz-copy-source-* header in the query string.
AccessDenied403The access key lacks Write objects or Read objects, or the source or destination is outside its buckets or object prefix, or an x-amz-* header is not signed.
InvalidAccessKeyId403The access key does not exist, has expired or been revoked, or was created moments ago and is not yet usable in this region, where you already store data. It becomes usable shortly after.
SignatureDoesNotMatch403The request signature does not match.
RequestTimeTooSkewed403The request time is more than 15 minutes from the server's.
AuthorizationHeaderMalformed400The signature's credential scope names a region other than the endpoint's.
InternalError200 or 500A temporary failure, which a retry of the request gets past.
SlowDown503Too many requests at once. The AWS CLI and SDKs retry after the Retry-After interval.
ServiceUnavailable503A temporary failure, which a retry of the request gets past.

Differences from Amazon S3​

Each row is something STRG does its own way, and what that means for a part copy.

AspectAmazon S3STRG
Source in another regionSupportedA copy stays within one region. Moving an object between regions is a download from one regional endpoint and an upload to the other.
Bucket in another region301 redirect with x-amz-bucket-region404 NoSuchBucket. The bucket's own regional endpoint reaches it.
Permissionss3:GetObject on the source, s3:PutObject on the destinationOne access key holding both Read objects and Write objects; its buckets and object prefix apply to the source and the destination
Minimum part size5 MiB5,000,000 bytes, except the last part - so a part size chosen for S3 works unchanged
x-amz-copy-source and x-amz-copy-source-* in a presigned URL's queryAccepted501 NotImplemented. They travel as headers
SSE-KMSSupportedAES256 with STRG-managed keys, or SSE-C, both set on CreateMultipartUpload
x-amz-expected-bucket-owner, x-amz-source-expected-bucket-ownerChecked against the bucket ownerWelcome, with nothing to check

Examples​

Copying a whole object into part 1 of a new upload, then completing it - which works for any source up to 5 GiB:

ENDPOINT=https://s3.eu-central-1.strg.com
REGION=eu-central-1

UPLOAD_ID=$(aws s3api create-multipart-upload \
--bucket my-bucket \
--key videos/big-copy.mp4 \
--endpoint-url "$ENDPOINT" --region "$REGION" \
--query UploadId --output text)

ETAG=$(aws s3api upload-part-copy \
--bucket my-bucket \
--key videos/big-copy.mp4 \
--upload-id "$UPLOAD_ID" \
--part-number 1 \
--copy-source my-bucket/videos/big.mp4 \
--endpoint-url "$ENDPOINT" --region "$REGION" \
--query CopyPartResult.ETag --output json)

aws s3api complete-multipart-upload \
--bucket my-bucket \
--key videos/big-copy.mp4 \
--upload-id "$UPLOAD_ID" \
--multipart-upload "{\"Parts\":[{\"PartNumber\":1,\"ETag\":$ETAG}]}" \
--endpoint-url "$ENDPOINT" --region "$REGION"

Copying an object of any size in 1 GiB ranges with x-amz-copy-source-range:

ENDPOINT=https://s3.eu-central-1.strg.com
REGION=eu-central-1
PART_SIZE=1073741824

SIZE=$(aws s3api head-object \
--bucket my-bucket \
--key videos/big.mp4 \
--endpoint-url "$ENDPOINT" --region "$REGION" \
--query ContentLength --output text)

UPLOAD_ID=$(aws s3api create-multipart-upload \
--bucket my-bucket \
--key videos/big-copy.mp4 \
--endpoint-url "$ENDPOINT" --region "$REGION" \
--query UploadId --output text)

PARTS=""
PART=1
for ((START = 0; START < SIZE; START += PART_SIZE)); do
END=$((START + PART_SIZE - 1))
if ((END >= SIZE)); then END=$((SIZE - 1)); fi
ETAG=$(aws s3api upload-part-copy \
--bucket my-bucket \
--key videos/big-copy.mp4 \
--upload-id "$UPLOAD_ID" \
--part-number "$PART" \
--copy-source my-bucket/videos/big.mp4 \
--copy-source-range "bytes=$START-$END" \
--endpoint-url "$ENDPOINT" --region "$REGION" \
--query CopyPartResult.ETag --output json)
PARTS="$PARTS${PARTS:+,}{\"PartNumber\":$PART,\"ETag\":$ETAG}"
PART=$((PART + 1))
done

aws s3api complete-multipart-upload \
--bucket my-bucket \
--key videos/big-copy.mp4 \
--upload-id "$UPLOAD_ID" \
--multipart-upload "{\"Parts\":[$PARTS]}" \
--endpoint-url "$ENDPOINT" --region "$REGION"
View as Markdown