Skip to content

Document.save() and Presentation.save() write the target in place, where the CLIs stage and publish #164

Description

@hadim

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions