]>
Commit | Line | Data |
---|---|---|
b3483782 DM |
1 | use crate::tools; |
2 | ||
f7d4e4b5 | 3 | use anyhow::{bail, format_err, Error}; |
b3483782 | 4 | use regex::Regex; |
c0977501 | 5 | use std::os::unix::io::RawFd; |
b3483782 | 6 | |
92acbd69 | 7 | use chrono::{DateTime, TimeZone, SecondsFormat, Utc}; |
b3483782 DM |
8 | |
9 | use std::path::{PathBuf, Path}; | |
10 | use lazy_static::lazy_static; | |
11 | ||
8f14e8fe DM |
12 | use super::manifest::MANIFEST_BLOB_NAME; |
13 | ||
b3483782 DM |
14 | macro_rules! BACKUP_ID_RE { () => (r"[A-Za-z0-9][A-Za-z0-9_-]+") } |
15 | macro_rules! BACKUP_TYPE_RE { () => (r"(?:host|vm|ct)") } | |
fa5d6977 | 16 | 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") } |
b3483782 DM |
17 | |
18 | lazy_static!{ | |
19 | static ref BACKUP_FILE_REGEX: Regex = Regex::new( | |
c6d203bb | 20 | r"^.*\.([fd]idx|blob)$").unwrap(); |
b3483782 DM |
21 | |
22 | static ref BACKUP_TYPE_REGEX: Regex = Regex::new( | |
23 | concat!(r"^(", BACKUP_TYPE_RE!(), r")$")).unwrap(); | |
24 | ||
25 | static ref BACKUP_ID_REGEX: Regex = Regex::new( | |
26 | concat!(r"^", BACKUP_ID_RE!(), r"$")).unwrap(); | |
27 | ||
28 | static ref BACKUP_DATE_REGEX: Regex = Regex::new( | |
29 | concat!(r"^", BACKUP_TIME_RE!() ,r"$")).unwrap(); | |
30 | ||
31 | static ref GROUP_PATH_REGEX: Regex = Regex::new( | |
8ce49a76 | 32 | concat!(r"^(", BACKUP_TYPE_RE!(), ")/(", BACKUP_ID_RE!(), r")$")).unwrap(); |
b3483782 DM |
33 | |
34 | static ref SNAPSHOT_PATH_REGEX: Regex = Regex::new( | |
8ce49a76 | 35 | concat!(r"^(", BACKUP_TYPE_RE!(), ")/(", BACKUP_ID_RE!(), ")/(", BACKUP_TIME_RE!(), r")$")).unwrap(); |
b3483782 DM |
36 | |
37 | } | |
38 | ||
d57474e0 | 39 | /// BackupGroup is a directory containing a list of BackupDir |
11d89239 | 40 | #[derive(Debug, Eq, PartialEq, Hash, Clone)] |
b3483782 DM |
41 | pub struct BackupGroup { |
42 | /// Type of backup | |
43 | backup_type: String, | |
44 | /// Unique (for this type) ID | |
45 | backup_id: String, | |
46 | } | |
47 | ||
48 | impl BackupGroup { | |
49 | ||
93b49ce3 | 50 | pub fn new<T: Into<String>, U: Into<String>>(backup_type: T, backup_id: U) -> Self { |
b3483782 DM |
51 | Self { backup_type: backup_type.into(), backup_id: backup_id.into() } |
52 | } | |
53 | ||
54 | pub fn backup_type(&self) -> &str { | |
55 | &self.backup_type | |
56 | } | |
57 | ||
58 | pub fn backup_id(&self) -> &str { | |
59 | &self.backup_id | |
60 | } | |
61 | ||
62 | pub fn parse(path: &str) -> Result<Self, Error> { | |
63 | ||
64 | let cap = GROUP_PATH_REGEX.captures(path) | |
65 | .ok_or_else(|| format_err!("unable to parse backup group path '{}'", path))?; | |
66 | ||
67 | Ok(Self { | |
68 | backup_type: cap.get(1).unwrap().as_str().to_owned(), | |
69 | backup_id: cap.get(2).unwrap().as_str().to_owned(), | |
70 | }) | |
71 | } | |
72 | ||
73 | pub fn group_path(&self) -> PathBuf { | |
74 | ||
75 | let mut relative_path = PathBuf::new(); | |
76 | ||
77 | relative_path.push(&self.backup_type); | |
78 | ||
79 | relative_path.push(&self.backup_id); | |
80 | ||
81 | relative_path | |
82 | } | |
c0977501 DM |
83 | |
84 | pub fn list_backups(&self, base_path: &Path) -> Result<Vec<BackupInfo>, Error> { | |
85 | ||
86 | let mut list = vec![]; | |
87 | ||
88 | let mut path = base_path.to_owned(); | |
89 | path.push(self.group_path()); | |
90 | ||
91 | tools::scandir(libc::AT_FDCWD, &path, &BACKUP_DATE_REGEX, |l2_fd, backup_time, file_type| { | |
92 | if file_type != nix::dir::Type::Directory { return Ok(()); } | |
93 | ||
fa5d6977 DM |
94 | let dt = backup_time.parse::<DateTime<Utc>>()?; |
95 | let backup_dir = BackupDir::new(self.backup_type.clone(), self.backup_id.clone(), dt.timestamp()); | |
c0977501 DM |
96 | let files = list_backup_files(l2_fd, backup_time)?; |
97 | ||
98 | list.push(BackupInfo { backup_dir, files }); | |
99 | ||
100 | Ok(()) | |
101 | })?; | |
102 | Ok(list) | |
103 | } | |
aeeac29b | 104 | |
8f14e8fe DM |
105 | pub fn last_successful_backup(&self, base_path: &Path) -> Result<Option<DateTime<Utc>>, Error> { |
106 | ||
107 | let mut last = None; | |
108 | ||
109 | let mut path = base_path.to_owned(); | |
110 | path.push(self.group_path()); | |
111 | ||
112 | tools::scandir(libc::AT_FDCWD, &path, &BACKUP_DATE_REGEX, |l2_fd, backup_time, file_type| { | |
113 | if file_type != nix::dir::Type::Directory { return Ok(()); } | |
114 | ||
115 | let mut manifest_path = PathBuf::from(backup_time); | |
116 | manifest_path.push(MANIFEST_BLOB_NAME); | |
117 | ||
118 | use nix::fcntl::{openat, OFlag}; | |
119 | match openat(l2_fd, &manifest_path, OFlag::O_RDONLY, nix::sys::stat::Mode::empty()) { | |
120 | Ok(_) => { /* manifest exists --> assume backup was successful */ }, | |
121 | Err(nix::Error::Sys(nix::errno::Errno::ENOENT)) => { return Ok(()); } | |
122 | Err(err) => { | |
123 | bail!("last_successful_backup: unexpected error - {}", err); | |
124 | } | |
125 | } | |
126 | ||
127 | let dt = backup_time.parse::<DateTime<Utc>>()?; | |
128 | if let Some(last_dt) = last { | |
129 | if dt > last_dt { last = Some(dt); } | |
130 | } else { | |
131 | last = Some(dt); | |
132 | } | |
133 | ||
134 | Ok(()) | |
135 | })?; | |
136 | ||
137 | Ok(last) | |
138 | } | |
139 | ||
11d89239 DM |
140 | pub fn list_groups(base_path: &Path) -> Result<Vec<BackupGroup>, Error> { |
141 | let mut list = Vec::new(); | |
142 | ||
143 | tools::scandir(libc::AT_FDCWD, base_path, &BACKUP_TYPE_REGEX, |l0_fd, backup_type, file_type| { | |
144 | if file_type != nix::dir::Type::Directory { return Ok(()); } | |
145 | tools::scandir(l0_fd, backup_type, &BACKUP_ID_REGEX, |_l1_fd, backup_id, file_type| { | |
146 | if file_type != nix::dir::Type::Directory { return Ok(()); } | |
147 | list.push(BackupGroup::new(backup_type, backup_id)); | |
148 | Ok(()) | |
149 | }) | |
150 | })?; | |
151 | Ok(list) | |
152 | } | |
b3483782 DM |
153 | } |
154 | ||
155 | /// Uniquely identify a Backup (relative to data store) | |
d57474e0 DM |
156 | /// |
157 | /// We also call this a backup snaphost. | |
35a2d8a6 | 158 | #[derive(Debug, Clone)] |
b3483782 DM |
159 | pub struct BackupDir { |
160 | /// Backup group | |
161 | group: BackupGroup, | |
162 | /// Backup timestamp | |
fa5d6977 | 163 | backup_time: DateTime<Utc>, |
b3483782 DM |
164 | } |
165 | ||
166 | impl BackupDir { | |
167 | ||
391d3107 WB |
168 | pub fn new<T, U>(backup_type: T, backup_id: U, timestamp: i64) -> Self |
169 | where | |
170 | T: Into<String>, | |
171 | U: Into<String>, | |
172 | { | |
b3483782 | 173 | // Note: makes sure that nanoseconds is 0 |
391d3107 WB |
174 | Self { |
175 | group: BackupGroup::new(backup_type.into(), backup_id.into()), | |
fa5d6977 | 176 | backup_time: Utc.timestamp(timestamp, 0), |
391d3107 | 177 | } |
b3483782 | 178 | } |
51a4f63f | 179 | pub fn new_with_group(group: BackupGroup, timestamp: i64) -> Self { |
fa5d6977 | 180 | Self { group, backup_time: Utc.timestamp(timestamp, 0) } |
51a4f63f | 181 | } |
b3483782 DM |
182 | |
183 | pub fn group(&self) -> &BackupGroup { | |
184 | &self.group | |
185 | } | |
186 | ||
fa5d6977 | 187 | pub fn backup_time(&self) -> DateTime<Utc> { |
b3483782 DM |
188 | self.backup_time |
189 | } | |
190 | ||
191 | pub fn parse(path: &str) -> Result<Self, Error> { | |
192 | ||
193 | let cap = SNAPSHOT_PATH_REGEX.captures(path) | |
194 | .ok_or_else(|| format_err!("unable to parse backup snapshot path '{}'", path))?; | |
195 | ||
196 | let group = BackupGroup::new(cap.get(1).unwrap().as_str(), cap.get(2).unwrap().as_str()); | |
fa5d6977 DM |
197 | let backup_time = cap.get(3).unwrap().as_str().parse::<DateTime<Utc>>()?; |
198 | Ok(BackupDir::from((group, backup_time.timestamp()))) | |
b3483782 DM |
199 | } |
200 | ||
201 | pub fn relative_path(&self) -> PathBuf { | |
202 | ||
203 | let mut relative_path = self.group.group_path(); | |
204 | ||
fa5d6977 | 205 | relative_path.push(Self::backup_time_to_string(self.backup_time)); |
b3483782 DM |
206 | |
207 | relative_path | |
208 | } | |
fa5d6977 DM |
209 | |
210 | pub fn backup_time_to_string(backup_time: DateTime<Utc>) -> String { | |
211 | backup_time.to_rfc3339_opts(SecondsFormat::Secs, true) | |
212 | } | |
b3483782 DM |
213 | } |
214 | ||
391d3107 WB |
215 | impl From<(BackupGroup, i64)> for BackupDir { |
216 | fn from((group, timestamp): (BackupGroup, i64)) -> Self { | |
fa5d6977 | 217 | Self { group, backup_time: Utc.timestamp(timestamp, 0) } |
391d3107 WB |
218 | } |
219 | } | |
220 | ||
d57474e0 | 221 | /// Detailed Backup Information, lists files inside a BackupDir |
b02a52e3 | 222 | #[derive(Debug, Clone)] |
b3483782 DM |
223 | pub struct BackupInfo { |
224 | /// the backup directory | |
225 | pub backup_dir: BackupDir, | |
226 | /// List of data files | |
227 | pub files: Vec<String>, | |
228 | } | |
229 | ||
230 | impl BackupInfo { | |
231 | ||
38a6cdda DM |
232 | pub fn new(base_path: &Path, backup_dir: BackupDir) -> Result<BackupInfo, Error> { |
233 | let mut path = base_path.to_owned(); | |
234 | path.push(backup_dir.relative_path()); | |
235 | ||
236 | let files = list_backup_files(libc::AT_FDCWD, &path)?; | |
237 | ||
238 | Ok(BackupInfo { backup_dir, files }) | |
239 | } | |
240 | ||
51a4f63f DM |
241 | /// Finds the latest backup inside a backup group |
242 | pub fn last_backup(base_path: &Path, group: &BackupGroup) -> Result<Option<BackupInfo>, Error> { | |
243 | let backups = group.list_backups(base_path)?; | |
244 | Ok(backups.into_iter().max_by_key(|item| item.backup_dir.backup_time())) | |
245 | } | |
246 | ||
b3483782 DM |
247 | pub fn sort_list(list: &mut Vec<BackupInfo>, ascendending: bool) { |
248 | if ascendending { // oldest first | |
249 | list.sort_unstable_by(|a, b| a.backup_dir.backup_time.cmp(&b.backup_dir.backup_time)); | |
250 | } else { // newest first | |
251 | list.sort_unstable_by(|a, b| b.backup_dir.backup_time.cmp(&a.backup_dir.backup_time)); | |
252 | } | |
253 | } | |
254 | ||
58e99e13 DM |
255 | pub fn list_files(base_path: &Path, backup_dir: &BackupDir) -> Result<Vec<String>, Error> { |
256 | let mut path = base_path.to_owned(); | |
257 | path.push(backup_dir.relative_path()); | |
258 | ||
c0977501 | 259 | let files = list_backup_files(libc::AT_FDCWD, &path)?; |
58e99e13 DM |
260 | |
261 | Ok(files) | |
262 | } | |
263 | ||
264 | pub fn list_backups(base_path: &Path) -> Result<Vec<BackupInfo>, Error> { | |
11d89239 | 265 | let mut list = Vec::new(); |
b3483782 | 266 | |
58e99e13 | 267 | tools::scandir(libc::AT_FDCWD, base_path, &BACKUP_TYPE_REGEX, |l0_fd, backup_type, file_type| { |
b3483782 DM |
268 | if file_type != nix::dir::Type::Directory { return Ok(()); } |
269 | tools::scandir(l0_fd, backup_type, &BACKUP_ID_REGEX, |l1_fd, backup_id, file_type| { | |
270 | if file_type != nix::dir::Type::Directory { return Ok(()); } | |
271 | tools::scandir(l1_fd, backup_id, &BACKUP_DATE_REGEX, |l2_fd, backup_time, file_type| { | |
272 | if file_type != nix::dir::Type::Directory { return Ok(()); } | |
273 | ||
fa5d6977 DM |
274 | let dt = backup_time.parse::<DateTime<Utc>>()?; |
275 | let backup_dir = BackupDir::new(backup_type, backup_id, dt.timestamp()); | |
b3483782 | 276 | |
c0977501 | 277 | let files = list_backup_files(l2_fd, backup_time)?; |
b3483782 | 278 | |
c0977501 | 279 | list.push(BackupInfo { backup_dir, files }); |
b3483782 DM |
280 | |
281 | Ok(()) | |
282 | }) | |
283 | }) | |
284 | })?; | |
285 | Ok(list) | |
286 | } | |
287 | } | |
c0977501 DM |
288 | |
289 | fn list_backup_files<P: ?Sized + nix::NixPath>(dirfd: RawFd, path: &P) -> Result<Vec<String>, Error> { | |
290 | let mut files = vec![]; | |
291 | ||
292 | tools::scandir(dirfd, path, &BACKUP_FILE_REGEX, |_, filename, file_type| { | |
293 | if file_type != nix::dir::Type::File { return Ok(()); } | |
294 | files.push(filename.to_owned()); | |
295 | Ok(()) | |
296 | })?; | |
297 | ||
298 | Ok(files) | |
299 | } |