Automatizar a conversão de documentos em Rust com unoserver
Converta um documento ou uma pasta em PDFs fazendo o Rust chamar o cliente
unoconvert do unoserver. A CLI abaixo mantém os nomes completos dos arquivos
de entrada, recusa diretórios de saída existentes e retorna um código de saída diferente de zero
se algum arquivo selecionado falhar. O LibreOffice continua em execução em um executor com
interfaces de loopback enquanto o Rust tenta converter os arquivos sequencialmente.
Instalar o unoserver e as dependências do Rust
Use Linux com os componentes Writer, Calc e Impress do LibreOffice, a integração Python UNO
correspondente e o suporte a venv do Python instalados. No Debian/Ubuntu,
esses componentes vêm dos pacotes libreoffice,
python3-uno e python3-venv. Instale uma cadeia de ferramentas
Rust com manutenção ativa e Cargo seguindo as
instruções de instalação do Rust.
As versões testadas foram Rust/Cargo 1.98.1, Python 3.14.7, LibreOffice 26.8.0.3 e unoserver 3.7;
a versão do compilador é um registro do teste, não uma versão exata exigida por este exemplo.
Crie um projeto sem dependências do Cargo. A tabela vazia [workspace] faz dele
sua própria raiz do espaço de trabalho,
portanto a configuração não adiciona um membro ao manifesto de um projeto que o contenha.
O subshell mantém o diretório do seu terminal inalterado, e mkdir recusa
um projeto existente:
(
command -v cargo >/dev/null &&
command -v rustc >/dev/null &&
mkdir doc_converter_rust &&
cd doc_converter_rust &&
mkdir src &&
cat > Cargo.toml <<'TOML'
[package]
name = "doc_converter_rust"
version = "0.1.0"
edition = "2021"
[workspace]
TOML
)
No diretório onde você criou o projeto, execute cd doc_converter_rust em cada terminal.
Use esse diretório do projeto para os comandos restantes. Instale o cliente e o servidor nas
versões fixadas no ambiente virtual deste projeto:
python3 -m venv --system-site-packages .venv &&
.venv/bin/python -c 'import uno' &&
.venv/bin/python -m pip install unoserver==3.7 &&
.venv/bin/unoconvert --version
A importação do UNO precisa funcionar antes de a instalação continuar. Se falhar, instale a
integração para esse interpretador Python específico e repita este bloco; ele reutiliza o
ambiente virtual parcial sem substituir os arquivos do seu projeto. Um ambiente
pipx isolado por padrão pode não ter acesso à integração. O
guia de instalação do unoserver
explica essa combinação de Python e LibreOffice e informa que macOS e Windows não são compatíveis;
este tutorial aborda Linux.
Em um terminal, execute o executor com ambas as interfaces vinculadas ao loopback e portas distintas:
.venv/bin/python - <<'PY' &&
import socket
for port in (2003, 2002):
with socket.socket() as listener:
listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
try:
listener.bind(('127.0.0.1', port))
except OSError as error:
raise SystemExit(f'Loopback port {port} is unavailable: {error}')
PY
.venv/bin/unoserver --interface 127.0.0.1 --port 2003 \
--uno-interface 127.0.0.1 --uno-port 2002 --conversion-timeout 120
Mantenha esse terminal aberto. Após a inicialização, verifique o executor no segundo terminal:
.venv/bin/unoping --host 127.0.0.1 --port 2003
Você deve ver informações de versão, incluindo unoserver 3.7 e LibreOffice. A verificação das portas recusa processos já em escuta antes de iniciar o LibreOffice. Se ela falhar, escolha um par de portas livres e atualize a sondagem, as opções do servidor e a porta do cliente Rust abaixo para que correspondam. Não se conecte a um processo em escuta não identificado. A verificação libera seus sockets antes da inicialização; se um processo começar a escutar nesse intervalo e a inicialização falhar, encerre o processo filho do LibreOffice que restar deste executor antes de tentar novamente. Nenhuma das interfaces tem autenticação, e o tempo limite de conversão encerra o LibreOffice e o servidor se uma conversão travar, conforme descrito na documentação do cliente e do servidor. Mantenha ambas as portas privadas. Pressione Ctrl+C no terminal do executor para encerrá-lo quando terminar.
Integrar o Rust ao unoserver
Salve esta CLI completa como src/main.rs. Ela aceita um arquivo ou um diretório
e exige um novo diretório de saída. Cada PDF mantém o nome completo do arquivo de entrada mais
.pdf, de modo que report.doc e
report.docx não gerem nomes conflitantes. Os caminhos do sistema de arquivos
permanecem em sua representação nativa, em vez de serem convertidos em strings com perda de
informação para os argumentos do processo.
use std::error::Error;
use std::ffi::OsStr;
use std::fs::{self, DirBuilder, File};
use std::io::{self, Read};
use std::os::unix::fs::DirBuilderExt;
use std::path::{Path, PathBuf};
use std::process::{Command, ExitCode};
type Result<T> = std::result::Result<T, Box<dyn Error>>;
fn supported(path: &Path) -> bool {
let extension = path.extension().and_then(OsStr::to_str).unwrap_or("").to_ascii_lowercase();
matches!(extension.as_str(), "doc" | "docx" | "odt" | "rtf" | "txt" |
"ppt" | "pptx" | "odp" | "xls" | "xlsx" | "ods" | "csv")
}
fn convert_one(input: &Path, output: &Path) -> Result<()> {
let candidate = output.join(".conversion.pdf");
let conversion = (|| -> Result<()> {
let status = Command::new("unoconvert")
.args(["--host", "127.0.0.1", "--port", "2003", "--host-location", "local"])
.arg(input).arg(&candidate).status()?;
if !status.success() {
return Err(io::Error::other("unoconvert failed").into());
}
let mut header = [0; 5];
File::open(&candidate)?.read_exact(&mut header)?;
if &header != b"%PDF-" {
return Err(io::Error::other("conversion did not produce a PDF").into());
}
let mut filename = input.file_name().ok_or_else(|| io::Error::other("missing filename"))?.to_os_string();
filename.push(".pdf");
// Same-filesystem publication must fail rather than overwrite a destination.
fs::hard_link(&candidate, output.join(filename))?;
Ok(())
})();
if candidate.exists() {
fs::remove_file(&candidate)?;
}
conversion
}
fn run() -> Result<()> {
let args: Vec<_> = std::env::args_os().skip(1).collect();
if args.len() != 2 {
return Err(io::Error::other("Usage: doc_converter_rust <input-file-or-directory> <new-output-directory>").into());
}
let source = PathBuf::from(&args[0]);
let metadata = fs::symlink_metadata(&source)?;
let mut inputs = Vec::new();
if metadata.is_file() && supported(&source) {
inputs.push(source.canonicalize()?);
} else if metadata.is_dir() {
for entry in fs::read_dir(&source)? {
let entry = entry?;
if entry.file_type()?.is_file() && supported(&entry.path()) {
inputs.push(entry.path().canonicalize()?);
}
}
} else {
return Err(io::Error::other("use a supported regular file or directory, not a symlink").into());
}
inputs.sort();
if inputs.is_empty() {
return Err(io::Error::other("no supported documents found").into());
}
let output = PathBuf::from(&args[1]);
DirBuilder::new().mode(0o700).create(&output)?;
let output = output.canonicalize()?;
let mut failures = 0;
for input in inputs {
match convert_one(&input, &output) {
Ok(()) => println!("Converted {}", input.display()),
Err(_) => {
failures += 1;
eprintln!("Conversion failed for {}", input.display());
}
}
}
if failures > 0 {
return Err(io::Error::other(format!("{failures} conversion(s) failed")).into());
}
Ok(())
}
fn main() -> ExitCode {
match run() {
Ok(()) => ExitCode::SUCCESS,
Err(error) => {
eprintln!("Document conversion failed: {error}");
ExitCode::FAILURE
}
}
}
Com um arquivo DOCX real no caminho de entrada, execute:
PATH="$PWD/.venv/bin:$PATH" cargo run -- /absolute/path/example.docx new-pdfs
Você deve ver uma linha Converted indicando a entrada e
new-pdfs/example.docx.pdf. Abra esse PDF e compare seu texto, número de páginas e layout com
o documento original. A verificação de %PDF- confirma apenas o cabeçalho;
ela não pode comprovar que todas as páginas ou objetos incorporados foram preservados na conversão.
O LibreOffice também pode detectar texto renomeado como .docx, portanto
um arquivo renomeado não é um teste confiável de falha de conversão.
Execute a CLI e o servidor com a mesma conta restrita, com acesso ao mesmo sistema de arquivos,
conforme exigido por --host-location local. O sistema de arquivos de saída deve oferecer
suporte a links físicos; a criação do link final
falha se esse nome de arquivo já existir.
Converter pastas inteiras em lote
O mesmo executável processa um lote de arquivos de uma pasta, sem percorrer subpastas nem iniciar uma quantidade ilimitada de tarefas:
PATH="$PWD/.venv/bin:$PATH" cargo run -- /absolute/path/documents new-batch-pdfs
Ele ignora subdiretórios, links simbólicos e extensões não compatíveis, tenta converter os arquivos compatíveis em ordem de classificação, preserva os resultados bem-sucedidos e termina com um código de saída de falha se alguma conversão falhar. Uma extensão de arquivo é apenas um filtro inicial, não uma prova de que o arquivo é válido ou seguro. Não deixe outros processos modificarem os diretórios de entrada ou saída durante uma tarefa.
Para report.doc e report.docx, você deve obter arquivos
report.doc.pdf e report.docx.pdf separados.
Se outro arquivo selecionado falhar, esses PDFs convertidos com sucesso permanecem, o nome do
arquivo que falhou aparece em stderr e o lote termina com código de saída 1. Tente novamente com
um novo diretório de saída após corrigir a entrada ou o executor; repetir o comando com o
diretório antigo falha sem substituir seus PDFs.
Tratar erros adequadamente
- Uma entrada ausente, uma seleção vazia ou um diretório de saída existente causa uma falha visível.
- Uma conversão que falha não publica seu PDF parcial. Verifique o código de saída não zero do lote, mesmo que algumas conversões anteriores tenham sido bem-sucedidas.
- Um tempo limite no servidor encerra o executor. Reinicie-o antes de tentar novamente e mantenha um limite para o número de tentativas.
- O cliente não é um supervisor: adicione um prazo máximo para a tarefa inteira que encerre a CLI e seu processo filho. Encerrar apenas o cliente não necessariamente cancela o trabalho que já está em execução no LibreOffice.
- Os diagnósticos locais podem incluir caminhos ou detalhes dos documentos. Mantenha-os em logs com acesso restrito, em vez de retornar erros brutos do conversor por uma API web.
A CLI fornecida usa um executor. Se uma aplicação coloca uploads em uma fila, mantenha o executor protegido pelas verificações de autenticação e armazenamento dessa aplicação; expor XML-RPC permite que quem o chama contorne essas verificações. Para executores adicionais, configure perfis separados do LibreOffice e pares de portas XML-RPC/UNO que não se sobreponham. Depois, encaminhe cada tarefa explicitamente, em vez de iniciar um servidor para cada conversão.
Visão geral dos formatos compatíveis
A compatibilidade real depende dos componentes e filtros instalados do LibreOffice. Comece com esta seleção conservadora e verifique documentos representativos:
| Categoria | Exemplos de entrada | Saída nesta CLI |
|---|---|---|
| Processamento de texto | DOCX, DOC, ODT, RTF, TXT | |
| Planilhas | XLSX, XLS, ODS, CSV | |
| Apresentações | PPTX, PPT, ODP |
Instale as fontes necessárias e examine a paginação, as fórmulas, os objetos incorporados e a
renderização. Um processo concluído com sucesso e uma verificação de %PDF-
no cabeçalho do arquivo não comprovam a fidelidade visual nem validam uma assinatura digital.
Para conversão gerenciada de documentos, consulte o Robot 🤖 /document/convert da Transloadit.
