Dokumentkonvertierung in Rust mit unoserver automatisieren
Rust kann LibreOffice-Dokumentkonvertierungen mithilfe von unoserver und dessen Client
unoconvert orchestrieren. Dieser Leitfaden nutzt die dokumentierte CLI statt eines
zusätzlichen Rust-Wrappers. Ein einzelner überwachter LibreOffice-Worker verarbeitet Anfragen
nacheinander, während Rust die Auswahl der Eingaben, die Zuständigkeit für die Ausgaben und
Fehlerfälle verwaltet.
unoserver und Rust-Abhängigkeiten installieren
Installieren Sie unter Ubuntu/Debian die LibreOffice-Komponenten, die Python-UNO-Bridge der
Distribution und ein Paket für virtuelle Umgebungen. Der für unoserver verwendete Python-Interpreter
muss uno importieren können; eine standardmäßig isolierte Umgebung mit
pipx hat darauf möglicherweise keinen Zugriff.
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
Die Beispiele richten sich an Linux und macOS, nicht an natives Windows. Folgen Sie unter macOS der
Installationsanleitung für unoserver, um einen mit LibreOffice
kompatiblen Python-Interpreter auszuwählen. Prüfen Sie import uno mit genau diesem Interpreter.
Starten Sie den Worker in einem Terminal, wobei beide Schnittstellen an Loopback und separate Ports gebunden sind:
unoserver --interface 127.0.0.1 --port 2003 \
--uno-interface 127.0.0.1 --uno-port 2002 --conversion-timeout 120
Warten Sie auf den erfolgreichen Start im zugehörigen Log, bevor Sie eine Konvertierung ausführen. Das Konvertierungs-Timeout beendet LibreOffice und den Server, wenn eine Konvertierung hängen bleibt; ein Supervisor muss den Worker daher vor späteren Jobs neu starten. Machen Sie keinen der beiden Ports öffentlich zugänglich; der Dienst ist keine authentifizierte Upload-API.
Installieren Sie Rust über Ihre übliche unterstützte Toolchain und legen Sie das Projekt in einem weiteren Terminal an:
cargo new doc_converter_rust
cd doc_converter_rust
Stellen Sie sicher, dass unoconvert aus der virtuellen Umgebung auch in PATH dieses
Terminals enthalten ist. Es sind keine Cargo-Abhängigkeiten erforderlich. Testen Sie mit einem
echten DOCX-, ODT- oder anderen unterstützten Dokument; eine Textdatei, die lediglich die
Dateiendung .docx erhalten hat, ist kein gültiges DOCX-Fixture.
Rust mit unoserver integrieren
Ersetzen Sie src/main.rs durch diese vollständige CLI. Sie akzeptiert eine Datei oder ein
Verzeichnis und setzt ein neues Ausgabeverzeichnis voraus. Jedes PDF behält den vollständigen
Eingabedateinamen plus .pdf, sodass report.doc und
report.docx nicht kollidieren können. Dateisystempfade bleiben native Pfade, statt für
Prozessargumente in verlustbehaftete Zeichenketten umgewandelt zu werden.
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
}
}
}
Führen Sie cargo run -- /absolute/path/example.docx new-pdfs aus. Das Ergebnis ist
new-pdfs/example.docx.pdf. Das Ausgabedateisystem muss Hardlinks unterstützen. Führen Sie die CLI und
den Server unter demselben eingeschränkten Worker-Konto mit Zugriff auf dasselbe Dateisystem aus,
wie es --host-location local erfordert.
Ganze Ordner im Batch konvertieren
Dieselbe ausführbare Datei verarbeitet einen nicht rekursiven Ordner-Batch, ohne unbegrenzt viele Tasks zu starten:
cargo run -- /absolute/path/documents new-batch-pdfs
Sie ignoriert Unterverzeichnisse, Symlinks und nicht unterstützte Dateierweiterungen, versucht die Konvertierung unterstützter Dateien in sortierter Reihenfolge, behält erfolgreiche Ergebnisse bei und beendet sich mit einem Fehler, wenn eine Konvertierung fehlschlägt. Eine Dateierweiterung ist nur ein erster Filter und kein Beleg dafür, dass die Datei gültig oder sicher ist. Lassen Sie während eines Jobs keine anderen Prozesse die Eingabe- oder Ausgabeverzeichnisse verändern.
Fehler sauber behandeln
- Eine fehlende Eingabe, eine leere Auswahl oder ein bereits vorhandenes Ausgabeverzeichnis ist ein sichtbarer Fehler.
- Eine fehlgeschlagene Konvertierung veröffentlicht ihr unvollständiges PDF nicht. Prüfen Sie den Batch-Status ungleich null, auch wenn einige frühere Konvertierungen erfolgreich waren.
- Ein serverseitiges Timeout beendet den Worker. Starten Sie ihn vor einem erneuten Versuch neu und begrenzen Sie die Anzahl der Wiederholungen.
- Der Client ist kein Supervisor: Legen Sie eine Frist für den gesamten Job fest, die die CLI und ihren Kindprozess beendet. Wenn Sie nur den Client beenden, wird die bereits in LibreOffice laufende Arbeit nicht zwangsläufig abgebrochen.
- Lokale Diagnosedaten können Pfade oder Dokumentdetails enthalten. Beschränken Sie sie auf zugriffskontrollierte Logs, statt rohe Konverterfehler über eine Web-API zurückzugeben.
Mit mehreren unoserver-Instanzen skalieren
Geben Sie jedem Worker ein eigenes LibreOffice-Profil, ein nicht überlappendes Portpaar, eigene
Ressourcenlimits und eine eigene Job-Queue. Verwenden Sie beispielsweise die XML-RPC/UNO-Paare
2003/2002, 2013/2012 und 2023/2022; verwenden Sie einen Port nicht
erneut für die Schnittstelle eines anderen Workers. Die bereitgestellte CLI richtet sich bewusst nur
an den ersten Worker. Fügen Sie beim Skalieren eine explizite Worker-Konfiguration hinzu, statt in
jeder Konvertierungsanfrage Server zu starten.
Unterstützte Formate auf einen Blick
Die tatsächliche Unterstützung hängt von den installierten LibreOffice-Komponenten und -Filtern ab. Beginnen Sie mit dieser konservativen Auswahl und prüfen Sie repräsentative Dokumente:
| Kategorie | Beispiel-Eingaben | Ausgabe dieser CLI |
|---|---|---|
| Textverarbeitung | DOCX, DOC, ODT, RTF, TXT | |
| Tabellenkalkulation | XLSX, XLS, ODS, CSV | |
| Präsentationen | PPTX, PPT, ODP |
Installieren Sie die erforderlichen Schriftarten und prüfen Sie Seitenumbrüche, Formeln,
eingebettete Objekte und das Rendering. Ein erfolgreicher Prozess und eine Prüfung des Dateikopfs
auf %PDF- belegen weder die visuelle Übereinstimmung noch die Gültigkeit einer digitalen
Signatur.
Praxisbeispiel: Uploads in einer Web-API konvertieren
Verwenden Sie eine authentifizierte Anwendungsschicht, um Uploads mit begrenzter Größe anzunehmen, die Eigentümerschaft von Jobs zu autorisieren und Konvertierungsaufträge einzureihen. Betreiben Sie LibreOffice in isolierten Workern ohne Anwendungs-Zugangsdaten und ohne uneingeschränkten Netzwerkzugriff. Geben Sie eine Job-ID und einen bereinigten Status zurück; veröffentlichen Sie einen Download erst, nachdem ein fertiges Ergebnis Ihre Prüfungen bestanden hat.
Die CLI ist die Konvertierungskomponente, kein einsatzfertiger Upload-Endpunkt. Regeln Sie Anfragevalidierung, Zuständigkeit für die Speicherung, Rate-Limits, Malware-Richtlinie, Abbruch und Aufbewahrung ausdrücklich in Ihrer Anwendung, statt den rohen XML-RPC-Server offenzulegen.
Fazit
Die Standardbibliothek von Rust kann den vorhandenen Client unoconvert ausführen und dabei die
Batch-Steuerung und die Zuständigkeit für die Ausgaben einfach halten. Prüfen Sie die UNO-Umgebung
einmalig, überwachen Sie den Worker und testen Sie echte Ausgaben, bevor Sie Nebenläufigkeit
hinzufügen.
Für verwaltete Dokumentkonvertierung sehen Sie sich den Robot 🤖 /document/convert von Transloadit an.
