Files
liberta/README.md
T

11 KiB
Raw Blame History

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)

Содержание

Состав репозитория

.
├── 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:

    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"}

Пример:

curl -X POST -F "file=@документ.doc" \
  https://liberta.containerk8s.dev.nubes.ru/convert \
  -o результат.docx

GET /health

Проверка живости и версии:

{"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 (без него конвертация завершится ошибкой).
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; других слоёв нет.

docker build -t contractor-convert:latest .
docker run --rm -p 5000:5000 contractor-convert:latest

Проверка:

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, что подтверждает соответствие развёрнутого сервиса данному репозиторию.