# Galerias e arquivos

## Galerias

### Listar galerias (`assets:read`)

Parâmetros: `project` (filtra por projeto) e `cursor` (veja a receita de paginação, a receita "Listar com cursor" abaixo).

```bash
curl -sS "$NEBULAR_API/v1/galleries" \
  -H "Authorization: Bearer $NEBULAR_TOKEN"
```
```json
{
  "items": [
    {
      "id": "gal_01J9ZK5Q",
      "key": "marcas-k7p2",
      "name": "Marcas",
      "bucket": {"id": "bkt_01J9ZG00", "label": "media", "kind": "shared", "host": "media.nebularcdn.com"},
      "project": null,
      "assets_count": 12,
      "bytes": 4823910,
      "created_at": "2026-10-09T14:03:11Z"
    }
  ],
  "next_cursor": null,
  "has_more": false
}
```

`key` é a identificação pública e **imutável** da galeria (nomes podem mudar): é ela que vai nas rotas e nas URLs.

### Criar galeria (`galleries:manage`)

`name` (1 a 80 caracteres) é obrigatório; `project_id` (opcional) apenas organiza a galeria sob um projeto.

```bash
curl -sS -X POST "$NEBULAR_API/v1/galleries" \
  -H "Authorization: Bearer $NEBULAR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name": "Marcas"}'
```
```json
{
  "id": "gal_01J9ZK5Q",
  "key": "marcas-k7p2",
  "name": "Marcas",
  "bucket": {"id": "bkt_01J9ZG00", "label": "media", "kind": "shared", "host": "media.nebularcdn.com"},
  "project": null,
  "assets_count": 0,
  "bytes": 0,
  "created_at": "2026-10-09T14:03:11Z"
}
```

Guarde o `key` retornado (nos exemplos: `$GALLERY`). Cada chamada cria uma galeria nova, então não repita sem necessidade.

## Arquivos

### Enviar ou versionar (`assets:write`)

Corpo `multipart/form-data` com `file` (obrigatório) e `name` (opcional; padrão: o nome do arquivo enviado
normalizado). Se o nome já existir, cria uma **nova versão** e o alias passa a apontar para ela. Enviar o mesmo
conteúdo (mesmo SHA-256) para o mesmo nome não cria versão nova.

