ic_backup/ops/persistence/json/
mod.rs1use crate::ops::persistence::PersistenceError;
8
9use std::{
10 ffi::OsString,
11 fs::{self, File, OpenOptions},
12 io::{self, Write},
13 path::{Path, PathBuf},
14 sync::atomic::{AtomicU64, Ordering},
15};
16
17use serde::{Serialize, de::DeserializeOwned};
18
19static TEMP_SEQUENCE: AtomicU64 = AtomicU64::new(0);
20
21pub(super) fn check_json_size(
23 value: &impl Serialize,
24 max_bytes: u64,
25) -> Result<(), PersistenceError> {
26 let mut writer = ic_host_tools::artifact::BoundedWriter::new(io::sink(), max_bytes);
27 let result = serde_json::to_writer_pretty(&mut writer, value);
28 if writer.limit_exceeded() {
29 return Err(PersistenceError::RecordTooLarge { limit: max_bytes });
30 }
31 result?;
32 Ok(())
33}
34
35pub fn write_json_durable<T>(path: &Path, value: &T) -> Result<(), PersistenceError>
40where
41 T: Serialize,
42{
43 let bytes = serde_json::to_vec_pretty(value)?;
44 replace_bytes_at_barriers(path, &bytes, |_| {}).map_err(PersistenceError::from)
45}
46
47pub fn create_json_durable<T>(path: &Path, value: &T) -> Result<(), PersistenceError>
52where
53 T: Serialize,
54{
55 let bytes = serde_json::to_vec_pretty(value)?;
56 create_bytes_at_barriers(path, &bytes, || {}, || {}).map_err(PersistenceError::from)
57}
58
59pub fn read_json<T>(path: &Path, max_bytes: u64) -> Result<T, PersistenceError>
67where
68 T: DeserializeOwned,
69{
70 #[cfg(unix)]
71 {
72 let bytes = ic_host_tools::artifact::read_file_no_follow(
73 path,
74 usize::try_from(max_bytes).unwrap_or(usize::MAX),
75 )
76 .map_err(|error| record_read_error(error, max_bytes))?;
77 Ok(serde_json::from_slice(&bytes)?)
78 }
79 #[cfg(not(unix))]
80 {
81 let _ = (path, max_bytes);
82 Err(io::Error::from(io::ErrorKind::Unsupported).into())
83 }
84}
85
86#[cfg(unix)]
87fn record_read_error(
88 error: ic_host_tools::artifact::ArtifactError,
89 max_bytes: u64,
90) -> PersistenceError {
91 use ic_host_tools::artifact::ArtifactError;
92 match error {
93 ArtifactError::Io(error) => PersistenceError::Io(error),
94 ArtifactError::NotRegularFile => PersistenceError::Io(io::Error::new(
95 io::ErrorKind::InvalidInput,
96 "record must be a regular file",
97 )),
98 ArtifactError::LimitExceeded { .. } => {
99 PersistenceError::RecordTooLarge { limit: max_bytes }
100 }
101 error => PersistenceError::Io(io::Error::other(error)),
102 }
103}
104
105fn create_bytes_at_barriers(
106 path: &Path,
107 bytes: &[u8],
108 mut before_publication: impl FnMut(),
109 mut after_directory_sync: impl FnMut(),
110) -> io::Result<()> {
111 let parent = path
112 .parent()
113 .filter(|parent| !parent.as_os_str().is_empty())
114 .unwrap_or_else(|| Path::new("."));
115 create_private_parents(parent)?;
116
117 let (temp_path, mut temp_file) = create_sibling_temp(path, parent)?;
118 if let Err(error) = temp_file
119 .write_all(bytes)
120 .and_then(|()| temp_file.sync_all())
121 {
122 drop(temp_file);
123 let _ = fs::remove_file(&temp_path);
124 return Err(error);
125 }
126 drop(temp_file);
127 before_publication();
128
129 if let Err(error) = fs::hard_link(&temp_path, path) {
130 let _ = fs::remove_file(&temp_path);
131 return Err(error);
132 }
133 fs::remove_file(&temp_path)?;
134 File::open(parent)?.sync_all()?;
135 after_directory_sync();
136 Ok(())
137}
138
139#[derive(Clone, Copy, Debug, Eq, PartialEq)]
140pub(crate) enum DurableWriteBarrier {
141 BeforeRename,
142 AfterDirectorySync,
143}
144
145fn replace_bytes_at_barriers(
146 path: &Path,
147 bytes: &[u8],
148 mut barrier: impl FnMut(DurableWriteBarrier),
149) -> io::Result<()> {
150 let parent = path
151 .parent()
152 .filter(|parent| !parent.as_os_str().is_empty())
153 .unwrap_or_else(|| Path::new("."));
154 create_private_parents(parent)?;
155
156 let (temp_path, mut temp_file) = create_sibling_temp(path, parent)?;
157 if let Err(error) = temp_file
158 .write_all(bytes)
159 .and_then(|()| temp_file.sync_all())
160 {
161 drop(temp_file);
162 let _ = fs::remove_file(&temp_path);
163 return Err(error);
164 }
165 drop(temp_file);
166 barrier(DurableWriteBarrier::BeforeRename);
167
168 if let Err(error) = fs::rename(&temp_path, path) {
169 let _ = fs::remove_file(&temp_path);
170 return Err(error);
171 }
172
173 File::open(parent)?.sync_all()?;
174 barrier(DurableWriteBarrier::AfterDirectorySync);
175 Ok(())
176}
177
178#[cfg(test)]
179pub(crate) fn write_json_durable_at_barriers<T>(
180 path: &Path,
181 value: &T,
182 barrier: impl FnMut(DurableWriteBarrier),
183) -> Result<(), PersistenceError>
184where
185 T: Serialize,
186{
187 let bytes = serde_json::to_vec_pretty(value)?;
188 replace_bytes_at_barriers(path, &bytes, barrier).map_err(PersistenceError::from)
189}
190
191#[cfg(test)]
192pub(crate) fn create_json_durable_at_barriers<T>(
193 path: &Path,
194 value: &T,
195 before_publication: impl FnMut(),
196 after_directory_sync: impl FnMut(),
197) -> Result<(), PersistenceError>
198where
199 T: Serialize,
200{
201 let bytes = serde_json::to_vec_pretty(value)?;
202 create_bytes_at_barriers(path, &bytes, before_publication, after_directory_sync)
203 .map_err(PersistenceError::from)
204}
205
206fn create_sibling_temp(path: &Path, parent: &Path) -> io::Result<(PathBuf, File)> {
207 let file_name = path.file_name().ok_or_else(|| {
208 io::Error::new(
209 io::ErrorKind::InvalidInput,
210 format!("durable write target has no file name: {}", path.display()),
211 )
212 })?;
213
214 for _ in 0..64 {
215 let sequence = TEMP_SEQUENCE.fetch_add(1, Ordering::Relaxed);
216 let mut temp_name = OsString::from(".");
217 temp_name.push(file_name);
218 temp_name.push(format!(".ic-backup-tmp-{}-{sequence}", std::process::id()));
219 let temp_path = parent.join(temp_name);
220 let mut options = OpenOptions::new();
221 options.write(true).create_new(true);
222 #[cfg(unix)]
223 {
224 use std::os::unix::fs::OpenOptionsExt;
225 options.mode(0o600);
226 }
227 match options.open(&temp_path) {
228 Ok(file) => return Ok((temp_path, file)),
229 Err(error) if error.kind() == io::ErrorKind::AlreadyExists => {}
230 Err(error) => return Err(error),
231 }
232 }
233
234 Err(io::Error::new(
235 io::ErrorKind::AlreadyExists,
236 format!(
237 "could not allocate a unique sibling temporary file for {}",
238 path.display()
239 ),
240 ))
241}
242
243#[cfg(test)]
248mod regressions;
249#[cfg(test)]
250mod tests;
251
252fn create_private_parents(parent: &Path) -> io::Result<()> {
253 let mut missing = Vec::new();
254 let mut current = parent;
255 while !current.try_exists()? {
256 missing.push(current.to_path_buf());
257 current = current
258 .parent()
259 .filter(|path| !path.as_os_str().is_empty())
260 .unwrap_or_else(|| Path::new("."));
261 }
262 let mut builder = fs::DirBuilder::new();
263 builder.recursive(true);
264 #[cfg(unix)]
265 {
266 use std::os::unix::fs::DirBuilderExt;
267 builder.mode(0o700);
268 }
269 builder.create(parent)?;
270 for directory in missing {
272 File::open(&directory)?.sync_all()?;
273 let ancestor = directory
274 .parent()
275 .filter(|path| !path.as_os_str().is_empty())
276 .unwrap_or_else(|| Path::new("."));
277 File::open(ancestor)?.sync_all()?;
278 }
279 Ok(())
280}