Automatize a conversão de documentos em Rust com o unoserver
O Rust pode orquestrar a conversão de documentos do LibreOffice por meio do unoserver e do seu cliente
unoconvert. Este guia usa a CLI documentada em vez de um wrapper adicional em Rust. Um único worker
supervisionado do LibreOffice processa as requisições em sequência, enquanto o Rust cuida da seleção
das entradas, da propriedade das saídas e das falhas.
Instale o unoserver e as dependências do Rust
No Ubuntu/Debian, instale os componentes do LibreOffice, a ponte UNO para Python da distribuição e
um pacote de ambiente virtual. O interpretador Python usado pelo unoserver precisa conseguir importar
uno; um ambiente pipx isolado padrão pode não enxergá-lo.
sudo apt-get update
sudo apt-get install libreoffice python3-uno python3-venv
python3 -m venv --system-site-packages .venv
. .venv/bin/activate
python -c 'import uno'
pip install unoserver==3.7
unoconvert --version
Os exemplos são voltados para Linux e macOS, não para o Windows nativo. No macOS, siga o
guia de instalação do unoserver para escolher um interpretador
Python compatível com o LibreOffice. Verifique import uno com exatamente esse interpretador.
Em um terminal, execute o worker com as duas interfaces vinculadas ao loopback e em portas separadas:
unoserver --interface 127.0.0.1 --port 2003 \
--uno-interface 127.0.0.1 --uno-port 2002 --conversion-timeout 120
Antes de executar uma conversão, aguarde o log indicar que a inicialização foi bem-sucedida. Se uma conversão travar, o timeout de conversão encerra o LibreOffice e finaliza o servidor, então um supervisor precisa reiniciar o worker antes dos trabalhos seguintes. Não exponha nenhuma das portas publicamente; o serviço não é uma API de upload autenticada.
Instale o Rust com a toolchain compatível que você costuma usar e crie o projeto em outro terminal:
cargo new doc_converter_rust
cd doc_converter_rust
Garanta que o unoconvert do ambiente virtual também esteja no PATH deste terminal. Nenhuma
dependência do Cargo é necessária. Teste com um DOCX, ODT ou outro documento compatível de verdade;
um arquivo de texto renomeado com a extensão .docx não é um arquivo de teste DOCX válido.
Integre o Rust ao unoserver
Substitua src/main.rs por esta CLI completa. 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, então
report.doc e report.docx não podem colidir. Os caminhos do sistema de arquivos continuam como caminhos
nativos, em vez de serem convertidos em strings com perda 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
}
}
}
Execute cargo run -- /absolute/path/example.docx new-pdfs. O resultado é
new-pdfs/example.docx.pdf. O sistema de arquivos de saída precisa ser compatível com hard links. Execute a CLI e
o servidor com a mesma conta restrita de worker, com acesso ao mesmo sistema de arquivos, conforme
exigido por --host-location local.
Converta pastas inteiras em lote
O mesmo executável processa um lote não recursivo de uma pasta sem disparar um número ilimitado de tarefas:
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 status 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 permita que outros processos modifiquem os diretórios de entrada ou de saída durante um trabalho.
Trate erros com elegância
- Uma entrada ausente, uma seleção vazia ou um diretório de saída já existente resulta em uma falha visível.
- Uma conversão com falha não publica seu PDF parcial. Verifique o status diferente de zero do lote, mesmo que algumas conversões anteriores tenham sido bem-sucedidas.
- Um timeout no lado do servidor finaliza o worker. Reinicie-o antes de tentar novamente e mantenha limitado o número de novas tentativas.
- O cliente não é um supervisor: adicione um prazo para o trabalho inteiro que encerre a CLI e seu processo filho. Encerrar apenas o cliente não cancela necessariamente o processamento que já está em andamento no LibreOffice.
- Diagnósticos locais podem incluir caminhos ou detalhes de documentos. Mantenha-os em logs restritos em vez de retornar erros brutos do conversor por meio de uma API web.
Escale com várias instâncias do unoserver
Dê a cada worker seu próprio perfil do LibreOffice, um par de portas que não se sobreponha aos
demais, seus próprios limites de recursos e sua própria fila de trabalhos. Por exemplo, use os pares
XML-RPC/UNO 2003/2002, 2013/2012 e 2023/2022; não reutilize uma porta para a interface de outro
worker. A CLI fornecida atende deliberadamente apenas o primeiro worker. Ao escalar, adicione uma
configuração explícita de workers em vez de iniciar servidores dentro de cada requisição de
conversão.
Visão geral dos formatos compatíveis
O suporte real depende dos componentes e filtros do LibreOffice instalados. 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 inspecione a paginação, as fórmulas, os objetos incorporados e a
renderização. Um processo bem-sucedido e uma verificação do cabeçalho de arquivo %PDF- não comprovam
a fidelidade visual nem validam uma assinatura digital.
Exemplo real: converter uploads em uma API web
Use uma camada de aplicação autenticada para aceitar uploads de tamanho limitado, autorizar a propriedade dos trabalhos e colocar as conversões na fila. Mantenha o LibreOffice em workers isolados, sem credenciais da aplicação nem acesso irrestrito à rede. Retorne um identificador do trabalho e um status sanitizado; publique um download somente depois que um resultado concluído passar pelas suas verificações.
A CLI é o componente de conversão, não um endpoint de upload pronto para implantação. Mantenha explícitos na sua aplicação a validação de requisições, a propriedade do armazenamento, os limites de taxa, a política contra malware, o cancelamento e a retenção, em vez de expor diretamente o servidor XML-RPC.
Conclusão
A biblioteca padrão do Rust pode executar o cliente unoconvert existente, mantendo simples o controle
de lotes e a propriedade das saídas. Verifique o ambiente UNO uma vez, supervisione o worker e teste
a saída real antes de adicionar concorrência.
Para conversão de documentos gerenciada, confira o Robot 🤖 /document/convert da Transloadit.
