Automatiza la conversión de documentos en Rust con unoserver
Rust puede orquestar la conversión de documentos de LibreOffice mediante unoserver
y su cliente unoconvert. Esta guía usa la CLI documentada en lugar de un wrapper
adicional en Rust. Un único proceso de trabajo supervisado de LibreOffice atiende las solicitudes de
forma secuencial, mientras que Rust gestiona la selección de entradas, la propiedad de las salidas y
los fallos.
Instala unoserver y las dependencias de Rust
En Ubuntu/Debian, instala los componentes de LibreOffice, el puente UNO de Python de la distribución
y un paquete de entorno virtual. El intérprete de Python que se use para unoserver debe poder
importar uno; un entorno pipx aislado por
defecto puede no verlo.
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
Los ejemplos están pensados para Linux y macOS, no para Windows nativo. En macOS, sigue la
guía de instalación de unoserver para elegir un intérprete de Python
compatible con LibreOffice. Verifica import uno con ese intérprete exacto.
En una terminal, ejecuta el proceso de trabajo con ambas interfaces enlazadas a loopback y en puertos separados:
unoserver --interface 127.0.0.1 --port 2003 \
--uno-interface 127.0.0.1 --uno-port 2002 --conversion-timeout 120
Espera a que su registro confirme un inicio correcto antes de ejecutar una conversión. El tiempo de espera de conversión termina LibreOffice y cierra el servidor si una conversión se queda colgada, así que un supervisor debe reiniciar el proceso de trabajo antes de los trabajos posteriores. No expongas públicamente ninguno de los dos puertos; el servicio no es una API de subida autenticada.
Instala Rust con tu cadena de herramientas compatible habitual y crea el proyecto en otra terminal:
cargo new doc_converter_rust
cd doc_converter_rust
Asegúrate de que unoconvert del entorno virtual también esté en el
PATH de esta terminal. No se necesitan dependencias de Cargo. Prueba con un
DOCX, ODT u otro documento compatible real; un archivo de texto renombrado con la extensión
.docx no es un archivo de prueba DOCX válido.
Integra Rust con unoserver
Reemplaza src/main.rs por esta CLI completa. Acepta un archivo o un directorio y
requiere un directorio de salida nuevo. Cada PDF conserva el nombre completo del archivo de entrada
más .pdf, de modo que report.doc y
report.docx no pueden colisionar. Las rutas del sistema de archivos se mantienen
como rutas nativas en lugar de convertirse en cadenas con pérdida para los argumentos de proceso.
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
}
}
}
Ejecuta cargo run -- /absolute/path/example.docx new-pdfs. El resultado es new-pdfs/example.docx.pdf. El sistema de
archivos de salida debe admitir enlaces duros. Ejecuta la CLI y el servidor con la misma cuenta de
trabajo restringida y con acceso al mismo sistema de archivos, tal como lo exige
--host-location local.
Convierte carpetas completas por lotes
El mismo ejecutable procesa un lote de carpeta no recursivo sin lanzar tareas ilimitadas:
cargo run -- /absolute/path/documents new-batch-pdfs
Ignora los subdirectorios, los enlaces simbólicos y las extensiones no compatibles, intenta procesar los archivos compatibles en orden, conserva los resultados correctos y termina con error si falla alguna conversión. Una extensión de archivo es solo un filtro inicial, no una prueba de que el archivo sea válido o seguro. No permitas que otros procesos modifiquen los directorios de entrada o de salida durante un trabajo.
Maneja los errores de forma controlada
- Una entrada faltante, una selección vacía o un directorio de salida ya existente constituye un fallo visible.
- Una conversión fallida no publica su PDF parcial. Revisa el estado del lote distinto de cero incluso si algunas conversiones anteriores tuvieron éxito.
- Un tiempo de espera agotado del lado del servidor cierra el proceso de trabajo. Reinícialo antes de volver a intentarlo y limita la cantidad de reintentos.
- El cliente no es un supervisor: añade un plazo límite para el trabajo completo que termine la CLI y su proceso hijo. Matar solo el cliente no cancela necesariamente el trabajo que ya se está ejecutando en LibreOffice.
- Los diagnósticos locales pueden incluir rutas o detalles del documento. Restríngelos a registros con control de acceso en lugar de devolver los errores sin procesar del conversor a través de una API web.
Escala con múltiples instancias de unoserver
Asigna a cada proceso de trabajo su propio perfil de LibreOffice, un par de puertos sin
solapamientos, límites de recursos y una cola de trabajos. Por ejemplo, usa los pares XML-RPC/UNO
2003/2002, 2013/2012 y 2023/2022; no
reutilices un puerto para la interfaz de otro proceso de trabajo. La CLI proporcionada apunta
deliberadamente solo al primer proceso de trabajo. Añade una configuración explícita de los procesos
de trabajo cuando escales, en lugar de iniciar servidores dentro de cada solicitud de conversión.
Formatos compatibles de un vistazo
El soporte real depende de los componentes y filtros de LibreOffice instalados. Empieza con esta selección conservadora y verifica documentos representativos:
| Categoría | Ejemplos de entrada | Salida en esta CLI |
|---|---|---|
| Procesamiento de textos | DOCX, DOC, ODT, RTF, TXT | |
| Hojas de cálculo | XLSX, XLS, ODS, CSV | |
| Presentaciones | PPTX, PPT, ODP |
Instala las fuentes necesarias e inspecciona la paginación, las fórmulas, los objetos incrustados y
el renderizado. Que el proceso termine correctamente y que la comprobación de la cabecera de archivo
%PDF- sea correcta no acredita la fidelidad visual ni valida una firma
digital.
Ejemplo real: convertir subidas en una API web
Usa una capa de aplicación autenticada para aceptar subidas con un tamaño limitado, autorizar la propiedad de los trabajos y encolar el trabajo de conversión. Ejecuta LibreOffice en procesos de trabajo aislados, sin credenciales de la aplicación ni acceso de red sin restricciones. Devuelve un identificador de trabajo y un estado saneado; publica una descarga solo después de que un resultado completado pase tus comprobaciones.
La CLI es el componente de conversión, no un endpoint de subida listo para desplegar. Define explícitamente en tu aplicación la validación de solicitudes, la propiedad del almacenamiento, los límites de tasa, la política antimalware, la cancelación y la retención, en lugar de exponer directamente el servidor XML-RPC.
Conclusión
La biblioteca estándar de Rust puede ejecutar el cliente unoconvert existente y
mantener sencillos el control por lotes y la propiedad de las salidas. Verifica el entorno UNO una
vez, supervisa el proceso de trabajo y prueba salidas reales antes de añadir concurrencia.
Para conversión de documentos gestionada, consulta el Robot 🤖 /document/convert de Transloadit.
