Files
image-gateway/docs/usage-guide.md
S.Gromov 0751c4b469 feat: добавить Image Gateway с кешем Souin
- добавлена сборка Caddy с Souin, Otter и NutsDB

- добавлена конфигурация dev, prod и test Docker Compose

- настроено кеширование через Otter L1 и NutsDB L2

- добавлены e2e-тесты Bun для кеша, restart и purge

- добавлена документация по запуску, API кеша и тестам
2026-05-04 12:18:37 +03:00

8.3 KiB
Raw Blame History

Гайд использования

Руководство по кейсам — копируй, вставляй, проверяй. Без обдумывания.

1. Запуск

cp .env.example .env
docker compose -f docker-compose.dev.yml up -d --build

Проверка:

curl -s -o /dev/null -w "%{http_code}" \
  "http://localhost:8888/unsafe/resize:fit:100:0:0/q:80/plain/https://picsum.photos/200/200"
# → 200

2. Обработка изображений

Resize — вписать в ширину 800px

curl -s -o /tmp/fit800.jpg \
  "http://localhost:8888/unsafe/resize:fit:800:0:0/q:80/plain/https://picsum.photos/1200/800"
# → файл /tmp/fit800.jpg, ширина = 800px

Resize — вписать в квадрат 200x200

curl -s -o /tmp/fit200.jpg \
  "http://localhost:8888/unsafe/resize:fit:200:200:0/q:80/plain/https://picsum.photos/1200/800"
# → файл 200x?, пропорции сохранены

Resize — заполнить 400x300 (с обрезкой)

curl -s -o /tmp/fill400.jpg \
  "http://localhost:8888/unsafe/resize:fill:400:300:0/g:ce/q:80/plain/https://picsum.photos/1200/800"
# → файл точно 400x300, лишнее обрезано по центру

Crop — обрезка 200x200

curl -s -o /tmp/crop200.jpg \
  "http://localhost:8888/unsafe/crop:200:200/plain/https://picsum.photos/1200/800"
# → файл 200x200

WebP — конвертация формата

curl -s -D - -o /tmp/webp.webp \
  "http://localhost:8888/unsafe/format:webp/q:80/plain/https://picsum.photos/800/600" \
  2>&1 | grep Content-Type
# → Content-Type: image/webp

AVIF

curl -s -D - -o /tmp/avif.avif \
  "http://localhost:8888/unsafe/format:avif/q:80/plain/https://picsum.photos/800/600" \
  2>&1 | grep Content-Type
# → Content-Type: image/avif

Качество — сравнить q:10 vs q:100

curl -s -o /tmp/q10.jpg \
  "http://localhost:8888/unsafe/resize:fit:400:0:0/q:10/plain/https://picsum.photos/800/600"
curl -s -o /tmp/q100.jpg \
  "http://localhost:8888/unsafe/resize:fit:400:0:0/q:100/plain/https://picsum.photos/800/600"
ls -la /tmp/q10.jpg /tmp/q100.jpg
# → q10 ~4KB, q100 ~50KB — разница ~12x

Комбинированные — resize + WebP + качество

curl -s -o /tmp/combined.webp \
  "http://localhost:8888/unsafe/resize:fit:800:0:0/g:ce/q:75/format:webp/plain/https://picsum.photos/1200/800"
# → 800px по ширине, WebP, качество 75

base64url — закодированный source URL

B64=$(echo -n "https://picsum.photos/800/600" | base64 -w0 | tr '+/' '-_' | tr -d '=')
curl -s -o /tmp/b64.jpg "http://localhost:8888/unsafe/resize:fit:100:0:0/q:80/$B64"
# → работает как plain/, но без /plain/ префикса

3. Кеширование

MISS → HIT — базовая проверка

URL="http://localhost:8888/unsafe/resize:fit:100:0:0/q:80/plain/https://picsum.photos/400/300"

# Первый запрос — MISS (обработка через imgproxy)
curl -s -o /dev/null -D - -w "\ntime: %{time_total}s\n" "$URL" | grep -E "Cache-Status|time"
# → Cache-Status: Souin; fwd=uri-miss; stored
# → time: 0.5s

# Второй запрос — HIT (из кеша)
curl -s -o /dev/null -D - -w "\ntime: %{time_total}s\n" "$URL" | grep -E "Cache-Status|time"
# → Cache-Status: Souin; hit; ttl=31535999; detail=DEFAULT
# → time: 0.001s

Что значит Cache-Status

Cache-Status: Souin; hit; ...                                    → из кеша
Cache-Status: Souin; fwd=uri-miss; stored; key=GET-...          → первый запрос, закешировано
Cache-Status: Souin; fwd=uri-miss; detail=UNCACHEABLE-...       → не закешировано (ошибка upstream)

Разные размеры = разные ключи

