Document liberta convert service
This commit is contained in:
@@ -0,0 +1,206 @@
|
|||||||
|
# liberta — HTTP-сервис конвертации документов Word
|
||||||
|
|
||||||
|
Сервис принимает устаревшие документы Microsoft Word (`.doc`, а также `.rtf` и `.odt`)
|
||||||
|
и возвращает современный `.docx`. Конвертация выполняется **LibreOffice в headless-режиме**
|
||||||
|
внутри контейнера; исходный файл на диске не сохраняется.
|
||||||
|
|
||||||
|
- **Публичный адрес (текущий стенд):** `https://liberta.containerk8s.dev.nubes.ru/`
|
||||||
|
- **Имя образа:** `gitea.services.ngcloud.ru/nail/contractor-convert:latest`
|
||||||
|
- **Репозиторий:** `https://gitea.services.ngcloud.ru/forcloud/liberta.git`
|
||||||
|
- **Версия приложения:** `1.0.0` (константа `VERSION` в `app.py`)
|
||||||
|
|
||||||
|
## Содержание
|
||||||
|
|
||||||
|
- [Состав репозитория](#состав-репозитория)
|
||||||
|
- [Как работает](#как-работает)
|
||||||
|
- [API](#api)
|
||||||
|
- [Запуск локально](#запуск-локально)
|
||||||
|
- [Сборка и запуск в Docker](#сборка-и-запуск-в-docker)
|
||||||
|
- [Развёртывание на платформе (Штурвал)](#развёртывание-на-платформе-штурвал)
|
||||||
|
- [Использование из Obfuscator / DrHider](#использование-из-obfuscator--drhider)
|
||||||
|
- [Ограничения и известные особенности](#ограничения-и-известные-особенности)
|
||||||
|
- [Проверка работоспособности](#проверка-работоспособности)
|
||||||
|
|
||||||
|
## Состав репозитория
|
||||||
|
|
||||||
|
```text
|
||||||
|
.
|
||||||
|
├── app.py Flask-приложение: UI, /convert, /health, /static/logo.svg
|
||||||
|
├── Dockerfile python:3.12 + libreoffice, EXPOSE 5000, CMD python3 app.py
|
||||||
|
└── requirements.txt flask
|
||||||
|
```
|
||||||
|
|
||||||
|
Никаких других зависимостей, конфигов, БД или внешних сервисов нет.
|
||||||
|
|
||||||
|
## Как работает
|
||||||
|
|
||||||
|
1. Клиент отправляет `POST /convert` с файлом в поле `file` (`multipart/form-data`).
|
||||||
|
2. Сервер сохраняет входной файл во временный файл с суффиксом `.doc`.
|
||||||
|
3. Вызывается LibreOffice:
|
||||||
|
|
||||||
|
```text
|
||||||
|
libreoffice --headless --convert-to docx --outdir <tmpdir> <input>
|
||||||
|
```
|
||||||
|
|
||||||
|
Таймаут вызова — **120 секунд**. `check=True`: ненулевой код выхода считается ошибкой.
|
||||||
|
4. Из временного каталога берётся первый `.docx`; если его нет — ошибка `conversion produced no .docx`.
|
||||||
|
5. Файл отдаётся клиенту как вложение (`Content-Disposition: attachment`) с MIME
|
||||||
|
`application/vnd.openxmlformats-officedocument.wordprocessingml.document`.
|
||||||
|
6. В `finally` удаляются входной файл, временный каталог и всё его содержимое —
|
||||||
|
**документы не сохраняются на сервере**.
|
||||||
|
|
||||||
|
Дополнительно на `GET /` отдаётся встроенная HTML-страница с формой конвертации
|
||||||
|
(тот же `/convert` вызывается из браузера через `fetch`).
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
### `POST /convert`
|
||||||
|
|
||||||
|
Конвертирует один файл в `.docx`.
|
||||||
|
|
||||||
|
| Параметр | Значение |
|
||||||
|
| --- | --- |
|
||||||
|
| Метод | `POST` |
|
||||||
|
| Content-Type | `multipart/form-data` |
|
||||||
|
| Поле файла | `file` |
|
||||||
|
| Поддерживаемые входы | `.doc`, `.rtf`, `.odt` (фактически — всё, что умеет LibreOffice) |
|
||||||
|
| Успех | `200`, тело — бинарный `.docx`, `Content-Disposition: attachment` |
|
||||||
|
| Ошибка | `400`/`500`, JSON `{"ok": false, "error": "..."}` |
|
||||||
|
|
||||||
|
Коды ошибок:
|
||||||
|
|
||||||
|
| Ситуация | Статус | Тело |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Нет поля `file` | `400` | `{"ok": false, "error": "no file"}` |
|
||||||
|
| Пустое имя файла | `400` | `{"ok": false, "error": "empty filename"}` |
|
||||||
|
| Превышен таймаут LibreOffice (120 с) | `500` | `{"ok": false, "error": "conversion timeout"}` |
|
||||||
|
| LibreOffice завершился с ошибкой | `500` | `{"ok": false, "error": "libreoffice: <stderr>"}` |
|
||||||
|
| `.docx` не создан | `500` | `{"ok": false, "error": "conversion produced no .docx"}` |
|
||||||
|
|
||||||
|
Пример:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST -F "file=@документ.doc" \
|
||||||
|
https://liberta.containerk8s.dev.nubes.ru/convert \
|
||||||
|
-o результат.docx
|
||||||
|
```
|
||||||
|
|
||||||
|
### `GET /health`
|
||||||
|
|
||||||
|
Проверка живости и версии:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"ok": true, "version": "1.0.0"}
|
||||||
|
```
|
||||||
|
|
||||||
|
Используется платформой и внешними клиентами для liveness/readiness-проверок.
|
||||||
|
|
||||||
|
### `GET /`
|
||||||
|
|
||||||
|
HTML-интерфейс: логотип, форма загрузки `.doc/.rtf/.odt`, кнопка «Конвертировать»,
|
||||||
|
краткая справка по API и пример `curl`.
|
||||||
|
|
||||||
|
### `GET /static/logo.svg`
|
||||||
|
|
||||||
|
Отдаёт встроенный SVG-логотип (data URI в коде страницы, отдельный файл не требуется).
|
||||||
|
|
||||||
|
## Запуск локально
|
||||||
|
|
||||||
|
Требования:
|
||||||
|
|
||||||
|
- Python 3.12+ и Flask (`pip install -r requirements.txt`);
|
||||||
|
- установленный **LibreOffice** в `PATH` (без него конвертация завершится ошибкой).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd liberta
|
||||||
|
python3 -m venv .venv && source .venv/bin/activate
|
||||||
|
pip install -r requirements.txt
|
||||||
|
python3 app.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Приложение слушает `0.0.0.0:5000`.
|
||||||
|
|
||||||
|
## Сборка и запуск в Docker
|
||||||
|
|
||||||
|
`Dockerfile` ставит LibreOffice и Flask; других слоёв нет.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build -t contractor-convert:latest .
|
||||||
|
docker run --rm -p 5000:5000 contractor-convert:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
Проверка:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -s http://localhost:5000/health
|
||||||
|
# {"ok":true,"version":"1.0.0"}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Развёртывание на платформе (Штурвал)
|
||||||
|
|
||||||
|
Текущий стенд: имя экземпляра `liberta`, реалм `iot-naeel`, домен `liberta`,
|
||||||
|
образ `gitea.services.ngcloud.ru/nail/contractor-convert:latest`.
|
||||||
|
|
||||||
|
Параметры, которые нужно учесть при переносе на другой стенд:
|
||||||
|
|
||||||
|
1. **Порт жёстко задан `5000`** (`app.run(host="0.0.0.0", port=5000)`).
|
||||||
|
Переменная окружения `PORT` приложением **не читается** — стенд должен
|
||||||
|
использовать/пробросить порт `5000` либо потребуется правка кода.
|
||||||
|
2. **Запускать только как скрипт** — `python3 app.py` (так задано в `CMD`).
|
||||||
|
`io` импортируется внутри блока `if __name__ == "__main__"`, а `/convert`
|
||||||
|
использует `io.BytesIO`. При запуске через `gunicorn`/`flask run` (импорт модуля)
|
||||||
|
`/convert` упадёт с `NameError`.
|
||||||
|
3. **Ресурсы.** LibreOffice требователен к CPU и памяти: в текущем стенде
|
||||||
|
`CPU=500m`, `memory=1024 MiB`. Меньшие лимиты или параллельные запросы
|
||||||
|
приводят к таймаутам/падениям. Запросы к сервису следует выполнять
|
||||||
|
последовательно (Obfuscator так и делает).
|
||||||
|
4. **`/tmp`** должен быть доступен для записи и иметь достаточный размер
|
||||||
|
(входной `.doc`, временный каталог, `.docx`).
|
||||||
|
5. **Сеть/DNS/TLS** — домен, сертификат и ingress настраиваются в стенде,
|
||||||
|
в образе их нет.
|
||||||
|
6. **Тег `latest` мутабелен** — для воспроизводимости фиксируйте digest
|
||||||
|
или версионированный тег.
|
||||||
|
7. **Healthcheck** в `Dockerfile` отсутствует, но есть `GET /health` —
|
||||||
|
probe настраивается на стенде.
|
||||||
|
|
||||||
|
## Использование из Obfuscator / DrHider
|
||||||
|
|
||||||
|
`drhider/extractor.py::doc_to_markdown()` отправляет `.doc` на этот сервис:
|
||||||
|
|
||||||
|
- URL берётся из переменной окружения `CONVERT_SERVICE_URL`
|
||||||
|
(дефолт в коде — `https://liberta.containerk8s.dev.nubes.ru/convert`);
|
||||||
|
- запрос: `POST`, `multipart/form-data`, поле `file`, имя файла `input.doc`;
|
||||||
|
- ожидается ответ `200` с телом `.docx`; далее документ обрабатывается штатным
|
||||||
|
`docx_to_markdown()`;
|
||||||
|
- вызовы выполняются **последовательно**: сервис не рассчитан на конкуренцию.
|
||||||
|
|
||||||
|
⚠️ Дефолт `CONVERT_SERVICE_URL` указывает на dev-домен
|
||||||
|
(`containerk8s.dev.nubes.ru`). В продакшене переменную нужно задать явно,
|
||||||
|
иначе прод обратится к dev-сервису.
|
||||||
|
|
||||||
|
## Ограничения и известные особенности
|
||||||
|
|
||||||
|
- **Нет аутентификации и rate-limit** — любой, кто знает URL, может конвертировать
|
||||||
|
файлы и нагружать CPU. Для публичного использования нужен внешний контроль
|
||||||
|
(ingress-auth / сетевые ограничения).
|
||||||
|
- **Нет CORS-заголовков** — вызовы из браузера с другого origin заблокируются
|
||||||
|
(Obfuscator вызывает сервис с бэкенда, CORS не нужен).
|
||||||
|
- **Один файл за запрос** — пакетная конвертация не поддерживается.
|
||||||
|
- **Нет кэширования** — каждый запрос запускает новый процесс LibreOffice.
|
||||||
|
- **Временные файлы** создаются в системном `/tmp`; при аварийном завершении
|
||||||
|
процесса теоретически могут остаться (штатный путь удаляет их в `finally`).
|
||||||
|
- **Версия в UI/health** берётся из константы `VERSION` в `app.py` — при изменении
|
||||||
|
кода поднимайте её.
|
||||||
|
|
||||||
|
## Проверка работоспособности
|
||||||
|
|
||||||
|
Проверено 2026-10-23 на `https://liberta.containerk8s.dev.nubes.ru`:
|
||||||
|
|
||||||
|
| Запрос | Ответ |
|
||||||
|
| --- | --- |
|
||||||
|
| `GET /` | `200`, HTML «Конвертер документов — Nubes» |
|
||||||
|
| `OPTIONS /convert` | `200`, `Allow: POST, OPTIONS` |
|
||||||
|
| `GET /health` | `200`, `{"ok":true,"version":"1.0.0"}` |
|
||||||
|
|
||||||
|
Версия `1.0.0` совпадает с константой в `app.py`, что подтверждает соответствие
|
||||||
|
развёрнутого сервиса данному репозиторию.
|
||||||
Reference in New Issue
Block a user