]> git.proxmox.com Git - proxmox-backup.git/blob - pbs-datastore/src/backup_info.rs
tree-wide: fix needless borrows
[proxmox-backup.git] / pbs-datastore / src / backup_info.rs
1 use std::os::unix::io::RawFd;
2 use std::path::{Path, PathBuf};
3 use std::str::FromStr;
4
5 use anyhow::{bail, format_err, Error};
6
7 use pbs_api_types::{
8 BACKUP_ID_REGEX,
9 BACKUP_TYPE_REGEX,
10 BACKUP_DATE_REGEX,
11 GROUP_PATH_REGEX,
12 SNAPSHOT_PATH_REGEX,
13 BACKUP_FILE_REGEX,
14 GroupFilter,
15 };
16
17 use super::manifest::MANIFEST_BLOB_NAME;
18
19 /// BackupGroup is a directory containing a list of BackupDir
20 #[derive(Debug, Eq, PartialEq, Hash, Clone)]
21 pub struct BackupGroup {
22 /// Type of backup
23 backup_type: String,
24 /// Unique (for this type) ID
25 backup_id: String,
26 }
27
28 impl std::cmp::Ord for BackupGroup {
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,
41 _ => self.backup_id.cmp(&other.backup_id),
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
52 impl BackupGroup {
53 pub fn new<T: Into<String>, U: Into<String>>(backup_type: T, backup_id: U) -> Self {
54 Self {
55 backup_type: backup_type.into(),
56 backup_id: backup_id.into(),
57 }
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
68 pub fn group_path(&self) -> PathBuf {
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 }
77
78 pub fn list_backups(&self, base_path: &Path) -> Result<Vec<BackupInfo>, Error> {
79 let mut list = vec![];
80
81 let mut path = base_path.to_owned();
82 path.push(self.group_path());
83
84 proxmox_sys::fs::scandir(
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 }
92
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)?;
96
97 let protected = backup_dir.is_protected(base_path.to_owned());
98
99 list.push(BackupInfo { backup_dir, files, protected });
100
101 Ok(())
102 },
103 )?;
104 Ok(list)
105 }
106
107 pub fn last_successful_backup(&self, base_path: &Path) -> Result<Option<i64>, Error> {
108 let mut last = None;
109
110 let mut path = base_path.to_owned();
111 path.push(self.group_path());
112
113 proxmox_sys::fs::scandir(
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(());
120 }
121
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
145 let timestamp = proxmox_time::parse_rfc3339(backup_time)?;
146 if let Some(last_timestamp) = last {
147 if timestamp > last_timestamp {
148 last = Some(timestamp);
149 }
150 } else {
151 last = Some(timestamp);
152 }
153
154 Ok(())
155 },
156 )?;
157
158 Ok(last)
159 }
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 }
171 }
172
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
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> {
188 let cap = GROUP_PATH_REGEX
189 .captures(path)
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
199 /// Uniquely identify a Backup (relative to data store)
200 ///
201 /// We also call this a backup snaphost.
202 #[derive(Debug, Eq, PartialEq, Clone)]
203 pub struct BackupDir {
204 /// Backup group
205 group: BackupGroup,
206 /// Backup timestamp
207 backup_time: i64,
208 // backup_time as rfc3339
209 backup_time_string: String,
210 }
211
212 impl BackupDir {
213 pub fn new<T, U>(backup_type: T, backup_id: U, backup_time: i64) -> Result<Self, Error>
214 where
215 T: Into<String>,
216 U: Into<String>,
217 {
218 let group = BackupGroup::new(backup_type.into(), backup_id.into());
219 BackupDir::with_group(group, backup_time)
220 }
221
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>
227 where
228 T: Into<String>,
229 U: Into<String>,
230 V: Into<String>,
231 {
232 let backup_time_string = backup_time_string.into();
233 let backup_time = proxmox_time::parse_rfc3339(&backup_time_string)?;
234 let group = BackupGroup::new(backup_type.into(), backup_id.into());
235 Ok(Self {
236 group,
237 backup_time,
238 backup_time_string,
239 })
240 }
241
242 pub fn with_group(group: BackupGroup, backup_time: i64) -> Result<Self, Error> {
243 let backup_time_string = Self::backup_time_to_string(backup_time)?;
244 Ok(Self {
245 group,
246 backup_time,
247 backup_time_string,
248 })
249 }
250
251 pub fn group(&self) -> &BackupGroup {
252 &self.group
253 }
254
255 pub fn backup_time(&self) -> i64 {
256 self.backup_time
257 }
258
259 pub fn backup_time_string(&self) -> &str {
260 &self.backup_time_string
261 }
262
263 pub fn relative_path(&self) -> PathBuf {
264 let mut relative_path = self.group.group_path();
265
266 relative_path.push(self.backup_time_string.clone());
267
268 relative_path
269 }
270
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
282 pub fn backup_time_to_string(backup_time: i64) -> Result<String, Error> {
283 // fixme: can this fail? (avoid unwrap)
284 Ok(proxmox_time::epoch_to_rfc3339_utc(backup_time)?)
285 }
286 }
287
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> {
295 let cap = SNAPSHOT_PATH_REGEX
296 .captures(path)
297 .ok_or_else(|| format_err!("unable to parse backup snapshot path '{}'", path))?;
298
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 )
304 }
305 }
306
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();
311 write!(f, "{}/{}/{}", backup_type, id, self.backup_time_string)
312 }
313 }
314
315 /// Detailed Backup Information, lists files inside a BackupDir
316 #[derive(Debug, Clone)]
317 pub struct BackupInfo {
318 /// the backup directory
319 pub backup_dir: BackupDir,
320 /// List of data files
321 pub files: Vec<String>,
322 /// Protection Status
323 pub protected: bool,
324 }
325
326 impl BackupInfo {
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)?;
332 let protected = backup_dir.is_protected(base_path.to_owned());
333
334 Ok(BackupInfo { backup_dir, files, protected })
335 }
336
337 /// Finds the latest backup inside a backup group
338 pub fn last_backup(
339 base_path: &Path,
340 group: &BackupGroup,
341 only_finished: bool,
342 ) -> Result<Option<BackupInfo>, Error> {
343 let backups = group.list_backups(base_path)?;
344 Ok(backups
345 .into_iter()
346 .filter(|item| !only_finished || item.is_finished())
347 .max_by_key(|item| item.backup_dir.backup_time()))
348 }
349
350 pub fn sort_list(list: &mut Vec<BackupInfo>, ascendending: bool) {
351 if ascendending {
352 // oldest first
353 list.sort_unstable_by(|a, b| a.backup_dir.backup_time.cmp(&b.backup_dir.backup_time));
354 } else {
355 // newest first
356 list.sort_unstable_by(|a, b| b.backup_dir.backup_time.cmp(&a.backup_dir.backup_time));
357 }
358 }
359
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
364 let files = list_backup_files(libc::AT_FDCWD, &path)?;
365
366 Ok(files)
367 }
368
369 pub fn list_backup_groups(base_path: &Path) -> Result<Vec<BackupGroup>, Error> {
370 let mut list = Vec::new();
371
372 proxmox_sys::fs::scandir(
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 }
380 proxmox_sys::fs::scandir(
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 )?;
396
397 Ok(list)
398 }
399
400 pub fn is_finished(&self) -> bool {
401 // backup is considered unfinished if there is no manifest
402 self.files
403 .iter()
404 .any(|name| name == MANIFEST_BLOB_NAME)
405 }
406 }
407
408 fn list_backup_files<P: ?Sized + nix::NixPath>(
409 dirfd: RawFd,
410 path: &P,
411 ) -> Result<Vec<String>, Error> {
412 let mut files = vec![];
413
414 proxmox_sys::fs::scandir(dirfd, path, &BACKUP_FILE_REGEX, |_, filename, file_type| {
415 if file_type != nix::dir::Type::File {
416 return Ok(());
417 }
418 files.push(filename.to_owned());
419 Ok(())
420 })?;
421
422 Ok(files)
423 }