# CreateMultipartUpload

CreateMultipartUpload is the first step of a multipart upload: it opens an upload for an object
and hands back an upload ID. The object's parts then follow with [UploadPart](https://docs.strg.com/s3-api/UploadPart)
or [UploadPartCopy](https://docs.strg.com/s3-api/UploadPartCopy), and [CompleteMultipartUpload](https://docs.strg.com/s3-api/CompleteMultipartUpload)
assembles them into the object - or [AbortMultipartUpload](https://docs.strg.com/s3-api/AbortMultipartUpload) discards
everything. The object's metadata, encryption and Object Lock settings belong to this request,
not to the parts.

A single [PutObject](https://docs.strg.com/s3-api/PutObject) stores up to 5 GiB, so a larger object travels this way.
For any large object, a multipart upload also means parts can go up in parallel, and a part that
fails is simply sent again on its own.

**Info: Good to know on STRG**

- Parts are 5,000,000 bytes to 5 GiB each (the last part may be smaller), numbered 1-10,000.
- `x-amz-tagging` and the Object Lock event-hold headers answer `501 NotImplemented`, so a client
  knows straight away.
- Encryption is `AES256` with STRG-managed keys, or SSE-C with your own key. SSE-KMS answers
  `501 NotImplemented`.
- An object is always exactly as public as its bucket: `x-amz-acl` takes `private` or
  `public-read`, matching the bucket's *Bucket Protection* setting, and `x-amz-grant-*` headers
  answer `501 NotImplemented`.
- `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**

The parts of an upload that is never completed or aborted stay stored, 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](https://docs.strg.com/lifecycle-policies#cleaning-up-abandoned-multipart-uploads) shows one.

## When you'd use it

- **A nightly backup of a database dump or a disk image**, often well over 5 GiB. The backup tool
  starts a multipart upload, sends the file in parts and completes it, so a dropped connection
  costs one part rather than the whole file.
- **A video platform taking uploads of large recordings.** Its server opens the upload here, and
  the parts can go up in parallel to finish sooner.
- **A data pipeline writing a large export as it is produced.** The object's content type and
  metadata are set once, at the start, and parts follow as the data becomes ready.
- **Anything using the AWS CLI's `aws s3 cp` or an SDK's transfer helper.** Above a size
  threshold they call CreateMultipartUpload for you, so it is often at work without being
  called by name.

## Request

```http
POST /photos/2024/cat.jpg?uploads 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
Content-Type: image/jpeg
x-amz-meta-camera: pinhole
```

Path-style, the same request is `POST /my-bucket/photos/2024/cat.jpg?uploads` on
`s3.eu-central-1.strg.com` - [Addressing](https://docs.strg.com/s3-api/overview#addressing) explains both forms.

### Permissions

Starting an upload takes **Write objects** (`objects:write`). For a key limited to certain
buckets or to a key prefix, the bucket and the object key need to be within them.

Locking the object as it is uploaded takes a little more, the same permission setting the lock
afterwards would:

| Header | Also takes |
| --- | --- |
| `x-amz-object-lock-mode`, `x-amz-object-lock-retain-until-date` | **Set retention** (`object-retention:write`) |
| `x-amz-object-lock-legal-hold` | **Set legal holds** (`object-legal-hold:write`) |

Without it, STRG answers `403 AccessDenied`. [Access keys](https://docs.strg.com/access-keys) has every
permission.

### URI parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `Bucket` | String | Yes | The bucket to upload into. |
| `Key` | String | Yes | The key of the object to create. A key with a `.` or `..` path segment (`a/../b`) cannot be put in a URL, and STRG answers `400 InvalidURI`. |
| `uploads` | - | Yes | Present, with no value. It marks the request as CreateMultipartUpload. |

Any other query parameter is answered with `501 NotImplemented`.

### Request headers

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `Cache-Control` | String | No | Stored with the object and returned when it is read. |
| `Content-Disposition` | String | No | Stored with the object and returned when it is read. |
| `Content-Encoding` | String | No | Stored with the object and returned when it is read. |
| `Content-Language` | String | No | Stored with the object and returned when it is read. |
| `Content-Type` | String | No | The object's media type, returned when it is read. |
| `Expires` | Timestamp | No | Stored with the object and returned when it is read. |
| `x-amz-meta-*` | String | No | User-defined metadata, stored with the object. |
| `x-amz-acl` | String | No | `private` or `public-read`, and only the value that matches the bucket: `private` for a bucket with *Bucket Protection* on, `public-read` for one with it off. Any other canned ACL is `501 NotImplemented`. An object is always exactly as public as its bucket. |
| `x-amz-grant-full-control` | String | No | `501 NotImplemented`: access comes from the access key and the bucket's *Bucket Protection*. |
| `x-amz-grant-read` | String | No | `501 NotImplemented`. |
| `x-amz-grant-read-acp` | String | No | `501 NotImplemented`. |
| `x-amz-grant-write-acp` | String | No | `501 NotImplemented`. |
| `x-amz-server-side-encryption` | String | No | `AES256` encrypts the object with STRG-managed keys. `aws:kms` and `aws:kms:dsse` are `501 NotImplemented`. |
| `x-amz-server-side-encryption-customer-algorithm` | String | No | SSE-C: `AES256`. The same three SSE-C headers go on every [UploadPart](https://docs.strg.com/s3-api/UploadPart) of the upload. |
| `x-amz-server-side-encryption-customer-key` | String | No | SSE-C: your base64-encoded 256-bit key. STRG does not store it, so losing it means losing the object. |
| `x-amz-server-side-encryption-customer-key-MD5` | String | No | SSE-C: the base64-encoded MD5 digest of the key. |
| `x-amz-server-side-encryption-aws-kms-key-id` | String | No | `501 NotImplemented`. |
| `x-amz-server-side-encryption-context` | String | No | `501 NotImplemented`. |
| `x-amz-server-side-encryption-bucket-key-enabled` | Boolean | No | `501 NotImplemented`. |
| `x-amz-storage-class` | String | No | STRG has a single storage class, so this header is best left out. |
| `x-amz-website-redirect-location` | String | No | STRG does not host static websites, so this header has nothing to act on and is best left out. |
| `x-amz-tagging` | String | No | `501 NotImplemented`. Objects carry no tags. |
| `x-amz-object-lock-mode` | String | No | `GOVERNANCE` or `COMPLIANCE`. Works in a bucket with Object Lock enabled, together with `x-amz-object-lock-retain-until-date`, and takes the **Set retention** permission. [Object Lock](https://docs.strg.com/object-locks) explains the modes. |
| `x-amz-object-lock-retain-until-date` | Timestamp | No | When the retention ends, in ISO 8601 (`2027-01-01T00:00:00Z`). Takes **Set retention**. |
| `x-amz-object-lock-legal-hold` | String | No | `ON` or `OFF`. Works in a bucket with Object Lock enabled and takes the **Set legal holds** permission. |
| `x-amz-object-lock-event-hold` | String | No | `501 NotImplemented`. |
| `x-amz-object-lock-event-hold-duration-days` | Integer | No | `501 NotImplemented`. |
| `x-amz-object-lock-event-hold-duration-years` | Integer | No | `501 NotImplemented`. |
| `x-amz-checksum-algorithm` | String | No | The checksum algorithm the parts will carry, such as `CRC32`. Every part then sends a checksum of that algorithm. |
| `x-amz-checksum-type` | String | No | Accepted and passed on with the request. |
| `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`.

### Request body

None.

## Response

A successful request answers `200 OK` with the upload ID in an XML body.

### Response headers

| Name | Description |
| --- | --- |
| `x-amz-request-id` | The request ID, handy for support requests. |
| `x-amz-server-side-encryption` | The server-side encryption used for the object, when it is encrypted. |
| `x-amz-server-side-encryption-customer-algorithm` | For SSE-C, the algorithm you sent. |
| `x-amz-server-side-encryption-customer-key-MD5` | For SSE-C, the MD5 digest of the key you sent. |
| `x-amz-checksum-algorithm` | The checksum algorithm of the upload, when you set one. |
| `x-amz-checksum-type` | The checksum type of the upload, when there is one. |

`x-amz-server-side-encryption-aws-kms-key-id`, `x-amz-server-side-encryption-context`,
`x-amz-server-side-encryption-bucket-key-enabled` and `x-amz-request-charged` are never returned.
`x-amz-abort-date` and `x-amz-abort-rule-id` are not something to count on for learning whether a
lifecycle rule will abort the upload; the bucket's rules, read with
[GetBucketLifecycleConfiguration](https://docs.strg.com/s3-api/GetBucketLifecycleConfiguration), are the reliable answer.

### Response body

| Element | Description |
| --- | --- |
| `Bucket` | The bucket the upload is in. |
| `Key` | The object key the upload is for. |
| `UploadId` | The upload ID. Every later request for this upload carries it unchanged; it is an opaque string. |

```xml
<?xml version="1.0" encoding="UTF-8"?>
<InitiateMultipartUploadResult xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
  <Bucket>my-bucket</Bucket>
  <Key>photos/2024/cat.jpg</Key>
  <UploadId>EXAMPLE-UPLOAD-ID</UploadId>
</InitiateMultipartUploadResult>
```

## Errors

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

| Code | HTTP status | When |
| --- | --- | --- |
| `NoSuchBucket` | 404 | The bucket does not exist, or it is in a region other than the endpoint's. |
| `InvalidURI` | 400 | The key contains a `.` or `..` path segment, such as `a/../b`. Nothing is started. |
| `AccessDenied` | 403 | The access key lacks **Write objects**, or **Set retention** / **Set legal holds** for an Object Lock header, or the bucket or key is outside its buckets or object prefix, or an `x-amz-*` header is not signed. |
| `NotImplemented` | 501 | The request carries `x-amz-tagging`, an event-hold header, SSE-KMS, an `x-amz-grant-*` header, a canned ACL other than `private` or `public-read`, or a query parameter this operation does not take. |
| `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 again shortly. |
| `SignatureDoesNotMatch` | 403 | The request signature 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 SDKs retry after the `Retry-After` interval. |
| `ServiceUnavailable` | 503 | A temporary failure; the request succeeds when retried. |

## Differences from Amazon S3

Most of these make STRG simpler to reason about; each says what a client sees.

| 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 |
| Part numbers | 1-10,000 | 1-10,000 |
| Bucket in another region | `301` redirect with `x-amz-bucket-region` | `404 NoSuchBucket`. The bucket's own regional endpoint reaches it |
| `x-amz-tagging` | Tags the object | `501 NotImplemented` |
| Object Lock event holds | Supported | `501 NotImplemented` |
| SSE-KMS | Supported | `501 NotImplemented`. `AES256` with STRG-managed keys, or SSE-C |
| ACLs | Any canned ACL, and grants | `private` or `public-read`, matching the bucket. Grants are `501 NotImplemented` |
| Storage classes | Several | One, so `x-amz-storage-class` is best left out |
| Website redirects | `x-amz-website-redirect-location` | STRG does not host static websites |
| Object Lock headers | Need `s3:PutObjectRetention` / `s3:PutObjectLegalHold` | Take **Set retention** / **Set legal holds** on the access key |
| `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

### Starting an upload

This starts an upload for `photos/2024/cat.jpg` with a content type and one metadata entry, and
keeps the upload ID for the parts that follow:

**AWS CLI**

```bash
UPLOAD_ID=$(aws s3api create-multipart-upload \
  --bucket my-bucket \
  --key photos/2024/cat.jpg \
  --content-type image/jpeg \
  --metadata camera=pinhole \
  --endpoint-url https://s3.eu-central-1.strg.com \
  --region eu-central-1 \
  --query UploadId \
  --output text)
echo "$UPLOAD_ID"
```

**Python (boto3)**

```python
import boto3

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

response = s3.create_multipart_upload(
    Bucket="my-bucket",
    Key="photos/2024/cat.jpg",
    ContentType="image/jpeg",
    Metadata={"camera": "pinhole"},
)
print(response["UploadId"])
```

**JavaScript (AWS SDK v3)**

```javascript
import { CreateMultipartUploadCommand, S3Client } 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 CreateMultipartUploadCommand({
    Bucket: "my-bucket",
    Key: "photos/2024/cat.jpg",
    ContentType: "image/jpeg",
    Metadata: { camera: "pinhole" },
  }),
);
console.log(response.UploadId);
```

### Uploading a large file

The high-level transfer tools call CreateMultipartUpload, UploadPart and CompleteMultipartUpload
for you, and abort the upload if it fails. A custom part size works anywhere between
5,000,000 bytes and 5 GiB, as long as the whole file fits in 10,000 parts.

**AWS CLI**

```bash
aws s3 cp ./backup.tar s3://my-bucket/backups/backup.tar \
  --endpoint-url https://s3.eu-central-1.strg.com \
  --region eu-central-1
```

**Python (boto3)**

```python
import boto3
from boto3.s3.transfer import TransferConfig

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

s3.upload_file(
    "./backup.tar",
    "my-bucket",
    "backups/backup.tar",
    Config=TransferConfig(
        multipart_threshold=64 * 1024 * 1024,
        multipart_chunksize=64 * 1024 * 1024,
    ),
)
```

**JavaScript (AWS SDK v3)**

This one uses `@aws-sdk/lib-storage` beside `@aws-sdk/client-s3`.

```javascript
import { createReadStream } from "node:fs";

import { S3Client } from "@aws-sdk/client-s3";
import { Upload } from "@aws-sdk/lib-storage";

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

const upload = new Upload({
  client,
  params: {
    Bucket: "my-bucket",
    Key: "backups/backup.tar",
    Body: createReadStream("./backup.tar"),
  },
  partSize: 64 * 1024 * 1024,
});
await upload.done();
```

### Locking the object as it is uploaded

In a bucket with Object Lock enabled, this starts an upload whose object is retained in
governance mode until 1 January 2027. The access key takes **Set retention** beside **Write
objects**.

**AWS CLI**

```bash
aws s3api create-multipart-upload \
  --bucket my-bucket \
  --key photos/2024/cat.jpg \
  --object-lock-mode GOVERNANCE \
  --object-lock-retain-until-date 2027-01-01T00:00:00Z \
  --endpoint-url https://s3.eu-central-1.strg.com \
  --region eu-central-1
```

**Python (boto3)**

```python
from datetime import datetime, timezone

import boto3

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

response = s3.create_multipart_upload(
    Bucket="my-bucket",
    Key="photos/2024/cat.jpg",
    ObjectLockMode="GOVERNANCE",
    ObjectLockRetainUntilDate=datetime(2027, 1, 1, tzinfo=timezone.utc),
)
print(response["UploadId"])
```

**JavaScript (AWS SDK v3)**

```javascript
import { CreateMultipartUploadCommand, S3Client } 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 CreateMultipartUploadCommand({
    Bucket: "my-bucket",
    Key: "photos/2024/cat.jpg",
    ObjectLockMode: "GOVERNANCE",
    ObjectLockRetainUntilDate: new Date("2027-01-01T00:00:00Z"),
  }),
);
console.log(response.UploadId);
```

## Related

- [UploadPart](https://docs.strg.com/s3-api/UploadPart)
- [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)
- [Object Lock](https://docs.strg.com/object-locks)
- [Access keys](https://docs.strg.com/access-keys)
- [S3 API overview](https://docs.strg.com/s3-api/overview)