Respostas: **201** (limpo e já publicado), **202** (`pending`: ainda não publicado, veja [Uploads pending](#uploads-pending-202-como-esperar)) ou um erro ([Erros](/erros/tabela/)).

```bash
curl -sS -X POST "$NEBULAR_API/v1/galleries/$GALLERY/assets" \
  -H "Authorization: Bearer $NEBULAR_TOKEN" \
  -H "Idempotency-Key: logo-acme-2026-10-09" \
  -F "file=@logo.png" \
  -F "name=logos/acme.png"
```
```json
{
  "id": "ast_01J9ZK8W",
  "name": "logos/acme.png",
  "gallery_key": "marcas-k7p2",
  "current": {
    "id": "ver_01J9ZK8X",
    "hash": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
    "bytes": 48213,
    "content_type": "image/png",
    "class": "image",
    "width": 1200,
    "height": 630,
    "variants": ["320", "640", "1280"],
    "scan": {"status": "clean", "reason": null, "engine": "clamav", "scanned_at": "2026-10-09T14:05:02Z"},
    "url": "https://media.nebularcdn.com/marcas-k7p2/9f86d081884c/logos/acme.png",
    "uploaded_by": {"kind": "token", "name": "agente-deploy"},
    "created_at": "2026-10-09T14:05:01Z"
  },
  "alias_url": "https://media.nebularcdn.com/marcas-k7p2/logos/acme.png",
  "versions_count": 1,
  "created_at": "2026-10-09T14:05:01Z"
}
```

Use sempre a `url`/`alias_url` devolvidas pela API, sem montá-las à mão (o domínio de classes de download pode mudar).
`url` e `alias_url` são `null` enquanto o arquivo não estiver `clean`.

### Idempotência (`Idempotency-Key`)

Envie `Idempotency-Key` (até 255 caracteres; um UUID ou algo derivado do conteúdo) em todo upload automatizado.
Mesma chave com o **mesmo corpo** devolve a mesma resposta, então repetir após timeout ou erro de rede é seguro e não cria
versões duplicadas. Mesma chave com **corpo diferente** devolve `409 idempotency_conflict`: gere uma chave nova.

```bash
# repetir a mesma chave com o mesmo arquivo devolve a mesma resposta
curl -sS -X POST "$NEBULAR_API/v1/galleries/$GALLERY/assets" \
  -H "Authorization: Bearer $NEBULAR_TOKEN" \
  -H "Idempotency-Key: logo-acme-2026-10-09" \
  -F "file=@logo.png" \
  -F "name=logos/acme.png"
```
```json
{"id": "ast_01J9ZK8W", "name": "logos/acme.png", "versions_count": 1, "alias_url": "https://media.nebularcdn.com/marcas-k7p2/logos/acme.png"}
```

Mesma chave, arquivo diferente:

```bash
curl -sS -X POST "$NEBULAR_API/v1/galleries/$GALLERY/assets" \
  -H "Authorization: Bearer $NEBULAR_TOKEN" \
  -H "Idempotency-Key: logo-acme-2026-10-09" \
  -F "file=@banner.png" \
  -F "name=logos/acme.png"
```
```json
{"error": "idempotency_conflict", "message": "Esta Idempotency-Key já foi usada com outro conteúdo. Gere uma chave nova."}
```

### Listar arquivos (`assets:read`)

Parâmetros: `prefix` (só nomes que começam com ele) e `cursor`.

```bash
curl -sS -G "$NEBULAR_API/v1/galleries/$GALLERY/assets" \
  --data-urlencode "prefix=logos/" \
  -H "Authorization: Bearer $NEBULAR_TOKEN"
```
```json
{
  "items": [
    {
      "id": "ast_01J9ZK8W",
      "name": "logos/acme.png",
      "gallery_key": "marcas-k7p2",
      "current": {
        "id": "ver_01J9ZK8X",
        "hash": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
        "bytes": 48213,
        "content_type": "image/png",
        "class": "image",
        "width": 1200,
        "height": 630,
        "variants": ["320", "640", "1280"],
        "scan": {"status": "clean", "reason": null, "engine": "clamav", "scanned_at": "2026-10-09T14:05:02Z"},
        "url": "https://media.nebularcdn.com/marcas-k7p2/9f86d081884c/logos/acme.png",
        "uploaded_by": {"kind": "token", "name": "agente-deploy"},
        "created_at": "2026-10-09T14:05:01Z"
      },
      "alias_url": "https://media.nebularcdn.com/marcas-k7p2/logos/acme.png",
      "versions_count": 1,
      "created_at": "2026-10-09T14:05:01Z"
    }
  ],
  "next_cursor": null,
  "has_more": false
}
```

### Detalhe, versões e status de varredura (`assets:read`)

O nome vai na rota; use `%2F` no lugar de cada `/` (a barra literal também é aceita).

```bash
curl -sS "$NEBULAR_API/v1/galleries/$GALLERY/assets/logos%2Facme.png" \
  -H "Authorization: Bearer $NEBULAR_TOKEN"
```
```json
{
  "asset": {
    "id": "ast_01J9ZK8W",
    "name": "logos/acme.png",
    "gallery_key": "marcas-k7p2",
    "current": {
      "id": "ver_01J9ZK8X",
      "hash": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
      "bytes": 48213,
      "content_type": "image/png",
      "class": "image",
      "width": 1200,
      "height": 630,
      "variants": ["320", "640", "1280"],
      "scan": {"status": "clean", "reason": null, "engine": "clamav", "scanned_at": "2026-10-09T14:05:02Z"},
      "url": "https://media.nebularcdn.com/marcas-k7p2/9f86d081884c/logos/acme.png",
      "uploaded_by": {"kind": "token", "name": "agente-deploy"},
      "created_at": "2026-10-09T14:05:01Z"
    },
    "alias_url": "https://media.nebularcdn.com/marcas-k7p2/logos/acme.png",
    "versions_count": 1,
    "created_at": "2026-10-09T14:05:01Z"
  },
  "versions": [
    {
      "id": "ver_01J9ZK8X",
      "hash": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
      "bytes": 48213,
      "content_type": "image/png",
      "class": "image",
      "width": 1200,
      "height": 630,
      "variants": ["320", "640", "1280"],
      "scan": {"status": "clean", "reason": null, "engine": "clamav", "scanned_at": "2026-10-09T14:05:02Z"},
      "url": "https://media.nebularcdn.com/marcas-k7p2/9f86d081884c/logos/acme.png",
      "uploaded_by": {"kind": "token", "name": "agente-deploy"},
      "created_at": "2026-10-09T14:05:01Z"
    }
  ]
}
```

### Excluir (`assets:write`)

Exclui o arquivo e purga o cache da CDN (a URL passa a dar 404). O backup retém os bytes por 30 dias.
Resposta: `204` sem corpo.

```bash
curl -sS -X POST "$NEBULAR_API/v1/galleries/$GALLERY/assets" \
  -H "Authorization: Bearer $NEBULAR_TOKEN" \
  -F "file=@hero.png" -F "name=temp/hero.png" -o /dev/null
curl -sS -X DELETE "$NEBULAR_API/v1/galleries/$GALLERY/assets/temp%2Fhero.png" \
  -H "Authorization: Bearer $NEBULAR_TOKEN" -w 'HTTP %{http_code}\n'
```

Depois disso o `GET` do mesmo nome devolve `404 not_found`.

## Uploads pending (202): como esperar

Se o scanner estiver ocupado ou fora do ar, o upload devolve **202** com `scan.status = "pending"`, `url` e `alias_url`
`null`: o arquivo foi recebido mas **não está publicado**. **Não reenvie.** Consulte o detalhe do arquivo com
backoff (comece em 2 s, dobre a cada tentativa até 30 s) até `scan.status` deixar de ser `pending`:
`clean` (publicado, `url`/`alias_url` preenchidos) ou `rejected` (recusado; o campo `scan.reason` diz o motivo).

```bash
curl -sS -X POST "$NEBULAR_API/v1/galleries/$GALLERY/assets" \
  -H "Authorization: Bearer $NEBULAR_TOKEN" \
  -F "file=@slow.png" -F "name=site/lento.png"
```
```json
{
  "id": "ast_01J9ZK8W",
  "name": "site/lento.png",
  "gallery_key": "marcas-k7p2",
  "current": {
    "id": "ver_01J9ZK8X",
    "hash": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
    "bytes": 48213,
    "content_type": "application/octet-stream",
    "class": "binary",
    "width": null,
    "height": null,
    "variants": [],
    "scan": {"status": "pending", "reason": null, "engine": null, "scanned_at": null},
    "url": null,
    "uploaded_by": {"kind": "token", "name": "agente-deploy"},
    "created_at": "2026-10-09T14:05:01Z"
  },
  "alias_url": null,
  "versions_count": 1,
  "created_at": "2026-10-09T14:05:01Z"
}
```

Enquanto `pending`, `content_type`/`class` são provisórios (`application/octet-stream`/`binary`) e `width`/`height` são `null`;
valem os valores do `GET` depois que o status virar `clean`.

Polling com backoff (no máximo 8 tentativas):

```bash
delay=2
for attempt in 1 2 3 4 5 6 7 8; do
  status=$(curl -sS "$NEBULAR_API/v1/galleries/$GALLERY/assets/site%2Flento.png" \
    -H "Authorization: Bearer $NEBULAR_TOKEN" | jq -r '.asset.current.scan.status')
  echo "tentativa $attempt: $status"
  [ "$status" = "pending" ] || break
  sleep "$delay"
  delay=$(( delay * 2 > 30 ? 30 : delay * 2 ))
done
[ "$status" = "clean" ]   # sai com erro se foi recusado ou se ainda está pendente
```

## Receitas

### (a) Subir e usar no Next com `next/image`

1. Suba a imagem e guarde a `alias_url`:

```bash
curl -sS -X POST "$NEBULAR_API/v1/galleries/$GALLERY/assets" \
  -H "Authorization: Bearer $NEBULAR_TOKEN" \
  -H "Idempotency-Key: hero-$(cksum < hero.png | tr ' ' '-')" \
  -F "file=@hero.png" -F "name=site/hero.png" | jq -r '.alias_url'
```

2. Um loader de `next/image` que acrescenta `?w=&q=` (larguras do `next/image` já estão na lista permitida):

```ts
// lib/nebularLoader.ts
import type { ImageLoaderProps } from "next/image";

export default function nebularLoader({ src, width, quality }: ImageLoaderProps): string {
  const url = new URL(src);
  url.searchParams.set("w", String(width));
  url.searchParams.set("q", String(quality ?? 75));
  return url.toString();
}
```

3. Libere o host e use o loader (o `src` é a `alias_url` sem parâmetros):

```ts
// next.config.ts
import type { NextConfig } from "next";

const config: NextConfig = {
  images: {
    loader: "custom",
    loaderFile: "./lib/nebularLoader.ts",
  },
};
export default config;
```

```tsx
import Image from "next/image";

export function Hero() {
  return (
    <Image
      src="https://media.nebularcdn.com/marcas-k7p2/site/hero.png"
      alt="Banner principal"
      width={1200}
      height={630}
      sizes="100vw"
    />
  );
}
```

Como o alias sempre aponta para a versão atual, trocar a imagem (receita c) atualiza o site sem novo deploy.

### (b) Subir em lote com laço shell (429, Retry-After e idempotência)

A `Idempotency-Key` derivada do conteúdo torna o laço seguro de repetir inteiro: o que já foi enviado não gera versão nova.
Erros permanentes (4xx exceto 429) não são repetidos; 429 e 503 esperam `Retry-After`.

```bash
fail=0
for f in imagens/*.png; do
  name="lote/$(basename "$f")"
  key="lote-$name-$(cksum < "$f" | tr ' ' '-')"
  for try in 1 2 3 4 5; do
    code=$(curl -sS -o resp.json -D hdr.txt -w '%{http_code}' \
      -X POST "$NEBULAR_API/v1/galleries/$GALLERY/assets" \
      -H "Authorization: Bearer $NEBULAR_TOKEN" \
      -H "Idempotency-Key: $key" \
      -F "file=@$f" -F "name=$name")
    case "$code" in
      201|202) echo "$name -> $code"; break ;;
      429|503)
        wait=$(awk -F': ' 'tolower($1)=="retry-after"{print $2+0}' hdr.txt)
        echo "$name -> $code, aguardando ${wait:-5}s (tentativa $try)" >&2
        sleep "${wait:-5}" ;;
      *) echo "$name FALHOU ($code): $(cat resp.json)" >&2; fail=1; break ;;
    esac
  done
done
[ "$fail" = 0 ]
```

Arquivos que voltarem `202` ainda precisam do polling da [Uploads pending](#uploads-pending-202-como-esperar) antes de a URL existir.

### (c) Trocar uma imagem mantendo a `alias_url`

Envie o novo arquivo **com o mesmo `name`**: nasce uma nova versão e o alias passa a servir a nova imagem; a `url`
imutável antiga continua válida (e imutável). Não precisa mudar nada em quem usa a `alias_url`.

```bash
alias_antes=$(curl -sS "$NEBULAR_API/v1/galleries/$GALLERY/assets/site%2Fhero.png" \
  -H "Authorization: Bearer $NEBULAR_TOKEN" | jq -r '.asset.alias_url')

curl -sS -X POST "$NEBULAR_API/v1/galleries/$GALLERY/assets" \
  -H "Authorization: Bearer $NEBULAR_TOKEN" \
  -F "file=@logo-v2.png" -F "name=site/hero.png" > nova.json

jq -r '"versões: \(.versions_count)"' nova.json
test "$(jq -r '.alias_url' nova.json)" = "$alias_antes"   # a alias_url não mudou
test "$(jq -r '.versions_count' nova.json)" = "2"
```

Se a `alias_url` ainda mostrar a imagem antiga por instantes, é o cache de 60 s do alias; a `url` imutável da nova
versão já serve o novo conteúdo.

### (d) Listar com cursor (`next_cursor` / `has_more`)

Siga `next_cursor` enquanto `has_more` for `true`. O cursor é opaco: não monte nem altere.

```bash
cursor=""
while :; do
  url="$NEBULAR_API/v1/galleries/$GALLERY/assets"
  [ -n "$cursor" ] && url="$url?cursor=$cursor"
  page=$(curl -sS "$url" -H "Authorization: Bearer $NEBULAR_TOKEN")
  echo "$page" | jq -r '.items[] | "\(.name)\t\(.alias_url)"'
  [ "$(echo "$page" | jq -r '.has_more')" = "true" ] || break
  cursor=$(echo "$page" | jq -r '.next_cursor')
done
```

O mesmo vale para `GET /v1/galleries`.
