git_policy_cli/
subvolume_journal.rs1use super::iso_time::{format_iso_milliseconds, now_iso};
17use super::subvolume_error::MigrateError;
18use serde_json::{Map, Value};
19use std::collections::BTreeMap;
20use std::io::Write;
21use std::path::{Path, PathBuf};
22
23const SCHEMA: u64 = 1;
25
26#[derive(Clone, Debug, Eq, PartialEq)]
37pub struct Journal {
38 path: PathBuf,
40 started: String,
42 states: BTreeMap<String, String>,
44 times: BTreeMap<String, i64>,
46 persisted: bool,
48}
49
50impl Journal {
51 pub fn load(path: &Path) -> Result<Journal, MigrateError> {
62 let text: String = match std::fs::read_to_string(path) {
63 Ok(found) => found,
64 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
65 return Ok(Journal { path: path.to_path_buf(), started: now_iso(), states: BTreeMap::new(), times: BTreeMap::new(), persisted: false });
66 }
67 Err(error) => return Err(MigrateError::io("read the journal", path, &error)),
68 };
69 let broken = |why: &str| { return MigrateError::Journal(format!("the journal {} {why}; read it, and delete it only if no migration is in progress", path.display())); };
70 let value: Value = serde_json::from_str(text.as_str()).map_err(|error: serde_json::Error| { return broken(format!("is not valid JSON ({error})").as_str()); })?;
71 if value.get("schema").and_then(Value::as_u64) != Some(SCHEMA) {
72 return Err(broken("has an unknown schema version"));
73 }
74 let started: String = value.get("started").and_then(Value::as_str).ok_or_else(|| { return broken("has no start time"); })?.to_string();
75 let steps: &Map<String, Value> = value.get("steps").and_then(Value::as_object).ok_or_else(|| { return broken("has no steps"); })?;
76 let mut states: BTreeMap<String, String> = BTreeMap::new();
77 let mut times: BTreeMap<String, i64> = BTreeMap::new();
78 for (name, record) in steps {
79 let state: &str = record.get("state").and_then(Value::as_str).ok_or_else(|| { return broken("has a step without a state"); })?;
80 states.insert(name.clone(), state.to_string());
81 if let Some(milliseconds) = record.get("epoch_ms").and_then(Value::as_i64) {
82 times.insert(name.clone(), milliseconds);
83 }
84 }
85 return Ok(Journal { path: path.to_path_buf(), started, states, times, persisted: true });
86 }
87
88 pub fn started(&self) -> &str {
90 return self.started.as_str();
91 }
92
93 pub fn path(&self) -> &Path {
95 return self.path.as_path();
96 }
97
98 pub fn is_persisted(&self) -> bool {
100 return self.persisted;
101 }
102
103 pub fn seconds_of(&self, step: &str) -> Option<i64> {
106 return self.times.get(step).map(|milliseconds: &i64| { return milliseconds.div_euclid(1000); });
107 }
108
109 pub fn state(&self, step: &str) -> Option<&str> {
111 return self.states.get(step).map(String::as_str);
112 }
113
114 pub fn is_done(&self, step: &str) -> bool {
116 return self.state(step) == Some("done");
117 }
118
119 pub fn has_begun(&self, step: &str) -> bool {
121 return matches!(self.state(step), Some("started" | "done"));
122 }
123
124 pub fn mark(&mut self, step: &str, state: &str) -> Result<(), MigrateError> {
135 self.states.insert(step.to_string(), state.to_string());
136 let now: i64 = std::time::SystemTime::now()
137 .duration_since(std::time::UNIX_EPOCH)
138 .map_or(0, |elapsed: std::time::Duration| { return i64::try_from(elapsed.as_millis()).unwrap_or(i64::MAX); });
139 self.times.insert(step.to_string(), now);
140 let mut steps: Map<String, Value> = Map::new();
141 for (name, recorded) in &self.states {
142 let milliseconds: i64 = self.times.get(name).copied().unwrap_or(now);
143 let mut record: Map<String, Value> = Map::new();
144 record.insert(String::from("state"), Value::String(recorded.clone()));
145 record.insert(String::from("at"), Value::String(format_iso_milliseconds(milliseconds)));
146 record.insert(String::from("epoch_ms"), Value::from(milliseconds));
147 steps.insert(name.clone(), Value::Object(record));
148 }
149 let mut document: Map<String, Value> = Map::new();
150 document.insert(String::from("schema"), Value::from(SCHEMA));
151 document.insert(String::from("started"), Value::String(self.started.clone()));
152 document.insert(String::from("steps"), Value::Object(steps));
153 let text: String = serde_json::to_string_pretty(&Value::Object(document)).unwrap_or_default();
154 let mut sibling: std::ffi::OsString = self.path.as_os_str().to_os_string();
155 sibling.push(".tmp");
156 let sibling_path: PathBuf = PathBuf::from(sibling);
157 let write = || -> std::io::Result<()> {
158 let mut file: std::fs::File = std::fs::File::create(&sibling_path)?;
159 file.write_all(text.as_bytes())?;
160 file.sync_all()?;
161 return std::fs::rename(&sibling_path, &self.path);
162 };
163 write().map_err(|error: std::io::Error| { return MigrateError::io("write the journal", self.path.as_path(), &error); })?;
164 self.persisted = true;
165 return Ok(());
166 }
167
168 pub fn delete(&mut self) -> Result<(), MigrateError> {
178 match std::fs::remove_file(&self.path) {
179 Ok(()) => {}
180 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
181 Err(error) => return Err(MigrateError::io("delete the journal", self.path.as_path(), &error)),
182 }
183 self.states.clear();
184 self.times.clear();
185 self.persisted = false;
186 return Ok(());
187 }
188}
189
190#[cfg(test)]
192#[path = "subvolume_journal_tests.rs"]
193mod tests;