1 use std
::os
::unix
::io
::RawFd
;
2 use std
::path
::{Path, PathBuf}
;
5 use anyhow
::{bail, format_err, Error}
;
17 use super::manifest
::MANIFEST_BLOB_NAME
;
19 /// BackupGroup is a directory containing a list of BackupDir
20 #[derive(Debug, Eq, PartialEq, Hash, Clone)]
21 pub struct BackupGroup
{
24 /// Unique (for this type) ID
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
{
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
),
46 impl std
::cmp
::PartialOrd
for BackupGroup
{
47 fn partial_cmp(&self, other
: &Self) -> Option
<std
::cmp
::Ordering
> {
53 pub fn new
<T
: Into
<String
>, U
: Into
<String
>>(backup_type
: T
, backup_id
: U
) -> Self {
55 backup_type
: backup_type
.into(),
56 backup_id
: backup_id
.into(),
60 pub fn backup_type(&self) -> &str {
64 pub fn backup_id(&self) -> &str {
68 pub fn group_path(&self) -> PathBuf
{
69 let mut relative_path
= PathBuf
::new();
71 relative_path
.push(&self.backup_type
);
73 relative_path
.push(&self.backup_id
);
78 pub fn list_backups(&self, base_path
: &Path
) -> Result
<Vec
<BackupInfo
>, Error
> {
79 let mut list
= vec
![];
81 let mut path
= base_path
.to_owned();
82 path
.push(self.group_path());
84 proxmox_sys
::fs
::scandir(
88 |l2_fd
, backup_time
, file_type
| {
89 if file_type
!= nix
::dir
::Type
::Directory
{
94 BackupDir
::with_rfc3339(&self.backup_type
, &self.backup_id
, backup_time
)?
;
95 let files
= list_backup_files(l2_fd
, backup_time
)?
;
97 let protected
= backup_dir
.is_protected(base_path
.to_owned());
99 list
.push(BackupInfo { backup_dir, files, protected }
);
107 pub fn last_successful_backup(&self, base_path
: &Path
) -> Result
<Option
<i64>, Error
> {
110 let mut path
= base_path
.to_owned();
111 path
.push(self.group_path());
113 proxmox_sys
::fs
::scandir(
117 |l2_fd
, backup_time
, file_type
| {
118 if file_type
!= nix
::dir
::Type
::Directory
{
122 let mut manifest_path
= PathBuf
::from(backup_time
);
123 manifest_path
.push(MANIFEST_BLOB_NAME
);
125 use nix
::fcntl
::{openat, OFlag}
;
130 nix
::sys
::stat
::Mode
::empty(),
133 /* manifest exists --> assume backup was successful */
134 /* close else this leaks! */
135 nix
::unistd
::close(rawfd
)?
;
137 Err(nix
::Error
::Sys(nix
::errno
::Errno
::ENOENT
)) => {
141 bail
!("last_successful_backup: unexpected error - {}", err
);
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
);
151 last
= Some(timestamp
);
161 pub fn matches(&self, filter
: &GroupFilter
) -> bool
{
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
167 GroupFilter
::BackupType(backup_type
) => self.backup_type() == backup_type
,
168 GroupFilter
::Regex(regex
) => regex
.is_match(&self.to_string()),
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
)
181 impl std
::str::FromStr
for BackupGroup
{
184 /// Parse a backup group path
186 /// This parses strings like `vm/100".
187 fn from_str(path
: &str) -> Result
<Self, Self::Err
> {
188 let cap
= GROUP_PATH_REGEX
190 .ok_or_else(|| format_err
!("unable to parse backup group path '{}'", path
))?
;
193 backup_type
: cap
.get(1).unwrap().as_str().to_owned(),
194 backup_id
: cap
.get(2).unwrap().as_str().to_owned(),
199 /// Uniquely identify a Backup (relative to data store)
201 /// We also call this a backup snaphost.
202 #[derive(Debug, Eq, PartialEq, Clone)]
203 pub struct BackupDir
{
208 // backup_time as rfc3339
209 backup_time_string
: String
,
213 pub fn new
<T
, U
>(backup_type
: T
, backup_id
: U
, backup_time
: i64) -> Result
<Self, Error
>
218 let group
= BackupGroup
::new(backup_type
.into(), backup_id
.into());
219 BackupDir
::with_group(group
, backup_time
)
222 pub fn with_rfc3339
<T
, U
, V
>(
225 backup_time_string
: V
,
226 ) -> Result
<Self, Error
>
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());
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
)?
;
251 pub fn group(&self) -> &BackupGroup
{
255 pub fn backup_time(&self) -> i64 {
259 pub fn backup_time_string(&self) -> &str {
260 &self.backup_time_string
263 pub fn relative_path(&self) -> PathBuf
{
264 let mut relative_path
= self.group
.group_path();
266 relative_path
.push(self.backup_time_string
.clone());
271 pub fn protected_file(&self, mut path
: PathBuf
) -> PathBuf
{
272 path
.push(self.relative_path());
273 path
.push(".protected");
277 pub fn is_protected(&self, base_path
: PathBuf
) -> bool
{
278 let path
= self.protected_file(base_path
);
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
)?
)
288 impl std
::str::FromStr
for BackupDir
{
291 /// Parse a snapshot path
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
297 .ok_or_else(|| format_err
!("unable to parse backup snapshot path '{}'", path
))?
;
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(),
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
)
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
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());
331 let files
= list_backup_files(libc
::AT_FDCWD
, &path
)?
;
332 let protected
= backup_dir
.is_protected(base_path
.to_owned());
334 Ok(BackupInfo { backup_dir, files, protected }
)
337 /// Finds the latest backup inside a backup group
342 ) -> Result
<Option
<BackupInfo
>, Error
> {
343 let backups
= group
.list_backups(base_path
)?
;
346 .filter(|item
| !only_finished
|| item
.is_finished())
347 .max_by_key(|item
| item
.backup_dir
.backup_time()))
350 pub fn sort_list(list
: &mut Vec
<BackupInfo
>, ascendending
: bool
) {
353 list
.sort_unstable_by(|a
, b
| a
.backup_dir
.backup_time
.cmp(&b
.backup_dir
.backup_time
));
356 list
.sort_unstable_by(|a
, b
| b
.backup_dir
.backup_time
.cmp(&a
.backup_dir
.backup_time
));
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());
364 let files
= list_backup_files(libc
::AT_FDCWD
, &path
)?
;
369 pub fn list_backup_groups(base_path
: &Path
) -> Result
<Vec
<BackupGroup
>, Error
> {
370 let mut list
= Vec
::new();
372 proxmox_sys
::fs
::scandir(
376 |l0_fd
, backup_type
, file_type
| {
377 if file_type
!= nix
::dir
::Type
::Directory
{
380 proxmox_sys
::fs
::scandir(
384 |_
, backup_id
, file_type
| {
385 if file_type
!= nix
::dir
::Type
::Directory
{
389 list
.push(BackupGroup
::new(backup_type
, backup_id
));
400 pub fn is_finished(&self) -> bool
{
401 // backup is considered unfinished if there is no manifest
404 .any(|name
| name
== MANIFEST_BLOB_NAME
)
408 fn list_backup_files
<P
: ?Sized
+ nix
::NixPath
>(
411 ) -> Result
<Vec
<String
>, Error
> {
412 let mut files
= vec
![];
414 proxmox_sys
::fs
::scandir(dirfd
, path
, &BACKUP_FILE_REGEX
, |_
, filename
, file_type
| {
415 if file_type
!= nix
::dir
::Type
::File
{
418 files
.push(filename
.to_owned());