# UploadPart

UploadPart sends one piece of an object that is being uploaded in parts. A multipart upload
opens with [CreateMultipartUpload](https://docs.strg.com/s3-api/CreateMultipartUpload), which hands back an upload ID;
each part then goes up with UploadPart - or is copied from an object already stored, with
[UploadPartCopy](https://docs.strg.com/s3-api/UploadPartCopy) - and [CompleteMultipartUpload](https://docs.strg.com/s3-api/CompleteMultipartUpload)
assembles the parts into the object, given the `ETag` each part returned.

The part number, 1-10,000, sets the part's position in the finished object. Sending a part again
under the same number replaces the earlier one, which is what makes a failed part so easy to
retry: only that part travels again.

**Info: Good to know on STRG**

- Every part but the last is at least 5,000,000 bytes, rather than 5 MiB, so a part size chosen
  for Amazon S3 works unchanged. A part can be up to 5 GiB.
- A body sent with `Transfer-Encoding: chunked` and no `Content-Length` is answered
  `411 MissingContentLength`. `aws-chunked` uploads, which the AWS SDKs send, work as they are.
- `x-amz-expected-bucket-owner` is welcome and has nothing to check: a key only ever reaches
  buckets of its own account.
- A bucket in another region answers `404 NoSuchBucket` rather than a redirect.

**Danger: Unfinished uploads are billed**

Uploaded parts stay stored, and are billed as stored data, until their upload is completed or
aborted. Each upload ends well with either CompleteMultipartUpload or
[AbortMultipartUpload](https://docs.strg.com/s3-api/AbortMultipartUpload), and a lifecycle rule that aborts incomplete
multipart uploads catches the ones a crashed script leaves behind -
[Cleaning up abandoned multipart uploads](https://docs.strg.com/lifecycle-policies#cleaning-up-abandoned-multipart-uploads)
shows one.

## When you'd use it

- **A nightly backup of a large database dump.** The backup tool sends the file in parts of, say,
  64 MiB, so a dropped connection costs one part rather than the whole file, and the upload picks
  up where it stopped.
- **A server uploading a long video recording.** Several parts can go up at once, over several
  connections, which finishes a large upload sooner than one long request.
- **A web app taking large uploads straight from the browser.** The app's server starts the
  upload and presigns one UploadPart URL per part; the browser sends each part with a plain
  `PUT`, and the server completes the upload with the `ETag`s it collected. The bucket needs
  [CORS rules](https://docs.strg.com/cors-rules) for the browser to reach it.
- **Anything using the AWS CLI's `aws s3 cp` or an SDK's transfer helper.** Above a size
  threshold they split the file and call UploadPart for each piece, so it is often at work
  without being called by name.

## Request

```http
PUT /backups/backup.tar?partNumber=1&uploadId=EXAMPLE-UPLOAD-ID 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: UNSIGNED-PAYLOAD
Content-Length: 67108864

<67,108,864 bytes of part data>
```

Path-style, the same request is `PUT /my-bucket/backups/backup.tar?partNumber=1&uploadId=EXAMPLE-UPLOAD-ID`
on `s3.eu-central-1.strg.com` - [Addressing](https://docs.strg.com/s3-api/overview#addressing) explains both forms.

### Permissions

Uploading a part takes **Write objects** (`objects:write`), the same permission that started the
upload. For a key limited to certain buckets or to a key prefix, the bucket and the object key
need to be within them. [Access keys](https://docs.strg.com/access-keys) has every permission.

### URI parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `Bucket` | String | Yes | The bucket the upload was started in. |
| `Key` | String | Yes | The object key the upload was started for. A key with a `.` or `..` path segment (`a/../b`) cannot be put in a URL, and STRG answers `400 InvalidURI`. |
| `partNumber` | Integer | Yes | The part's number, 1-10,000. Anything else is answered `400 InvalidArgument`. |
| `uploadId` | String | Yes | The upload ID [CreateMultipartUpload](https://docs.strg.com/s3-api/CreateMultipartUpload) returned, exactly as it came. |

Any other query parameter is answered with `501 NotImplemented`, so a client knows straight away.
A request carrying `x-amz-copy-source` is [UploadPartCopy](https://docs.strg.com/s3-api/UploadPartCopy) rather than
UploadPart.

### Request headers

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `Content-Length` | Integer | Yes | The size of the body in bytes. A part can be up to 5 GiB. A body without it - `Transfer-Encoding: chunked` - is answered `411 MissingContentLength`. |
| `Content-MD5` | String | No | The base64-encoded MD5 digest of the part, for an integrity check. A part that does not match is answered `400 BadDigest`, and nothing of it is stored. |
| `x-amz-sdk-checksum-algorithm` | String | No | The checksum algorithm the SDK used, such as `CRC32`, sent beside the matching `x-amz-checksum-*` header or trailer. An upload started with `x-amz-checksum-algorithm` has every part carry a checksum of that algorithm. |
| `x-amz-checksum-crc32` | String | No | Base64-encoded CRC32 checksum of the part. |
| `x-amz-checksum-crc32c` | String | No | Base64-encoded CRC32C checksum of the part. |
| `x-amz-checksum-crc64nvme` | String | No | Base64-encoded CRC64NVME checksum of the part. |
| `x-amz-checksum-sha1` | String | No | Base64-encoded SHA-1 digest of the part. |
| `x-amz-checksum-sha256` | String | No | Base64-encoded SHA-256 digest of the part. |
| `x-amz-checksum-sha512` | String | No | Passed on with the request. The five checksums above are the ones to reach for. |
| `x-amz-checksum-md5` | String | No | Passed on with the request. `Content-MD5` or one of the five checksums above covers the same need. |
| `x-amz-checksum-xxhash64` | String | No | Passed on with the request. The five checksums above are the ones to reach for. |
| `x-amz-checksum-xxhash3` | String | No | Passed on with the request. The five checksums above are the ones to reach for. |
| `x-amz-checksum-xxhash128` | String | No | Passed on with the request. The five checksums above are the ones to reach for. |
| `x-amz-server-side-encryption-customer-algorithm` | String | Conditional | SSE-C: `AES256`. Needed on every part when the upload was started with SSE-C. |
| `x-amz-server-side-encryption-customer-key` | String | Conditional | SSE-C: the same base64-encoded 256-bit key sent to CreateMultipartUpload. |
| `x-amz-server-side-encryption-customer-key-MD5` | String | Conditional | SSE-C: the base64-encoded MD5 digest of the key. |
| `x-amz-expected-bucket-owner` | String | No | Welcome, with nothing to check: an access key only ever reaches its own account's buckets. |
| `x-amz-request-payer` | String | No | Not applicable: STRG has no Requester Pays buckets. |

Every `x-amz-*` header in the request is part of the signature - the AWS CLI and SDKs take care
of that. One left unsigned is answered with `403 AccessDenied`.

Streaming uploads work too: an `aws-chunked` body (`STREAMING-AWS4-HMAC-SHA256-PAYLOAD`,
`STREAMING-AWS4-HMAC-SHA256-PAYLOAD-TRAILER` or `STREAMING-UNSIGNED-PAYLOAD-TRAILER`) with
`x-amz-decoded-content-length`, and a trailing checksum declared in `x-amz-trailer` - CRC32,
CRC32C, CRC64NVME, SHA-1 or SHA-256. STRG verifies every chunk signature and the trailing
checksum, and stores nothing of a part that fails. Two combinations get a different answer:

| Request | Answer |
| --- | --- |
| A streaming payload in a presigned URL | `400 InvalidRequest` |
| A SHA-1 or SHA-256 trailer with a `Content-Encoding` besides `aws-chunked` (`aws-chunked,gzip`) | `501 NotImplemented`. A CRC trailer works with any encoding |

The AWS SDKs' default upload behaviour works as it is; there is nothing to turn off.

### Request body

The part's bytes.

## Response

A successful request answers `200 OK` with an empty body.

### Response headers

| Name | Description |
| --- | --- |
| `ETag` | The part's entity tag. Together with the part number, it is what [CompleteMultipartUpload](https://docs.strg.com/s3-api/CompleteMultipartUpload) needs. |
| `x-amz-request-id` | The request ID, handy for a support request. |
| `x-amz-server-side-encryption` | The server-side encryption used, when the object is encrypted with STRG-managed keys (`AES256`). |
| `x-amz-server-side-encryption-customer-algorithm` | For SSE-C, the algorithm that was sent. |
| `x-amz-server-side-encryption-customer-key-MD5` | For SSE-C, the MD5 digest of the key that was sent. |
| `x-amz-checksum-crc32`, `x-amz-checksum-crc32c`, `x-amz-checksum-crc64nvme`, `x-amz-checksum-sha1`, `x-amz-checksum-sha256` | The part's checksum, when the request carried one. |

`x-amz-server-side-encryption-aws-kms-key-id`, `x-amz-server-side-encryption-bucket-key-enabled`
and `x-amz-request-charged` never appear: STRG has no SSE-KMS and no Requester Pays.

### Response body

None.

## Errors

These are the answers an UploadPart can get besides `200 OK`, and the situation behind each:

| Code | HTTP status | When |
| --- | --- | --- |
| `NoSuchUpload` | 404 | The upload ID does not exist, or the upload was already completed or aborted. |
| `NoSuchBucket` | 404 | The bucket does not exist, it is in a region other than the endpoint's, or your account stores nothing in this endpoint's region. |
| `InvalidArgument` | 400 | `partNumber` is not an integer from 1 to 10,000. |
| `InvalidURI` | 400 | The key contains a `.` or `..` path segment, such as `a/../b`. Nothing is stored. |
| `EntityTooLarge` | 400 | The declared part size is over 5 GiB. |
| `MissingContentLength` | 411 | The body has no `Content-Length`. |
| `IncompleteBody` | 400 | The body is shorter than `Content-Length` or `x-amz-decoded-content-length` declares. |
| `BadDigest` | 400 | The part does not match `Content-MD5` or an `x-amz-checksum-*` value, sent as a header or as a trailer. Nothing of the part is stored. |
| `InvalidRequest` | 400 | An `aws-chunked` body is malformed, lacks `x-amz-decoded-content-length` or `x-amz-trailer`, carries a trailing checksum that is not a valid value, or is used in a presigned URL. |
| `AccessDenied` | 403 | The access key lacks **Write objects**, or the bucket or key is outside its buckets or prefix, or an `x-amz-*` header is not signed, or a presigned URL has expired. |
| `NotImplemented` | 501 | A query parameter this operation does not take, an ECDSA-signed `aws-chunked` body, a trailer other than the five listed above, or a SHA trailer with a combined `Content-Encoding`. |
| `InvalidAccessKeyId` | 403 | The access key does not exist, has expired or been revoked, or it is not yet usable in a region where you already store data - a key created a moment ago, which works shortly afterwards. |
| `SignatureDoesNotMatch` | 403 | The request signature, or a chunk signature of an `aws-chunked` body, does not match. |
| `RequestTimeTooSkewed` | 403 | The request time is more than 15 minutes from the server's. |
| `AuthorizationHeaderMalformed` | 400 | The signature's credential scope names a region other than the endpoint's. |
| `SlowDown` | 503 | Too many requests at once. The response carries a `Retry-After` interval, and the AWS SDKs retry on their own. |
| `ServiceUnavailable` | 503 | A temporary failure; the same request can be retried. |

## Differences from Amazon S3

Where STRG answers differently from Amazon S3, and what that means for a client:

| Aspect | Amazon S3 | STRG |
| --- | --- | --- |
| Part size | 5 MiB minimum (except the last part), 5 GiB maximum | 5,000,000 bytes minimum (except the last part), 5 GiB maximum - so a part size chosen for S3 works unchanged |
| Body without a length | `Transfer-Encoding: chunked` accepted in some cases | `411 MissingContentLength`. `aws-chunked` uploads work as they are |
| Streaming uploads | Every `aws-chunked` variant, ECDSA signatures included | The HMAC and unsigned variants, with trailing checksums CRC32, CRC32C, CRC64NVME, SHA-1 and SHA-256. A SHA trailer with a combined `Content-Encoding` is `501 NotImplemented`; a CRC trailer works with any encoding |
| Bucket in another region | `301` redirect with `x-amz-bucket-region` | `404 NoSuchBucket`. A bucket is reached through its own regional endpoint |
| `x-amz-expected-bucket-owner` | Checked against the bucket owner | Nothing to check: a key reaches only its own account's buckets |
| Keys with a `.` or `..` path segment | Accepted | `400 InvalidURI` |

## Examples

Each example uploads part 1 of an upload started with
[CreateMultipartUpload](https://docs.strg.com/s3-api/CreateMultipartUpload), whose upload ID is in the `UPLOAD_ID`
environment variable, from a file `part1.bin` of at least 5,000,000 bytes - unless it is the
last part - and at most 5 GiB.

### Uploading a part

**AWS CLI**

```bash
aws s3api upload-part \
  --bucket my-bucket \
  --key backups/backup.tar \
  --part-number 1 \
  --upload-id "$UPLOAD_ID" \
  --body ./part1.bin \
  --endpoint-url https://s3.eu-central-1.strg.com \
  --region eu-central-1 \
  --query ETag \
  --output text
```

**Python (boto3)**

```python
import os

import boto3

s3 = boto3.client(
    "s3",
    endpoint_url="https://s3.eu-central-1.strg.com",
    region_name="eu-central-1",
)

with open("./part1.bin", "rb") as part:
    response = s3.upload_part(
        Bucket="my-bucket",
        Key="backups/backup.tar",
        PartNumber=1,
        UploadId=os.environ["UPLOAD_ID"],
        Body=part,
    )
print(response["ETag"])
```

**JavaScript (AWS SDK v3)**

```javascript
import { readFile } from "node:fs/promises";

import { S3Client, UploadPartCommand } from "@aws-sdk/client-s3";

const client = new S3Client({
  endpoint: "https://s3.eu-central-1.strg.com",
  region: "eu-central-1",
});

const response = await client.send(
  new UploadPartCommand({
    Bucket: "my-bucket",
    Key: "backups/backup.tar",
    PartNumber: 1,
    UploadId: process.env.UPLOAD_ID,
    Body: await readFile("./part1.bin"),
  }),
);
console.log(response.ETag);
```

For a whole file, the SDKs' transfer tools split it into parts and upload them for you -
[Uploading a large file](https://docs.strg.com/s3-api/CreateMultipartUpload#uploading-a-large-file) on the
CreateMultipartUpload page shows how.

### Uploading a part through a presigned URL

A presigned URL lets a client without credentials - a browser, a partner's script - upload one
part with a plain `PUT`. The URL is valid for anything from 1 second to 7 days. The AWS CLI does
not presign UploadPart, so the URL comes from an SDK and the upload is shown with `curl`.

**Warning: Presigning with the JavaScript SDK**

With its default settings, the JavaScript SDK signs the checksum of an empty body into a
presigned upload URL, so real data sent through it is answered `400 BadDigest` - on Amazon S3
as on STRG. A client created with `requestChecksumCalculation: "WHEN_REQUIRED"`, as below,
presigns URLs that work.

**Python (boto3)**

```python
import os

import boto3

s3 = boto3.client(
    "s3",
    endpoint_url="https://s3.eu-central-1.strg.com",
    region_name="eu-central-1",
)

url = s3.generate_presigned_url(
    "upload_part",
    Params={
        "Bucket": "my-bucket",
        "Key": "backups/backup.tar",
        "PartNumber": 1,
        "UploadId": os.environ["UPLOAD_ID"],
    },
    ExpiresIn=3600,
)
print(url)
```

**JavaScript (AWS SDK v3)**

Needs `@aws-sdk/s3-request-presigner` beside `@aws-sdk/client-s3`.

```javascript
import { S3Client, UploadPartCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";

const presigner = new S3Client({
  endpoint: "https://s3.eu-central-1.strg.com",
  region: "eu-central-1",
  requestChecksumCalculation: "WHEN_REQUIRED",
});

const url = await getSignedUrl(
  presigner,
  new UploadPartCommand({
    Bucket: "my-bucket",
    Key: "backups/backup.tar",
    PartNumber: 1,
    UploadId: process.env.UPLOAD_ID,
  }),
  { expiresIn: 3600 },
);
console.log(url);
```

The uploader then sends the part to the URL, here held in `PRESIGNED_URL`, and the `ETag` is in
the response headers:

```bash
curl -i -X PUT --upload-file ./part1.bin "$PRESIGNED_URL"
```

## Related

- [CreateMultipartUpload](https://docs.strg.com/s3-api/CreateMultipartUpload)
- [UploadPartCopy](https://docs.strg.com/s3-api/UploadPartCopy)
- [CompleteMultipartUpload](https://docs.strg.com/s3-api/CompleteMultipartUpload)
- [AbortMultipartUpload](https://docs.strg.com/s3-api/AbortMultipartUpload)
- [ListParts](https://docs.strg.com/s3-api/ListParts)
- [ListMultipartUploads](https://docs.strg.com/s3-api/ListMultipartUploads)
- [PutObject](https://docs.strg.com/s3-api/PutObject)
- [Lifecycle policies](https://docs.strg.com/lifecycle-policies)
- [Access keys](https://docs.strg.com/access-keys)
- [S3 API overview](https://docs.strg.com/s3-api/overview)
