# S3 兼容接口

用 aws、rclone、Cyberduck、boto3 或任何 AWS SDK 读写 Side Space。一个空间就是一个存储桶，路径就是对象键，每次写入都是一个版本。

会说 S3 的工具可以直接读写空间。给令牌加 `--s3` 就得到一对 S3 密钥：

```sh
sidespace token create rclone --scope files:write --space "Film A" --s3
# aws_access_key_id     = SSAK7QF2M4XKD9WZ3T0B
# aws_secret_access_key = ssp_…   （同时也是一个普通令牌）
```

端点是服务器的 `8070` 端口（生产部署里由 Caddy 用同一张证书提供 HTTPS）。用 path-style 寻址；region 随便填，例如 `us-east-1`。

```sh tab=aws
aws configure set aws_access_key_id SSAK7QF2M4XKD9WZ3T0B
aws configure set aws_secret_access_key ssp_…
aws --endpoint-url https://side.example.com:8070 s3 ls
aws --endpoint-url https://side.example.com:8070 s3 cp ./Ep01 s3://film-a/Ep01 --recursive
```

```ini tab=rclone
# ~/.config/rclone/rclone.conf
[side]
type = s3
provider = Other
access_key_id = SSAK7QF2M4XKD9WZ3T0B
secret_access_key = ssp_…
endpoint = https://side.example.com:8070
force_path_style = true

# rclone copy side:film-a/Ep01 ./Ep01
```

```python tab=boto3
import boto3

s3 = boto3.client("s3", endpoint_url="https://side.example.com:8070",
                  aws_access_key_id="SSAK7QF2M4XKD9WZ3T0B", aws_secret_access_key="ssp_…",
                  region_name="us-east-1")
s3.upload_file("A001_C003.mov", "film-a", "Ep01/Shots/A001_C003.mov")
url = s3.generate_presigned_url("get_object", Params={"Bucket": "film-a", "Key": "Ep01/notes.md"}, ExpiresIn=3600)
```

## 存储桶和对象键

- 存储桶名是空间名的小写，字母和数字以外的字符换成 `-`（`Film A` → `film-a`）。重名或太短时用空间 id（`spc_01…` 写成 `spc-01…`）。
- 对象键就是空间里的路径。
- 令牌带 `admin` 权限的密钥可以用 CreateBucket 新建一个和桶同名的空间；同时带上 `x-sidespace-fork-source: 桶名[/文件夹]` 头，就是开一个[分支](/docs/forks)。DeleteBucket 只删除从没放过东西的空间，和 S3 只删空桶一样；其余的返回 `BucketNotEmpty`，要在 Side Space 里删。

## 写入会发生什么

一次写入就是一次正常保存：生成新版本，通知、Webhook、流水线照常触发。删除进回收站，30 天内可恢复。在同一空间及其分支内 `CopyObject` 不复制字节。

## 版本和过去

每个桶的版本控制都是 `Enabled`，因为 Side Space 里每次保存都是一个版本。认得 S3 版本的工具（rclone `--s3-versions`、备份工具、boto3）都能看到完整历史。

```sh
aws --endpoint-url https://side.example.com:8070 s3api list-object-versions --bucket film-a --prefix Ep01/
aws --endpoint-url https://side.example.com:8070 s3api get-object --bucket film-a --key Ep01/script.md \
  --version-id ver_01JD… old.md
# 恢复旧版本：把它复制到原来的键上
aws --endpoint-url https://side.example.com:8070 s3api copy-object --bucket film-a --key Ep01/script.md \
  --copy-source "film-a/Ep01/script.md?versionId=ver_01JD…"
```

- S3 版本 id 就是 Side Space 的版本 id（`ver_…`）。PutObject、CopyObject 和 CompleteMultipartUpload 返回 `x-amz-version-id`。
- ListObjectVersions 列出每个文件的所有版本，同一个键里新的在前，支持键和版本 id 的分页标记，以及分隔符前缀。
- GetObject、HeadObject 和 CopyObject 的源都接受 `versionId`。
- 回收站里的文件显示为删除标记，id 是 `del.<节点 id>`。删除这个标记就把文件恢复回来，和 S3 一样。对一个键 DeleteObject 会留下删除标记：文件进回收站。
- 想看桶在过去的样子，带 `x-sidespace-as-of` 头（时间、`-2h` 或日志序号；预签名链接用同名查询参数），列表和读取得到的就是那一刻的内容。见[看过去和整体回滚](/docs/history)。
- 版本一直保留，直到文件被它的所有者在 Side Space 里永久删除。删除单个版本返回 `MethodNotAllowed`，暂停版本控制返回 `InvalidArgument`。

## 支持的操作

ListBuckets、HeadBucket、CreateBucket、DeleteBucket、ListObjects v1 和 v2、ListObjectVersions、GetBucketVersioning（前缀、分隔符、分页）、GetObject 和 HeadObject（含 Range）、PutObject、CopyObject、DeleteObject、DeleteObjects、分段上传（Create、UploadPart、Complete、Abort、ListParts、ListMultipartUploads；分段没有 5 MB 下限）、预签名链接（SigV4 和 SigV2），以及 aws-chunked 流式上传。

不支持：桶策略、ACL、对象标签、暂停版本控制、删除单个版本。ETag 是内容哈希，不是 MD5。

权限照令牌来：`files:read` 只能读，`files:write` 才能写；限定空间的令牌只看得到那几个桶。吊销令牌后 S3 密钥立即失效。
