1use std::io::Write as _;
6
7use crate::{
8 error::{Error, Operation, Result},
9 path::Path,
10};
11
12pub fn replace(path: &Path, contents: impl AsRef<[u8]>) -> Result<()> {
23 let parent = destination_parent(path)?;
24 let temporary = prepare(parent, path, contents)?;
25
26 commit(temporary, parent, path, CommitMode::Replace)
27}
28
29pub fn create(path: &Path, contents: impl AsRef<[u8]>) -> Result<()> {
40 let parent = destination_parent(path)?;
41 let temporary = prepare(parent, path, contents)?;
42
43 commit(temporary, parent, path, CommitMode::Create)
44}
45
46#[derive(Clone, Copy, Debug)]
47enum CommitMode {
48 Create,
49 Replace,
50}
51
52fn destination_parent(path: &Path) -> Result<&Path> {
53 path.parent().ok_or_else(|| {
54 Error::new(
55 Operation::CreateFile,
56 path,
57 std::io::Error::new(
58 std::io::ErrorKind::InvalidInput,
59 "atomic destination has no parent",
60 ),
61 )
62 })
63}
64
65fn prepare(
66 parent: &Path,
67 path: &Path,
68 contents: impl AsRef<[u8]>,
69) -> Result<tempfile::NamedTempFile> {
70 let mut temporary = tempfile::NamedTempFile::new_in(parent)
71 .map_err(|source| Error::new(Operation::CreateFile, path, source))?;
72
73 temporary
74 .write_all(contents.as_ref())
75 .map_err(|source| Error::new(Operation::WriteFile, path, source))?;
76 temporary
77 .as_file_mut()
78 .flush()
79 .map_err(|source| Error::new(Operation::WriteFile, path, source))?;
80 temporary
81 .as_file()
82 .sync_all()
83 .map_err(|source| Error::new(Operation::Sync, path, source))?;
84
85 Ok(temporary)
86}
87
88fn commit(
89 temporary: tempfile::NamedTempFile,
90 parent: &Path,
91 path: &Path,
92 mode: CommitMode,
93) -> Result<()> {
94 let committed = match mode {
95 CommitMode::Create => temporary.persist_noclobber(path),
96 CommitMode::Replace => temporary.persist(path),
97 }
98 .map_err(|error| Error::new(Operation::Rename, path, error.error))?;
99
100 committed
101 .sync_all()
102 .map_err(|source| Error::new(Operation::Sync, path, source))?;
103
104 sync_parent(parent, path)
105}
106
107#[cfg(unix)]
108pub(crate) fn sync_parent(parent: &Path, path: &Path) -> Result<()> {
109 std::fs::File::open(parent)
110 .and_then(|directory| directory.sync_all())
111 .map_err(|source| Error::new(Operation::Sync, path, source))
112}
113
114#[cfg(not(unix))]
115pub(crate) fn sync_parent(_parent: &Path, _path: &Path) -> Result<()> {
116 Ok(())
117}
118
119#[cfg(test)]
120mod tests {
121 use googletest::prelude::*;
122
123 use super::{create, replace};
124 use crate::{directory, file, temporary::Directory};
125
126 #[gtest]
127 fn replace_commits_exact_bytes_without_visible_temporary_entries() -> Result<()> {
128 let directory = Directory::new().or_fail()?;
129 let path = directory.path().join("artifact.txt");
130
131 replace(&path, "first").or_fail()?;
132 replace(&path, "second").or_fail()?;
133
134 verify_that!(file::read_text(&path).or_fail()?, eq("second"))?;
135
136 verify_that!(directory::entries(directory.path()).or_fail()?, len(eq(1)))
137 }
138
139 #[gtest]
140 fn create_commits_once_without_clobbering() -> Result<()> {
141 let directory = Directory::new().or_fail()?;
142 let path = directory.path().join("config.toml");
143
144 create(&path, "first").or_fail()?;
145 let error = create(&path, "second").unwrap_err();
146
147 verify_true!(error.is_already_exists())?;
148 verify_that!(file::read_text(&path).or_fail()?, eq("first"))?;
149
150 verify_that!(directory::entries(directory.path()).or_fail()?, len(eq(1)))
151 }
152}