1use 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
21pub 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
48pub 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}