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