# S3 API

S3-compatible object storage helpers are available under `ptool.s3` and `p.s3`.

## ptool.s3.connect

> `Unreleased` - Introduced.

`ptool.s3.connect(options)` opens an S3-compatible object storage connection and returns a `Connection` object.

`options` fields:

- `bucket` (string, required): The bucket name.
- `region` (string, optional): The AWS region or provider region.
- `endpoint` (string, optional): A custom S3-compatible endpoint URL such as MinIO, R2, or another object storage service.
- `access_key_id` (string, optional): The access key ID.
- `secret_access_key` (string, optional): The secret access key.
- `session_token` (string, optional): The session token.
- `root` (string, optional): A root prefix applied to all object operations.
- `allow_anonymous` (boolean, optional): When `true`, allow unsigned requests if credentials are not configured. Defaults to `false`.

Environment fallback:

- Explicit `options` values win.
- Missing `region`, `endpoint`, `access_key_id`, `secret_access_key`, and `session_token` values fall back to:
  - `AWS_REGION`
  - `AWS_ENDPOINT`, `AWS_ENDPOINT_URL`, or `AWS_S3_ENDPOINT`
  - `AWS_ACCESS_KEY_ID`
  - `AWS_SECRET_ACCESS_KEY`
  - `AWS_SESSION_TOKEN`
- Environment fallback uses `ptool`'s runtime environment view, so values set through `p.os.setenv(...)` are also visible to `ptool.s3.connect(...)`.

Example:

```lua
local s3 = ptool.s3.connect({
  bucket = "artifacts",
  region = "auto",
  endpoint = "https://<account>.r2.cloudflarestorage.com",
  access_key_id = p.os.getenv("AWS_ACCESS_KEY_ID"),
  secret_access_key = p.os.getenv("AWS_SECRET_ACCESS_KEY"),
  root = "builds/",
})
```

## Connection

> `Unreleased` - Introduced.

`Connection` represents an open object storage connection returned by `ptool.s3.connect()`.

It is implemented as a Lua userdata.

Methods:

- `conn:read(path)` -> `string`
- `conn:write(path, content)` -> `nil`
- `conn:delete(path)` -> `nil`
- `conn:exists(path)` -> `boolean`
- `conn:list([prefix])` -> `table`
- `conn:stat(path)` -> `table`

Path rules:

- Object paths must be non-empty strings unless otherwise noted.
- Leading `/` is ignored, so `/foo/bar.txt` and `foo/bar.txt` target the same object.
- Paths are relative to `root` when `root` is configured on the connection.

Entry table shape:

- `path` (string): The object path relative to the connection root.
- `size` (integer): The object size in bytes.
- `etag` (string | nil): The object ETag when available.
- `last_modified` (string | nil): The last-modified timestamp when available.
- `content_type` (string | nil): The object content type when available.
- `is_file` (boolean): Whether the entry is a file.
- `is_dir` (boolean): Whether the entry is a directory.
- `mode` (string): One of `"file"`, `"dir"`, or `"unknown"`.

### read

> `Unreleased` - Introduced.

Canonical API name: `ptool.s3.Connection:read`.

`conn:read(path)` reads an object as raw bytes and returns a Lua string.

- `path` (string, required): The object path.
- Returns: `string`.

Example:

```lua
local s3 = ptool.s3.connect({ bucket = "artifacts" })
local content = s3:read("releases/v1.0.0/notes.txt")
print(content)
```

### write

> `Unreleased` - Introduced.

Canonical API name: `ptool.s3.Connection:write`.

`conn:write(path, content)` writes a Lua string to an object as raw bytes.

- `path` (string, required): The object path.
- `content` (string, required): The bytes to upload.

Behavior:

- `content` is uploaded byte-for-byte.
- Embedded NUL bytes and non-UTF-8 bytes are preserved.

Example:

```lua
local s3 = ptool.s3.connect({ bucket = "artifacts" })
s3:write("tmp/hello.txt", "hello\n")
s3:write("tmp/blob.bin", "\x00\xffABC")
```

### delete

> `Unreleased` - Introduced.

Canonical API name: `ptool.s3.Connection:delete`.

`conn:delete(path)` deletes an object.

- `path` (string, required): The object path.

### exists

> `Unreleased` - Introduced.

Canonical API name: `ptool.s3.Connection:exists`.

`conn:exists(path)` checks whether an object exists.

- `path` (string, required): The object path.
- Returns: `boolean`.

### list

> `Unreleased` - Introduced.

Canonical API name: `ptool.s3.Connection:list`.

`conn:list([prefix])` lists entries under a prefix and returns a dense Lua array table.

- `prefix` (string, optional): The prefix to list. Defaults to the connection root.
- Returns: `table`.

Example:

```lua
local s3 = ptool.s3.connect({ bucket = "artifacts", root = "builds/" })
local entries = s3:list("2026/")

for _, entry in ipairs(entries) do
  print(entry.path, entry.mode, entry.size)
end
```

### stat

> `Unreleased` - Introduced.

Canonical API name: `ptool.s3.Connection:stat`.

`conn:stat(path)` returns metadata for a single object.

- `path` (string, required): The object path.
- Returns: `table`.

Example:

```lua
local s3 = ptool.s3.connect({ bucket = "artifacts" })
local meta = s3:stat("releases/v1.0.0/app.tar.zst")
print(meta.size, meta.etag, meta.last_modified)
```
