3 use anyhow
::{bail, format_err, Error}
;
5 use std
::os
::unix
::io
::RawFd
;
7 use std
::path
::{PathBuf, Path}
;
8 use lazy_static
::lazy_static
;
10 use super::manifest
::MANIFEST_BLOB_NAME
;
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)") }
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
") }
17 static ref BACKUP_FILE_REGEX: Regex = Regex::new(
18 r"^
.*\.([fd
]idx
|blob
)$
").unwrap();
20 static ref BACKUP_TYPE_REGEX: Regex = Regex::new(
21 concat!(r"^
(", BACKUP_TYPE_RE!(), r")$
")).unwrap();
23 static ref BACKUP_ID_REGEX: Regex = Regex::new(
24 concat!(r"^
", BACKUP_ID_RE!(), r"$
")).unwrap();
26 static ref BACKUP_DATE_REGEX: Regex = Regex::new(
27 concat!(r"^
", BACKUP_TIME_RE!() ,r"$
")).unwrap();
29 static ref GROUP_PATH_REGEX: Regex = Regex::new(
30 concat!(r"^
(", BACKUP_TYPE_RE!(), ")/(", BACKUP_ID_RE!(), r")$
")).unwrap();
32 static ref SNAPSHOT_PATH_REGEX: Regex = Regex::new(
33 concat!(r"^
(", BACKUP_TYPE_RE!(), ")/(", BACKUP_ID_RE!(), ")/(", BACKUP_TIME_RE!(), r")$
")).unwrap();
37 /// BackupGroup is a directory containing a list of BackupDir
38 #[derive(Debug, Eq, PartialEq, Hash, Clone)]
39 pub struct BackupGroup {
42 /// Unique (for this type) ID
46 impl std::cmp::Ord for BackupGroup {
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 {
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),
65 impl std::cmp::PartialOrd for BackupGroup {
66 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
73 pub fn new<T: Into<String>, U: Into<String>>(backup_type: T, backup_id: U) -> Self {
74 Self { backup_type: backup_type.into(), backup_id: backup_id.into() }
77 pub fn backup_type(&self) -> &str {
81 pub fn backup_id(&self) -> &str {
85 pub fn group_path(&self) -> PathBuf {
87 let mut relative_path = PathBuf::new();
89 relative_path.push(&self.backup_type);
91 relative_path.push(&self.backup_id);
96 pub fn list_backups(&self, base_path: &Path) -> Result<Vec<BackupInfo>, Error> {
98 let mut list = vec![];
100 let mut path = base_path.to_owned();
101 path.push(self.group_path());
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(()); }
106 let backup_dir = BackupDir::with_rfc3339(&self.backup_type, &self.backup_id, backup_time)?;
107 let files = list_backup_files(l2_fd, backup_time)?;
109 list.push(BackupInfo { backup_dir, files });
116 pub fn last_successful_backup(&self, base_path: &Path) -> Result<Option<i64>, Error> {
120 let mut path = base_path.to_owned();
121 path.push(self.group_path());
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(()); }
126 let mut manifest_path = PathBuf::from(backup_time);
127 manifest_path.push(MANIFEST_BLOB_NAME);
129 use nix::fcntl::{openat, OFlag};
130 match openat(l2_fd, &manifest_path, OFlag::O_RDONLY, nix::sys::stat::Mode::empty()) {
132 /* manifest exists --> assume backup was successful */
133 /* close else this leaks! */
134 nix::unistd::close(rawfd)?;
136 Err(nix::Error::Sys(nix::errno::Errno::ENOENT)) => { return Ok(()); }
138 bail!("last_successful_backup
: unexpected error
- {}
", err);
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); }
146 last = Some(timestamp);
155 pub fn list_groups(base_path: &Path) -> Result<Vec<BackupGroup>, Error> {
156 let mut list = Vec::new();
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));
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)
178 impl std::str::FromStr for BackupGroup {
181 /// Parse a backup group path
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
))?
;
189 backup_type
: cap
.get(1).unwrap().as_str().to_owned(),
190 backup_id
: cap
.get(2).unwrap().as_str().to_owned(),
195 /// Uniquely identify a Backup (relative to data store)
197 /// We also call this a backup snaphost.
198 #[derive(Debug, Eq, PartialEq, Clone)]
199 pub struct BackupDir
{
204 // backup_time as rfc3339
205 backup_time_string
: String
210 pub fn new
<T
, U
>(backup_type
: T
, backup_id
: U
, backup_time
: i64) -> Result
<Self, Error
>
215 let group
= BackupGroup
::new(backup_type
.into(), backup_id
.into());
216 BackupDir
::with_group(group
, backup_time
)
219 pub fn with_rfc3339
<T
,U
,V
>(backup_type
: T
, backup_id
: U
, backup_time_string
: V
) -> Result
<Self, Error
>
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 }
)
231 pub fn with_group(group
: BackupGroup
, backup_time
: i64) -> Result
<Self, Error
> {
232 let backup_time_string
= Self::backup_time_to_string(backup_time
)?
;
233 Ok(Self { group, backup_time, backup_time_string }
)
236 pub fn group(&self) -> &BackupGroup
{
240 pub fn backup_time(&self) -> i64 {
244 pub fn backup_time_string(&self) -> &str {
245 &self.backup_time_string
248 pub fn relative_path(&self) -> PathBuf
{
250 let mut relative_path
= self.group
.group_path();
252 relative_path
.push(self.backup_time_string
.clone());
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
)
263 impl std
::str::FromStr
for BackupDir
{
266 /// Parse a snapshot path
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
))?
;
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(),
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();
285 write
!(f
, "{}/{}/{}", backup_type
, id
, self.backup_time_string
)
289 /// Detailed Backup Information, lists files inside a BackupDir
290 #[derive(Debug, Clone)]
291 pub struct BackupInfo
{
292 /// the backup directory
293 pub backup_dir
: BackupDir
,
294 /// List of data files
295 pub files
: Vec
<String
>,
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());
304 let files
= list_backup_files(libc
::AT_FDCWD
, &path
)?
;
306 Ok(BackupInfo { backup_dir, files }
)
309 /// Finds the latest backup inside a backup group
310 pub fn last_backup(base_path
: &Path
, group
: &BackupGroup
, only_finished
: bool
)
311 -> Result
<Option
<BackupInfo
>, Error
>
313 let backups
= group
.list_backups(base_path
)?
;
314 Ok(backups
.into_iter()
315 .filter(|item
| !only_finished
|| item
.is_finished())
316 .max_by_key(|item
| item
.backup_dir
.backup_time()))
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
));
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());
331 let files
= list_backup_files(libc
::AT_FDCWD
, &path
)?
;
336 pub fn list_backups(base_path
: &Path
) -> Result
<Vec
<BackupInfo
>, Error
> {
337 let mut list
= Vec
::new();
339 tools
::scandir(libc
::AT_FDCWD
, base_path
, &BACKUP_TYPE_REGEX
, |l0_fd
, backup_type
, file_type
| {
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(()); }
343 tools
::scandir(l1_fd
, backup_id
, &BACKUP_DATE_REGEX
, |l2_fd
, backup_time_string
, file_type
| {
344 if file_type
!= nix
::dir
::Type
::Directory { return Ok(()); }
346 let backup_dir
= BackupDir
::with_rfc3339(backup_type
, backup_id
, backup_time_string
)?
;
348 let files
= list_backup_files(l2_fd
, backup_time_string
)?
;
350 list
.push(BackupInfo { backup_dir, files }
);
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
)
365 fn list_backup_files
<P
: ?Sized
+ nix
::NixPath
>(dirfd
: RawFd
, path
: &P
) -> Result
<Vec
<String
>, Error
> {
366 let mut files
= vec
![];
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());