loftspec_store/
journal.rs1use std::collections::{BTreeMap, BTreeSet};
8use std::fs;
9use std::io::{self, Write};
10use std::path::{Path, PathBuf};
11
12use loftspec::codec::{Files, SpecPath};
13use serde::{Deserialize, Serialize};
14
15use crate::StoreError;
16use crate::error::io_error;
17use crate::fs::SPEC;
18
19pub const JOURNAL: &str = ".loft/save";
21
22const PLAN: &str = "plan.json";
23const PLAN_STAGED: &str = "plan.json.tmp";
24const STAGED: &str = "files";
25const FORMAT: &str = "loftspec-save/1";
26
27#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
29#[serde(transparent)]
30pub struct RootPath(String);
31
32impl RootPath {
33 #[must_use]
35 pub fn in_spec(path: &SpecPath) -> Self {
36 Self(format!("{SPEC}/{}", path.as_str()))
37 }
38
39 #[must_use]
41 pub fn beside(name: &str) -> Self {
42 Self(name.to_owned())
43 }
44
45 pub(crate) fn as_str(&self) -> &str {
46 &self.0
47 }
48}
49
50#[derive(Debug, Clone, Default, PartialEq, Eq)]
52pub struct Changes {
53 pub write: BTreeMap<RootPath, Vec<u8>>,
54 pub remove: BTreeSet<RootPath>,
55}
56
57impl Changes {
58 #[must_use]
60 pub fn of_tree(write: Files, remove: &[SpecPath]) -> Self {
61 Self {
62 write: write
63 .into_iter()
64 .map(|(path, bytes)| (RootPath::in_spec(&path), bytes))
65 .collect(),
66 remove: remove.iter().map(RootPath::in_spec).collect(),
67 }
68 }
69
70 fn is_empty(&self) -> bool {
71 self.write.is_empty() && self.remove.is_empty()
72 }
73}
74
75#[derive(Debug, Serialize, Deserialize)]
76struct Plan {
77 format: String,
78 write: Vec<Staged>,
79 remove: Vec<RootPath>,
80}
81
82#[derive(Debug, Serialize, Deserialize)]
83struct Staged {
84 path: RootPath,
85 staged: String,
86}
87
88#[derive(Debug)]
90pub struct Prepared {
91 root: PathBuf,
92 plan: Plan,
93}
94
95#[derive(Debug)]
97pub struct Committed(Prepared);
98
99pub fn save(root: &Path, changes: &Changes) -> Result<(), StoreError> {
107 if changes.is_empty() {
108 return Ok(());
109 }
110 prepare(root, changes)?.commit()?.finish()
111}
112
113pub fn prepare(root: &Path, changes: &Changes) -> Result<Prepared, StoreError> {
119 let journal = root.join(JOURNAL);
120 if journal.exists() {
121 return Err(StoreError::Pending(journal));
122 }
123 let staged_dir = journal.join(STAGED);
124 fs::create_dir_all(&staged_dir).map_err(io_error(&staged_dir))?;
125 let mut write = Vec::with_capacity(changes.write.len());
126 for (index, (path, bytes)) in changes.write.iter().enumerate() {
127 let staged = index.to_string();
128 write_synced(&staged_dir.join(&staged), bytes)?;
129 write.push(Staged {
130 path: path.clone(),
131 staged,
132 });
133 }
134 sync_dir(&staged_dir)?;
135 Ok(Prepared {
136 root: root.to_owned(),
137 plan: Plan {
138 format: FORMAT.to_owned(),
139 write,
140 remove: changes.remove.iter().cloned().collect(),
141 },
142 })
143}
144
145impl Prepared {
146 pub fn commit(self) -> Result<Committed, StoreError> {
153 let journal = self.root.join(JOURNAL);
154 let staged = journal.join(PLAN_STAGED);
155 let bytes = serde_json::to_vec_pretty(&self.plan).expect("a plan serializes");
156 write_synced(&staged, &bytes)?;
157 let plan = journal.join(PLAN);
158 fs::rename(&staged, &plan).map_err(io_error(&plan))?;
159 sync_dir(&journal)?;
160 Ok(Committed(self))
161 }
162}
163
164impl Committed {
165 pub fn finish(self) -> Result<(), StoreError> {
171 finish(&self.0.root, &self.0.plan)
172 }
173}
174
175pub fn recover(root: &Path) -> Result<(), StoreError> {
182 let journal = root.join(JOURNAL);
183 if !journal.exists() {
184 return Ok(());
185 }
186 let plan_path = journal.join(PLAN);
187 match fs::read(&plan_path) {
188 Ok(bytes) => {
189 let plan: Plan = serde_json::from_slice(&bytes)
190 .map_err(|error| StoreError::Journal(plan_path.clone(), error.to_string()))?;
191 if plan.format != FORMAT {
192 return Err(StoreError::Journal(
193 plan_path,
194 format!("unknown format `{}`", plan.format),
195 ));
196 }
197 finish(root, &plan)
198 }
199 Err(error) if error.kind() == io::ErrorKind::NotFound => remove_journal(root),
200 Err(error) => Err(io_error(&plan_path)(error)),
201 }
202}
203
204fn finish(root: &Path, plan: &Plan) -> Result<(), StoreError> {
206 let staged_dir = root.join(JOURNAL).join(STAGED);
207 let mut touched = BTreeSet::new();
208 for entry in &plan.write {
209 let staged = staged_dir.join(&entry.staged);
210 let target = root.join(entry.path.as_str());
211 if staged.exists() {
212 if let Some(parent) = target.parent() {
213 fs::create_dir_all(parent).map_err(io_error(parent))?;
214 }
215 fs::rename(&staged, &target).map_err(io_error(&target))?;
216 }
217 touched.extend(target.parent().map(Path::to_owned));
218 }
219 for path in &plan.remove {
220 let target = root.join(path.as_str());
221 match fs::remove_file(&target) {
222 Ok(()) => {}
223 Err(error) if error.kind() == io::ErrorKind::NotFound => {}
224 Err(error) => return Err(io_error(&target)(error)),
225 }
226 remove_empty_parents(root, &target);
227 touched.extend(target.parent().map(Path::to_owned));
228 }
229 for dir in touched.iter().filter(|dir| dir.exists()) {
231 sync_dir(dir)?;
232 }
233 remove_journal(root)
234}
235
236fn remove_empty_parents(root: &Path, target: &Path) {
238 let spec = root.join(SPEC);
239 let mut dir = target.parent();
240 while let Some(current) = dir {
241 if current == spec || !current.starts_with(&spec) || fs::remove_dir(current).is_err() {
242 break;
243 }
244 dir = current.parent();
245 }
246}
247
248fn remove_journal(root: &Path) -> Result<(), StoreError> {
250 let journal = root.join(JOURNAL);
251 fs::remove_dir_all(&journal).map_err(io_error(&journal))?;
252 if let Some(parent) = journal.parent() {
253 let _ = fs::remove_dir(parent);
254 }
255 Ok(())
256}
257
258fn write_synced(path: &Path, bytes: &[u8]) -> Result<(), StoreError> {
259 let mut file = fs::File::create(path).map_err(io_error(path))?;
260 file.write_all(bytes).map_err(io_error(path))?;
261 file.sync_all().map_err(io_error(path))
262}
263
264fn sync_dir(dir: &Path) -> Result<(), StoreError> {
267 #[cfg(unix)]
268 fs::File::open(dir)
269 .and_then(|handle| handle.sync_all())
270 .map_err(io_error(dir))?;
271 #[cfg(not(unix))]
272 let _ = dir;
273 Ok(())
274}