]> git.proxmox.com Git - proxmox-backup.git/blobdiff - src/backup/backup_info.rs
cleanup: implement FromStr for BackupGroup
[proxmox-backup.git] / src / backup / backup_info.rs
index 2aee800b30b8a5b8fa27d12b944775a32c9f01c0..0ae4f06617cef950f4d53e211117069ee5e0b932 100644 (file)
@@ -1,20 +1,23 @@
 use crate::tools;
 
-use failure::*;
+use anyhow::{bail, format_err, Error};
 use regex::Regex;
+use std::os::unix::io::RawFd;
 
-use chrono::{DateTime, TimeZone, Local};
+use chrono::{DateTime, TimeZone, SecondsFormat, Utc};
 
 use std::path::{PathBuf, Path};
 use lazy_static::lazy_static;
 
+use super::manifest::MANIFEST_BLOB_NAME;
+
 macro_rules! BACKUP_ID_RE { () => (r"[A-Za-z0-9][A-Za-z0-9_-]+") }
 macro_rules! BACKUP_TYPE_RE { () => (r"(?:host|vm|ct)") }
-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}\+[0-9]{2}:[0-9]{2}") }
+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") }
 
 lazy_static!{
     static ref BACKUP_FILE_REGEX: Regex = Regex::new(
-        r"^.*\.([fd]idx)$").unwrap();
+        r"^.*\.([fd]idx|blob)$").unwrap();
 
     static ref BACKUP_TYPE_REGEX: Regex = Regex::new(
         concat!(r"^(", BACKUP_TYPE_RE!(), r")$")).unwrap();
@@ -26,15 +29,15 @@ lazy_static!{
         concat!(r"^", BACKUP_TIME_RE!() ,r"$")).unwrap();
 
     static ref GROUP_PATH_REGEX: Regex = Regex::new(
-        concat!(r"(", BACKUP_TYPE_RE!(), ")/(", BACKUP_ID_RE!(), r")$")).unwrap();
+        concat!(r"^(", BACKUP_TYPE_RE!(), ")/(", BACKUP_ID_RE!(), r")$")).unwrap();
 
     static ref SNAPSHOT_PATH_REGEX: Regex = Regex::new(
-        concat!(r"(", BACKUP_TYPE_RE!(), ")/(", BACKUP_ID_RE!(), ")/(", BACKUP_TIME_RE!(), r")$")).unwrap();
+        concat!(r"^(", BACKUP_TYPE_RE!(), ")/(", BACKUP_ID_RE!(), ")/(", BACKUP_TIME_RE!(), r")$")).unwrap();
 
 }
 
 /// BackupGroup is a directory containing a list of BackupDir
