Dokumentkonvertierung in Rust mit unoserver automatisieren
Konvertieren Sie ein Dokument oder einen Ordner in PDFs, indem Rust den Client
unoconvert von unoserver aufruft. Die folgende CLI behält vollständige
Eingabedateinamen bei, lehnt vorhandene Ausgabeverzeichnisse ab und liefert einen Exit-Status
ungleich null, wenn eine ausgewählte Datei fehlschlägt. LibreOffice läuft in einem Loopback-Worker
weiter, während Rust die Dateien nacheinander zu konvertieren versucht.
unoserver und Rust-Abhängigkeiten installieren
Verwenden Sie Linux mit den LibreOffice-Komponenten Writer, Calc und Impress, der passenden
Python-UNO-Brücke und installierter Unterstützung für venv in Python.
Unter Debian/Ubuntu stammen diese aus den Paketen libreoffice,
python3-uno und python3-venv. Installieren Sie eine gepflegte
Rust-Toolchain mit Cargo gemäß der Rust-Installationsanleitung.
Getestet wurden Rust/Cargo 1.98.1, Python 3.14.7, LibreOffice 26.8.0.3 und unoserver 3.7;
die Compiler-Version dokumentiert den Test und ist keine exakt vorgeschriebene Version für dieses
Beispiel.
Erstellen Sie ein neues Projekt ohne Cargo-Abhängigkeiten. Die leere Tabelle
[workspace] macht es zu einem eigenen
Workspace-Stammverzeichnis,
sodass die Einrichtung dem Manifest eines übergeordneten Projekts kein Mitglied hinzufügt.
Die Subshell lässt das Verzeichnis Ihres Terminals unverändert, und
mkdir lehnt ein vorhandenes Projekt ab:
(
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
)
Führen Sie in jedem Terminal aus dem Verzeichnis, in dem Sie das Projekt erstellt haben,
cd doc_converter_rust aus. Verwenden Sie dieses Projektverzeichnis für die übrigen Befehle.
Installieren Sie Client und Server in der festgelegten Version in der venv dieses Projekts:
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
Der UNO-Import muss erfolgreich sein, bevor die Installation fortgesetzt wird. Falls er
fehlschlägt, installieren Sie die Brücke für genau diesen Python-Interpreter und wiederholen Sie
den Block. Er verwendet die teilweise eingerichtete venv weiter, ohne Ihre Projektdateien zu
ersetzen. Eine standardmäßig isolierte Umgebung mit pipx findet die
Brücke möglicherweise nicht. Die unoserver-Installationsanleitung
erläutert diese Kombination aus Python und LibreOffice und führt macOS und Windows als nicht
unterstützt auf. Diese Anleitung behandelt Linux.
Starten Sie den Worker in einem Terminal mit beiden Schnittstellen an Loopback-Adressen und getrennten Ports:
.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
Lassen Sie dieses Terminal geöffnet. Prüfen Sie nach dem Start den Worker im zweiten Terminal:
.venv/bin/unoping --host 127.0.0.1 --port 2003
Erwarten Sie Versionsinformationen, darunter unoserver 3.7 und LibreOffice. Die Portprüfung lehnt vorhandene Listener ab, bevor LibreOffice gestartet wird. Falls sie fehlschlägt, wählen Sie ein freies Portpaar und passen Sie die Prüfkommando-Ports, Serveroptionen und den Port des folgenden Rust-Clients entsprechend an. Verbinden Sie sich nicht mit einem unbekannten vorhandenen Listener. Die Prüfung gibt ihre Sockets vor dem Start frei. Falls in dieser Zeit ein Listener erscheint und der Start fehlschlägt, beenden Sie den verbleibenden LibreOffice-Kindprozess dieses Workers vor einem erneuten Versuch. Beide Schnittstellen haben keine Authentifizierung. Wenn eine Konvertierung hängt, beendet das Konvertierungs-Timeout LibreOffice und den Server, wie in der Client-/Server-Dokumentation beschrieben. Halten Sie beide Ports privat. Drücken Sie zum Abschluss im Worker-Terminal Ctrl+C, um ihn zu beenden.
Rust mit unoserver integrieren
Speichern Sie diese vollständige CLI als src/main.rs. Sie akzeptiert eine Datei
oder ein Verzeichnis und erfordert ein neues Ausgabeverzeichnis. Jede PDF behält den vollständigen
Eingabedateinamen mit dem Zusatz .pdf, sodass
report.doc und report.docx nicht kollidieren können.
Dateisystempfade bleiben native Pfade, statt für Prozessargumente in Zeichenfolgen mit möglichen
Informationsverlusten 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 mit einer tatsächlichen DOCX-Datei am Eingabepfad Folgendes aus:
PATH="$PWD/.venv/bin:$PATH" cargo run -- /absolute/path/example.docx new-pdfs
Erwarten Sie eine Zeile mit Converted, die die Eingabe und
new-pdfs/example.docx.pdf nennt. Öffnen Sie diese PDF und vergleichen Sie Text, Seitenzahl und
Layout mit der Quelle. Die Prüfung auf %PDF- prüft nur den Dateiheader.
Sie kann nicht belegen, dass jede Seite oder jedes eingebettete Objekt die Konvertierung
überstanden hat. LibreOffice kann auch Text erkennen, der in .docx
umbenannt wurde. Eine umbenannte Datei ist daher kein zuverlässiger Test für einen
Konvertierungsfehler.
Führen Sie CLI und Server unter demselben eingeschränkten Benutzerkonto mit Zugriff auf dasselbe
Dateisystem aus, wie von --host-location local verlangt. Das Ausgabedateisystem muss
Hardlinks unterstützen. Das Erstellen des endgültigen Links schlägt
fehl, wenn dieser Dateiname bereits existiert.
Ganze Ordner im Batch konvertieren
Dasselbe Programm verarbeitet einen Ordner nicht rekursiv als Batch, ohne unbegrenzt viele Aufgaben zu starten:
PATH="$PWD/.venv/bin:$PATH" cargo run -- /absolute/path/documents new-batch-pdfs
Es ignoriert Unterverzeichnisse, symbolische Links und nicht unterstützte Dateiendungen, versucht unterstützte Dateien in sortierter Reihenfolge zu konvertieren, behält erfolgreiche Ergebnisse bei und beendet sich mit einem Fehlerstatus, wenn eine Konvertierung fehlschlägt. Eine Dateiendung ist nur ein erster Filter und kein Beleg dafür, dass die Datei gültig oder sicher ist. Lassen Sie andere Prozesse die Eingabe- oder Ausgabeverzeichnisse während eines Jobs nicht verändern.
Für report.doc und report.docx sind separate Dateien namens
report.doc.pdf und report.docx.pdf zu erwarten.
Falls eine andere ausgewählte Datei fehlschlägt, bleiben diese erfolgreich erstellten PDFs erhalten,
der Name der fehlgeschlagenen Datei erscheint in stderr, und der Batch endet mit Status 1.
Wiederholen Sie den Versuch mit einem neuen Ausgabeverzeichnis, nachdem Sie den Fehler an der
Eingabe oder am Worker behoben haben. Wenn Sie den Befehl mit dem alten Verzeichnis wiederholen,
schlägt er fehl, ohne dessen PDFs zu ersetzen.
Fehler kontrolliert behandeln
- Eine fehlende Eingabe, eine leere Auswahl oder ein vorhandenes Ausgabeverzeichnis führt zu einem sichtbaren Fehler.
- Eine fehlgeschlagene Konvertierung veröffentlicht ihre unvollständige PDF nicht. Prüfen Sie den Batch-Status ungleich null, auch wenn 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 Wiederholungsversuche.
- Der Client ist kein Supervisor. Ergänzen Sie eine Zeitgrenze für den gesamten Job, die die CLI und ihren Kindprozess beendet. Nur den Client zu beenden, bricht die bereits in LibreOffice laufende Arbeit nicht unbedingt ab.
- Lokale Diagnosemeldungen können Pfade oder Dokumentdetails enthalten. Speichern Sie diese in zugriffsbeschränkten Logs, statt unverarbeitete Konvertierungsfehler über eine Web-API auszugeben.
Die bereitgestellte CLI ist auf einen Worker ausgelegt. Falls eine Anwendung Uploads in eine Warteschlange stellt, betreiben Sie den Worker hinter den Authentifizierungs- und Speicherprüfungen dieser Anwendung. Ein zugänglicher XML-RPC-Endpunkt ermöglicht es Aufrufenden, diese Prüfungen zu umgehen. Konfigurieren Sie für zusätzliche Worker separate LibreOffice-Profile und XML-RPC-/UNO-Portpaare ohne Überschneidungen. Leiten Sie dann jeden Job explizit an den jeweiligen Worker weiter, statt für jede Konvertierung einen Server zu starten.
Unterstützte Formate im Überblick
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 | Beispiele für 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 Seitenaufteilung, Formeln,
eingebettete Objekte und Darstellung. Ein erfolgreicher Prozess und eine Dateiheader-Prüfung auf
%PDF- belegen weder visuelle Übereinstimmung noch die Gültigkeit einer
digitalen Signatur.
Für verwaltete Dokumentkonvertierung sehen Sie sich den Robot 🤖 /document/convert von Transloadit an.
