Go API для personal art archive. Read-only, метаданные читаются из
artworks.json в памяти (см. architecture.md в проекте Obsidian для
полной архитектуры). Функциональные эндпоинты защищены статическими
API-ключами; инфраструктурные — открыты.
Текущая версия: v0.2.0 (Auth + Docker, ART-SPRINT-07). См. CHANGELOG.md
для полной истории изменений.
Самый простой способ — через Docker Compose, поднимает Postgres, прогоняет миграции и стартует сервис:
cp .env.example .env # при желании поменять дефолты
docker compose up --buildВыдать первый API-ключ (без него /artworks, /artists, /taxonomy
вернут 401):
docker compose run --rm artkeys create --label "локальный тест"
# → Ключ создан (id=..., label="локальный тест").
# Сохрани его — он больше никогда не будет показан:
# wa_live_...Проверить:
curl http://localhost:8080/healthz
curl -H "Authorization: Bearer wa_live_..." http://localhost:8080/artworksТребует доступный Postgres и применённые миграции (migrate -path migrations -database "$DATABASE_URL" up, см. golang-migrate).
export DATABASE_URL="postgres://user:pass@localhost:5432/art_api?sslmode=disable"
go run ./cmd/art-api -addr=:8080 -data=data/artworks.json
go run ./cmd/artkeys create --label "локальный тест"Флаги art-api:
-addr— адрес для прослушивания (по умолчанию:8080)-data— путь кartworks.json(по умолчаниюdata/artworks.json)
DATABASE_URL — обязательная переменная окружения, не флаг (credentials не
должны попадать в аргументы процесса, видные в ps/логах).
Статические API-ключи, не OAuth/sessions — соответствует масштабу
(read-only данные, нет пользовательских аккаунтов). Подробное архитектурное
решение и почему отклонён вариант с Telegram-логином — в CHANGELOG.md
(v0.2.0) и architecture.md.
- Формат ключа:
wa_live_<64 hex chars>. Хранится только SHA-256 хеш — сам ключ показывается один раз при создании и не восстановим. - Передача:
Authorization: Bearer <key>илиX-API-Key: <key>. - Выдача: только вручную, через
cmd/artkeys(create/list/revoke). Нет публичногоPOST /keysэндпоинта — кому давать доступ решается человеком, не самообслуживанием. - Закрытые эндпоинты:
/artworks,/artworks/{id},/artworks/random,/artists,/taxonomy. - Открытые эндпоинты:
/healthz,/readyz,/metrics— не должны зависеть от ключа (k8s-пробы, Prometheus scrape).
artkeys create --label "MCP server" # выдать новый ключ
artkeys list # все ключи, включая отозванные
artkeys revoke <id> # отозвать по idТребует DATABASE_URL в окружении — подключается к той же базе, что и сам
сервис.
go test ./... # все тесты
go test ./... -race # с детектором гонок (store использует мьютексы)45 тестов в трёх пакетах:
internal/store(11) — фильтры, пагинация, random с пустым результатом, группировка по artists, taxonomy с пустыми значениями. Тесты дёргаютStoreнапрямую, без HTTP.internal/auth(12) — генерация и детерминированность SHA-256 хеша (включая регрессионный тест на то, что хеш никогда не равен исходному ключу), полный контрактStoreчерезMemoryStore: create→verify, unknown hash →ErrKeyNotFound, revoked key → та же ошибка что unknown (middleware не должен различать эти случаи для клиента), идемпотентностьRevoke,Touchобновляетlast_used_at,Listвключает отозванные ключи.internal/router(22) — HTTP-тесты черезhttptest.Server, поднимающие тот жеrouter.New(...), что иmain.goв проде:/healthzвсегда 200, даже если данные не загружены/readyz200 при загруженных данных, 503 еслиStore.Load()не выполнялся/artworks: фильтрация,limit/offset, невалидныйlimitигнорируется (не схлопывает список до нуля)/artworks/{id}: найдено / 404/artworks/random: регрессионный тест на конфликт маршрутов —randomне попадает вGetByIDкак значение{id}/artists,/taxonomy: группировка, дедупликация, пустые значения исключены из словаря/metrics: кастомные счётчики и стандартные go-метрики из одного реестра- Auth (4): 401 без ключа, 401 с синтаксически верным но не выданным
ключом,
X-API-Keyпринимается как альтернативаAuthorization, инфраструктурные эндпоинты не требуют ключ ни при каких условиях
Ручная проверка через curl/Postman для этих сценариев не обязательна —
весь HTTP-слой, включая auth, проверяется go test ./.... Postman-коллекция
(postman_collection.json в корне репозитория, либо в architecture.md)
остаётся полезной для исследовательского тестирования и smoke-проверки
после деплоя.
| Метод | Путь | Описание |
|---|---|---|
| GET | /healthz |
Всегда 200, если процесс жив |
| GET | /readyz |
200 если artworks.json загружен, иначе 503 |
| GET | /metrics |
Prometheus-формат |
| Метод | Путь | Query-параметры |
|---|---|---|
| GET | /artworks |
style, mood, museum, artist_name, limit, offset |
| GET | /artworks/{id} |
— (UUID в пути) |
| GET | /artworks/random |
style (опционально) |
| GET | /artists |
— |
| GET | /taxonomy |
— |
Реальный artworks.json (выход sheets-export) отличается от
идеализированной схемы из conventions.md. Код написан под то, что
фактически приходит на вход, а не под документ:
- Первичный ключ в JSON —
uuid, неid. API используетuuidвезде (включая путь/artworks/{id}— туда передаётся значениеuuid). tags— это[]stringв JSON, не строка через запятую.yearиdimensionsостаютсяstring— нормализация в числа/структуры отложена до Фазы 5 (Database), как и зафиксировано вroadmap.md.- Пустые
museum/dimensions/notes— нормальное явление;storeи handlers их не валидируют как required, просто не включают вTaxonomy().
/taxonomy строит словарь из фактически встречающихся значений
style/mood/museum/tags в artworks.json, а не из отдельного
экспорта листа taxonomy в Sheets (такого экспорта не существует в
пайплайне). Если в taxonomy.md/Sheets заведено новое значение, ещё не
присвоенное ни одной работе — оно не появится в ответе API, пока хотя бы
одна работа его не получит.
Сознательный компромисс ради простоты — если станет мешать (например,
полный список значений нужен для UI-дропдауна на Фазе 7/Frontend),
естественное решение — добавить экспорт листа taxonomy отдельным
taxonomy.json и переключить store.Taxonomy() читать оттуда.
Go 1.22 http.ServeMux корректно отдаёт приоритет литералу /artworks/random
над wildcard-паттерном /artworks/{id}. Закреплено тестом
TestArtworksRandom_DoesNotMatchAsID. Регистрация в router.go всё равно
держит random отдельной строкой до {id} для читаемости, а не из
необходимости порядка.
art-api/
├── cmd/
│ ├── art-api/main.go # роутинг, запуск сервера, DATABASE_URL
│ └── artkeys/main.go # CLI: create / list / revoke API-ключей
├── internal/
│ ├── auth/ # apikey.go, store.go (Postgres), memory_store.go,
│ │ # middleware.go + тесты
│ ├── handlers/ # artworks.go, artists.go, taxonomy.go, health.go
│ ├── router/ # router.go (сборка mux) + router_test.go
│ ├── metrics/ # prometheus-коллекторы + мидлвара
│ ├── store/ # store.go (artworks.json) + store_test.go
│ └── models/ # artwork.go
├── migrations/ # 0001_create_api_keys.{up,down}.sql
├── data/artworks.json # снимок данных (положить свой перед запуском)
├── Dockerfile # multi-stage, distroless, art-api + artkeys
├── docker-compose.yml # postgres → migrate → art-api; artkeys через --profile tools
├── .env.example
├── postman_collection.json
├── CHANGELOG.md
├── go.mod / go.sum
└── README.md
go.mod:
github.com/prometheus/client_golang v1.19.1— совместима с Go 1.22github.com/jackc/pgx/v5 v5.6.0— без ORM, совместима с Go 1.22 (более новые минорные версииpgx/v5требуют Go 1.25+)
Несколько replace указывают на GitHub-зеркала вместо golang.org/
gopkg.in-путей:
replace golang.org/x/sys => github.com/golang/sys v0.17.0
replace google.golang.org/protobuf => github.com/protocolbuffers/protobuf-go v1.33.0
replace gopkg.in/yaml.v3 => github.com/go-yaml/yaml v3.0.1+incompatible
replace golang.org/x/crypto => github.com/golang/crypto v0.17.0
replace golang.org/x/text => github.com/golang/text v0.14.0
replace gopkg.in/check.v1 => github.com/go-check/check v0.0.0-20180628173108-788fd7840127
replace golang.org/x/sync => github.com/golang/sync v0.3.0
Они были нужны только в среде разработки, где не было прямого доступа к
proxy.golang.org/golang.org/gopkg.in (отдельная сетевая песочница),
поэтому пришлось резолвить модули через их фактические зеркала на GitHub.
Если у тебя есть штатный доступ к Go module proxy — все строки replace
можно удалить и пересобрать go.sum:
# убери все replace-строки из go.mod, затем:
go mod tidyСемантически это не меняет ничего — те же версии тех же модулей, просто получены через стандартный путь, а не через зеркало.
Не реализован в этом релизе. См. architecture.md, раздел "Планы" → Деплой
в кластер: k3s + ArgoCD, тот же паттерн что у остальной инфраструктуры
(s3.week-book.ru), публичный ingress через ingress-nginx + cert-manager.
Порядок принципиален: auth (этот релиз) должен быть в проде раньше, чем
появится публичный ingress — см. roadmap.md, "не открывать API публично
без auth — даже read-only".