SRC="https://picsum.photos/id/42/800/600"
curl -s -o /dev/null "http://localhost:8888/unsafe/resize:fit:200:0:0/q:80/plain/$SRC"
curl -s -o /dev/null "http://localhost:8888/unsafe/resize:fit:400:0:0/q:80/plain/$SRC"
curl -s "http://localhost:2019/souin-api/souin/"
# → 2 ключа: ...resize:fit:200... и ...resize:fit:400...

4. Purge кеша

Все purge-запросы идут через Caddy Admin API на порту 2019.

Purge — сбросить всё

# Закешировать
curl -s -o /dev/null "http://localhost:8888/unsafe/resize:fit:100:0:0/q:80/plain/https://picsum.photos/400/300"

# Purge
curl -s -w "status: %{http_code}\n" -X PURGE http://localhost:2019/souin-api/souin/flush
# → status: 204

# Проверить — снова MISS
curl -s -o /dev/null -D - "http://localhost:8888/unsafe/resize:fit:100:0:0/q:80/plain/https://picsum.photos/400/300" \
  | grep Cache-Status
# → Cache-Status: Souin; fwd=uri-miss; stored

Purge — конкретное изображение (все размеры)

# Закешировать 2 размера
SRC="https://picsum.photos/id/77/800/600"
curl -s -o /dev/null "http://localhost:8888/unsafe/resize:fit:200:0:0/q:80/plain/$SRC"
curl -s -o /dev/null "http://localhost:8888/unsafe/resize:fit:400:0:0/q:80/plain/$SRC"

# Purge по regex (id/77)
curl -s -w "status: %{http_code}\n" -X PURGE "http://localhost:2019/souin-api/souin/.*id/77.*"
# → status: 204

# Оба размера сброшены

Purge — конкретный размер

curl -s -w "status: %{http_code}\n" -X PURGE \
  "http://localhost:2019/souin-api/souin/.*resize:fit:200.*id/77.*$"
# → status: 204 — только 200px сброшен, 400px остался

5. Caddy Admin API (Souin)

Порт 2019, только localhost.

Список закешированных ключей

curl -s http://localhost:2019/souin-api/souin/
# → ["GET-http-localhost:8888-/unsafe/resize:fit:200:0:0/q:80/plain/https://..."]

Purge — все методы

# Полный сброс
curl -X PURGE http://localhost:2019/souin-api/souin/flush
# → 204

# По regex
curl -X PURGE "http://localhost:2019/souin-api/souin/.*example.com/photo.jpg"
# → 204

# Точное совпадение (с $ в конце)
curl -X PURGE "http://localhost:2019/souin-api/souin/.*resize:fit:800.*photo\.jpg$"
# → 204

6. Signed vs Unsigned

Unsigned — по умолчанию

Ключи не заданы → /unsafe/ работает:

# С /unsafe/ → работает
curl -s -o /dev/null -w "%{http_code}" \
  "http://localhost:8888/unsafe/resize:fit:100:0:0/q:80/plain/https://picsum.photos/200/200"
# → 200

# Без /unsafe/ → тоже работает (ключи не заданы, подпись не проверяется)
curl -s -o /dev/null -w "%{http_code}" \
  "http://localhost:8888/resize:fit:100:0:0/q:80/plain/https://picsum.photos/200/200"
# → 200

Signed — включить подпись

  1. Сгенерировать ключи:
openssl rand -hex 32  # → KEY
openssl rand -hex 32  # → SALT
  1. Добавить в .env:
IMGPROXY_KEY=<сгенерированный_ключ>
IMGPROXY_SALT=<сгенерированная_соль>
  1. Перезапустить:
docker compose -f docker-compose.dev.yml restart imgproxy
  1. Проверить:
# Без /unsafe/ и без подписи → ошибка
curl -s -o /dev/null -w "%{http_code}" \
  "http://localhost:8888/resize:fit:100:0:0/q:80/plain/https://picsum.photos/200/200"
# → 403

# С /unsafe/ → тоже ошибка (ключи заданы, unsafe запрещён)
curl -s -o /dev/null -w "%{http_code}" \
  "http://localhost:8888/unsafe/resize:fit:100:0:0/q:80/plain/https://picsum.photos/200/200"
# → 403
  1. Подписать URL:
KEY="<сгенерированный_ключ>"
SALT="<сгенерированная_соль>"
PATH_URL="/resize:fit:100:0:0/q:80/plain/https://picsum.photos/200/200"

SIG=$(echo -n "$SALT" | xxd -r -p | openssl dgst -sha256 -hmac "$(echo -n "$KEY" | xxd -r -p)" -binary | base64 | tr '+/' '-_' | tr -d '=' | head -c 32)

curl -s -o /dev/null -w "%{http_code}" "http://localhost:8888/${SIG}${PATH_URL}"
# → 200
  1. Вернуть unsigned — убрать ключи из .env и перезапустить.