- Go 93.9%
- Python 3%
- Shell 2.6%
- Dockerfile 0.5%
|
|
||
|---|---|---|
| cmd/conference-convert | ||
| docs | ||
| internal | ||
| scripts | ||
| testdata | ||
| .dockerignore | ||
| .gitignore | ||
| .woodpecker.yml | ||
| docker-compose.yml | ||
| Dockerfile | ||
| Dockerfile.ci | ||
| FontSubstTable.xcu | ||
| go.mod | ||
| go.sum | ||
| install-deps.sh | ||
| LICENSE | ||
| README.md | ||
Converter Service
Микросервис конвертации презентаций в набор изображений слайдов (PNG/WebP), упакованных в ZIP-архив, с генерацией PDF. Асинхронная обработка с webhook-уведомлениями.
Технологии: Go 1.26 (net/http, archive/zip, channels), LibreOffice Headless, Poppler-utils (pdftoppm), pdfcpu, gowebp (pure Go WebP), Prometheus.
Входные и выходные форматы
| Вход | → | origin | zip | |
|---|---|---|---|---|
| загруженный PDF | тот же PDF | pdftoppm → PNG/WebP → ZIP | ||
| PPT / PPTX / PPS / PPSX / PPTM / POTX / POT / ODP / FODP / SXI / KEY | копия оригинала | LibreOffice → PDF | LO → PDF → pdftoppm → PNG/WebP → ZIP | |
| ZIP (с metadata.json + слайдами) | загруженный ZIP | pdfcpu → PDF из слайдов | тот же ZIP |
Возможности
- 12 входных форматов: PPT, PPTX, PPS, PPSX, PPTM, POTX, POT, ODP, FODP, SXI, KEY, PDF + готовый ZIP
- Конвертация в PDF через LibreOffice Headless или pdfcpu (для ZIP со слайдами)
- Извлечение слайдов в PNG (96 DPI, масштабирование до 1920px)
- Сжатие в WebP (pure Go, без CGO/libwebp)
- Автоматическая транслитерация имён файлов (кириллица → ASCII) через go-unidecode
- ZIP-архив с metadata.json и слайдами
- Webhook-уведомления с HMAC-SHA256 подписью
- In-memory очередь с Worker Pool (ограничение параллелизма)
- Rate limiting per-IP (скользящее окно)
- Prometheus метрики (
/api/metrics) - Graceful shutdown + Cleanup по TTL
- Docker (Ubuntu 26.04, LibreOffice 25.2.3, Poppler-utils 25.03.0)
Конфигурация
Все параметры задаются через переменные окружения или флаги командной строки.
| Переменная | Флаг | Тип | По умолчанию | Описание |
|---|---|---|---|---|
PORT |
--port |
int | 8080 |
Порт HTTP-сервера |
API_KEY |
--api-key |
string | required | API-ключ аутентификации |
STORAGE_PATH |
--storage-path |
string | /data/converted |
Директория готовых файлов |
UPLOAD_PATH |
--upload-path |
string | /data/uploads |
Временная директория загрузок |
MAX_CONCURRENT_TASKS |
--max-tasks |
int | 3 |
Макс. параллельных задач |
MAX_FILE_SIZE |
--max-file-size |
int | 524288000 |
Макс. размер файла (байт) |
TASK_TIMEOUT_SLIDE |
--timeout-slide |
int | 60 |
Таймаут на слайд (сек) |
LIBREOFFICE_TIMEOUT |
--timeout-libre |
int | 300 |
Таймаут LibreOffice (сек) |
OUTPUT_WIDTH |
--output-width |
int | 1920 |
Ширина слайда (px, 0=исходная) |
OUTPUT_HEIGHT |
--output-height |
int | 0 |
Высота (0 = пропорционально ширине) |
WEBP_QUALITY |
--webp-quality |
int | 90 |
Качество WebP (1–100) |
WEBHOOK_MAX_RETRIES |
--webhook-retries |
int | 10 |
Попыток webhook-доставки |
WEBHOOK_BACKOFF_BASE |
--webhook-backoff |
int | 1 |
Начальная задержка retry (сек) |
CALLBACK_URL |
--callback-url |
string | "" |
URL webhook по умолчанию |
LOG_LEVEL |
--log-level |
string | off |
Уровень логов: off (нет логов, по умолч.), debug, info, warn, error |
CLEANUP_TTL |
--cleanup-ttl |
int | 24 |
Время жизни архива (часы, 0=откл) |
CLEANUP_INTERVAL |
--cleanup-interval |
int | 60 |
Интервал проверки cleanup (мин) |
RATE_LIMIT_REQUESTS |
--rate-limit-reqs |
int | 100 |
Запросов в окно rate limit |
RATE_LIMIT_WINDOW |
--rate-limit-window |
int | 60 |
Окно rate limit (сек) |
CONVERTER_RETRIES |
--converter-retries |
int | 3 |
Retry LibreOffice/pdftoppm (0=без retry) |
Быстрый старт
# Сборка бинарника
CGO_ENABLED=0 go build -o converter-service ./cmd/conference-convert/
# Docker
docker build -t converter-service:latest .
docker run -p 8081:8080 \
-e API_KEY=my-secret-key \
converter-service:latest
# Проверка
curl -s http://localhost:8081/api/health
# {"status":"ok"}
API (кратко)
| Метод | Путь | Описание |
|---|---|---|
| POST | /api/convert |
Создать задачу (multipart, 202 Accepted) |
| GET | /api/result/{task_id} |
Статус задачи |
| GET | /api/result/{task_id}/origin |
Скачать оригинальный файл |
| GET | /api/result/{task_id}/pdf |
Скачать PDF |
| GET | /api/result/{task_id}/zip |
Скачать ZIP со слайдами |
| DELETE | /api/result/{task_id} |
Удалить все файлы задачи |
| POST | /api/callback/retry/{task_id} |
Повторная отправка webhook |
| GET | /api/health |
Health check |
| GET | /api/readyz |
Readiness probe |
| GET | /api/metrics |
Prometheus метрики |
Полная спецификация: docs/api.md
Протокол интеграции
Жизненный цикл задачи
1. POST /api/convert (multipart: file + talk_id)
↓
2. 202 Accepted {task_id: "conv_xxx", status: "processing"}
↓
3. Асинхронная конвертация (секунды–минуты)
↓
4. Один из двух вариантов:
A) Polling B) Webhook
──────────── ────────────
GET /api/result/{task_id} POST <callback_url>
до status = "completed" {event, task_id, status, ...}
или "failed"
Поток данных
sequenceDiagram
participant Client as Внешний сервис
participant API as Converter API
participant Store as Хранилище
Client->>API: POST /api/convert (file + talk_id)
API-->>Client: 202 {task_id, status: "processing"}
Note over API: Асинхронная обработка...
Client->>API: GET /api/result/{task_id}
API-->>Client: 200 {status: "processing"}
Note over API: ...процесс завершён...
Client->>API: GET /api/result/{task_id}
API-->>Client: 200 {status: "completed", total_slides, ...}
Client->>API: GET /api/result/{task_id}/origin
API-->>Client: 200 original file
Client->>API: GET /api/result/{task_id}/pdf
API-->>Client: 200 application/pdf
Client->>API: GET /api/result/{task_id}/zip
API-->>Client: 200 application/zip
Client->>API: DELETE /api/result/{task_id}
API-->>Client: 200 {status: "deleted"}
Стратегии получения результата
A. Polling — клиент периодически опрашивает статус:
# Создать задачу
curl -s -X POST http://localhost:8080/api/convert \
-H "Authorization: Bearer $API_KEY" \
-F "file=@presentation.pptx" -F "talk_id=42"
# {"task_id":"conv_abc123","status":"processing"}
# Опрашивать, пока status не станет completed/failed
curl -s http://localhost:8080/api/result/conv_abc123 \
-H "Authorization: Bearer $API_KEY"
# {"status":"completed","total_slides":10,"format":"webp",...}
Рекомендуемый интервал: 1–2 секунды.
B. Webhook — сервис сам уведомит клиента:
curl -s -X POST http://localhost:8080/api/convert \
-H "Authorization: Bearer $API_KEY" \
-F "file=@presentation.pptx" -F "talk_id=42" \
-F "callback_url=https://my-service.example/webhook"
После завершения конвертации Converter Service отправляет POST на callback_url:
{
"event": "conversion.completed",
"task_id": "conv_abc123",
"talk_id": 42,
"status": "completed",
"result": {
"origin_url": "/api/result/conv_abc123/origin",
"pdf_url": "/api/result/conv_abc123/pdf",
"zip_url": "/api/result/conv_abc123/zip",
"format": "webp",
"total_slides": 10,
"size_bytes": 1048576
}
}
При ошибке:
{
"event": "conversion.failed",
"task_id": "conv_abc123",
"talk_id": 42,
"status": "failed",
"error": {
"code": "LIBREOFFICE_TIMEOUT",
"message": "conversion to PDF failed",
"details": "LIBREOFFICE_TIMEOUT: libreoffice timed out after 300s"
}
}
Контракт webhook
| Параметр | Значение |
|---|---|
| HTTP метод | POST |
| Content-Type | application/json |
| Таймаут на ответ | 10 секунд |
| Retry | до WEBHOOK_MAX_RETRIES раз, экспоненциальная задержка |
| Подпись | X-Webhook-Signature: sha256=<hex> (HMAC-SHA256, если указан callback_secret) |
| Код успеха | 2xx |
| Код неудачи | не-2xx → retry |
| Исчерпание retry | статус задачи → failed_to_notify |
Сервис не переотправляет webhook автоматически после исчерпания retry. Для повторной отправки используйте POST /api/callback/retry/{task_id}.
Коды ошибок конвертации
| Код | Причина | Что делать |
|---|---|---|
INVALID_FILE |
Файл повреждён | Проверить/перезалить файл |
UNSUPPORTED_FORMAT |
Формат не из списка | Проверить расширение |
LIBREOFFICE_TIMEOUT |
LibreOffice не уложился в таймаут | Увеличить LIBREOFFICE_TIMEOUT |
LIBREOFFICE_ERROR |
LibreOffice упал | Проверить файл, логи |
PDFTOPP_TIMEOUT |
pdftoppm на одном слайде завис | Увеличить TASK_TIMEOUT_SLIDE |
PDFTOPP_ERROR |
pdftoppm не смог обработать PDF | Проверить PDF |
PDF_PASSWORD_REQUIRED |
PDF защищён паролем | Снять пароль |
EMPTY_PRESENTATION |
0 слайдов после конвертации | Проверить файл |
CWEBP_ERROR |
Ошибка кодирования WebP | Редкая ошибка, retry |
STORAGE_ERROR |
Ошибка записи на диск | Проверить STORAGE_PATH |
INVALID_ZIP |
ZIP не соответствует формату | Проверить структуру архива |
Тайм-ауты и TTL
| Тайм-аут | Где задаётся | Дефолт | Описание |
|---|---|---|---|
| На один слайд | TASK_TIMEOUT_SLIDE |
60 с | pdftoppm на извлечение одного слайда |
| LibreOffice | LIBREOFFICE_TIMEOUT |
300 с | Конвертация файла в PDF |
| Webhook | жёстко | 10 с | Ожидание ответа от callback_url |
| Retry pdftoppm/LO | CONVERTER_RETRIES |
3 | Число попыток (с задержкой) |
| Retry webhook | WEBHOOK_MAX_RETRIES |
10 | Число попыток доставки |
| TTL архива | CLEANUP_TTL |
24 ч | Через CLEANUP_TTL часов файлы удаляются |
Ограничения
- Максимальный размер файла:
MAX_FILE_SIZE(дефолт 500 MB) - Параллельные задачи:
MAX_CONCURRENT_TASKS(дефолт 3), остальные ждут в очереди - Rate limit на HTTP API:
RATE_LIMIT_REQUESTSзапросов заRATE_LIMIT_WINDOWсекунд на IP - Все данные хранятся локально, внешнее S3/блочное хранилище не используется
- Нет переотправки webhook после исчерпания retry — используйте
POST /api/callback/retry/{task_id}
Зависимости
- Go 1.26.4 — основная логика, ZIP, HTTP, JSON
- LibreOffice 25.2.3 — конвертация PPT/ODP/KEY → PDF (установлен в Docker)
- Poppler-utils 25.03.0 —
pdftoppm: PDF → PNG - pdfcpu — генерация PDF из изображений (pure Go)
- gowebp — сжатие PNG → WebP (pure Go, без CGO)
Мониторинг
| Метрика | Тип | Описание |
|---|---|---|
conversion_tasks_total |
Counter | Всего задач (теги: status) |
conversion_duration_seconds |
Histogram | Длительность конвертации |
conversion_tasks_active |
Gauge | Текущие активные задачи |
webhook_delivery_attempts_total |
Counter | Попытки отправки webhook |
Архитектура
HTTP API → in-memory Queue → Worker Pool → Pipeline
├─ LibreOffice (презентация → PDF)
├─ pdftoppm (PDF → PNG)
├─ gowebp (PNG → WebP)
├─ pdfcpu (слайды → PDF)
├─ NotesExtractor (аннотации)
└─ ZIP (слайды + metadata.json)
→ Локальное хранилище
→ Webhook-уведомление
Подробнее: docs/architecture.md
Права доступа
/api/health,/api/readyz,/api/metrics— без аутентификации- Все остальные эндпоинты —
Authorization: Bearer <API_KEY>