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