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
- Запуск локально
- Сборка и запуск в Docker
- Развёртывание на платформе (Штурвал)
- Использование из Obfuscator / DrHider
- Ограничения и известные особенности
- Проверка работоспособности
Состав репозитория
.
├── app.py Flask-приложение: UI, /convert, /health, /static/logo.svg
├── Dockerfile python:3.12 + libreoffice, EXPOSE 5000, CMD python3 app.py
└── requirements.txt flask
Никаких других зависимостей, конфигов, БД или внешних сервисов нет.
Как работает
-
Клиент отправляет
POST /convertс файлом в полеfile(multipart/form-data). -
Сервер сохраняет входной файл во временный файл с суффиксом
.doc. -
Вызывается LibreOffice:
libreoffice --headless --convert-to docx --outdir <tmpdir> <input>Таймаут вызова — 120 секунд.
check=True: ненулевой код выхода считается ошибкой. -
Из временного каталога берётся первый
.docx; если его нет — ошибкаconversion produced no .docx. -
Файл отдаётся клиенту как вложение (
Content-Disposition: attachment) с MIMEapplication/vnd.openxmlformats-officedocument.wordprocessingml.document. -
В
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.
Параметры, которые нужно учесть при переносе на другой стенд:
- Порт жёстко задан
5000(app.run(host="0.0.0.0", port=5000)). Переменная окруженияPORTприложением не читается — стенд должен использовать/пробросить порт5000либо потребуется правка кода. - Запускать только как скрипт —
python3 app.py(так задано вCMD).ioимпортируется внутри блокаif __name__ == "__main__", а/convertиспользуетio.BytesIO. При запуске черезgunicorn/flask run(импорт модуля)/convertупадёт сNameError. - Ресурсы. LibreOffice требователен к CPU и памяти: в текущем стенде
CPU=500m,memory=1024 MiB. Меньшие лимиты или параллельные запросы приводят к таймаутам/падениям. Запросы к сервису следует выполнять последовательно (Obfuscator так и делает). /tmpдолжен быть доступен для записи и иметь достаточный размер (входной.doc, временный каталог,.docx).- Сеть/DNS/TLS — домен, сертификат и ingress настраиваются в стенде, в образе их нет.
- Тег
latestмутабелен — для воспроизводимости фиксируйте digest или версионированный тег. - 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, что подтверждает соответствие
развёрнутого сервиса данному репозиторию.