# 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 ``` Таймаут вызова — **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: "}` | | `.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`, что подтверждает соответствие развёрнутого сервиса данному репозиторию.