.pub files they could not open anymore. PubRestore turns those files back into a PDF, an editable PowerPoint, and an editable Word document.The obvious answer is "run it through LibreOffice". That gets you about halfway. The other half is the interesting part, so here is how a file moves through the system and what happens to it on the way.
The path of one file
Everything runs on one Cloudflare account. The website is Next.js on Workers through OpenNext. Uploads go straight to R2 through a short-lived signed link, so the file never sits in a Worker's memory. Starting a conversion puts a message on a Queue, and the jobs Worker picks it up.
No polling
Progress pages usually poll: "is it done yet?" every couple of seconds. PubRestore keeps one server-sent events stream open instead. Each file gets its own Durable Object, a JobRoom, and every step the converter reports (opened, repaired, exported, previews ready) is pushed into that room and out to the browser.
The D1 row stays the source of truth. If the stream goes quiet for 15 seconds, it re-reads the row, in case an event was lost on the way. It is a small thing, but it means the page can never get stuck showing "converting" for a file that finished.
A fresh container for every file
The conversion itself runs in a Cloudflare Container: Ubuntu, LibreOffice, and a Python engine. Every file gets a fresh container, with the internet switched off and no secrets inside it. Storage, the database, and the queue are reached only through Worker bindings, so there are no keys anywhere for a malicious file to find.
The engine is careful about what it opens, too. Only real Publisher files are accepted (it checks the file signature), they are opened with macros and linked content off, and every job has a hard time limit. If a broken file hangs or crashes LibreOffice, the engine restarts it and tries the file once more before giving up.
Files are deleted after 48 hours by an hourly cron sweep, with an R2 lifecycle rule as a backstop.
The other half: repairing the layout
LibreOffice can read Publisher files, but it makes very specific mistakes. Most of the engine is a list of repairs, each one its own module:
- Linked text boxes. In Publisher a story flows from one box to the next. LibreOffice copies the whole story into every box. The engine flows it box to box instead, across pages when it really overflowed, and scales a slightly-too-long story down evenly so all its columns keep one size.
- Text wrapping. Pictures should push text into the free space beside them. The engine tries each wrap on a scratch copy first and drops it if the text would land on another box.
- Overflow and collisions. Text that runs past its box is reduced in measured steps, then hidden past the edge the way Publisher does it. Text that runs into the box below moves up into free space first.
- Fonts. About a hundred Microsoft and Publisher fonts are mapped to free look-alikes of similar width, and the ones that end up in Word or PowerPoint files are embedded, so they look the same on any computer.
- Encoding. Old files with garbled Russian, Greek, or Turkish text get repaired.
Every conversion also counts the words in the original file that did not make it into the output. That number is the quality alarm: if it is not zero, something went wrong, and I want to know before the customer does. The engine runs against a corpus of test files, plus several hundred real files from the web kept outside git.
What I would do the same way again
Pushing every heavy thing to the edge of the system kept the core simple. The web Worker never touches file bytes, the jobs Worker never runs LibreOffice, and the container never talks to the internet. Each part has one job, and the boundaries between them are also the security boundaries.
And free previews before payment were worth the extra work. People can see their own newsletter, repaired, before they pay a cent. After that it is credit packs that never expire, and the per-file price drops quickly for schools and churches with a whole archive to convert:
