# Object storage quick start
Zaroz object storage speaks the S3 API, so the tools you already use (the AWS CLI, boto3, the AWS SDK for JavaScript, rclone, most backup programs) work with a different endpoint and your own keys. Each object storage subscription has **one bucket**.

## 1. Create the bucket

1. Open **Storage → Object storage** in the console and buy a plan (or open the subscription you already have).
2. Create the bucket: give it a name you will recognise, and optionally a **storage cap** (1 GiB to 100 TiB, 100 GiB by default).
3. The bucket page shows the four values every S3 client asks for:

| Value | What it is | Example |
|---|---|---|
| **Endpoint** | The S3 URL to point your client at | the URL on your bucket page |
| **Bucket name** | The real name to use in requests, `zb-` plus 32 characters. It is not the display name you chose. | `zb-3f2a9c...` |
| **Region** | `us-east-1`. Clients insist on a region; this is the one to give them. It does not say where the data lives. | `us-east-1` |
| **Addressing** | **Path-style**: `ENDPOINT/BUCKET/KEY`. Virtual-hosted style (`BUCKET.ENDPOINT`) is not served. | `forcePathStyle` / `addressing_style: path` |

## 2. Make an access key

Open the bucket's **Access keys** tab and create a key. Choose its scope:

- **Read and write** can list, download, upload, copy and delete objects.
- **Read only** can list and download. Use it for anything that only reads, such as a website, a deploy script or a colleague who only needs to look.

A key only ever works on its own bucket. You can hold up to **20** active keys per bucket; give each one a name for the thing that uses it, so you can revoke exactly that one later.

:::warning{title="The secret is shown once"}

Copy the **secret access key** the moment the key is created. Zaroz keeps only a fingerprint of it and can never show it again. If you lose it, revoke the key and create another.

:::

Revoking a key stops it immediately. Nothing else changes.

## 3. Connect

Put the values in the environment (the AWS CLI, boto3 and the JavaScript SDK all read these two variables):

```bash
export AWS_ACCESS_KEY_ID="<your access key id>"
export AWS_SECRET_ACCESS_KEY="<your secret access key>"
export AWS_DEFAULT_REGION="us-east-1"
export ENDPOINT="<the endpoint on your bucket page>"
export BUCKET="<the bucket name on your bucket page>"
```

### AWS CLI

```bash
aws s3 cp ./photo.jpg "s3://$BUCKET/photos/photo.jpg" --endpoint-url "$ENDPOINT"
aws s3 ls "s3://$BUCKET/photos/" --endpoint-url "$ENDPOINT"
aws s3 sync ./site "s3://$BUCKET/site" --endpoint-url "$ENDPOINT"
aws s3 cp "s3://$BUCKET/photos/photo.jpg" ./copy.jpg --endpoint-url "$ENDPOINT"
```

Large files are uploaded in parts automatically. You can also set the endpoint once in `~/.aws/config` with `endpoint_url = ...` and `s3 = addressing_style = path`.

### Python (boto3)

```python
import os
import boto3
from botocore.config import Config

s3 = boto3.client(
    "s3",
    endpoint_url=os.environ["ENDPOINT"],
    region_name="us-east-1",
    config=Config(s3={"addressing_style": "path"}),
)

bucket = os.environ["BUCKET"]
s3.put_object(Bucket=bucket, Key="hello.txt", Body=b"hello")
print(s3.get_object(Bucket=bucket, Key="hello.txt")["Body"].read())
for item in s3.list_objects_v2(Bucket=bucket).get("Contents", []):
    print(item["Key"], item["Size"])
s3.upload_file("backup.tar.gz", bucket, "backups/backup.tar.gz")
```

### JavaScript (AWS SDK v3)

```js
import { S3Client, PutObjectCommand, GetObjectCommand, ListObjectsV2Command } from "@aws-sdk/client-s3";

const s3 = new S3Client({
  endpoint: process.env.ENDPOINT,
  region: "us-east-1",
  forcePathStyle: true, // required: virtual-hosted style is not served
  requestChecksumCalculation: "WHEN_REQUIRED", // needed for presigned uploads, see below
  // credentials come from AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY
});

const Bucket = process.env.BUCKET;
await s3.send(new PutObjectCommand({ Bucket, Key: "hello.txt", Body: "hello" }));
const object = await s3.send(new GetObjectCommand({ Bucket, Key: "hello.txt" }));
console.log(await object.Body.transformToString());
const listing = await s3.send(new ListObjectsV2Command({ Bucket }));
console.log(listing.Contents?.map((o) => o.Key));
```

Any other S3 tool takes the same four values. In rclone, choose provider "Other", set the endpoint, and set `force_path_style = true`.