-#[derive(Debug)]
+#[derive(Debug, Eq, PartialEq, Hash, Clone)]
 pub struct BackupGroup {
     /// Type of backup
     backup_type: String,
@@ -44,7 +47,7 @@ pub struct BackupGroup {
 
 impl BackupGroup {
 
-    pub fn new<T: Into<String>>(backup_type: T, backup_id: T) -> Self {
+    pub fn new<T: Into<String>, U: Into<String>>(backup_type: T, backup_id: U) -> Self {
         Self { backup_type: backup_type.into(), backup_id: backup_id.into() }
     }
 
@@ -56,17 +59,6 @@ impl BackupGroup {
         &self.backup_id
     }
 
-    pub fn parse(path: &str) -> Result<Self, Error> {
-
-        let cap = GROUP_PATH_REGEX.captures(path)
-            .ok_or_else(|| format_err!("unable to parse backup group path '{}'", path))?;
-
-        Ok(Self {
-            backup_type: cap.get(1).unwrap().as_str().to_owned(),
-            backup_id: cap.get(2).unwrap().as_str().to_owned(),
-        })
-    }
-
     pub fn group_path(&self) ->  PathBuf  {
 
         let mut relative_path = PathBuf::new();
@@ -77,56 +69,178 @@ impl BackupGroup {
 
         relative_path
     }
+
+    pub fn list_backups(&self, base_path: &Path) -> Result<Vec<BackupInfo>, Error> {
+
+        let mut list = vec![];
+
+        let mut path = base_path.to_owned();
+        path.push(self.group_path());
+
+        tools::scandir(libc::AT_FDCWD, &path, &BACKUP_DATE_REGEX, |l2_fd, backup_time, file_type| {
+            if file_type != nix::dir::Type::Directory { return Ok(()); }
+
+            let dt = backup_time.parse::<DateTime<Utc>>()?;
+            let backup_dir = BackupDir::new(self.backup_type.clone(), self.backup_id.clone(), dt.timestamp());
+            let files = list_backup_files(l2_fd, backup_time)?;
+
+            list.push(BackupInfo { backup_dir, files });
+
+            Ok(())
+        })?;
+        Ok(list)
+    }
+
+    pub fn last_successful_backup(&self,  base_path: &Path) -> Result<Option<DateTime<Utc>>, Error> {
+
+        let mut last = None;
+
+        let mut path = base_path.to_owned();
+        path.push(self.group_path());
+
+        tools::scandir(libc::AT_FDCWD, &path, &BACKUP_DATE_REGEX, |l2_fd, backup_time, file_type| {
+            if file_type != nix::dir::Type::Directory { return Ok(()); }
+
+            let mut manifest_path = PathBuf::from(backup_time);
+            manifest_path.push(MANIFEST_BLOB_NAME);
+
+            use nix::fcntl::{openat, OFlag};
+            match openat(l2_fd, &manifest_path, OFlag::O_RDONLY, nix::sys::stat::Mode::empty()) {
+                Ok(_) => { /* manifest exists --> assume backup was successful */ },
+                Err(nix::Error::Sys(nix::errno::Errno::ENOENT)) => { return Ok(()); }
+                Err(err) => {
+                    bail!("last_successful_backup: unexpected error - {}", err);
+                }
+            }
+
+            let dt = backup_time.parse::<DateTime<Utc>>()?;
+            if let Some(last_dt) = last {
+                if dt > last_dt { last = Some(dt); }
+            } else {
+                last = Some(dt);
+            }
+
+            Ok(())
+        })?;
+
+        Ok(last)
+    }
+
+    pub fn list_groups(base_path: &Path) -> Result<Vec<BackupGroup>, Error> {
+        let mut list = Vec::new();
+
+        tools::scandir(libc::AT_FDCWD, base_path, &BACKUP_TYPE_REGEX, |l0_fd, backup_type, file_type| {
+            if file_type != nix::dir::Type::Directory { return Ok(()); }
+            tools::scandir(l0_fd, backup_type, &BACKUP_ID_REGEX, |_l1_fd, backup_id, file_type| {
+                if file_type != nix::dir::Type::Directory { return Ok(()); }
+                list.push(BackupGroup::new(backup_type, backup_id));
+                Ok(())
+            })
+        })?;
+        Ok(list)
+    }
+}
+
+impl std::str::FromStr for BackupGroup {
+    type Err = Error;
+
+    /// Parse a backup group path
+    ///
+    /// This parses strings like `vm/100".
+    fn from_str(path: &str) -> Result<Self, Self::Err> {
+        let cap = GROUP_PATH_REGEX.captures(path)
+            .ok_or_else(|| format_err!("unable to parse backup group path '{}'", path))?;
+
+        Ok(Self {
+            backup_type: cap.get(1).unwrap().as_str().to_owned(),
+            backup_id: cap.get(2).unwrap().as_str().to_owned(),
+        })
+    }
 }
 
 /// Uniquely identify a Backup (relative to data store)
 ///
 /// We also call this a backup snaphost.
-#[derive(Debug)]
+#[derive(Debug, Clone)]
 pub struct BackupDir {
     /// Backup group
     group: BackupGroup,
     /// Backup timestamp
-    backup_time: DateTime<Local>,
+    backup_time: DateTime<Utc>,
 }
 
 impl BackupDir {
 
-    pub fn new(group: BackupGroup, timestamp: i64) -> Self {
+    pub fn new<T, U>(backup_type: T, backup_id: U, timestamp: i64) -> Self
+    where
+        T: Into<String>,
+        U: Into<String>,
+    {
         // Note: makes sure that nanoseconds is 0
-        Self { group, backup_time: Local.timestamp(timestamp, 0) }
+        Self {
+            group: BackupGroup::new(backup_type.into(), backup_id.into()),
+            backup_time: Utc.timestamp(timestamp, 0),
+        }
+    }
+    pub fn new_with_group(group: BackupGroup, timestamp: i64) -> Self {
+        Self { group, backup_time: Utc.timestamp(timestamp, 0) }
     }
 
     pub fn group(&self) -> &BackupGroup {
         &self.group
     }
 
-    pub fn backup_time(&self) -> DateTime<Local> {
+    pub fn backup_time(&self) -> DateTime<Utc> {
         self.backup_time
     }
 
-    pub fn parse(path: &str) -> Result<Self, Error> {
+    pub fn relative_path(&self) ->  PathBuf  {
+
+        let mut relative_path = self.group.group_path();
+
+        relative_path.push(Self::backup_time_to_string(self.backup_time));
+
+        relative_path
+    }
+
+    pub fn backup_time_to_string(backup_time: DateTime<Utc>) -> String {
+        backup_time.to_rfc3339_opts(SecondsFormat::Secs, true)
+    }
+}
+
+impl std::str::FromStr for BackupDir {
+    type Err = Error;
 
+    /// Parse a snapshot path
+    ///
+    /// This parses strings like `host/elsa/2020-06-15T05:18:33Z".
+    fn from_str(path: &str) -> Result<Self, Self::Err> {
         let cap = SNAPSHOT_PATH_REGEX.captures(path)
             .ok_or_else(|| format_err!("unable to parse backup snapshot path '{}'", path))?;
 
         let group = BackupGroup::new(cap.get(1).unwrap().as_str(), cap.get(2).unwrap().as_str());
-        let backup_time = cap.get(3).unwrap().as_str().parse::<DateTime<Local>>()?;
-        Ok(BackupDir::new(group, backup_time.timestamp()))
+        let backup_time = cap.get(3).unwrap().as_str().parse::<DateTime<Utc>>()?;
+        Ok(BackupDir::from((group, backup_time.timestamp())))
     }
+}
 
-    pub fn relative_path(&self) ->  PathBuf  {
-
-        let mut relative_path = self.group.group_path();
-
-        relative_path.push(self.backup_time.to_rfc3339());
+impl std::fmt::Display for BackupDir {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        let backup_type = self.group.backup_type();
+        let id = self.group.backup_id();
+        let time = Self::backup_time_to_string(self.backup_time);
+        write!(f, "{}/{}/{}", backup_type, id, time)
+    }
+}
 
-        relative_path
+impl From<(BackupGroup, i64)> for BackupDir {
+    fn from((group, timestamp): (BackupGroup, i64)) -> Self {
+        Self { group, backup_time: Utc.timestamp(timestamp, 0) }
     }
 }
 
 /// Detailed Backup Information, lists files inside a BackupDir
-#[derive(Debug)]
+#[derive(Debug, Clone)]
 pub struct BackupInfo {
     /// the backup directory
     pub backup_dir: BackupDir,
@@ -136,6 +250,21 @@ pub struct BackupInfo {
 
 impl BackupInfo {
 
+    pub fn new(base_path: &Path, backup_dir: BackupDir) -> Result<BackupInfo, Error> {
+        let mut path = base_path.to_owned();
+        path.push(backup_dir.relative_path());
+
+        let files = list_backup_files(libc::AT_FDCWD, &path)?;
+
+        Ok(BackupInfo { backup_dir, files })
+    }
+
+    /// Finds the latest backup inside a backup group
+    pub fn last_backup(base_path: &Path, group: &BackupGroup) -> Result<Option<BackupInfo>, Error> {
+        let backups = group.list_backups(base_path)?;
+        Ok(backups.into_iter().max_by_key(|item| item.backup_dir.backup_time()))
+    }
+
     pub fn sort_list(list: &mut Vec<BackupInfo>, ascendending: bool) {
         if ascendending { // oldest first
             list.sort_unstable_by(|a, b| a.backup_dir.backup_time.cmp(&b.backup_dir.backup_time));
@@ -144,30 +273,31 @@ impl BackupInfo {
         }
     }
 
-    pub fn list_backups(path: &Path) -> Result<Vec<BackupInfo>, Error> {
-        let mut list = vec![];
+    pub fn list_files(base_path: &Path, backup_dir: &BackupDir) -> Result<Vec<String>, Error> {
+        let mut path = base_path.to_owned();
+        path.push(backup_dir.relative_path());
+
+        let files = list_backup_files(libc::AT_FDCWD, &path)?;
+
+        Ok(files)
+    }
+
+    pub fn list_backups(base_path: &Path) -> Result<Vec<BackupInfo>, Error> {
+        let mut list = Vec::new();
 
-        tools::scandir(libc::AT_FDCWD, path, &BACKUP_TYPE_REGEX, |l0_fd, backup_type, file_type| {
+        tools::scandir(libc::AT_FDCWD, base_path, &BACKUP_TYPE_REGEX, |l0_fd, backup_type, file_type| {
             if file_type != nix::dir::Type::Directory { return Ok(()); }
             tools::scandir(l0_fd, backup_type, &BACKUP_ID_REGEX, |l1_fd, backup_id, file_type| {
                 if file_type != nix::dir::Type::Directory { return Ok(()); }
                 tools::scandir(l1_fd, backup_id, &BACKUP_DATE_REGEX, |l2_fd, backup_time, file_type| {
                     if file_type != nix::dir::Type::Directory { return Ok(()); }
 
-                    let dt = backup_time.parse::<DateTime<Local>>()?;
+                    let dt = backup_time.parse::<DateTime<Utc>>()?;
+                    let backup_dir = BackupDir::new(backup_type, backup_id, dt.timestamp());
 
-                    let mut files = vec![];
+                    let files = list_backup_files(l2_fd, backup_time)?;
 
-                    tools::scandir(l2_fd, backup_time, &BACKUP_FILE_REGEX, |_, filename, file_type| {
-                        if file_type != nix::dir::Type::File { return Ok(()); }
-                        files.push(filename.to_owned());
-                        Ok(())
-                    })?;
-
-                    list.push(BackupInfo {
-                        backup_dir: BackupDir::new(BackupGroup::new(backup_type, backup_id), dt.timestamp()),
-                        files,
-                    });
+                    list.push(BackupInfo { backup_dir, files });
 
                     Ok(())
                 })
@@ -176,3 +306,15 @@ impl BackupInfo {
         Ok(list)
     }
 }
+
+fn list_backup_files<P: ?Sized + nix::NixPath>(dirfd: RawFd, path: &P) -> Result<Vec<String>, Error> {
+    let mut files = vec![];
+
+    tools::scandir(dirfd, path, &BACKUP_FILE_REGEX, |_, filename, file_type| {
+        if file_type != nix::dir::Type::File { return Ok(()); }
+        files.push(filename.to_owned());
+        Ok(())
+    })?;
+
+    Ok(files)
+}