3 use anyhow
::{bail, format_err, Error}
;
4 use std
::os
::unix
::io
::RawFd
;
6 use std
::path
::{PathBuf, Path}
;
8 use proxmox
::const_regex
;
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 BACKUP_FILE_REGEX = r"^
.*\.([fd
]idx
|blob
)$
";
19 BACKUP_TYPE_REGEX = concat!(r"^
(", BACKUP_TYPE_RE!(), r")$
");
21 pub BACKUP_ID_REGEX = concat!(r"^
", BACKUP_ID_RE!(), r"$
");
23 BACKUP_DATE_REGEX = concat!(r"^
", BACKUP_TIME_RE!() ,r"$
");
25 GROUP_PATH_REGEX = concat!(r"^
(", BACKUP_TYPE_RE!(), ")/(", BACKUP_ID_RE!(), r")$
");
27 SNAPSHOT_PATH_REGEX = concat!(
28 r"^
(", BACKUP_TYPE_RE!(), ")/(", BACKUP_ID_RE!(), ")/(", BACKUP_TIME_RE!(), r")$
");
31 /// BackupGroup is a directory containing a list of BackupDir
32 #[derive(Debug, Eq, PartialEq, Hash, Clone)]
33 pub struct BackupGroup {
36 /// Unique (for this type) ID
40 impl std::cmp::Ord for BackupGroup {
42 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
43 let type_order = self.backup_type.cmp(&other.backup_type);
44 if type_order != std::cmp::Ordering::Equal {
47 // try to compare IDs numerically
48 let id_self = self.backup_id.parse::<u64>();
49 let id_other = other.backup_id.parse::<u64>();
50 match (id_self, id_other) {
51 (Ok(id_self), Ok(id_other)) => id_self.cmp(&id_other),
52 (Ok(_), Err(_)) => std::cmp::Ordering::Less,
53 (Err(_), Ok(_)) => std::cmp::Ordering::Greater,
54 _ => self.backup_id.cmp(&other.backup_id),
59 impl std::cmp::PartialOrd for BackupGroup {
60 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
67 pub fn new<T: Into<String>, U: Into<String>>(backup_type: T, backup_id: U) -> Self {
68 Self { backup_type: backup_type.into(), backup_id: backup_id.into() }
71 pub fn backup_type(&self) -> &str {
75 pub fn backup_id(&self) -> &str {
79 pub fn group_path(&self) -> PathBuf {
81 let mut relative_path = PathBuf::new();
83 relative_path.push(&self.backup_type);
85 relative_path.push(&self.backup_id);
90 pub fn list_backups(&self, base_path: &Path) -> Result<Vec<BackupInfo>, Error> {
92 let mut list = vec![];
94 let mut path = base_path.to_owned();
95 path.push(self.group_path());
97 tools::scandir(libc::AT_FDCWD, &path, &BACKUP_DATE_REGEX, |l2_fd, backup_time, file_type| {
98 if file_type != nix::dir::Type::Directory { return Ok(()); }
100 let backup_dir = BackupDir::with_rfc3339(&self.backup_type, &self.backup_id, backup_time)?;
101 let files = list_backup_files(l2_fd, backup_time)?;
103 list.push(BackupInfo { backup_dir, files });
110 pub fn last_successful_backup(&self, base_path: &Path) -> Result<Option<i64>, Error> {
114 let mut path = base_path.to_owned();
115 path.push(self.group_path());
117 tools::scandir(libc::AT_FDCWD, &path, &BACKUP_DATE_REGEX, |l2_fd, backup_time, file_type| {
118 if file_type != nix::dir::Type::Directory { return Ok(()); }
120 let mut manifest_path = PathBuf::from(backup_time);
121 manifest_path.push(MANIFEST_BLOB_NAME);
123 use nix::fcntl::{openat, OFlag};
124 match openat(l2_fd, &manifest_path, OFlag::O_RDONLY, nix::sys::stat::Mode::empty()) {
126 /* manifest exists --> assume backup was successful */
127 /* close else this leaks! */
128 nix::unistd::close(rawfd)?;
130 Err(nix::Error::Sys(nix::errno::Errno::ENOENT)) => { return Ok(()); }
132 bail!("last_successful_backup
: unexpected error
- {}
", err);
136 let timestamp = proxmox::tools::time::parse_rfc3339(backup_time)?;
137 if let Some(last_timestamp) = last {
138 if timestamp > last_timestamp { last = Some(timestamp); }
140 last = Some(timestamp);
149 pub fn list_groups(base_path: &Path) -> Result<Vec<BackupGroup>, Error> {
150 let mut list = Vec::new();
152 tools::scandir(libc::AT_FDCWD, base_path, &BACKUP_TYPE_REGEX, |l0_fd, backup_type, file_type| {
153 if file_type != nix::dir::Type::Directory { return Ok(()); }
154 tools::scandir(l0_fd, backup_type, &BACKUP_ID_REGEX, |_l1_fd, backup_id, file_type| {
155 if file_type != nix::dir::Type::Directory { return Ok(()); }
156 list.push(BackupGroup::new(backup_type, backup_id));
164 impl std::fmt::Display for BackupGroup {
165 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
166 let backup_type = self.backup_type();
167 let id = self.backup_id();
168 write!(f, "{}
/{}
", backup_type, id)
172 impl std::str::FromStr for BackupGroup {
175 /// Parse a backup group path
177 /// This parses strings like `vm/100".
178 fn from_str(path
: &str) -> Result
<Self, Self::Err
> {
179 let cap
= GROUP_PATH_REGEX
.captures(path
)
180 .ok_or_else(|| format_err
!("unable to parse backup group path '{}'", path
))?
;
183 backup_type
: cap
.get(1).unwrap().as_str().to_owned(),
184 backup_id
: cap
.get(2).unwrap().as_str().to_owned(),
189 /// Uniquely identify a Backup (relative to data store)
191 /// We also call this a backup snaphost.
192 #[derive(Debug, Eq, PartialEq, Clone)]
193 pub struct BackupDir
{
198 // backup_time as rfc3339
199 backup_time_string
: String
204 pub fn new
<T
, U
>(backup_type
: T
, backup_id
: U
, backup_time
: i64) -> Result
<Self, Error
>
209 let group
= BackupGroup
::new(backup_type
.into(), backup_id
.into());
210 BackupDir
::with_group(group
, backup_time
)
213 pub fn with_rfc3339
<T
,U
,V
>(backup_type
: T
, backup_id
: U
, backup_time_string
: V
) -> Result
<Self, Error
>
219 let backup_time_string
= backup_time_string
.into();
220 let backup_time
= proxmox
::tools
::time
::parse_rfc3339(&backup_time_string
)?
;
221 let group
= BackupGroup
::new(backup_type
.into(), backup_id
.into());
222 Ok(Self { group, backup_time, backup_time_string }
)
225 pub fn with_group(group
: BackupGroup
, backup_time
: i64) -> Result
<Self, Error
> {
226 let backup_time_string
= Self::backup_time_to_string(backup_time
)?
;
227 Ok(Self { group, backup_time, backup_time_string }
)
230 pub fn group(&self) -> &BackupGroup
{
234 pub fn backup_time(&self) -> i64 {
238 pub fn backup_time_string(&self) -> &str {
239 &self.backup_time_string
242 pub fn relative_path(&self) -> PathBuf
{
244 let mut relative_path
= self.group
.group_path();
246 relative_path
.push(self.backup_time_string
.clone());
251 pub fn backup_time_to_string(backup_time
: i64) -> Result
<String
, Error
> {
252 // fixme: can this fail? (avoid unwrap)
253 proxmox
::tools
::time
::epoch_to_rfc3339_utc(backup_time
)
257 impl std
::str::FromStr
for BackupDir
{
260 /// Parse a snapshot path
262 /// This parses strings like `host/elsa/2020-06-15T05:18:33Z".
263 fn from_str(path
: &str) -> Result
<Self, Self::Err
> {
264 let cap
= SNAPSHOT_PATH_REGEX
.captures(path
)
265 .ok_or_else(|| format_err
!("unable to parse backup snapshot path '{}'", path
))?
;
267 BackupDir
::with_rfc3339(
268 cap
.get(1).unwrap().as_str(),
269 cap
.get(2).unwrap().as_str(),
270 cap
.get(3).unwrap().as_str(),
275 impl std
::fmt
::Display
for BackupDir
{
276 fn fmt(&self, f
: &mut std
::fmt
::Formatter
<'_
>) -> std
::fmt
::Result
{
277 let backup_type
= self.group
.backup_type();
278 let id
= self.group
.backup_id();
279 write
!(f
, "{}/{}/{}", backup_type
, id
, self.backup_time_string
)
283 /// Detailed Backup Information, lists files inside a BackupDir
284 #[derive(Debug, Clone)]
285 pub struct BackupInfo
{
286 /// the backup directory
287 pub backup_dir
: BackupDir
,
288 /// List of data files
289 pub files
: Vec
<String
>,
294 pub fn new(base_path
: &Path
, backup_dir
: BackupDir
) -> Result
<BackupInfo
, Error
> {
295 let mut path
= base_path
.to_owned();
296 path
.push(backup_dir
.relative_path());
298 let files
= list_backup_files(libc
::AT_FDCWD
, &path
)?
;
300 Ok(BackupInfo { backup_dir, files }
)
303 /// Finds the latest backup inside a backup group
304 pub fn last_backup(base_path
: &Path
, group
: &BackupGroup
, only_finished
: bool
)
305 -> Result
<Option
<BackupInfo
>, Error
>
307 let backups
= group
.list_backups(base_path
)?
;
308 Ok(backups
.into_iter()
309 .filter(|item
| !only_finished
|| item
.is_finished())
310 .max_by_key(|item
| item
.backup_dir
.backup_time()))
313 pub fn sort_list(list
: &mut Vec
<BackupInfo
>, ascendending
: bool
) {
314 if ascendending
{ // oldest first
315 list
.sort_unstable_by(|a
, b
| a
.backup_dir
.backup_time
.cmp(&b
.backup_dir
.backup_time
));
316 } else { // newest first
317 list
.sort_unstable_by(|a
, b
| b
.backup_dir
.backup_time
.cmp(&a
.backup_dir
.backup_time
));
321 pub fn list_files(base_path
: &Path
, backup_dir
: &BackupDir
) -> Result
<Vec
<String
>, Error
> {
322 let mut path
= base_path
.to_owned();
323 path
.push(backup_dir
.relative_path());
325 let files
= list_backup_files(libc
::AT_FDCWD
, &path
)?
;
330 pub fn list_backups(base_path
: &Path
) -> Result
<Vec
<BackupInfo
>, Error
> {
331 let mut list
= Vec
::new();
333 tools
::scandir(libc
::AT_FDCWD
, base_path
, &BACKUP_TYPE_REGEX
, |l0_fd
, backup_type
, file_type
| {
334 if file_type
!= nix
::dir
::Type
::Directory { return Ok(()); }
335 tools
::scandir(l0_fd
, backup_type
, &BACKUP_ID_REGEX
, |l1_fd
, backup_id
, file_type
| {
336 if file_type
!= nix
::dir
::Type
::Directory { return Ok(()); }
337 tools
::scandir(l1_fd
, backup_id
, &BACKUP_DATE_REGEX
, |l2_fd
, backup_time_string
, file_type
| {
338 if file_type
!= nix
::dir
::Type
::Directory { return Ok(()); }
340 let backup_dir
= BackupDir
::with_rfc3339(backup_type
, backup_id
, backup_time_string
)?
;
342 let files
= list_backup_files(l2_fd
, backup_time_string
)?
;
344 list
.push(BackupInfo { backup_dir, files }
);
353 pub fn is_finished(&self) -> bool
{
354 // backup is considered unfinished if there is no manifest
355 self.files
.iter().any(|name
| name
== super::MANIFEST_BLOB_NAME
)
359 fn list_backup_files
<P
: ?Sized
+ nix
::NixPath
>(dirfd
: RawFd
, path
: &P
) -> Result
<Vec
<String
>, Error
> {
360 let mut files
= vec
![];
362 tools
::scandir(dirfd
, path
, &BACKUP_FILE_REGEX
, |_
, filename
, file_type
| {
363 if file_type
!= nix
::dir
::Type
::File { return Ok(()); }
364 files
.push(filename
.to_owned());