# PutObject

PutObject stores an object in a bucket in one request: the bytes in the body, and the object's
content type, metadata, checksum, encryption and Object Lock settings in the headers. Every
STRG bucket keeps versions, so an upload to a key that already exists adds a new version and
the previous one stays safely in place. Every version is billed as stored data until it is deleted
or a [lifecycle policy](https://docs.strg.com/lifecycle-policies) removes it.

A single PutObject stores up to 5 GiB. A larger object travels as a multipart upload -
[CreateMultipartUpload](https://docs.strg.com/s3-api/CreateMultipartUpload), [UploadPart](https://docs.strg.com/s3-api/UploadPart) and
[CompleteMultipartUpload](https://docs.strg.com/s3-api/CompleteMultipartUpload) - which the high-level upload commands
take care of on their own, as [Uploading a large file](https://docs.strg.com/s3-api/PutObject#uploading-a-large-file) shows.

**Info: Good to know on STRG**

- Buckets are always versioned: an upload to an existing key adds a version and keeps the one
  before it.
- Checksums are verified and stored for CRC32, CRC32C, CRC64NVME, SHA-1 and SHA-256.
- `If-None-Match` and `If-Match` (conditional writes), `x-amz-tagging`, `x-amz-write-offset-bytes`,
  event holds, SSE-KMS and `x-amz-grant-*` get a `501 NotImplemented` answer, so a client knows
  straight away.
- An object is always exactly as public as its bucket: `x-amz-acl` takes `private` or
  `public-read`, the value matching the bucket's *Bucket Protection* setting.
- Setting Object Lock on upload takes **Set retention** or **Set legal holds** on the access key,
  beside **Write objects**.
- A body travels with a `Content-Length`, or as an `aws-chunked` upload; one with neither is
  answered `411 MissingContentLength`.
- A key with a `.` or `..` path segment (`a/../b`, `a/./b`, or their `%2e` forms) is answered
  `400 InvalidURI`, and nothing is stored.
- `x-amz-expected-bucket-owner` is welcome and has nothing to check: a key only ever reaches
  buckets of its own account.

## When you'd use it

- **A web app taking uploads.** A photo-sharing app stores each profile picture its users send. Its
  server can upload them itself, or hand the browser a presigned PUT URL so the file goes straight
  to STRG - with [CORS rules](https://docs.strg.com/cors-rules) on the bucket letting the browser through.
- **A nightly backup.** A backup script writes the day's database dump as
  `backups/2026-09-26.sql.gz` with one PutObject; a dump that grows past 5 GiB travels as a
  multipart upload instead.
- **An agent keeping its memory.** An AI agent writes its notes to `memory/notes.json` at the end
  of every session. Each upload is a new version, so yesterday's notes are still there if today's
  turn out wrong.
- **Records that must not change.** An accounting system stores each invoice PDF with a retention
  date in a bucket with [Object Lock](https://docs.strg.com/object-locks), so it cannot be altered or deleted
  before its retention period ends.

## Request

```http
PUT /photos/2024/cat.jpg 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-Type: image/jpeg
Content-Length: 48213
x-amz-meta-photographer: alex

<object data>
```

Path-style, the same request is `PUT /my-bucket/photos/2024/cat.jpg` on
`s3.eu-central-1.strg.com`, which is how a bucket whose name contains a dot is reached -
[Addressing](https://docs.strg.com/s3-api/overview#addressing) has both styles.

Every `x-amz-*` header is part of the signature; the AWS CLI and SDKs take care of that.

### Permissions

An upload takes **Write objects**, and setting Object Lock on the new version takes one more
permission per header:

| Permission | Dashboard label | For |
| --- | --- | --- |
| `objects:write` | **Write objects** | Every upload |
| `object-retention:write` | **Set retention** | Also needed when the request carries `x-amz-object-lock-mode` or `x-amz-object-lock-retain-until-date` |
| `object-legal-hold:write` | **Set legal holds** | Also needed when the request carries `x-amz-object-lock-legal-hold` |

For a key that reaches certain buckets or an object prefix, the bucket and the object key fall
within them; an upload reaching outside them is answered `403 AccessDenied`.
[Access keys](https://docs.strg.com/access-keys) has the details.

### URI parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `Bucket` | String | Yes | The bucket to upload to, in the endpoint's region. |
| `Key` | String | Yes | The object key. A key with a `.` or `..` path segment (`a/../b`, `a/./b`, `./a`, percent-encoded as `%2e` or not) is answered `400 InvalidURI`, and nothing is stored under either name. |

PutObject takes no query parameters. A query parameter S3 does not define for PutObject gets a
`501 NotImplemented` answer. Presigned URLs carry their `X-Amz-*` signing parameters in the query
as usual.

### Request headers

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

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `Content-Length` | Integer | Yes | The size of the body in bytes. A plain body without it is answered `411 MissingContentLength`. For an `aws-chunked` body it is the framed length, and `x-amz-decoded-content-length` carries the object's size. |
| `Content-Type` | 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 - `gzip`, for example. The body is stored exactly as sent. For an `aws-chunked` upload, `aws-chunked` is taken out and the rest is stored. |
| `Content-Disposition` | 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. |
| `Cache-Control` | String | No | Stored with the object and returned when it is read. |
| `Expires` | Timestamp | No | Stored with the object and returned when it is read. |
| `Content-MD5` | String | No | The base64 MD5 digest of the body, for an integrity check. |
| `x-amz-meta-*` | String | No | User metadata, one header per entry. Returned by [GetObject](https://docs.strg.com/s3-api/GetObject) and [HeadObject](https://docs.strg.com/s3-api/HeadObject). |
| `x-amz-content-sha256` | String | Yes, unless presigned | The SHA-256 of the body in hex, `UNSIGNED-PAYLOAD`, or one of the `aws-chunked` streaming values - [Authentication](https://docs.strg.com/s3-api/overview#authentication) lists them. A streaming value in a presigned URL is answered `400 InvalidRequest`. The SDKs set it. |
| `x-amz-decoded-content-length` | Integer | For `aws-chunked` | The object's size in bytes. An `aws-chunked` upload without it is answered `400 InvalidRequest`. |
| `x-amz-trailer` | String | For a trailer | The checksum sent after the body in an `aws-chunked` upload: `x-amz-checksum-crc32`, `-crc32c`, `-crc64nvme`, `-sha1` or `-sha256`. Any other is answered `501 NotImplemented`. A SHA-1 or SHA-256 trailer together with a `Content-Encoding` other than `aws-chunked` is answered `501 NotImplemented` too; a CRC checksum suits a compressed upload. |
| `x-amz-sdk-checksum-algorithm` | String | No | Sent by the SDKs alongside a checksum, and welcome. |
| `x-amz-checksum-crc32` | String | No | The base64 CRC32 of the object. Verified and stored; a mismatch is answered `400 BadDigest`. |
| `x-amz-checksum-crc32c` | String | No | The base64 CRC32C of the object. Verified and stored. |
| `x-amz-checksum-crc64nvme` | String | No | The base64 CRC64NVME of the object. Verified and stored. |
| `x-amz-checksum-sha1` | String | No | The base64 SHA-1 of the object. Verified and stored. |
| `x-amz-checksum-sha256` | String | No | The base64 SHA-256 of the object. Verified and stored. |
| `x-amz-checksum-sha512` | String | No | Not one of the algorithms STRG verifies and stores - the five above are. |
| `x-amz-checksum-md5` | String | No | Not one of the algorithms STRG verifies and stores; `Content-MD5` or one of the five above covers the same need. |
| `x-amz-checksum-xxhash64` | String | No | Not one of the algorithms STRG verifies and stores - the five above are. |
| `x-amz-checksum-xxhash3` | String | No | Not one of the algorithms STRG verifies and stores - the five above are. |
| `x-amz-checksum-xxhash128` | String | No | Not one of the algorithms STRG verifies and stores - the five above are. |
| `If-Match` | String | No | STRG answers `501 NotImplemented`. |
| `If-None-Match` | String | No | STRG answers `501 NotImplemented`, so there is no create-only-if-absent upload. |
| `x-amz-acl` | String | No | `private` or `public-read`, matching the bucket: `private` for a bucket with *Bucket Protection* on, `public-read` for one with it off. Any other canned ACL is answered `501 NotImplemented`. An object is always exactly as public as its bucket. |
| `x-amz-grant-full-control` | String | No | STRG answers `501 NotImplemented`. |
| `x-amz-grant-read` | String | No | STRG answers `501 NotImplemented`. |
| `x-amz-grant-read-acp` | String | No | STRG answers `501 NotImplemented`. |
| `x-amz-grant-write-acp` | String | No | STRG answers `501 NotImplemented`. |
| `x-amz-tagging` | String | No | STRG answers `501 NotImplemented`: objects carry no tags. |
| `x-amz-write-offset-bytes` | Integer | No | STRG answers `501 NotImplemented`: an object is written whole, never appended to. |
| `x-amz-server-side-encryption` | String | No | `AES256` - server-side encryption with STRG-managed keys. `aws:kms` and `aws:kms:dsse` are answered `501 NotImplemented`. Without it, the bucket's default encryption applies - [PutBucketEncryption](https://docs.strg.com/s3-api/PutBucketEncryption) sets it. |
| `x-amz-server-side-encryption-aws-kms-key-id` | String | No | STRG answers `501 NotImplemented`. |
| `x-amz-server-side-encryption-context` | String | No | STRG answers `501 NotImplemented`. |
| `x-amz-server-side-encryption-bucket-key-enabled` | Boolean | No | STRG answers `501 NotImplemented`. |
| `x-amz-server-side-encryption-customer-algorithm` | String | No | `AES256`, to encrypt the object with your own key (SSE-C). Reading it back takes the same key. |
| `x-amz-server-side-encryption-customer-key` | String | No | The base64 256-bit key to encrypt the object with (SSE-C). It is not stored. |
| `x-amz-server-side-encryption-customer-key-MD5` | String | No | The base64 MD5 digest of that key. |
| `x-amz-object-lock-mode` | String | No | `GOVERNANCE` or `COMPLIANCE`, with `x-amz-object-lock-retain-until-date`, for a bucket with Object Lock enabled. Needs **Set retention**. [Object Lock](https://docs.strg.com/object-locks) explains the modes. |
| `x-amz-object-lock-retain-until-date` | Timestamp | No | When the object's retention ends, in ISO 8601 format. Needs **Set retention**. |
| `x-amz-object-lock-legal-hold` | String | No | `ON` or `OFF`, for a bucket with Object Lock enabled. Needs **Set legal holds**. |
| `x-amz-object-lock-event-hold` | String | No | STRG answers `501 NotImplemented`. |
| `x-amz-object-lock-event-hold-duration-days` | Integer | No | STRG answers `501 NotImplemented`. |
| `x-amz-object-lock-event-hold-duration-years` | Integer | No | STRG answers `501 NotImplemented`. |
| `x-amz-storage-class` | String | No | STRG has one storage class, `STANDARD`, so the header can be left out or sent as `STANDARD`. |
| `x-amz-website-redirect-location` | String | No | STRG does not host bucket websites, so this header has no effect to count on. |
| `x-amz-request-payer` | String | No | Has nothing to do: STRG has no Requester Pays buckets. |
| `x-amz-expected-bucket-owner` | String | No | Welcome, and has nothing to check: an access key only ever reaches its own account's buckets. |

**Danger: A locked object cannot be deleted**

In a bucket with Object Lock enabled, an object uploaded with a retention or a legal hold - or
under the bucket's default retention - cannot be deleted until the lock ends, and stays billed as
stored data until then. A retention in `COMPLIANCE` mode cannot be shortened or removed by anyone.
[Object Lock](https://docs.strg.com/object-locks) explains both modes, and is worth reading before setting one.

### Request body

The object's bytes, stored exactly as sent, up to 5 GiB;
[Uploading a large file](https://docs.strg.com/s3-api/PutObject#uploading-a-large-file) covers anything larger.

A body sent with `Transfer-Encoding: chunked` and no `Content-Length` is answered
`411 MissingContentLength`. `aws-chunked` uploads - what the AWS SDKs send by default for a
streamed body - work too: each chunk signature and the trailing checksum are
verified, and a failure stores nothing.

## Response

A successful upload answers `200 OK` with no body.

### Response headers

| Name | Description |
| --- | --- |
| `ETag` | The entity tag of the new object. |
| `x-amz-version-id` | The version ID of the new object. Buckets are always versioned, so every upload creates a new version. |
| `x-amz-checksum-crc32`, `x-amz-checksum-crc32c`, `x-amz-checksum-crc64nvme`, `x-amz-checksum-sha1`, `x-amz-checksum-sha256` | The checksum stored with the object, when the upload carried one. |
| `x-amz-server-side-encryption` | `AES256` when the object is encrypted with STRG-managed keys. |
| `x-amz-server-side-encryption-customer-algorithm` | `AES256` when the object is encrypted with your own key. |
| `x-amz-server-side-encryption-customer-key-MD5` | The MD5 digest of your key, for SSE-C. |
| `x-amz-request-id` | The request ID, handy when contacting support. |

Version IDs are opaque strings, best stored and sent back exactly as they are.

`x-amz-expiration`, `x-amz-object-size`, `x-amz-request-charged`,
`x-amz-server-side-encryption-aws-kms-key-id`, `x-amz-server-side-encryption-context`,
`x-amz-server-side-encryption-bucket-key-enabled` and the `x-amz-checksum-*` headers of the other
algorithms are not part of the answer, and `x-amz-checksum-type` is not something to count on.

### Response body

None.

## Errors

These are the answers an upload can get instead of `200 OK`, and the situation behind each.

| Code | HTTP status | When |
| --- | --- | --- |
| `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. |
| `BadDigest` | 400 | The body does not match `Content-MD5` or an `x-amz-checksum-*` value, sent as a header or as a trailer. Nothing is stored. |
| `EntityTooLarge` | 400 | The declared object size - `Content-Length`, or `x-amz-decoded-content-length` for an `aws-chunked` upload - is over 5 GiB. Nothing is stored. |
| `IncompleteBody` | 400 | The body is shorter than `Content-Length` or `x-amz-decoded-content-length` declared. |
| `InvalidRequest` | 400 | An `aws-chunked` upload is malformed or lacks `x-amz-decoded-content-length` or `x-amz-trailer`, or a presigned URL declares a streaming payload. |
| `InvalidURI` | 400 | The key contains a `.` or `..` path segment, such as `a/../b`. Nothing is stored. |
| `InvalidArgument` | 400 | `x-amz-content-sha256` is not a SHA-256 hex digest or one of the accepted values. |
| `MissingContentLength` | 411 | The body has no `Content-Length` and is not an `aws-chunked` upload. |
| `NotImplemented` | 501 | The request carries a header or query parameter STRG does not offer - [Request headers](https://docs.strg.com/s3-api/PutObject#request-headers) lists each one. |
| `AccessDenied` | 403 | The access key lacks **Write objects**, or the Object Lock permission a lock header needs, or the bucket or key is outside its buckets or object prefix. Also an expired presigned URL, and an unsigned `x-amz-*` header. |
| `InvalidAccessKeyId` | 403 | The 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. |
| `SignatureDoesNotMatch` | 403 | The request signature, a chunk signature or the trailer signature does not match. Nothing is stored. |
| `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 AWS CLI and SDKs retry after the `Retry-After` interval. |
| `ServiceUnavailable` | 503 | A 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 an upload.

| Aspect | Amazon S3 | STRG |
| --- | --- | --- |
| Bucket in another region | `301` redirect with `x-amz-bucket-region` | `404 NoSuchBucket`. The bucket's own regional endpoint reaches it |
| Conditional writes (`If-None-Match: *`, `If-Match`) | Supported | `501 NotImplemented` |
| Versioning | Depends on the bucket | Always on: an upload to an existing key adds a version and keeps the previous one |
| ACLs | Any canned ACL and `x-amz-grant-*` | `private` or `public-read`, matching the bucket; grants are answered `501` |
| Tags | `x-amz-tagging` | `501 NotImplemented`. Objects carry no tags |
| SSE-KMS | Supported | `501 NotImplemented`. `AES256` and SSE-C both encrypt the object |
| Checksum algorithms | CRC32, CRC32C, CRC64NVME, SHA-1, SHA-256, SHA-512, MD5, XXHASH64, XXHASH3, XXHASH128 | CRC32, CRC32C, CRC64NVME, SHA-1 and SHA-256 |
| SHA-1 or SHA-256 trailer with a compressed `Content-Encoding` | Supported | `501 NotImplemented`. A CRC checksum suits a compressed upload |
| Object Lock headers | `s3:PutObject` plus the Object Lock actions | **Write objects** plus **Set retention** or **Set legal holds** |
| Event holds | Supported | `501 NotImplemented` |
| Appending (`x-amz-write-offset-bytes`) | Directory buckets only | `501 NotImplemented` |
| Storage classes | Several | One, `STANDARD` |
| `Transfer-Encoding: chunked` without `Content-Length` | Accepted in some cases | `411 MissingContentLength`. `aws-chunked` uploads work as usual |
| `x-amz-expected-bucket-owner` | Checked against the bucket owner | Welcome, with nothing to check |
| Keys with a `.` or `..` path segment (`a/../b`, `a/./b`) | Stored under the key as sent | `400 InvalidURI`; nothing is stored. A key without such segments works everywhere |

## Examples

### Uploading a file

Uploading `cat.jpg` as `photos/2024/cat.jpg`, with its content type and one metadata entry:

**AWS CLI**

```bash
aws s3api put-object \
  --bucket my-bucket \
  --key photos/2024/cat.jpg \
  --body cat.jpg \
  --content-type image/jpeg \
  --metadata photographer=alex \
  --endpoint-url https://s3.eu-central-1.strg.com \
  --region eu-central-1
```

**Python (boto3)**

```python
import boto3

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

with open("cat.jpg", "rb") as body:
    response = s3.put_object(
        Bucket="my-bucket",
        Key="photos/2024/cat.jpg",
        Body=body,
        ContentType="image/jpeg",
        Metadata={"photographer": "alex"},
    )
print(response["ETag"], response["VersionId"])
```

**JavaScript (AWS SDK v3)**

```javascript
import { readFile } from "node:fs/promises";
import { PutObjectCommand, 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 PutObjectCommand({
    Bucket: "my-bucket",
    Key: "photos/2024/cat.jpg",
    Body: await readFile("cat.jpg"),
    ContentType: "image/jpeg",
    Metadata: { photographer: "alex" },
  }),
);
console.log(response.ETag, response.VersionId);
```

### Uploading with a SHA-256 checksum

The client computes the checksum, and STRG verifies it, stores it with the object and returns
it from [GetObject](https://docs.strg.com/s3-api/GetObject) and [HeadObject](https://docs.strg.com/s3-api/HeadObject) when checksum mode is
enabled - a simple way to know the bytes arrived exactly as they left:

**AWS CLI**

```bash
aws s3api put-object \
  --bucket my-bucket \
  --key photos/2024/cat.jpg \
  --body cat.jpg \
  --checksum-algorithm SHA256 \
  --endpoint-url https://s3.eu-central-1.strg.com \
  --region eu-central-1
```

**Python (boto3)**

```python
import boto3

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

with open("cat.jpg", "rb") as body:
    response = s3.put_object(
        Bucket="my-bucket",
        Key="photos/2024/cat.jpg",
        Body=body,
        ChecksumAlgorithm="SHA256",
    )
print(response["ChecksumSHA256"])
```

**JavaScript (AWS SDK v3)**

```javascript
import { readFile } from "node:fs/promises";
import { PutObjectCommand, 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 PutObjectCommand({
    Bucket: "my-bucket",
    Key: "photos/2024/cat.jpg",
    Body: await readFile("cat.jpg"),
    ChecksumAlgorithm: "SHA256",
  }),
);
console.log(response.ChecksumSHA256);
```

### Uploading with a retention

In a bucket with Object Lock enabled, this locks the new version in governance mode until the
start of 2027. The access key holds **Set retention** as well as **Write objects**:

**AWS CLI**

```bash
aws s3api put-object \
  --bucket my-bucket \
  --key photos/2024/cat.jpg \
  --body 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",
)

with open("cat.jpg", "rb") as body:
    s3.put_object(
        Bucket="my-bucket",
        Key="photos/2024/cat.jpg",
        Body=body,
        ObjectLockMode="GOVERNANCE",
        ObjectLockRetainUntilDate=datetime(2027, 1, 1, tzinfo=timezone.utc),
    )
```

**JavaScript (AWS SDK v3)**

```javascript
import { readFile } from "node:fs/promises";
import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3";

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

await client.send(
  new PutObjectCommand({
    Bucket: "my-bucket",
    Key: "photos/2024/cat.jpg",
    Body: await readFile("cat.jpg"),
    ObjectLockMode: "GOVERNANCE",
    ObjectLockRetainUntilDate: new Date("2027-01-01T00:00:00Z"),
  }),
);
```

### Uploading a large file

A single PutObject stores up to 5 GiB, and a multipart upload is the better fit well before that
size: parts go up in parallel, and a part that fails is retried on its own rather than restarting
the whole file. The high-level upload commands switch to a multipart upload automatically. The
examples set a 64 MiB part size; any size from 5 MB (5,000,000 bytes) to 5 GiB works, with up to
10,000 parts per upload:

**AWS CLI**

```bash
aws configure set default.s3.multipart_chunksize 64MB

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_chunksize=64 * 1024 * 1024),
)
```

**JavaScript (AWS SDK v3)**

Needs `@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",
});

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

### Taking uploads through a presigned URL

A presigned PUT URL lets someone without an access key upload one object - a browser, a partner's
script, a phone app. The URL is valid for anything from 1 second to 7 days, and the uploader sends
a plain `PUT` with the body.

**Warning: Presigning with the JavaScript SDK**

With its default settings, the JavaScript SDK signs the checksum of an empty body into a
presigned PUT URL, so every upload of real data through it fails with `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 boto3
import urllib.request

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

url = s3.generate_presigned_url(
    "put_object",
    Params={"Bucket": "my-bucket", "Key": "photos/2024/cat.jpg"},
    ExpiresIn=3600,
)

# What the uploader runs:
with open("cat.jpg", "rb") as body:
    request = urllib.request.Request(
        url, data=body.read(), method="PUT", headers={"Content-Type": "image/jpeg"}
    )
    with urllib.request.urlopen(request) as response:
        print(response.status, response.headers["ETag"])
```

**JavaScript (AWS SDK v3)**

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

```javascript
import { readFile } from "node:fs/promises";
import { PutObjectCommand, S3Client } 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 PutObjectCommand({ Bucket: "my-bucket", Key: "photos/2024/cat.jpg" }),
  { expiresIn: 3600 },
);

// What the uploader runs:
const response = await fetch(url, { method: "PUT", body: await readFile("cat.jpg") });
console.log(response.status, response.headers.get("etag"));
```

The AWS CLI's `aws s3 presign` makes download URLs, so an upload URL comes from an SDK.

## Related

- [GetObject](https://docs.strg.com/s3-api/GetObject)
- [HeadObject](https://docs.strg.com/s3-api/HeadObject)
- [CopyObject](https://docs.strg.com/s3-api/CopyObject)
- [DeleteObject](https://docs.strg.com/s3-api/DeleteObject)
- [CreateMultipartUpload](https://docs.strg.com/s3-api/CreateMultipartUpload)
- [UploadPart](https://docs.strg.com/s3-api/UploadPart)
- [CompleteMultipartUpload](https://docs.strg.com/s3-api/CompleteMultipartUpload)
- [ListObjectVersions](https://docs.strg.com/s3-api/ListObjectVersions)
- [Object Lock](https://docs.strg.com/object-locks)
- [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)
