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_len, wal) = wal_len_and_ident(&dir.join("wal.bin"))?.unwrap_or((0, None));
52 let intent_nonempty = not_found_is_none(std::fs::metadata(dir.join("snapshot.intent")))?
53 .is_some_and(|m| m.len() > 0);
54 Ok(DiskState {
55 snapshot,
56 wal_len,
57 wal,
58 intent_nonempty,
59 })
60}
61
62impl DiskState {
63 pub fn unchanged_until(&self, after: &DiskState) -> bool {
67 !self.intent_nonempty
68 && !after.intent_nonempty
69 && self.snapshot == after.snapshot
70 && self.wal == after.wal
71 && after.wal_len >= self.wal_len
72 }
73}
74
75fn wal_len_and_ident(path: &Path) -> io::Result<Option<(u64, Option<u64>)>> {
83 #[cfg(windows)]
84 let look = std::fs::File::open(path).and_then(|f| {
85 let len = f.metadata()?.len();
86 Ok((len, win_file_ident::ident(&f)))
87 });
88 #[cfg(unix)]
89 let look = std::fs::metadata(path).map(|m| {
90 use std::os::unix::fs::MetadataExt as _;
91 (m.len(), Some(m.ino()))
92 });
93 #[cfg(not(any(unix, windows)))]
94 let look = std::fs::metadata(path).map(|m| (m.len(), None));
95 not_found_is_none(look)
96}
97
98#[cfg(windows)]
100mod win_file_ident {
101 use std::fs::File;
102 use std::os::windows::io::AsRawHandle as _;
103
104 #[repr(C)]
105 struct ByHandleFileInformation {
106 dw_file_attributes: u32,
107 ft_creation_time: [u32; 2],
108 ft_last_access_time: [u32; 2],
109 ft_last_write_time: [u32; 2],
110 dw_volume_serial_number: u32,
111 n_file_size_high: u32,
112 n_file_size_low: u32,
113 n_number_of_links: u32,
114 n_file_index_high: u32,
115 n_file_index_low: u32,
116 }
117
118 #[link(name = "kernel32")]
119 extern "system" {
120 fn GetFileInformationByHandle(
121 h_file: *mut std::ffi::c_void,
122 lp_file_information: *mut ByHandleFileInformation,
123 ) -> i32;
124 }
125
126 pub(super) fn ident(f: &File) -> Option<u64> {
127 let mut info = ByHandleFileInformation {
130 dw_file_attributes: 0,
131 ft_creation_time: [0; 2],
132 ft_last_access_time: [0; 2],
133 ft_last_write_time: [0; 2],
134 dw_volume_serial_number: 0,
135 n_file_size_high: 0,
136 n_file_size_low: 0,
137 n_number_of_links: 0,
138 n_file_index_high: 0,
139 n_file_index_low: 0,
140 };
141 let ok = unsafe { GetFileInformationByHandle(f.as_raw_handle(), &mut info) };
142 if ok != 0 {
143 Some((info.n_file_index_high as u64) << 32 | info.n_file_index_low as u64)
144 } else {
145 None
146 }
147 }
148}
149
150pub fn open_snapshot(dir: &Path) -> io::Result<Option<(std::fs::File, Ident)>> {
152 let Some(f) = not_found_is_none(std::fs::File::open(dir.join("snapshot.bin")))? else {
153 return Ok(None);
154 };
155 let ident = ident_of(&f.metadata()?);
156 Ok(Some((f, ident)))
157}
158
159pub fn local_archives(dir: &Path) -> io::Result<Vec<(u64, u64)>> {
161 let mut out = Vec::new();
162 for entry in std::fs::read_dir(dir)? {
163 let entry = entry?;
164 let name = entry.file_name();
165 if let Some(n) = crate::manifest::parse_archive_name(&name.to_string_lossy()) {
166 if let Some(m) = not_found_is_none(entry.metadata())? {
167 out.push((n, m.len()));
168 }
169 }
170 }
171 out.sort_unstable();
172 Ok(out)
173}
174
175pub fn read_side(
178 dir: &Path,
179 name: &str,
180 known: Option<Ident>,
181) -> io::Result<Option<(Ident, Option<Vec<u8>>)>> {
182 let path = dir.join(name);
183 let Some(meta) = not_found_is_none(std::fs::metadata(&path))? else {
184 return Ok(None);
185 };
186 let ident = ident_of(&meta);
187 if known == Some(ident) {
188 return Ok(Some((ident, None)));
189 }
190 match not_found_is_none(std::fs::read(&path))? {
191 Some(bytes) => Ok(Some((ident, Some(bytes)))),
192 None => Ok(None),
193 }
194}
195
196pub const LINEAGE_FILE: &str = "backup.lineage";
198
199pub fn read_lineage(dir: &Path) -> io::Result<Option<String>> {
201 Ok(
202 not_found_is_none(std::fs::read_to_string(dir.join(LINEAGE_FILE)))?
203 .map(|s| s.trim().to_string())
204 .filter(|s| !s.is_empty()),
205 )
206}
207
208pub fn create_lineage(dir: &Path, id: &str) -> io::Result<()> {
212 use std::io::Write as _;
213 let tmp = dir.join(format!("{LINEAGE_FILE}.tmp"));
214 {
215 let mut f = std::fs::File::create(&tmp)?;
216 f.write_all(id.as_bytes())?;
217 f.sync_all()?;
218 }
219 std::fs::rename(&tmp, dir.join(LINEAGE_FILE))?;
220 #[cfg(unix)]
221 std::fs::File::open(dir)?.sync_all()?;
222 Ok(())
223}
224
225pub const PARENT_FILE: &str = "backup.parent";
228
229#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
238pub struct ParentRecord {
239 pub lineage: String,
241 pub manifest: String,
243 pub commit_seq: u64,
245}
246
247pub fn read_parent(dir: &Path) -> io::Result<Option<ParentRecord>> {
250 Ok(not_found_is_none(std::fs::read(dir.join(PARENT_FILE)))?
251 .and_then(|b| serde_json::from_slice(&b).ok()))
252}
253
254pub fn create_parent(dir: &Path, record: &ParentRecord) -> io::Result<()> {
257 use std::io::Write as _;
258 let bytes = serde_json::to_vec(record).map_err(io::Error::other)?;
259 let tmp = dir.join(format!("{PARENT_FILE}.tmp"));
260 {
261 let mut f = std::fs::File::create(&tmp)?;
262 f.write_all(&bytes)?;
263 f.sync_all()?;
264 }
265 std::fs::rename(&tmp, dir.join(PARENT_FILE))?;
266 #[cfg(unix)]
267 std::fs::File::open(dir)?.sync_all()?;
268 Ok(())
269}
270
271pub fn crc32_of(file: &mut std::fs::File) -> io::Result<String> {
274 use std::io::{Read as _, Seek as _, SeekFrom};
275 file.seek(SeekFrom::Start(0))?;
276 let mut h = crc32fast::Hasher::new();
277 let mut buf = vec![0u8; 1 << 20];
278 loop {
279 let n = file.read(&mut buf)?;
280 if n == 0 {
281 break;
282 }
283 h.update(&buf[..n]);
284 }
285 file.seek(SeekFrom::Start(0))?;
286 Ok(crate::manifest::format_crc32(h.finalize()))
287}
288
289pub fn floor(dir: &Path) -> u64 {
291 std::fs::read(dir.join("wal.floor"))
292 .ok()
293 .and_then(|b| {
294 b.get(..8)
295 .map(|s| u64::from_le_bytes(s.try_into().unwrap()))
296 })
297 .unwrap_or(0)
298}