]>
Commit | Line | Data |
---|---|---|
b3483782 DM |
1 | use crate::tools; |
2 | ||
f7d4e4b5 | 3 | use anyhow::{bail, format_err, Error}; |
c0977501 | 4 | use std::os::unix::io::RawFd; |
b3483782 | 5 | |
cded320e | 6 | use std::path::{Path, PathBuf}; |
038ee599 DM |
7 | |
8 | use proxmox::const_regex; | |
b3483782 | 9 | |
8f14e8fe DM |
10 | use super::manifest::MANIFEST_BLOB_NAME; |
11 | ||
cded320e TL |
12 | macro_rules! BACKUP_ID_RE { |
13 | () => { | |
14 | r"[A-Za-z0-9_][A-Za-z0-9._\-]*" | |
15 | }; | |
16 | } | |
17 | macro_rules! BACKUP_TYPE_RE { | |
18 | () => { | |
19 | r"(?:host|vm|ct)" | |
20 | }; | |
21 | } | |
22 | macro_rules! BACKUP_TIME_RE { | |
23 | () => { | |
24 | r"[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z" | |
25 | }; | |
26 | } | |
b3483782 | 27 | |
5e42d385 DC |
28 | #[macro_export] |
29 | macro_rules! SNAPSHOT_PATH_REGEX_STR { | |
30 | () => ( | |
31 | concat!(r"(", BACKUP_TYPE_RE!(), ")/(", BACKUP_ID_RE!(), ")/(", BACKUP_TIME_RE!(), r")") | |
32 | ); | |
33 | } | |
34 | ||
cded320e | 35 | const_regex! { |
038ee599 | 36 | BACKUP_FILE_REGEX = r"^.*\.([fd]idx|blob)$"; |
b3483782 | 37 | |
038ee599 | 38 | BACKUP_TYPE_REGEX = concat!(r"^(", BACKUP_TYPE_RE!(), r")$"); |
b3483782 | 39 | |
038ee599 | 40 | pub BACKUP_ID_REGEX = concat!(r"^", BACKUP_ID_RE!(), r"$"); |
b3483782 | 41 | |
038ee599 | 42 | BACKUP_DATE_REGEX = concat!(r"^", BACKUP_TIME_RE!() ,r"$"); |
b3483782 | 43 | |
038ee599 | 44 | GROUP_PATH_REGEX = concat!(r"^(", BACKUP_TYPE_RE!(), ")/(", BACKUP_ID_RE!(), r")$"); |
b3483782 | 45 | |
038ee599 | 46 | SNAPSHOT_PATH_REGEX = concat!( |
5e42d385 | 47 | r"^", SNAPSHOT_PATH_REGEX_STR!(), r"$"); |
b3483782 DM |
48 | } |
49 | ||
d57474e0 | 50 | /// BackupGroup is a directory containing a list of BackupDir |
11d89239 | 51 | #[derive(Debug, Eq, PartialEq, Hash, Clone)] |
b3483782 DM |
52 | pub struct BackupGroup { |
53 | /// Type of backup | |
54 | backup_type: String, | |
55 | /// Unique (for this type) ID | |
56 | backup_id: String, | |
57 | } | |
58 | ||
4264c502 | 59 | impl std::cmp::Ord for BackupGroup { |
4264c502 DM |
60 | fn cmp(&self, other: &Self) -> std::cmp::Ordering { |
61 | let type_order = self.backup_type.cmp(&other.backup_type); | |
62 | if type_order != std::cmp::Ordering::Equal { | |
63 | return type_order; | |
64 | } | |
65 | // try to compare IDs numerically | |
66 | let id_self = self.backup_id.parse::<u64>(); | |
67 | let id_other = other.backup_id.parse::<u64>(); | |
68 | match (id_self, id_other) { | |
69 | (Ok(id_self), Ok(id_other)) => id_self.cmp(&id_other), | |
70 | (Ok(_), Err(_)) => std::cmp::Ordering::Less, | |
71 | (Err(_), Ok(_)) => std::cmp::Ordering::Greater, | |
cded320e | 72 | _ => self.backup_id.cmp(&other.backup_id), |
4264c502 DM |
73 | } |
74 | } | |
75 | } | |
76 | ||
77 | impl std::cmp::PartialOrd for BackupGroup { | |
78 | fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> { | |
79 | Some(self.cmp(other)) | |
80 | } | |
81 | } | |
82 | ||
b3483782 | 83 | impl BackupGroup { |
93b49ce3 | 84 | pub fn new<T: Into<String>, U: Into<String>>(backup_type: T, backup_id: U) -> Self { |
cded320e TL |
85 | Self { |
86 | backup_type: backup_type.into(), | |
87 | backup_id: backup_id.into(), | |
88 | } | |
b3483782 DM |
89 | } |
90 | ||
91 | pub fn backup_type(&self) -> &str { | |
92 | &self.backup_type | |
93 | } | |
94 | ||
95 | pub fn backup_id(&self) -> &str { | |
96 | &self.backup_id | |
97 | } | |
98 | ||
cded320e | 99 | pub fn group_path(&self) -> PathBuf { |
b3483782 DM |
100 | let mut relative_path = PathBuf::new(); |
101 | ||
102 | relative_path.push(&self.backup_type); | |
103 | ||
104 | relative_path.push(&self.backup_id); | |
105 | ||
106 | relative_path | |
107 | } | |
c0977501 DM |
108 | |
109 | pub fn list_backups(&self, base_path: &Path) -> Result<Vec<BackupInfo>, Error> { | |
c0977501 DM |
110 | let mut list = vec![]; |
111 | ||
112 | let mut path = base_path.to_owned(); | |
113 | path.push(self.group_path()); | |
114 | ||
cded320e TL |
115 | tools::scandir( |
116 | libc::AT_FDCWD, | |
117 | &path, | |
118 | &BACKUP_DATE_REGEX, | |
119 | |l2_fd, backup_time, file_type| { | |
120 | if file_type != nix::dir::Type::Directory { | |
121 | return Ok(()); | |
122 | } | |
c0977501 | 123 | |
cded320e TL |
124 | let backup_dir = |
125 | BackupDir::with_rfc3339(&self.backup_type, &self.backup_id, backup_time)?; | |
126 | let files = list_backup_files(l2_fd, backup_time)?; | |
c0977501 | 127 | |
cded320e | 128 | list.push(BackupInfo { backup_dir, files }); |
c0977501 | 129 | |
cded320e TL |
130 | Ok(()) |
131 | }, | |
132 | )?; | |
c0977501 DM |
133 | Ok(list) |
134 | } | |
aeeac29b | 135 | |
cded320e | 136 | pub fn last_successful_backup(&self, base_path: &Path) -> Result<Option<i64>, Error> { |
8f14e8fe DM |
137 | let mut last = None; |
138 | ||
139 | let mut path = base_path.to_owned(); | |
140 | path.push(self.group_path()); | |
141 | ||
cded320e TL |
142 | tools::scandir( |
143 | libc::AT_FDCWD, | |
144 | &path, | |
145 | &BACKUP_DATE_REGEX, | |
146 | |l2_fd, backup_time, file_type| { | |
147 | if file_type != nix::dir::Type::Directory { | |
148 | return Ok(()); | |
8f14e8fe | 149 | } |
8f14e8fe | 150 | |
cded320e TL |
151 | let mut manifest_path = PathBuf::from(backup_time); |
152 | manifest_path.push(MANIFEST_BLOB_NAME); | |
153 | ||
154 | use nix::fcntl::{openat, OFlag}; | |
155 | match openat( | |
156 | l2_fd, | |
157 | &manifest_path, | |
158 | OFlag::O_RDONLY, | |
159 | nix::sys::stat::Mode::empty(), | |
160 | ) { | |
161 | Ok(rawfd) => { | |
162 | /* manifest exists --> assume backup was successful */ | |
163 | /* close else this leaks! */ | |
164 | nix::unistd::close(rawfd)?; | |
165 | } | |
166 | Err(nix::Error::Sys(nix::errno::Errno::ENOENT)) => { | |
167 | return Ok(()); | |
168 | } | |
169 | Err(err) => { | |
170 | bail!("last_successful_backup: unexpected error - {}", err); | |
171 | } | |
172 | } | |
173 | ||
174 | let timestamp = proxmox::tools::time::parse_rfc3339(backup_time)?; | |
175 | if let Some(last_timestamp) = last { | |
176 | if timestamp > last_timestamp { | |
177 | last = Some(timestamp); | |
178 | } | |
179 | } else { | |
180 | last = Some(timestamp); | |
181 | } | |
8f14e8fe | 182 | |
cded320e TL |
183 | Ok(()) |
184 | }, | |
185 | )?; | |
8f14e8fe DM |
186 | |
187 | Ok(last) | |
188 | } | |
b3483782 DM |
189 | } |
190 | ||
23f74c19 DM |
191 | impl std::fmt::Display for BackupGroup { |
192 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | |
193 | let backup_type = self.backup_type(); | |
194 | let id = self.backup_id(); | |
195 | write!(f, "{}/{}", backup_type, id) | |
196 | } | |
197 | } | |
198 | ||
d6d3b353 DM |
199 | impl std::str::FromStr for BackupGroup { |
200 | type Err = Error; | |
201 | ||
202 | /// Parse a backup group path | |
203 | /// | |
204 | /// This parses strings like `vm/100". | |
205 | fn from_str(path: &str) -> Result<Self, Self::Err> { | |
cded320e TL |
206 | let cap = GROUP_PATH_REGEX |
207 | .captures(path) | |
d6d3b353 DM |
208 | .ok_or_else(|| format_err!("unable to parse backup group path '{}'", path))?; |
209 | ||
210 | Ok(Self { | |
211 | backup_type: cap.get(1).unwrap().as_str().to_owned(), | |
212 | backup_id: cap.get(2).unwrap().as_str().to_owned(), | |
213 | }) | |
214 | } | |
215 | } | |
216 | ||
b3483782 | 217 | /// Uniquely identify a Backup (relative to data store) |
d57474e0 DM |
218 | /// |
219 | /// We also call this a backup snaphost. | |
8b5f72b1 | 220 | #[derive(Debug, Eq, PartialEq, Clone)] |
b3483782 DM |
221 | pub struct BackupDir { |
222 | /// Backup group | |
223 | group: BackupGroup, | |
224 | /// Backup timestamp | |
6a7be83e DM |
225 | backup_time: i64, |
226 | // backup_time as rfc3339 | |
cded320e | 227 | backup_time_string: String, |
b3483782 DM |
228 | } |
229 | ||
230 | impl BackupDir { | |
6a7be83e | 231 | pub fn new<T, U>(backup_type: T, backup_id: U, backup_time: i64) -> Result<Self, Error> |
391d3107 WB |
232 | where |
233 | T: Into<String>, | |
234 | U: Into<String>, | |
235 | { | |
e0e5b442 | 236 | let group = BackupGroup::new(backup_type.into(), backup_id.into()); |
d09db6c2 | 237 | BackupDir::with_group(group, backup_time) |
b3483782 | 238 | } |
e0e5b442 | 239 | |
cded320e TL |
240 | pub fn with_rfc3339<T, U, V>( |
241 | backup_type: T, | |
242 | backup_id: U, | |
243 | backup_time_string: V, | |
244 | ) -> Result<Self, Error> | |
bc871bd1 DM |
245 | where |
246 | T: Into<String>, | |
247 | U: Into<String>, | |
248 | V: Into<String>, | |
249 | { | |
cded320e | 250 | let backup_time_string = backup_time_string.into(); |
bc871bd1 DM |
251 | let backup_time = proxmox::tools::time::parse_rfc3339(&backup_time_string)?; |
252 | let group = BackupGroup::new(backup_type.into(), backup_id.into()); | |
cded320e TL |
253 | Ok(Self { |
254 | group, | |
255 | backup_time, | |
256 | backup_time_string, | |
257 | }) | |
bc871bd1 DM |
258 | } |
259 | ||
d09db6c2 | 260 | pub fn with_group(group: BackupGroup, backup_time: i64) -> Result<Self, Error> { |
6a7be83e | 261 | let backup_time_string = Self::backup_time_to_string(backup_time)?; |
cded320e TL |
262 | Ok(Self { |
263 | group, | |
264 | backup_time, | |
265 | backup_time_string, | |
266 | }) | |
51a4f63f | 267 | } |
b3483782 DM |
268 | |
269 | pub fn group(&self) -> &BackupGroup { | |
270 | &self.group | |
271 | } | |
272 | ||
6a7be83e | 273 | pub fn backup_time(&self) -> i64 { |
b3483782 DM |
274 | self.backup_time |
275 | } | |
276 | ||
6a7be83e DM |
277 | pub fn backup_time_string(&self) -> &str { |
278 | &self.backup_time_string | |
279 | } | |
280 | ||
cded320e | 281 | pub fn relative_path(&self) -> PathBuf { |
b3483782 DM |
282 | let mut relative_path = self.group.group_path(); |
283 | ||
6a7be83e | 284 | relative_path.push(self.backup_time_string.clone()); |
b3483782 DM |
285 | |
286 | relative_path | |
287 | } | |
fa5d6977 | 288 | |
6a7be83e DM |
289 | pub fn backup_time_to_string(backup_time: i64) -> Result<String, Error> { |
290 | // fixme: can this fail? (avoid unwrap) | |
291 | proxmox::tools::time::epoch_to_rfc3339_utc(backup_time) | |
fa5d6977 | 292 | } |
b3483782 | 293 | } |
d6d3b353 | 294 | |
a67f7d0a DM |
295 | impl std::str::FromStr for BackupDir { |
296 | type Err = Error; | |
297 | ||
298 | /// Parse a snapshot path | |
299 | /// | |
300 | /// This parses strings like `host/elsa/2020-06-15T05:18:33Z". | |
301 | fn from_str(path: &str) -> Result<Self, Self::Err> { | |
cded320e TL |
302 | let cap = SNAPSHOT_PATH_REGEX |
303 | .captures(path) | |
a67f7d0a DM |
304 | .ok_or_else(|| format_err!("unable to parse backup snapshot path '{}'", path))?; |
305 | ||
bc871bd1 DM |
306 | BackupDir::with_rfc3339( |
307 | cap.get(1).unwrap().as_str(), | |
308 | cap.get(2).unwrap().as_str(), | |
309 | cap.get(3).unwrap().as_str(), | |
310 | ) | |
a67f7d0a DM |
311 | } |
312 | } | |
b3483782 | 313 | |
abdb9763 DC |
314 | impl std::fmt::Display for BackupDir { |
315 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | |
316 | let backup_type = self.group.backup_type(); | |
317 | let id = self.group.backup_id(); | |
6a7be83e | 318 | write!(f, "{}/{}/{}", backup_type, id, self.backup_time_string) |
391d3107 WB |
319 | } |
320 | } | |
321 | ||
d57474e0 | 322 | /// Detailed Backup Information, lists files inside a BackupDir |
b02a52e3 | 323 | #[derive(Debug, Clone)] |
b3483782 DM |
324 | pub struct BackupInfo { |
325 | /// the backup directory | |
326 | pub backup_dir: BackupDir, | |
327 | /// List of data files | |
328 | pub files: Vec<String>, | |
329 | } | |
330 | ||
331 | impl BackupInfo { | |
38a6cdda DM |
332 | pub fn new(base_path: &Path, backup_dir: BackupDir) -> Result<BackupInfo, Error> { |
333 | let mut path = base_path.to_owned(); | |
334 | path.push(backup_dir.relative_path()); | |
335 | ||
336 | let files = list_backup_files(libc::AT_FDCWD, &path)?; | |
337 | ||
338 | Ok(BackupInfo { backup_dir, files }) | |
339 | } | |
340 | ||
51a4f63f | 341 | /// Finds the latest backup inside a backup group |
cded320e TL |
342 | pub fn last_backup( |
343 | base_path: &Path, | |
344 | group: &BackupGroup, | |
345 | only_finished: bool, | |
346 | ) -> Result<Option<BackupInfo>, Error> { | |
51a4f63f | 347 | let backups = group.list_backups(base_path)?; |
cded320e TL |
348 | Ok(backups |
349 | .into_iter() | |
4dbe1292 SR |
350 | .filter(|item| !only_finished || item.is_finished()) |
351 | .max_by_key(|item| item.backup_dir.backup_time())) | |
51a4f63f DM |
352 | } |
353 | ||
b3483782 | 354 | pub fn sort_list(list: &mut Vec<BackupInfo>, ascendending: bool) { |
cded320e TL |
355 | if ascendending { |
356 | // oldest first | |
b3483782 | 357 | list.sort_unstable_by(|a, b| a.backup_dir.backup_time.cmp(&b.backup_dir.backup_time)); |
cded320e TL |
358 | } else { |
359 | // newest first | |
b3483782 DM |
360 | list.sort_unstable_by(|a, b| b.backup_dir.backup_time.cmp(&a.backup_dir.backup_time)); |
361 | } | |
362 | } | |
363 | ||
58e99e13 DM |
364 | pub fn list_files(base_path: &Path, backup_dir: &BackupDir) -> Result<Vec<String>, Error> { |
365 | let mut path = base_path.to_owned(); | |
366 | path.push(backup_dir.relative_path()); | |
367 | ||
c0977501 | 368 | let files = list_backup_files(libc::AT_FDCWD, &path)?; |
58e99e13 DM |
369 | |
370 | Ok(files) | |
371 | } | |
372 | ||
0d08fcee | 373 | pub fn list_backup_groups(base_path: &Path) -> Result<Vec<BackupGroup>, Error> { |
11d89239 | 374 | let mut list = Vec::new(); |
b3483782 | 375 | |
cded320e TL |
376 | tools::scandir( |
377 | libc::AT_FDCWD, | |
378 | base_path, | |
379 | &BACKUP_TYPE_REGEX, | |
380 | |l0_fd, backup_type, file_type| { | |
381 | if file_type != nix::dir::Type::Directory { | |
382 | return Ok(()); | |
383 | } | |
384 | tools::scandir( | |
385 | l0_fd, | |
386 | backup_type, | |
387 | &BACKUP_ID_REGEX, | |
388 | |_, backup_id, file_type| { | |
389 | if file_type != nix::dir::Type::Directory { | |
390 | return Ok(()); | |
391 | } | |
392 | ||
393 | list.push(BackupGroup::new(backup_type, backup_id)); | |
394 | ||
395 | Ok(()) | |
396 | }, | |
397 | ) | |
398 | }, | |
399 | )?; | |
0d08fcee | 400 | |
b3483782 DM |
401 | Ok(list) |
402 | } | |
c9756b40 SR |
403 | |
404 | pub fn is_finished(&self) -> bool { | |
405 | // backup is considered unfinished if there is no manifest | |
cded320e TL |
406 | self.files |
407 | .iter() | |
408 | .any(|name| name == super::MANIFEST_BLOB_NAME) | |
c9756b40 | 409 | } |
b3483782 | 410 | } |
c0977501 | 411 | |
cded320e TL |
412 | fn list_backup_files<P: ?Sized + nix::NixPath>( |
413 | dirfd: RawFd, | |
414 | path: &P, | |
415 | ) -> Result<Vec<String>, Error> { | |
c0977501 DM |
416 | let mut files = vec![]; |
417 | ||
418 | tools::scandir(dirfd, path, &BACKUP_FILE_REGEX, |_, filename, file_type| { | |
cded320e TL |
419 | if file_type != nix::dir::Type::File { |
420 | return Ok(()); | |
421 | } | |
c0977501 DM |
422 | files.push(filename.to_owned()); |
423 | Ok(()) | |
424 | })?; | |
425 | ||
426 | Ok(files) | |
427 | } |