initial commit
Build and Push Docker Images / build-and-push (push) Failing after 3m33s

This commit is contained in:
admin committed 2026-11-07 16:39:43 +03:00
commit 46af290db3
100 files changed
+16663

No files matched your search

+239
View File
@@ -0,0 +1,239 @@
"""
Сервис экспорта документов в различные форматы (EPUB, FB2, PDF, DOCX) через Pandoc.
"""
import os
import shutil
import subprocess
import tempfile
from typing import List
from fastapi import HTTPException
from models.document import DocumentNode
from services.document_service import document_service
from services.project_service import project_service
class ExportService:
def __init__(self):
# Проверка наличия pandoc при инициализации
try:
subprocess.run(["pandoc", "--version"], capture_output=True, check=True)
self.available = True
except FileNotFoundError:
print("WARNING: Pandoc not found. Export functionality will be limited.")
self.available = False
def _get_document_and_children(
self, documents: List[DocumentNode], target_id: str
) -> List[DocumentNode]:
"""Рекурсивно получает документ и всех его потомков, отсортированных по order."""
result: List[DocumentNode] = []
doc_map = {d.id: d for d in documents}
def collect_children(parent_id: str):
children = [d for d in documents if d.parent_id == parent_id]
children.sort(key=lambda x: x.order)
for child in children:
result.append(child)
collect_children(child.id)
if target_id in doc_map:
result.append(doc_map[target_id])
collect_children(target_id)
return result
def _build_book_html(
self,
project_id: str,
user_id: str,
document_id: str | None = None,
base_path: str = "", # Абсолютный путь для относительных ресурсов
) -> str:
"""Собирает HTML-контент книги или конкретного документа с изображениями."""
documents = document_service.get_all_documents(project_id, user_id)
project_dir = project_service.get_project_dir(project_id, user_id)
images_dir = project_dir / "images"
# Проверяем наличие обложки
cover_path = images_dir / "cover.png"
has_cover = cover_path.exists()
if document_id:
target_docs = self._get_document_and_children(documents, document_id)
else:
# Экспортируем всё: берем корневые документы (книги) и всех потомков
tree = document_service.get_documents_tree(project_id, user_id)
target_docs = self._flatten_tree(tree)
# Собираем все PNG-файлы изображений
all_images = []
if images_dir.exists():
all_images = [
f.name
for f in images_dir.iterdir()
if f.is_file() and f.suffix.lower() == ".png"
]
# Определяем ID всех глав для поиска привязанных изображений
chapter_ids = {d.id for d in documents if d.type == "chapter"}
# "Бесхозные" изображения — не cover.png и не {chapter_id}.png
# (старые файлы с UUID, сгенерированные до введения конвенции)
orphan_images = [
img
for img in all_images
if img != "cover.png" and img.removesuffix(".png") not in chapter_ids
]
orphan_index = (
0 # Индекс для последовательного использования orphan-изображений
)
# Формируем префикс пути: абсолютный путь если задан, иначе относительный
img_prefix = f"{base_path}/images" if base_path else "images"
html_parts: List[str] = []
# Добавляем обложку, если есть и это полная книга (не один документ)
if has_cover and not document_id:
html_parts.append(
'<div style="text-align: center; page-break-after: always;">'
)
html_parts.append(
f'<img src="{img_prefix}/cover.png" alt="Cover" style="max-width: 100%; height: auto;" />'
)
html_parts.append("</div>")
html_parts.append("<br/><br/>")
for doc in target_docs:
if doc.type == "book":
html_parts.append(f"<h1>{doc.title}</h1>")
elif doc.type == "chapter":
# Ищем изображение главы: сначала по конвенции {chapter_id}.png
chapter_image_path = images_dir / f"{doc.id}.png"
has_chapter_image = chapter_image_path.exists()
chapter_image_filename = f"{doc.id}.png" if has_chapter_image else None
# Fallback: если нет файла по конвенции, берём "бесхозное" изображение
if not has_chapter_image and orphan_index < len(orphan_images):
chapter_image_filename = orphan_images[orphan_index]
orphan_index += 1
html_parts.append(f"<h2>{doc.title}</h2>")
# Вставляем изображение главы, если есть
if chapter_image_filename:
html_parts.append(
'<div style="text-align: center; margin: 20px 0;">'
)
html_parts.append(
f'<img src="{img_prefix}/{chapter_image_filename}" alt="{doc.title}" style="max-width: 100%; height: auto;" />'
)
html_parts.append("</div>")
elif doc.type == "scene" and doc.content:
html_parts.append(f"<h3>{doc.title}</h3>")
html_parts.append(doc.content)
html_parts.append("<br/><br/>")
return "\n".join(html_parts)
def _flatten_tree(self, tree: List[dict]) -> List[DocumentNode]:
"""Разворачивает дерево документов в плоский список в порядке обхода."""
result: List[DocumentNode] = []
for node in tree:
result.append(DocumentNode(**node))
for child in node.get("children", []):
result.extend(self._flatten_tree([child]))
return result
def export_document(
self, project_id: str, user_id: str, document_id: str | None, format: str
) -> bytes:
"""Экспортирует документ (или всю книгу) в указанный формат."""
if not self.available:
raise HTTPException(
status_code=503,
detail="Pandoc is not installed. Export functionality is unavailable.",
)
valid_formats = ["epub", "fb2", "pdf", "docx"]
if format not in valid_formats:
raise HTTPException(
status_code=400,
detail=f"Unsupported format. Choose from {valid_formats}",
)
# Получаем проект для метаданных
project = project_service.get_project(project_id)
title = project.title if project else "Без названия"
# Собираем HTML
html_content = self._build_book_html(project_id, user_id, document_id)
# Получаем путь к папке проекта для копирования изображений
project_dir = project_service.get_project_dir(project_id, user_id)
images_dir = project_dir / "images"
# Создаём временную директорию
temp_dir = tempfile.mkdtemp()
temp_html = os.path.join(temp_dir, "content.html")
output_path = os.path.join(temp_dir, f"output.{format}")
# Копируем папку images во временную директорию для Pandoc
if images_dir.exists():
shutil.copytree(images_dir, os.path.join(temp_dir, "images"))
try:
# Записываем HTML
html_content = self._build_book_html(
project_id, user_id, document_id, base_path=temp_dir
)
with open(temp_html, "w", encoding="utf-8") as f:
f.write(html_content)
# Формируем команду pandoc
cmd = [
"pandoc",
temp_html,
"-o",
output_path,
"--from",
"html",
"--to",
format,
]
# Метаданные для EPUB/FB2
if format in ["epub", "fb2"]:
cmd.extend(["--metadata", f"title={title}"])
cmd.extend(["--metadata", "author=Author"])
# Указываем путь к ресурсам (изображениям)
cmd.extend(["--resource-path", temp_dir])
# PDF через WeasyPrint (не требует LaTeX)
if format == "pdf":
cmd.extend(["--pdf-engine=weasyprint"])
# Выполняем
subprocess.run(cmd, check=True, capture_output=True, text=True)
# Читаем результат
with open(output_path, "rb") as f:
file_content = f.read()
return file_content
except subprocess.CalledProcessError as e:
raise HTTPException(status_code=500, detail=f"Pandoc error: {e.stderr}")
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
finally:
# Очистка временных файлов
if os.path.exists(temp_dir):
shutil.rmtree(temp_dir)
# Singleton instance
export_service = ExportService()