]>
Commit | Line | Data |
---|---|---|
b3483782 DM |
1 | use crate::tools; |
2 | ||
3 | use failure::*; | |
4 | use regex::Regex; | |
c0977501 | 5 | use std::os::unix::io::RawFd; |
aeeac29b | 6 | use std::collections::HashSet; |
b3483782 | 7 | |
aeeac29b | 8 | use chrono::{DateTime, Datelike, TimeZone, SecondsFormat, Utc, Local}; |
b3483782 DM |
9 | |
10 | use std::path::{PathBuf, Path}; | |
11 | use lazy_static::lazy_static; | |
12 | ||
13 | macro_rules! BACKUP_ID_RE { () => (r"[A-Za-z0-9][A-Za-z0-9_-]+") } | |
14 | macro_rules! BACKUP_TYPE_RE { () => (r"(?:host|vm|ct)") } | |
fa5d6977 | 15 | macro_rules! BACKUP_TIME_RE { () => (r"[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z") } |
b3483782 DM |
16 | |
17 | lazy_static!{ | |
18 | static ref BACKUP_FILE_REGEX: Regex = Regex::new( | |
c6d203bb | 19 | r"^.*\.([fd]idx|blob)$").unwrap(); |
b3483782 DM |
20 | |
21 | static ref BACKUP_TYPE_REGEX: Regex = Regex::new( | |
22 | concat!(r"^(", BACKUP_TYPE_RE!(), r")$")).unwrap(); | |
23 | ||
24 | static ref BACKUP_ID_REGEX: Regex = Regex::new( | |
25 | concat!(r"^", BACKUP_ID_RE!(), r"$")).unwrap(); | |
26 | ||
27 | static ref BACKUP_DATE_REGEX: Regex = Regex::new( | |
28 | concat!(r"^", BACKUP_TIME_RE!() ,r"$")).unwrap(); | |
29 | ||
30 | static ref GROUP_PATH_REGEX: Regex = Regex::new( | |
31 | concat!(r"(", BACKUP_TYPE_RE!(), ")/(", BACKUP_ID_RE!(), r")$")).unwrap(); | |
32 | ||
33 | static ref SNAPSHOT_PATH_REGEX: Regex = Regex::new( | |
34 | concat!(r"(", BACKUP_TYPE_RE!(), ")/(", BACKUP_ID_RE!(), ")/(", BACKUP_TIME_RE!(), r")$")).unwrap(); | |
35 | ||
36 | } | |
37 | ||
d57474e0 | 38 | /// BackupGroup is a directory containing a list of BackupDir |
35a2d8a6 | 39 | #[derive(Debug, Clone)] |
b3483782 DM |
40 | pub struct BackupGroup { |
41 | /// Type of backup | |
42 | backup_type: String, | |
43 | /// Unique (for this type) ID | |
44 | backup_id: String, | |
45 | } | |
46 | ||
47 | impl BackupGroup { | |
48 | ||
93b49ce3 | 49 | pub fn new<T: Into<String>, U: Into<String>>(backup_type: T, backup_id: U) -> Self { |
b3483782 DM |
50 | Self { backup_type: backup_type.into(), backup_id: backup_id.into() } |
51 | } | |
52 | ||
53 | pub fn backup_type(&self) -> &str { | |
54 | &self.backup_type | |
55 | } | |
56 | ||
57 | pub fn backup_id(&self) -> &str { | |
58 | &self.backup_id | |
59 | } | |
60 | ||
61 | pub fn parse(path: &str) -> Result<Self, Error> { | |
62 | ||
63 | let cap = GROUP_PATH_REGEX.captures(path) | |
64 | .ok_or_else(|| format_err!("unable to parse backup group path '{}'", path))?; | |
65 | ||
66 | Ok(Self { | |
67 | backup_type: cap.get(1).unwrap().as_str().to_owned(), | |
68 | backup_id: cap.get(2).unwrap().as_str().to_owned(), | |
69 | }) | |
70 | } | |
71 | ||
72 | pub fn group_path(&self) -> PathBuf { | |
73 | ||
74 | let mut relative_path = PathBuf::new(); | |
75 | ||
76 | relative_path.push(&self.backup_type); | |
77 | ||
78 | relative_path.push(&self.backup_id); | |
79 | ||
80 | relative_path | |
81 | } | |
c0977501 DM |
82 | |
83 | pub fn list_backups(&self, base_path: &Path) -> Result<Vec<BackupInfo>, Error> { | |
84 | ||
85 | let mut list = vec![]; | |
86 | ||
87 | let mut path = base_path.to_owned(); | |
88 | path.push(self.group_path()); | |
89 | ||
90 | tools::scandir(libc::AT_FDCWD, &path, &BACKUP_DATE_REGEX, |l2_fd, backup_time, file_type| { | |
91 | if file_type != nix::dir::Type::Directory { return Ok(()); } | |
92 | ||
fa5d6977 DM |
93 | let dt = backup_time.parse::<DateTime<Utc>>()?; |
94 | let backup_dir = BackupDir::new(self.backup_type.clone(), self.backup_id.clone(), dt.timestamp()); | |
c0977501 DM |
95 | let files = list_backup_files(l2_fd, backup_time)?; |
96 | ||
97 | list.push(BackupInfo { backup_dir, files }); | |
98 | ||
99 | Ok(()) | |
100 | })?; | |
101 | Ok(list) | |
102 | } | |
aeeac29b DM |
103 | |
104 | fn mark_selections<F: Fn(DateTime<Local>, &BackupInfo) -> String> ( | |
105 | mark: &mut HashSet<PathBuf>, | |
106 | list: &Vec<BackupInfo>, | |
107 | keep: usize, | |
108 | select_id: F, | |
109 | ){ | |
110 | let mut hash = HashSet::new(); | |
111 | for info in list { | |
112 | let local_time = info.backup_dir.backup_time().with_timezone(&Local); | |
113 | if hash.len() >= keep as usize { break; } | |
114 | let backup_id = info.backup_dir.relative_path(); | |
115 | let sel_id: String = select_id(local_time, &info); | |
116 | if !hash.contains(&sel_id) { | |
117 | hash.insert(sel_id); | |
118 | mark.insert(backup_id); | |
119 | } | |
120 | } | |
121 | } | |
122 | ||
123 | pub fn compute_prune_list( | |
124 | mut list: Vec<BackupInfo>, | |
125 | keep_last: Option<u64>, | |
126 | keep_daily: Option<u64>, | |
127 | keep_weekly: Option<u64>, | |
128 | keep_monthly: Option<u64>, | |
129 | keep_yearly: Option<u64>, | |
130 | ) -> Result<Vec<BackupInfo>, Error> { | |
131 | ||
132 | let mut mark = HashSet::new(); | |
133 | ||
134 | BackupInfo::sort_list(&mut list, false); | |
135 | ||
136 | if let Some(keep_last) = keep_last { | |
137 | list.iter().take(keep_last as usize).for_each(|info| { | |
138 | mark.insert(info.backup_dir.relative_path()); | |
139 | }); | |
140 | } | |
141 | ||
142 | if let Some(keep_daily) = keep_daily { | |
143 | Self::mark_selections(&mut mark, &list, keep_daily as usize, |local_time, _info| { | |
144 | format!("{}/{}/{}", local_time.year(), local_time.month(), local_time.day()) | |
145 | }); | |
146 | } | |
147 | ||
148 | if let Some(keep_weekly) = keep_weekly { | |
149 | Self::mark_selections(&mut mark, &list, keep_weekly as usize, |local_time, _info| { | |
150 | format!("{}/{}", local_time.year(), local_time.iso_week().week()) | |
151 | }); | |
152 | } | |
153 | ||
154 | if let Some(keep_monthly) = keep_monthly { | |
155 | Self::mark_selections(&mut mark, &list, keep_monthly as usize, |local_time, _info| { | |
156 | format!("{}/{}", local_time.year(), local_time.month()) | |
157 | }); | |
158 | } | |
159 | ||
160 | if let Some(keep_yearly) = keep_yearly { | |
161 | Self::mark_selections(&mut mark, &list, keep_yearly as usize, |local_time, _info| { | |
162 | format!("{}/{}", local_time.year(), local_time.year()) | |
163 | }); | |
164 | } | |
165 | ||
166 | let mut remove_list: Vec<BackupInfo> = list.into_iter() | |
167 | .filter(|info| !mark.contains(&info.backup_dir.relative_path())).collect(); | |
168 | ||
169 | BackupInfo::sort_list(&mut remove_list, true); | |
170 | ||
171 | Ok(remove_list) | |
172 | } | |
b3483782 DM |
173 | } |
174 | ||
175 | /// Uniquely identify a Backup (relative to data store) | |
d57474e0 DM |
176 | /// |
177 | /// We also call this a backup snaphost. | |
35a2d8a6 | 178 | #[derive(Debug, Clone)] |
b3483782 DM |
179 | pub struct BackupDir { |
180 | /// Backup group | |
181 | group: BackupGroup, | |
182 | /// Backup timestamp | |
fa5d6977 | 183 | backup_time: DateTime<Utc>, |
b3483782 DM |
184 | } |
185 | ||
186 | impl BackupDir { | |
187 | ||
391d3107 WB |
188 | pub fn new<T, U>(backup_type: T, backup_id: U, timestamp: i64) -> Self |
189 | where | |
190 | T: Into<String>, | |
191 | U: Into<String>, | |
192 | { | |
b3483782 | 193 | // Note: makes sure that nanoseconds is 0 |
391d3107 WB |
194 | Self { |
195 | group: BackupGroup::new(backup_type.into(), backup_id.into()), | |
fa5d6977 | 196 | backup_time: Utc.timestamp(timestamp, 0), |
391d3107 | 197 | } |
b3483782 | 198 | } |
51a4f63f | 199 | pub fn new_with_group(group: BackupGroup, timestamp: i64) -> Self { |
fa5d6977 | 200 | Self { group, backup_time: Utc.timestamp(timestamp, 0) } |
51a4f63f | 201 | } |
b3483782 DM |
202 | |
203 | pub fn group(&self) -> &BackupGroup { | |
204 | &self.group | |
205 | } | |
206 | ||
fa5d6977 | 207 | pub fn backup_time(&self) -> DateTime<Utc> { |
b3483782 DM |
208 | self.backup_time |
209 | } | |
210 | ||
211 | pub fn parse(path: &str) -> Result<Self, Error> { | |
212 | ||
213 | let cap = SNAPSHOT_PATH_REGEX.captures(path) | |
214 | .ok_or_else(|| format_err!("unable to parse backup snapshot path '{}'", path))?; | |
215 | ||
216 | let group = BackupGroup::new(cap.get(1).unwrap().as_str(), cap.get(2).unwrap().as_str()); | |
fa5d6977 DM |
217 | let backup_time = cap.get(3).unwrap().as_str().parse::<DateTime<Utc>>()?; |
218 | Ok(BackupDir::from((group, backup_time.timestamp()))) | |
b3483782 DM |
219 | } |
220 | ||
221 | pub fn relative_path(&self) -> PathBuf { | |
222 | ||
223 | let mut relative_path = self.group.group_path(); | |
224 | ||
fa5d6977 | 225 | relative_path.push(Self::backup_time_to_string(self.backup_time)); |
b3483782 DM |
226 | |
227 | relative_path | |
228 | } | |
fa5d6977 DM |
229 | |
230 | pub fn backup_time_to_string(backup_time: DateTime<Utc>) -> String { | |
231 | backup_time.to_rfc3339_opts(SecondsFormat::Secs, true) | |
232 | } | |
b3483782 DM |
233 | } |
234 | ||
391d3107 WB |
235 | impl From<(BackupGroup, i64)> for BackupDir { |
236 | fn from((group, timestamp): (BackupGroup, i64)) -> Self { | |
fa5d6977 | 237 | Self { group, backup_time: Utc.timestamp(timestamp, 0) } |
391d3107 WB |
238 | } |
239 | } | |
240 | ||
d57474e0 | 241 | /// Detailed Backup Information, lists files inside a BackupDir |
b02a52e3 | 242 | #[derive(Debug, Clone)] |
b3483782 DM |
243 | pub struct BackupInfo { |
244 | /// the backup directory | |
245 | pub backup_dir: BackupDir, | |
246 | /// List of data files | |
247 | pub files: Vec<String>, | |
248 | } | |
249 | ||
250 | impl BackupInfo { | |
251 | ||
38a6cdda DM |
252 | pub fn new(base_path: &Path, backup_dir: BackupDir) -> Result<BackupInfo, Error> { |
253 | let mut path = base_path.to_owned(); | |
254 | path.push(backup_dir.relative_path()); | |
255 | ||
256 | let files = list_backup_files(libc::AT_FDCWD, &path)?; | |
257 | ||
258 | Ok(BackupInfo { backup_dir, files }) | |
259 | } | |
260 | ||
51a4f63f DM |
261 | /// Finds the latest backup inside a backup group |
262 | pub fn last_backup(base_path: &Path, group: &BackupGroup) -> Result<Option<BackupInfo>, Error> { | |
263 | let backups = group.list_backups(base_path)?; | |
264 | Ok(backups.into_iter().max_by_key(|item| item.backup_dir.backup_time())) | |
265 | } | |
266 | ||
b3483782 DM |
267 | pub fn sort_list(list: &mut Vec<BackupInfo>, ascendending: bool) { |
268 | if ascendending { // oldest first | |
269 | list.sort_unstable_by(|a, b| a.backup_dir.backup_time.cmp(&b.backup_dir.backup_time)); | |
270 | } else { // newest first | |
271 | list.sort_unstable_by(|a, b| b.backup_dir.backup_time.cmp(&a.backup_dir.backup_time)); | |
272 | } | |
273 | } | |
274 | ||
58e99e13 DM |
275 | pub fn list_files(base_path: &Path, backup_dir: &BackupDir) -> Result<Vec<String>, Error> { |
276 | let mut path = base_path.to_owned(); | |
277 | path.push(backup_dir.relative_path()); | |
278 | ||
c0977501 | 279 | let files = list_backup_files(libc::AT_FDCWD, &path)?; |
58e99e13 DM |
280 | |
281 | Ok(files) | |
282 | } | |
283 | ||
284 | pub fn list_backups(base_path: &Path) -> Result<Vec<BackupInfo>, Error> { | |
b3483782 DM |
285 | let mut list = vec![]; |
286 | ||
58e99e13 | 287 | tools::scandir(libc::AT_FDCWD, base_path, &BACKUP_TYPE_REGEX, |l0_fd, backup_type, file_type| { |
b3483782 DM |
288 | if file_type != nix::dir::Type::Directory { return Ok(()); } |
289 | tools::scandir(l0_fd, backup_type, &BACKUP_ID_REGEX, |l1_fd, backup_id, file_type| { | |
290 | if file_type != nix::dir::Type::Directory { return Ok(()); } | |
291 | tools::scandir(l1_fd, backup_id, &BACKUP_DATE_REGEX, |l2_fd, backup_time, file_type| { | |
292 | if file_type != nix::dir::Type::Directory { return Ok(()); } | |
293 | ||
fa5d6977 DM |
294 | let dt = backup_time.parse::<DateTime<Utc>>()?; |
295 | let backup_dir = BackupDir::new(backup_type, backup_id, dt.timestamp()); | |
b3483782 | 296 | |
c0977501 | 297 | let files = list_backup_files(l2_fd, backup_time)?; |
b3483782 | 298 | |
c0977501 | 299 | list.push(BackupInfo { backup_dir, files }); |
b3483782 DM |
300 | |
301 | Ok(()) | |
302 | }) | |
303 | }) | |
304 | })?; | |
305 | Ok(list) | |
306 | } | |
307 | } | |
c0977501 DM |
308 | |
309 | fn list_backup_files<P: ?Sized + nix::NixPath>(dirfd: RawFd, path: &P) -> Result<Vec<String>, Error> { | |
310 | let mut files = vec![]; | |
311 | ||
312 | tools::scandir(dirfd, path, &BACKUP_FILE_REGEX, |_, filename, file_type| { | |
313 | if file_type != nix::dir::Type::File { return Ok(()); } | |
314 | files.push(filename.to_owned()); | |
315 | Ok(()) | |
316 | })?; | |
317 | ||
318 | Ok(files) | |
319 | } |