221 lines
7.3 KiB
Python
221 lines
7.3 KiB
Python
import os
|
|
import subprocess
|
|
from pathlib import Path
|
|
import uuid
|
|
|
|
from fastapi import HTTPException, status
|
|
|
|
from actions.data.text import Text
|
|
from schemas.process_document_schema import ProcessDocumentSchema
|
|
|
|
|
|
# Caminho absoluto do executável do LibreOffice no Windows.
|
|
# Usar caminho fixo evita problemas de PATH em serviços, venv e produção.
|
|
SOFFICE_PATH = r"C:\Program Files\LibreOffice\program\soffice.exe"
|
|
|
|
|
|
class ProcessDocumentAction:
|
|
"""
|
|
Action responsável por processar documentos editáveis.
|
|
|
|
REGRAS IMPORTANTES:
|
|
- TODO texto recebido vem COMPRIMIDO (RTF ou DOCX)
|
|
- A descompressão acontece SEMPRE
|
|
- Após descompressão:
|
|
- DOCX → salvo diretamente
|
|
- RTF → convertido para DOCX via LibreOffice
|
|
|
|
A saída final é SEMPRE um arquivo DOCX em disco.
|
|
"""
|
|
|
|
# ======================================================
|
|
# MÉTODO PÚBLICO (ENTRYPOINT DA ACTION)
|
|
# ======================================================
|
|
def execute(self, data: ProcessDocumentSchema) -> str:
|
|
"""
|
|
Orquestra todo o fluxo de processamento do documento.
|
|
"""
|
|
|
|
# 1) Prepara diretório e caminhos únicos
|
|
diretorio = self._prepare_directory()
|
|
rtf_path, docx_path = self._generate_paths(diretorio, data.id)
|
|
|
|
# 2) Descompacta obrigatoriamente o conteúdo
|
|
conteudo = self._decompress_content(data.texto)
|
|
|
|
# Remove apenas espaços iniciais para detecção segura
|
|
conteudo_strip = conteudo.lstrip()
|
|
|
|
# 3) Se após descompressão for DOCX, salva direto
|
|
if self._is_docx(conteudo_strip):
|
|
self._save_docx(docx_path, conteudo)
|
|
return docx_path.name
|
|
|
|
# 4) Caso contrário, valida se é RTF
|
|
self._validate_rtf(conteudo_strip)
|
|
|
|
# 5) Normaliza e salva RTF (simples ou complexo)
|
|
texto_rtf = conteudo_strip.strip()
|
|
self._save_rtf(rtf_path, texto_rtf)
|
|
|
|
# 6) Converte RTF → DOCX via LibreOffice
|
|
self._convert_rtf_to_docx(diretorio, rtf_path)
|
|
|
|
# 7) Valida resultado final
|
|
self._validate_docx(docx_path)
|
|
|
|
# 8) Retorno final
|
|
return docx_path.name
|
|
|
|
# ======================================================
|
|
# MÉTODOS PRIVADOS — PREPARAÇÃO
|
|
# ======================================================
|
|
def _prepare_directory(self) -> Path:
|
|
"""
|
|
Garante que o diretório temporário exista.
|
|
"""
|
|
diretorio = Path("./storage/temp").resolve()
|
|
diretorio.mkdir(parents=True, exist_ok=True)
|
|
return diretorio
|
|
|
|
def _generate_paths(self, diretorio: Path, document_id) -> tuple[Path, Path]:
|
|
"""
|
|
Gera nomes de arquivos únicos por execução,
|
|
evitando conflitos e lock de arquivos no Windows.
|
|
"""
|
|
base_name = f"{document_id}_{uuid.uuid4().hex}"
|
|
return (
|
|
diretorio / f"{base_name}.rtf",
|
|
diretorio / f"{base_name}.docx",
|
|
)
|
|
|
|
# ======================================================
|
|
# MÉTODOS PRIVADOS — CONTEÚDO
|
|
# ======================================================
|
|
def _decompress_content(self, texto: bytes | None) -> str:
|
|
"""
|
|
Descompacta obrigatoriamente o conteúdo recebido.
|
|
Caso não exista texto, cria um RTF vazio válido.
|
|
"""
|
|
if not texto:
|
|
return r"{\rtf1\ansi\deff0\pard\par}"
|
|
|
|
return Text.decompress(texto) or ""
|
|
|
|
def _is_docx(self, conteudo: str) -> bool:
|
|
"""
|
|
DOCX é um arquivo ZIP e sempre inicia com PK\\x03\\x04.
|
|
"""
|
|
return conteudo.startswith("PK\x03\x04")
|
|
|
|
def _validate_rtf(self, conteudo: str) -> None:
|
|
"""
|
|
Garante que o conteúdo seja um RTF válido.
|
|
"""
|
|
if not conteudo.startswith("{\\rtf"):
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail="Conteúdo descompactado não é RTF nem DOCX válido",
|
|
)
|
|
|
|
# ======================================================
|
|
# MÉTODOS PRIVADOS — ESCRITA DE ARQUIVOS
|
|
# ======================================================
|
|
def _save_docx(self, path: Path, conteudo: str) -> None:
|
|
"""
|
|
Salva DOCX diretamente em disco.
|
|
"""
|
|
with open(path, "wb") as f:
|
|
# DOCX descompactado ainda está em latin1
|
|
f.write(conteudo.encode("latin1"))
|
|
|
|
def _save_rtf(self, path: Path, texto_rtf: str) -> None:
|
|
"""
|
|
Salva RTF em disco.
|
|
Detecta automaticamente RTFs complexos (WPTools, tabelas, bookmarks, unicode)
|
|
para escolher a forma correta de escrita.
|
|
"""
|
|
rtf_complexo = self._is_complex_rtf(texto_rtf)
|
|
|
|
if rtf_complexo:
|
|
# Escrita binária preserva estruturas complexas
|
|
rtf_bytes = texto_rtf.encode("cp1252", errors="ignore")
|
|
with open(path, "wb") as f:
|
|
f.write(rtf_bytes)
|
|
else:
|
|
# Escrita textual para RTFs simples
|
|
with open(path, "w", encoding="cp1252", errors="replace") as f:
|
|
f.write(texto_rtf)
|
|
|
|
def _is_complex_rtf(self, texto_rtf: str) -> bool:
|
|
"""
|
|
Detecta padrões comuns de RTFs complexos gerados por WPTools,
|
|
OnlyOffice, Word, etc.
|
|
"""
|
|
return any(
|
|
token in texto_rtf
|
|
for token in (
|
|
"\\u",
|
|
"{\\*\\bkmkstart",
|
|
"\\trowd",
|
|
"\\cellx",
|
|
"\\wppr",
|
|
)
|
|
)
|
|
|
|
# ======================================================
|
|
# MÉTODOS PRIVADOS — CONVERSÃO
|
|
# ======================================================
|
|
def _convert_rtf_to_docx(self, diretorio: Path, rtf_path: Path) -> None:
|
|
"""
|
|
Executa o LibreOffice em modo headless para conversão RTF → DOCX.
|
|
"""
|
|
cmd = [
|
|
SOFFICE_PATH,
|
|
"--headless",
|
|
"--nologo",
|
|
"--nolockcheck",
|
|
"--nodefault",
|
|
"--nofirststartwizard",
|
|
"--convert-to",
|
|
"docx",
|
|
"--outdir",
|
|
str(diretorio),
|
|
str(rtf_path),
|
|
]
|
|
|
|
clean_env = {
|
|
"SYSTEMROOT": os.environ.get("SYSTEMROOT", ""),
|
|
"WINDIR": os.environ.get("WINDIR", ""),
|
|
"PATH": os.environ.get("PATH", ""),
|
|
"TEMP": os.environ.get("TEMP", ""),
|
|
"TMP": os.environ.get("TMP", ""),
|
|
}
|
|
|
|
result = subprocess.run(
|
|
cmd,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
text=True,
|
|
timeout=180,
|
|
env=clean_env,
|
|
)
|
|
|
|
if result.returncode != 0:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail="Erro ao converter RTF para DOCX via LibreOffice",
|
|
)
|
|
|
|
# ======================================================
|
|
# MÉTODOS PRIVADOS — VALIDAÇÃO FINAL
|
|
# ======================================================
|
|
def _validate_docx(self, docx_path: Path) -> None:
|
|
"""
|
|
Garante que o DOCX final realmente foi gerado.
|
|
"""
|
|
if not docx_path.exists():
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail="DOCX não foi gerado pelo LibreOffice",
|
|
)
|