The CLIs publish the documents they write through a temporary sibling (oxml-cli-support/src/lib.rs,
rpptx replace since #120, and thanks for that; the PDF, HTML and Markdown outputs of convert and the rpptx thumbnail still go
through std::fs::write, and overwrite an existing output, #156). The library saves do not: OpcPackage::save (oxml-opc/src/package.rs:251) serialises,
then File::create(path) truncates the target and compresses and writes into it; rpptx's
Presentation::save calls std::fs::write (rpptx/src/lib.rs:1521), although rpptx has its own
write_atomic_file (lib.rs:784), used by save_encrypted. Document::save_as_package_class, save_encrypted and the flat-OPC, HTML, ODT, RTF and EPUB
savers already go through write_atomic_file (rdocx/src/document.rs:22977); the plain .docx and
.pptx saves, the ones every script calls, are the exception.
Why it matters: the common use of the bindings is "open, edit, save over the same file" in a folder that
something else watches (a sync client, an editor with the file open, a file-server share). While the write
runs, and if it fails half-way (disk full, interrupted process), the watcher sees a truncated zip. With a
sync client that is enough to upload the broken file as a new version.
The check below uses the inode (its value varies from run to run): a file replaced by rename gets a new one,
a file written in place keeps it.
import os
import rdocx, rpptx
from docx import Document
from pptx import Presentation
Document().save("sa.docx"); Presentation().save("sa.pptx")
for label, path, save in (("rdocx Document.save", "sa.docx", lambda p: rdocx.Document.open(p).save(p)),
("rpptx Presentation.save", "sa.pptx", lambda p: rpptx.Presentation(p).save(p))):
before = os.stat(path).st_ino
save(path)
after = os.stat(path).st_ino
print(f"{label:<26} inode before {before} after {after} -> {'written in place' if before == after else 'replaced by rename'}")
rdocx Document.save inode before 934014 after 934014 -> written in place
rpptx Presentation.save inode before 934015 after 934015 -> written in place
Suggestion: route save() through the same write-to-sibling-then-rename as the other savers (in rdocx the
parts are serialised before File::create, but the zip is compressed after the target is truncated, so
the whole write has to move).
Keeping the target's permissions would be nice; an opt-out for callers who rely on the inode staying the
same could be a keyword if anyone needs it.
Environment: main at 9a7ed714 (S75), release build, linux x86_64, Python 3.11.
The CLIs publish the documents they write through a temporary sibling (
oxml-cli-support/src/lib.rs,rpptx replacesince #120, and thanks for that; the PDF, HTML and Markdown outputs ofconvertand the rpptx thumbnail still gothrough
std::fs::write, and overwrite an existing output, #156). The library saves do not:OpcPackage::save(oxml-opc/src/package.rs:251) serialises,then
File::create(path)truncates the target and compresses and writes into it;rpptx'sPresentation::savecallsstd::fs::write(rpptx/src/lib.rs:1521), although rpptx has its ownwrite_atomic_file(lib.rs:784), used bysave_encrypted.Document::save_as_package_class,save_encryptedand the flat-OPC, HTML, ODT, RTF and EPUBsavers already go through
write_atomic_file(rdocx/src/document.rs:22977); the plain.docxand.pptxsaves, the ones every script calls, are the exception.Why it matters: the common use of the bindings is "open, edit, save over the same file" in a folder that
something else watches (a sync client, an editor with the file open, a file-server share). While the write
runs, and if it fails half-way (disk full, interrupted process), the watcher sees a truncated zip. With a
sync client that is enough to upload the broken file as a new version.
The check below uses the inode (its value varies from run to run): a file replaced by rename gets a new one,
a file written in place keeps it.
Suggestion: route
save()through the same write-to-sibling-then-rename as the other savers (in rdocx theparts are serialised before
File::create, but the zip is compressed after the target is truncated, sothe whole write has to move).
Keeping the target's permissions would be nice; an opt-out for callers who rely on the inode staying the
same could be a keyword if anyone needs it.
Environment:
mainat9a7ed714(S75), release build, linux x86_64, Python 3.11.