Skip to main content

rightkit_pdf/
commit.rs

1//! Atomic on-disk commit for a staged save token.
2//!
3//! [`save::resolve_commit`] is filesystem-free; this module supplies the real source re-read and a
4//! crash-safe replace (write temp sibling, fsync, rename). Path authorization is the caller's job.
5
6use std::fs;
7use std::io::Write;
8use std::path::{Path, PathBuf};
9
10use crate::model::PdfEditError;
11use crate::save::{resolve_commit, PdfCommitResult, PdfSaveTokenStore};
12
13#[derive(Debug, thiserror::Error)]
14pub enum CommitError {
15    #[error(transparent)]
16    Edit(#[from] PdfEditError),
17    #[error("write failed: {0}")]
18    Io(#[from] std::io::Error),
19}
20
21/// Writes `bytes` to `target` atomically: a temp file in the same directory is fsynced and then
22/// renamed over the target, so readers observe either the old or the new file, never a mix.
23pub fn write_atomic(target: &Path, bytes: &[u8]) -> std::io::Result<()> {
24    let dir = target
25        .parent()
26        .filter(|p| !p.as_os_str().is_empty())
27        .unwrap_or(Path::new("."));
28    let name = target
29        .file_name()
30        .map(|n| n.to_string_lossy().into_owned())
31        .unwrap_or_default();
32    let mut nonce = [0_u8; 6];
33    getrandom::fill(&mut nonce).map_err(|e| std::io::Error::other(e.to_string()))?;
34    let suffix: String = nonce.iter().map(|b| format!("{b:02x}")).collect();
35    let tmp: PathBuf = dir.join(format!(".{name}.{suffix}.tmp"));
36    let result = (|| {
37        let mut file = fs::File::create(&tmp)?;
38        file.write_all(bytes)?;
39        file.sync_all()?;
40        fs::rename(&tmp, target)
41    })();
42    if result.is_err() {
43        let _ = fs::remove_file(&tmp);
44    }
45    result
46}
47
48/// Consumes `token`, re-checks the on-disk source for same-path saves, and atomically writes the
49/// verified candidate to `target` (or back to the source path when `None`).
50pub fn commit_pdf_save(
51    store: &PdfSaveTokenStore,
52    token: &str,
53    target: Option<&Path>,
54) -> Result<PdfCommitResult, CommitError> {
55    let (_source, bytes, result) = resolve_commit(
56        store,
57        token,
58        target.map(|p| p.to_string_lossy().into_owned()),
59        |source| fs::read(source).map_err(|_| PdfEditError::WriteConflict),
60    )?;
61    write_atomic(Path::new(&result.target_path), &bytes)?;
62    Ok(result)
63}