## Browse and upload in the console

The bucket's **Objects** tab is a file browser: folders, drag-and-drop upload (large files go up in parts, with progress and a cancel button, up to 625 GiB per object), download, delete and, for a public bucket, a button that copies an object's public URL.

The console never carries your files. It asks Zaroz for a short-lived link (15 minutes) and your browser talks to the storage directly, so the same billing rules apply as for any S3 client:

- **Downloads count as data transfer out and are billed per GiB.** So do previews: the console loads nothing on its own (no thumbnails), shows a preview only when you click it, and asks first for files over 1 MiB.
- **Uploads are free.**
- Listing folders is not billed.

While a bucket is over its cap you can still browse, download and delete, but not upload. While it is suspended you can see the names and sizes but not download, upload or delete.

## Share files publicly

In the bucket's **Settings**, turn on **Public read**. Anyone can then download an object without a key at:

```text
ENDPOINT/BUCKET/path/to/object.jpg
```

- Public read covers **downloading objects only**. Nobody can list the bucket, upload or delete without a key, and your other buckets are unaffected.
- Turning it off takes effect within a few minutes. Everything downloaded in the meantime is billed as normal egress.
- If you only need to share a file for a while, prefer a presigned link and keep the bucket private.

## Presigned links

A presigned URL lets someone download (or upload) one object for a limited time without having a key. Your code makes it with its own key; the URL then stands alone.

```bash
aws s3 presign "s3://$BUCKET/report.pdf" --expires-in 3600 --endpoint-url "$ENDPOINT"
```

```python
url = s3.generate_presigned_url(
    "put_object", Params={"Bucket": bucket, "Key": "uploads/a.png"}, ExpiresIn=900
)
```

```js
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
const url = await getSignedUrl(s3, new GetObjectCommand({ Bucket, Key: "report.pdf" }), { expiresIn: 3600 });
```

The link carries the permissions of the key that made it, and stops working if that key is revoked or the bucket is suspended.

:::info{title="JavaScript presigned uploads"}

Recent versions of the AWS SDK for JavaScript v3 add a checksum of the (empty) body to a presigned upload URL, and the upload then fails with `BadDigest`. Create the client with `requestChecksumCalculation: "WHEN_REQUIRED"`, as in the example above, and presigned uploads work. Downloads and boto3 are not affected.

:::

## Calling your bucket from a web page

Browsers only call the bucket directly from the Zaroz console: the bucket's CORS rules allow the console's origin and nothing else, and your keys cannot change them. A page on your own site cannot upload or download straight from the bucket. Generate presigned URLs on your server (see above) and hand those to the page, or use public read for files that anyone may download.

## Limits

| | |
|---|---|
| Buckets per subscription | 1 |
| Active keys per bucket | 20 |
| Storage cap | 1 GiB to 100 TiB (default 100 GiB), changeable any time |
| Addressing | Path-style only |
| Region name | `us-east-1` |

**The storage cap protects your bill.** When the stored data reaches the cap, uploads stop: every key can still list and download, and read-write keys can still **delete** objects, so you can free space yourself. Delete enough, or raise the cap in **Settings**, and uploads resume within about ten minutes. Usage is measured hourly, so the cap can be overshot slightly and the change takes up to an hour to show.

## What happens to your data

- **Unpaid or suspended subscription:** your keys stop working and public links go dark, but the data is kept. Pay the invoice and access returns within about ten minutes.
- **Cancelled or ended subscription:** the bucket is scheduled for deletion and its data is **permanently removed after 7 days**. Download what you need before then.
- **Delete the bucket yourself:** the same 7-day period applies before the data is purged.

## What it costs

The bill has three parts, shown on your plan's page:

1. A **monthly minimum** (the plan price), charged up front.
2. **Storage**, per GiB-month, measured hourly and averaged over the month.
3. **Downloads (egress)**, per GiB sent out of the bucket.

Uploads (ingress) are free. The minimum counts toward storage and egress first: you pay extra only for usage worth more than the minimum, so a small bucket costs exactly the plan price. The bucket's **Usage** tab shows storage and egress so far this month.

## Troubleshooting

| You see | Usually |
|---|---|
| `AccessDenied` on an upload | The key is read-only, the bucket is over its cap (deleting still works), or the subscription is suspended |
| `AccessDenied` listing a public bucket without a key | Expected: public read serves objects, not listings |
| `SignatureDoesNotMatch` or a DNS error | The client is using virtual-hosted addressing: turn on path-style |
| `BadDigest` on a presigned upload from JavaScript | Set `requestChecksumCalculation: "WHEN_REQUIRED"` |
| `NoSuchBucket` | You used the display name instead of the `zb-...` bucket name |
