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