Files
liberta/README.md
T

207 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`, что подтверждает соответствие
развёрнутого сервиса данному репозиторию.