switchyard/fs/backup/
index.rs1use std::{
2 collections::HashSet,
3 fs,
4 path::{Path, PathBuf},
5};
6
7use super::sidecar::sidecar_path_for_backup;
8
9pub(crate) fn find_latest_backup_and_sidecar(
11 target: &Path,
12 tag: &str,
13) -> Option<(Option<PathBuf>, PathBuf)> {
14 let name = target.file_name()?.to_str()?; let parent = target.parent().unwrap_or_else(|| Path::new("."));
16 let rd = fs::read_dir(parent).ok()?;
17
18 let mut best: Option<(u128, PathBuf)> = None;
20
21 for entry in rd.flatten() {
22 let fname = entry.file_name();
23 let Some(s) = fname.to_str() else { continue }; let (rest_opt, wildcard) = if tag.is_empty() {
29 let pre = format!(".{name}.");
30 (s.strip_prefix(&pre), true)
31 } else {
32 let pre = format!(".{name}.{tag}.");
33 (s.strip_prefix(&pre), false)
34 };
35 let Some(rest) = rest_opt else { continue };
36
37 let core = if let Some(core) = rest.strip_suffix(".bak") {
39 core
40 } else if let Some(core) = rest.strip_suffix(".bak.meta.json") {
41 core
42 } else {
43 continue;
44 };
45
46 let ts_part = if wildcard {
49 core.rsplit('.').next().unwrap_or(core)
50 } else {
51 core
52 };
53 let Ok(ts) = ts_part.parse::<u128>() else {
54 continue;
55 };
56
57 let base = if s.ends_with(".bak.meta.json") {
60 parent.join(&s[..s.len() - ".meta.json".len()])
61 } else {
62 parent.join(s)
63 };
64
65 let is_better = match best.as_ref() {
66 None => true,
67 Some((cur, _)) => ts > *cur,
68 };
69 if is_better {
70 best = Some((ts, base));
71 }
72 }
73
74 let (_, base) = best?;
75 let sidecar = sidecar_path_for_backup(&base);
76 let backup_present = if base.exists() { Some(base) } else { None };
77 Some((backup_present, sidecar))
78}
79
80pub(crate) fn find_previous_backup_and_sidecar(
82 target: &Path,
83 tag: &str,
84) -> Option<(Option<PathBuf>, PathBuf)> {
85 let name = target.file_name()?.to_str()?;
86 let parent = target.parent().unwrap_or_else(|| Path::new("."));
87
88 let mut seen = HashSet::<u128>::new();
89 let mut stamps: Vec<(u128, PathBuf)> = fs::read_dir(parent)
90 .ok()?
91 .filter_map(Result::ok)
92 .filter_map(|e| e.file_name().into_string().ok())
93 .filter_map(|s| {
94 let ok_prefix = if tag.is_empty() {
96 s.strip_prefix(&format!(".{name}.")).is_some()
97 } else {
98 s.strip_prefix(&format!(".{name}.{tag}.")).is_some()
99 };
100 if !ok_prefix {
101 return None;
102 }
103
104 let rest_opt = s
106 .strip_suffix(".bak")
107 .or_else(|| s.strip_suffix(".bak.meta.json"))
108 .map(ToString::to_string);
109 let core = rest_opt?;
110
111 let ts_s = core.rsplit('.').next().unwrap_or("");
113 let Ok(ts) = ts_s.parse::<u128>() else {
114 return None;
115 };
116
117 if !seen.insert(ts) {
118 return None;
119 }
120 let base = if s.ends_with(".bak.meta.json") {
122 parent.join(&s[..s.len() - ".meta.json".len()])
123 } else {
124 parent.join(&s)
125 };
126 Some((ts, base))
127 })
128 .collect();
129
130 if stamps.len() < 2 {
131 return None;
132 }
133 stamps.sort_unstable_by_key(|(ts, _)| *ts);
134 let (_ts, base) = stamps.get(stamps.len() - 2)?.clone();
135
136 let sidecar = sidecar_path_for_backup(&base);
137 let backup_present = if base.exists() { Some(base) } else { None };
138 Some((backup_present, sidecar))
139}