1use std::io;
8use std::path::Path;
9use std::time::UNIX_EPOCH;
10
11pub type Ident = (u64, u64);
14
15pub fn ident_of(meta: &std::fs::Metadata) -> Ident {
17 let mtime_nanos = meta
18 .modified()
19 .ok()
20 .and_then(|t| t.duration_since(UNIX_EPOCH).ok())
21 .map(|d| d.as_nanos() as u64)
22 .unwrap_or(0);
23 (meta.len(), mtime_nanos)
24}
25
26fn not_found_is_none<T>(r: io::Result<T>) -> io::Result<Option<T>> {
27 match r {
28 Ok(v) => Ok(Some(v)),
29 Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(None),
30 Err(e) => Err(e),
31 }
32}
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub struct DiskState {
37 pub snapshot: Option<Ident>,
39 pub wal_len: u64,
41 pub wal: Option<u64>,
43 pub intent_nonempty: bool,
46}
47
48pub fn observe(dir: &Path) -> io::Result<DiskState> {
49 let snapshot =
50 not_found_is_none(std::fs::metadata(dir.join("snapshot.bin")))?.map(|m| ident_of(&m));
51 let wal_meta = not_found_is_none(std::fs::metadata(dir.join("wal.bin")))?;
52 let wal_len = wal_meta.as_ref().map_or(0, |m| m.len());
53 let wal = wal_meta.as_ref().and_then(file_ident);
54 let intent_nonempty = not_found_is_none(std::fs::metadata(dir.join("snapshot.intent")))?
55 .is_some_and(|m| m.len() > 0);
56 Ok(DiskState {
57 snapshot,
58 wal_len,
59 wal,
60 intent_nonempty,
61 })
62}
63
64impl DiskState {
65 pub fn unchanged_until(&self, after: &DiskState) -> bool {
69 !self.intent_nonempty
70 && !after.intent_nonempty
71 && self.snapshot == after.snapshot
72 && self.wal == after.wal
73 && after.wal_len >= self.wal_len
74 }
75}
76
77fn file_ident(meta: &std::fs::Metadata) -> Option<u64> {
80 #[cfg(unix)]
81 {
82 use std::os::unix::fs::MetadataExt as _;
83 Some(meta.ino())
84 }
85 #[cfg(not(unix))]
86 {
87 let _ = meta;
88 None
89 }
90}
91
92pub fn open_snapshot(dir: &Path) -> io::Result<Option<(std::fs::File, Ident)>> {
94 let Some(f) = not_found_is_none(std::fs::File::open(dir.join("snapshot.bin")))? else {
95 return Ok(None);
96 };
97 let ident = ident_of(&f.metadata()?);
98 Ok(Some((f, ident)))
99}
100
101pub fn local_archives(dir: &Path) -> io::Result<Vec<(u64, u64)>> {
103 let mut out = Vec::new();
104 for entry in std::fs::read_dir(dir)? {
105 let entry = entry?;
106 let name = entry.file_name();
107 if let Some(n) = crate::manifest::parse_archive_name(&name.to_string_lossy()) {
108 if let Some(m) = not_found_is_none(entry.metadata())? {
109 out.push((n, m.len()));
110 }
111 }
112 }
113 out.sort_unstable();
114 Ok(out)
115}
116
117pub fn read_side(
120 dir: &Path,
121 name: &str,
122 known: Option<Ident>,
123) -> io::Result<Option<(Ident, Option<Vec<u8>>)>> {
124 let path = dir.join(name);
125 let Some(meta) = not_found_is_none(std::fs::metadata(&path))? else {
126 return Ok(None);
127 };
128 let ident = ident_of(&meta);
129 if known == Some(ident) {
130 return Ok(Some((ident, None)));
131 }
132 match not_found_is_none(std::fs::read(&path))? {
133 Some(bytes) => Ok(Some((ident, Some(bytes)))),
134 None => Ok(None),
135 }
136}
137
138pub const LINEAGE_FILE: &str = "backup.lineage";
140
141pub fn read_lineage(dir: &Path) -> io::Result<Option<String>> {
143 Ok(
144 not_found_is_none(std::fs::read_to_string(dir.join(LINEAGE_FILE)))?
145 .map(|s| s.trim().to_string())
146 .filter(|s| !s.is_empty()),
147 )
148}
149
150pub fn create_lineage(dir: &Path, id: &str) -> io::Result<()> {
154 use std::io::Write as _;
155 let tmp = dir.join(format!("{LINEAGE_FILE}.tmp"));
156 {
157 let mut f = std::fs::File::create(&tmp)?;
158 f.write_all(id.as_bytes())?;
159 f.sync_all()?;
160 }
161 std::fs::rename(&tmp, dir.join(LINEAGE_FILE))?;
162 #[cfg(unix)]
163 std::fs::File::open(dir)?.sync_all()?;
164 Ok(())
165}
166
167pub const PARENT_FILE: &str = "backup.parent";
170
171#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
180pub struct ParentRecord {
181 pub lineage: String,
183 pub manifest: String,
185 pub commit_seq: u64,
187}
188
189pub fn read_parent(dir: &Path) -> io::Result<Option<ParentRecord>> {
192 Ok(not_found_is_none(std::fs::read(dir.join(PARENT_FILE)))?
193 .and_then(|b| serde_json::from_slice(&b).ok()))
194}
195
196pub fn create_parent(dir: &Path, record: &ParentRecord) -> io::Result<()> {
199 use std::io::Write as _;
200 let bytes = serde_json::to_vec(record).map_err(io::Error::other)?;
201 let tmp = dir.join(format!("{PARENT_FILE}.tmp"));
202 {
203 let mut f = std::fs::File::create(&tmp)?;
204 f.write_all(&bytes)?;
205 f.sync_all()?;
206 }
207 std::fs::rename(&tmp, dir.join(PARENT_FILE))?;
208 #[cfg(unix)]
209 std::fs::File::open(dir)?.sync_all()?;
210 Ok(())
211}
212
213pub fn crc32_of(file: &mut std::fs::File) -> io::Result<String> {
216 use std::io::{Read as _, Seek as _, SeekFrom};
217 file.seek(SeekFrom::Start(0))?;
218 let mut h = crc32fast::Hasher::new();
219 let mut buf = vec![0u8; 1 << 20];
220 loop {
221 let n = file.read(&mut buf)?;
222 if n == 0 {
223 break;
224 }
225 h.update(&buf[..n]);
226 }
227 file.seek(SeekFrom::Start(0))?;
228 Ok(crate::manifest::format_crc32(h.finalize()))
229}
230
231pub fn floor(dir: &Path) -> u64 {
233 std::fs::read(dir.join("wal.floor"))
234 .ok()
235 .and_then(|b| {
236 b.get(..8)
237 .map(|s| u64::from_le_bytes(s.try_into().unwrap()))
238 })
239 .unwrap_or(0)
240}