]>
Commit | Line | Data |
---|---|---|
6da20161 | 1 | use std::fmt; |
c0977501 | 2 | use std::os::unix::io::RawFd; |
6da20161 WB |
3 | use std::path::PathBuf; |
4 | use std::sync::Arc; | |
038ee599 | 5 | |
5c9c23b6 | 6 | use anyhow::{bail, format_err, Error}; |
770a36e5 | 7 | |
9ccf933b | 8 | use proxmox_sys::fs::{lock_dir_noblock, replace_file, CreateOptions}; |
f03649b8 | 9 | |
11ffd737 | 10 | use pbs_api_types::{ |
133d718f | 11 | Authid, BackupNamespace, BackupType, GroupFilter, BACKUP_DATE_REGEX, BACKUP_FILE_REGEX, |
11ffd737 | 12 | }; |
5c9c23b6 | 13 | use pbs_config::{open_backup_lockfile, BackupLockGuard}; |
b3483782 | 14 | |
55660998 TL |
15 | use crate::manifest::{ |
16 | BackupManifest, CLIENT_LOG_BLOB_NAME, MANIFEST_BLOB_NAME, MANIFEST_LOCK_NAME, | |
17 | }; | |
0f198b82 | 18 | use crate::{DataBlob, DataStore}; |
8f14e8fe | 19 | |
524ed404 CE |
20 | #[derive(Default)] |
21 | pub struct BackupGroupDeleteStats { | |
22 | // Count of protected snapshots, therefore not removed | |
23 | unremoved_protected: usize, | |
24 | // Count of deleted snapshots | |
25 | removed_snapshots: usize, | |
26 | } | |
27 | ||
28 | impl BackupGroupDeleteStats { | |
29 | pub fn all_removed(&self) -> bool { | |
30 | self.unremoved_protected == 0 | |
31 | } | |
32 | ||
33 | pub fn removed_snapshots(&self) -> usize { | |
34 | self.removed_snapshots | |
35 | } | |
36 | ||
37 | pub fn protected_snapshots(&self) -> usize { | |
38 | self.unremoved_protected | |
39 | } | |
40 | ||
41 | fn increment_removed_snapshots(&mut self) { | |
42 | self.removed_snapshots += 1; | |
43 | } | |
44 | ||
45 | fn increment_protected_snapshots(&mut self) { | |
46 | self.unremoved_protected += 1; | |
47 | } | |
48 | } | |
49 | ||
d57474e0 | 50 | /// BackupGroup is a directory containing a list of BackupDir |
6da20161 | 51 | #[derive(Clone)] |
b3483782 | 52 | pub struct BackupGroup { |
6da20161 WB |
53 | store: Arc<DataStore>, |
54 | ||
133d718f | 55 | ns: BackupNamespace, |
db87d93e | 56 | group: pbs_api_types::BackupGroup, |
b3483782 DM |
57 | } |
58 | ||
6da20161 WB |
59 | impl fmt::Debug for BackupGroup { |
60 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { | |
61 | f.debug_struct("BackupGroup") | |
62 | .field("store", &self.store.name()) | |
d22363ad | 63 | .field("ns", &self.ns) |
6da20161 WB |
64 | .field("group", &self.group) |
65 | .finish() | |
66 | } | |
67 | } | |
68 | ||
b3483782 | 69 | impl BackupGroup { |
133d718f WB |
70 | pub(crate) fn new( |
71 | store: Arc<DataStore>, | |
72 | ns: BackupNamespace, | |
73 | group: pbs_api_types::BackupGroup, | |
74 | ) -> Self { | |
75 | Self { store, ns, group } | |
b3483782 DM |
76 | } |
77 | ||
5116453b WB |
78 | /// Access the underlying [`BackupGroup`](pbs_api_types::BackupGroup). |
79 | #[inline] | |
80 | pub fn group(&self) -> &pbs_api_types::BackupGroup { | |
81 | &self.group | |
82 | } | |
83 | ||
11ffd737 WB |
84 | #[inline] |
85 | pub fn backup_ns(&self) -> &BackupNamespace { | |
133d718f | 86 | &self.ns |
11ffd737 WB |
87 | } |
88 | ||
89 | #[inline] | |
988d575d | 90 | pub fn backup_type(&self) -> BackupType { |
db87d93e | 91 | self.group.ty |
b3483782 DM |
92 | } |
93 | ||
11ffd737 | 94 | #[inline] |
b3483782 | 95 | pub fn backup_id(&self) -> &str { |
db87d93e | 96 | &self.group.id |
b3483782 DM |
97 | } |
98 | ||
6da20161 | 99 | pub fn full_group_path(&self) -> PathBuf { |
133d718f | 100 | self.store.group_path(&self.ns, &self.group) |
6da20161 WB |
101 | } |
102 | ||
1f6a45c9 | 103 | pub fn relative_group_path(&self) -> PathBuf { |
a756b0c1 | 104 | let mut path = self.ns.path(); |
133d718f WB |
105 | path.push(self.group.ty.as_str()); |
106 | path.push(&self.group.id); | |
107 | path | |
b3483782 | 108 | } |
c0977501 | 109 | |
487c5be9 WB |
110 | /// Simple check whether a group exists. This does not check whether there are any snapshots, |
111 | /// but rather it simply checks whether the directory exists. | |
112 | pub fn exists(&self) -> bool { | |
113 | self.full_group_path().exists() | |
114 | } | |
115 | ||
6da20161 | 116 | pub fn list_backups(&self) -> Result<Vec<BackupInfo>, Error> { |
c0977501 DM |
117 | let mut list = vec![]; |
118 | ||
4b77d300 | 119 | let path = self.full_group_path(); |
c0977501 | 120 | |
25877d05 | 121 | proxmox_sys::fs::scandir( |
cded320e TL |
122 | libc::AT_FDCWD, |
123 | &path, | |
124 | &BACKUP_DATE_REGEX, | |
125 | |l2_fd, backup_time, file_type| { | |
126 | if file_type != nix::dir::Type::Directory { | |
127 | return Ok(()); | |
128 | } | |
c0977501 | 129 | |
db87d93e | 130 | let backup_dir = self.backup_dir_with_rfc3339(backup_time)?; |
cded320e | 131 | let files = list_backup_files(l2_fd, backup_time)?; |
c0977501 | 132 | |
6da20161 | 133 | let protected = backup_dir.is_protected(); |
92c5cf42 | 134 | |
42c2b5be TL |
135 | list.push(BackupInfo { |
136 | backup_dir, | |
137 | files, | |
138 | protected, | |
139 | }); | |
c0977501 | 140 | |
cded320e TL |
141 | Ok(()) |
142 | }, | |
143 | )?; | |
c0977501 DM |
144 | Ok(list) |
145 | } | |
aeeac29b | 146 | |
c4b2d26c | 147 | /// Finds the latest backup inside a backup group |
6da20161 WB |
148 | pub fn last_backup(&self, only_finished: bool) -> Result<Option<BackupInfo>, Error> { |
149 | let backups = self.list_backups()?; | |
c4b2d26c WB |
150 | Ok(backups |
151 | .into_iter() | |
152 | .filter(|item| !only_finished || item.is_finished()) | |
153 | .max_by_key(|item| item.backup_dir.backup_time())) | |
154 | } | |
155 | ||
6da20161 | 156 | pub fn last_successful_backup(&self) -> Result<Option<i64>, Error> { |
8f14e8fe DM |
157 | let mut last = None; |
158 | ||
4b77d300 | 159 | let path = self.full_group_path(); |
8f14e8fe | 160 | |
25877d05 | 161 | proxmox_sys::fs::scandir( |
cded320e TL |
162 | libc::AT_FDCWD, |
163 | &path, | |
164 | &BACKUP_DATE_REGEX, | |
165 | |l2_fd, backup_time, file_type| { | |
166 | if file_type != nix::dir::Type::Directory { | |
167 | return Ok(()); | |
8f14e8fe | 168 | } |
8f14e8fe | 169 | |
cded320e TL |
170 | let mut manifest_path = PathBuf::from(backup_time); |
171 | manifest_path.push(MANIFEST_BLOB_NAME); | |
172 | ||
173 | use nix::fcntl::{openat, OFlag}; | |
174 | match openat( | |
175 | l2_fd, | |
176 | &manifest_path, | |
177 | OFlag::O_RDONLY, | |
178 | nix::sys::stat::Mode::empty(), | |
179 | ) { | |
180 | Ok(rawfd) => { | |
181 | /* manifest exists --> assume backup was successful */ | |
182 | /* close else this leaks! */ | |
183 | nix::unistd::close(rawfd)?; | |
184 | } | |
11ca8343 | 185 | Err(nix::errno::Errno::ENOENT) => { |
cded320e TL |
186 | return Ok(()); |
187 | } | |
188 | Err(err) => { | |
189 | bail!("last_successful_backup: unexpected error - {}", err); | |
190 | } | |
191 | } | |
192 | ||
6ef1b649 | 193 | let timestamp = proxmox_time::parse_rfc3339(backup_time)?; |
cded320e TL |
194 | if let Some(last_timestamp) = last { |
195 | if timestamp > last_timestamp { | |
196 | last = Some(timestamp); | |
197 | } | |
198 | } else { | |
199 | last = Some(timestamp); | |
200 | } | |
8f14e8fe | 201 | |
cded320e TL |
202 | Ok(()) |
203 | }, | |
204 | )?; | |
8f14e8fe DM |
205 | |
206 | Ok(last) | |
207 | } | |
0ceb9753 FG |
208 | |
209 | pub fn matches(&self, filter: &GroupFilter) -> bool { | |
db87d93e WB |
210 | self.group.matches(filter) |
211 | } | |
212 | ||
213 | pub fn backup_dir(&self, time: i64) -> Result<BackupDir, Error> { | |
214 | BackupDir::with_group(self.clone(), time) | |
215 | } | |
216 | ||
217 | pub fn backup_dir_with_rfc3339<T: Into<String>>( | |
218 | &self, | |
219 | time_string: T, | |
220 | ) -> Result<BackupDir, Error> { | |
221 | BackupDir::with_rfc3339(self.clone(), time_string.into()) | |
222 | } | |
6da20161 WB |
223 | |
224 | pub fn iter_snapshots(&self) -> Result<crate::ListSnapshots, Error> { | |
225 | crate::ListSnapshots::new(self.clone()) | |
226 | } | |
f03649b8 TL |
227 | |
228 | /// Destroy the group inclusive all its backup snapshots (BackupDir's) | |
229 | /// | |
524ed404 CE |
230 | /// Returns `BackupGroupDeleteStats`, containing the number of deleted snapshots |
231 | /// and number of protected snaphsots, which therefore were not removed. | |
232 | pub fn destroy(&self) -> Result<BackupGroupDeleteStats, Error> { | |
f03649b8 TL |
233 | let path = self.full_group_path(); |
234 | let _guard = | |
235 | proxmox_sys::fs::lock_dir_noblock(&path, "backup group", "possible running backup")?; | |
236 | ||
237 | log::info!("removing backup group {:?}", path); | |
524ed404 | 238 | let mut delete_stats = BackupGroupDeleteStats::default(); |
f03649b8 TL |
239 | for snap in self.iter_snapshots()? { |
240 | let snap = snap?; | |
241 | if snap.is_protected() { | |
524ed404 | 242 | delete_stats.increment_protected_snapshots(); |
f03649b8 TL |
243 | continue; |
244 | } | |
245 | snap.destroy(false)?; | |
524ed404 | 246 | delete_stats.increment_removed_snapshots(); |
f03649b8 TL |
247 | } |
248 | ||
524ed404 | 249 | if delete_stats.all_removed() { |
f03649b8 TL |
250 | std::fs::remove_dir_all(&path).map_err(|err| { |
251 | format_err!("removing group directory {:?} failed - {}", path, err) | |
252 | })?; | |
253 | } | |
254 | ||
524ed404 | 255 | Ok(delete_stats) |
f03649b8 | 256 | } |
133d718f WB |
257 | |
258 | /// Returns the backup owner. | |
259 | /// | |
260 | /// The backup owner is the entity who first created the backup group. | |
261 | pub fn get_owner(&self) -> Result<Authid, Error> { | |
262 | self.store.get_owner(&self.ns, self.as_ref()) | |
263 | } | |
264 | ||
265 | /// Set the backup owner. | |
266 | pub fn set_owner(&self, auth_id: &Authid, force: bool) -> Result<(), Error> { | |
267 | self.store | |
ca3f8757 | 268 | .set_owner(&self.ns, self.as_ref(), auth_id, force) |
133d718f WB |
269 | } |
270 | } | |
271 | ||
272 | impl AsRef<pbs_api_types::BackupNamespace> for BackupGroup { | |
273 | #[inline] | |
274 | fn as_ref(&self) -> &pbs_api_types::BackupNamespace { | |
275 | &self.ns | |
276 | } | |
db87d93e WB |
277 | } |
278 | ||
279 | impl AsRef<pbs_api_types::BackupGroup> for BackupGroup { | |
280 | #[inline] | |
281 | fn as_ref(&self) -> &pbs_api_types::BackupGroup { | |
282 | &self.group | |
0ceb9753 | 283 | } |
b3483782 DM |
284 | } |
285 | ||
988d575d WB |
286 | impl From<&BackupGroup> for pbs_api_types::BackupGroup { |
287 | fn from(group: &BackupGroup) -> pbs_api_types::BackupGroup { | |
db87d93e | 288 | group.group.clone() |
988d575d WB |
289 | } |
290 | } | |
291 | ||
292 | impl From<BackupGroup> for pbs_api_types::BackupGroup { | |
293 | fn from(group: BackupGroup) -> pbs_api_types::BackupGroup { | |
db87d93e | 294 | group.group |
988d575d WB |
295 | } |
296 | } | |
297 | ||
db87d93e WB |
298 | impl From<BackupDir> for BackupGroup { |
299 | fn from(dir: BackupDir) -> BackupGroup { | |
300 | BackupGroup { | |
6da20161 | 301 | store: dir.store, |
133d718f | 302 | ns: dir.ns, |
db87d93e WB |
303 | group: dir.dir.group, |
304 | } | |
305 | } | |
306 | } | |
d6d3b353 | 307 | |
db87d93e WB |
308 | impl From<&BackupDir> for BackupGroup { |
309 | fn from(dir: &BackupDir) -> BackupGroup { | |
310 | BackupGroup { | |
6da20161 | 311 | store: Arc::clone(&dir.store), |
133d718f | 312 | ns: dir.ns.clone(), |
db87d93e WB |
313 | group: dir.dir.group.clone(), |
314 | } | |
d6d3b353 DM |
315 | } |
316 | } | |
317 | ||
b3483782 | 318 | /// Uniquely identify a Backup (relative to data store) |
d57474e0 DM |
319 | /// |
320 | /// We also call this a backup snaphost. | |
6da20161 | 321 | #[derive(Clone)] |
b3483782 | 322 | pub struct BackupDir { |
6da20161 | 323 | store: Arc<DataStore>, |
133d718f | 324 | ns: BackupNamespace, |
db87d93e | 325 | dir: pbs_api_types::BackupDir, |
6a7be83e | 326 | // backup_time as rfc3339 |
cded320e | 327 | backup_time_string: String, |
b3483782 DM |
328 | } |
329 | ||
6da20161 WB |
330 | impl fmt::Debug for BackupDir { |
331 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { | |
332 | f.debug_struct("BackupDir") | |
333 | .field("store", &self.store.name()) | |
d22363ad | 334 | .field("ns", &self.ns) |
6da20161 WB |
335 | .field("dir", &self.dir) |
336 | .field("backup_time_string", &self.backup_time_string) | |
337 | .finish() | |
338 | } | |
339 | } | |
340 | ||
b3483782 | 341 | impl BackupDir { |
db87d93e WB |
342 | /// Temporarily used for tests. |
343 | #[doc(hidden)] | |
344 | pub fn new_test(dir: pbs_api_types::BackupDir) -> Self { | |
345 | Self { | |
6da20161 | 346 | store: unsafe { DataStore::new_test() }, |
db87d93e | 347 | backup_time_string: Self::backup_time_to_string(dir.time).unwrap(), |
133d718f | 348 | ns: BackupNamespace::root(), |
db87d93e WB |
349 | dir, |
350 | } | |
351 | } | |
352 | ||
353 | pub(crate) fn with_group(group: BackupGroup, backup_time: i64) -> Result<Self, Error> { | |
354 | let backup_time_string = Self::backup_time_to_string(backup_time)?; | |
cded320e | 355 | Ok(Self { |
6da20161 | 356 | store: group.store, |
133d718f | 357 | ns: group.ns, |
db87d93e | 358 | dir: (group.group, backup_time).into(), |
cded320e TL |
359 | backup_time_string, |
360 | }) | |
bc871bd1 DM |
361 | } |
362 | ||
db87d93e WB |
363 | pub(crate) fn with_rfc3339( |
364 | group: BackupGroup, | |
365 | backup_time_string: String, | |
366 | ) -> Result<Self, Error> { | |
367 | let backup_time = proxmox_time::parse_rfc3339(&backup_time_string)?; | |
cded320e | 368 | Ok(Self { |
6da20161 | 369 | store: group.store, |
133d718f | 370 | ns: group.ns, |
db87d93e | 371 | dir: (group.group, backup_time).into(), |
cded320e TL |
372 | backup_time_string, |
373 | }) | |
51a4f63f | 374 | } |
b3483782 | 375 | |
11ffd737 WB |
376 | #[inline] |
377 | pub fn backup_ns(&self) -> &BackupNamespace { | |
133d718f | 378 | &self.ns |
11ffd737 WB |
379 | } |
380 | ||
db87d93e WB |
381 | #[inline] |
382 | pub fn backup_type(&self) -> BackupType { | |
383 | self.dir.group.ty | |
b3483782 DM |
384 | } |
385 | ||
db87d93e WB |
386 | #[inline] |
387 | pub fn backup_id(&self) -> &str { | |
388 | &self.dir.group.id | |
389 | } | |
390 | ||
391 | #[inline] | |
6a7be83e | 392 | pub fn backup_time(&self) -> i64 { |
db87d93e | 393 | self.dir.time |
b3483782 DM |
394 | } |
395 | ||
6a7be83e DM |
396 | pub fn backup_time_string(&self) -> &str { |
397 | &self.backup_time_string | |
398 | } | |
399 | ||
f15601f1 FG |
400 | pub fn dir(&self) -> &pbs_api_types::BackupDir { |
401 | &self.dir | |
402 | } | |
403 | ||
404 | pub fn group(&self) -> &pbs_api_types::BackupGroup { | |
405 | &self.dir.group | |
406 | } | |
407 | ||
cded320e | 408 | pub fn relative_path(&self) -> PathBuf { |
30ccc300 | 409 | let mut path = self.ns.path(); |
133d718f WB |
410 | path.push(self.dir.group.ty.as_str()); |
411 | path.push(&self.dir.group.id); | |
412 | path.push(&self.backup_time_string); | |
413 | path | |
db87d93e | 414 | } |
b3483782 | 415 | |
db87d93e | 416 | /// Returns the absolute path for backup_dir, using the cached formatted time string. |
6da20161 | 417 | pub fn full_path(&self) -> PathBuf { |
133d718f | 418 | self.store.snapshot_path(&self.ns, &self.dir) |
b3483782 | 419 | } |
fa5d6977 | 420 | |
6da20161 | 421 | pub fn protected_file(&self) -> PathBuf { |
53d073ec | 422 | let mut path = self.full_path(); |
92c5cf42 DC |
423 | path.push(".protected"); |
424 | path | |
425 | } | |
426 | ||
6da20161 WB |
427 | pub fn is_protected(&self) -> bool { |
428 | let path = self.protected_file(); | |
92c5cf42 DC |
429 | path.exists() |
430 | } | |
431 | ||
6a7be83e DM |
432 | pub fn backup_time_to_string(backup_time: i64) -> Result<String, Error> { |
433 | // fixme: can this fail? (avoid unwrap) | |
dcf5a0f6 | 434 | proxmox_time::epoch_to_rfc3339_utc(backup_time) |
fa5d6977 | 435 | } |
5c9c23b6 | 436 | |
1eef52c2 TL |
437 | /// load a `DataBlob` from this snapshot's backup dir. |
438 | pub fn load_blob(&self, filename: &str) -> Result<DataBlob, Error> { | |
439 | let mut path = self.full_path(); | |
440 | path.push(filename); | |
441 | ||
442 | proxmox_lang::try_block!({ | |
443 | let mut file = std::fs::File::open(&path)?; | |
444 | DataBlob::load_from_reader(&mut file) | |
445 | }) | |
446 | .map_err(|err| format_err!("unable to load blob '{:?}' - {}", path, err)) | |
447 | } | |
448 | ||
5c9c23b6 TL |
449 | /// Returns the filename to lock a manifest |
450 | /// | |
451 | /// Also creates the basedir. The lockfile is located in | |
f2fe00f1 FG |
452 | /// '/run/proxmox-backup/locks/{datastore}/[ns/{ns}/]+{type}/{id}/{timestamp}.index.json.lck' |
453 | fn manifest_lock_path(&self) -> Result<PathBuf, Error> { | |
454 | let mut path = PathBuf::from(&format!("/run/proxmox-backup/locks/{}", self.store.name())); | |
455 | path.push(self.relative_path()); | |
456 | ||
5c9c23b6 | 457 | std::fs::create_dir_all(&path)?; |
5c9c23b6 | 458 | let ts = self.backup_time_string(); |
f2fe00f1 | 459 | path.push(&format!("{ts}{MANIFEST_LOCK_NAME}")); |
5c9c23b6 TL |
460 | |
461 | Ok(path) | |
462 | } | |
463 | ||
464 | /// Locks the manifest of a snapshot, for example, to update or delete it. | |
465 | pub(crate) fn lock_manifest(&self) -> Result<BackupLockGuard, Error> { | |
466 | let path = self.manifest_lock_path()?; | |
467 | ||
468 | // actions locking the manifest should be relatively short, only wait a few seconds | |
469 | open_backup_lockfile(&path, Some(std::time::Duration::from_secs(5)), true) | |
470 | .map_err(|err| format_err!("unable to acquire manifest lock {:?} - {}", &path, err)) | |
471 | } | |
f03649b8 TL |
472 | |
473 | /// Destroy the whole snapshot, bails if it's protected | |
474 | /// | |
475 | /// Setting `force` to true skips locking and thus ignores if the backup is currently in use. | |
476 | pub fn destroy(&self, force: bool) -> Result<(), Error> { | |
477 | let full_path = self.full_path(); | |
478 | ||
479 | let (_guard, _manifest_guard); | |
480 | if !force { | |
481 | _guard = lock_dir_noblock(&full_path, "snapshot", "possibly running or in use")?; | |
482 | _manifest_guard = self.lock_manifest()?; | |
483 | } | |
484 | ||
485 | if self.is_protected() { | |
486 | bail!("cannot remove protected snapshot"); // use special error type? | |
487 | } | |
488 | ||
489 | log::info!("removing backup snapshot {:?}", full_path); | |
490 | std::fs::remove_dir_all(&full_path).map_err(|err| { | |
491 | format_err!("removing backup snapshot {:?} failed - {}", full_path, err,) | |
492 | })?; | |
493 | ||
494 | // the manifest doesn't exist anymore, no need to keep the lock (already done by guard?) | |
495 | if let Ok(path) = self.manifest_lock_path() { | |
496 | let _ = std::fs::remove_file(path); // ignore errors | |
497 | } | |
498 | ||
499 | Ok(()) | |
500 | } | |
133d718f WB |
501 | |
502 | /// Get the datastore. | |
503 | pub fn datastore(&self) -> &Arc<DataStore> { | |
504 | &self.store | |
505 | } | |
506 | ||
507 | /// Returns the backup owner. | |
508 | /// | |
509 | /// The backup owner is the entity who first created the backup group. | |
510 | pub fn get_owner(&self) -> Result<Authid, Error> { | |
511 | self.store.get_owner(&self.ns, self.as_ref()) | |
512 | } | |
513 | ||
514 | /// Lock the snapshot and open a reader. | |
515 | pub fn locked_reader(&self) -> Result<crate::SnapshotReader, Error> { | |
516 | crate::SnapshotReader::new_do(self.clone()) | |
517 | } | |
518 | ||
519 | /// Load the manifest without a lock. Must not be written back. | |
520 | pub fn load_manifest(&self) -> Result<(BackupManifest, u64), Error> { | |
521 | let blob = self.load_blob(MANIFEST_BLOB_NAME)?; | |
522 | let raw_size = blob.raw_size(); | |
523 | let manifest = BackupManifest::try_from(blob)?; | |
524 | Ok((manifest, raw_size)) | |
525 | } | |
526 | ||
527 | /// Update the manifest of the specified snapshot. Never write a manifest directly, | |
528 | /// only use this method - anything else may break locking guarantees. | |
529 | pub fn update_manifest( | |
530 | &self, | |
531 | update_fn: impl FnOnce(&mut BackupManifest), | |
532 | ) -> Result<(), Error> { | |
9ccf933b TL |
533 | let _guard = self.lock_manifest()?; |
534 | let (mut manifest, _) = self.load_manifest()?; | |
535 | ||
536 | update_fn(&mut manifest); | |
537 | ||
538 | let manifest = serde_json::to_value(manifest)?; | |
539 | let manifest = serde_json::to_string_pretty(&manifest)?; | |
540 | let blob = DataBlob::encode(manifest.as_bytes(), None, true)?; | |
541 | let raw_data = blob.raw_data(); | |
542 | ||
543 | let mut path = self.full_path(); | |
544 | path.push(MANIFEST_BLOB_NAME); | |
545 | ||
546 | // atomic replace invalidates flock - no other writes past this point! | |
547 | replace_file(&path, raw_data, CreateOptions::new(), false)?; | |
548 | Ok(()) | |
133d718f | 549 | } |
55660998 TL |
550 | |
551 | /// Cleans up the backup directory by removing any file not mentioned in the manifest. | |
552 | pub fn cleanup_unreferenced_files(&self, manifest: &BackupManifest) -> Result<(), Error> { | |
553 | let full_path = self.full_path(); | |
554 | ||
555 | let mut wanted_files = std::collections::HashSet::new(); | |
556 | wanted_files.insert(MANIFEST_BLOB_NAME.to_string()); | |
557 | wanted_files.insert(CLIENT_LOG_BLOB_NAME.to_string()); | |
558 | manifest.files().iter().for_each(|item| { | |
559 | wanted_files.insert(item.filename.clone()); | |
560 | }); | |
561 | ||
562 | for item in proxmox_sys::fs::read_subdir(libc::AT_FDCWD, &full_path)?.flatten() { | |
563 | if let Some(file_type) = item.file_type() { | |
564 | if file_type != nix::dir::Type::File { | |
565 | continue; | |
566 | } | |
567 | } | |
568 | let file_name = item.file_name().to_bytes(); | |
569 | if file_name == b"." || file_name == b".." { | |
570 | continue; | |
571 | }; | |
572 | if let Ok(name) = std::str::from_utf8(file_name) { | |
573 | if wanted_files.contains(name) { | |
574 | continue; | |
575 | } | |
576 | } | |
577 | println!("remove unused file {:?}", item.file_name()); | |
578 | let dirfd = item.parent_fd(); | |
579 | let _res = unsafe { libc::unlinkat(dirfd, item.file_name().as_ptr(), 0) }; | |
580 | } | |
581 | ||
582 | Ok(()) | |
583 | } | |
133d718f WB |
584 | } |
585 | ||
586 | impl AsRef<pbs_api_types::BackupNamespace> for BackupDir { | |
587 | fn as_ref(&self) -> &pbs_api_types::BackupNamespace { | |
588 | &self.ns | |
589 | } | |
b3483782 | 590 | } |
d6d3b353 | 591 | |
db87d93e WB |
592 | impl AsRef<pbs_api_types::BackupDir> for BackupDir { |
593 | fn as_ref(&self) -> &pbs_api_types::BackupDir { | |
594 | &self.dir | |
988d575d WB |
595 | } |
596 | } | |
597 | ||
db87d93e WB |
598 | impl AsRef<pbs_api_types::BackupGroup> for BackupDir { |
599 | fn as_ref(&self) -> &pbs_api_types::BackupGroup { | |
600 | &self.dir.group | |
988d575d WB |
601 | } |
602 | } | |
603 | ||
db87d93e WB |
604 | impl From<&BackupDir> for pbs_api_types::BackupGroup { |
605 | fn from(dir: &BackupDir) -> pbs_api_types::BackupGroup { | |
606 | dir.dir.group.clone() | |
607 | } | |
608 | } | |
a67f7d0a | 609 | |
db87d93e WB |
610 | impl From<BackupDir> for pbs_api_types::BackupGroup { |
611 | fn from(dir: BackupDir) -> pbs_api_types::BackupGroup { | |
ca3f8757 | 612 | dir.dir.group |
db87d93e WB |
613 | } |
614 | } | |
a67f7d0a | 615 | |
db87d93e WB |
616 | impl From<&BackupDir> for pbs_api_types::BackupDir { |
617 | fn from(dir: &BackupDir) -> pbs_api_types::BackupDir { | |
618 | dir.dir.clone() | |
619 | } | |
620 | } | |
621 | ||
622 | impl From<BackupDir> for pbs_api_types::BackupDir { | |
623 | fn from(dir: BackupDir) -> pbs_api_types::BackupDir { | |
624 | dir.dir | |
a67f7d0a DM |
625 | } |
626 | } | |
b3483782 | 627 | |
d57474e0 | 628 | /// Detailed Backup Information, lists files inside a BackupDir |
6da20161 | 629 | #[derive(Clone, Debug)] |
b3483782 DM |
630 | pub struct BackupInfo { |
631 | /// the backup directory | |
632 | pub backup_dir: BackupDir, | |
633 | /// List of data files | |
634 | pub files: Vec<String>, | |
92c5cf42 DC |
635 | /// Protection Status |
636 | pub protected: bool, | |
b3483782 DM |
637 | } |
638 | ||
639 | impl BackupInfo { | |
6da20161 | 640 | pub fn new(backup_dir: BackupDir) -> Result<BackupInfo, Error> { |
4b77d300 | 641 | let path = backup_dir.full_path(); |
38a6cdda DM |
642 | |
643 | let files = list_backup_files(libc::AT_FDCWD, &path)?; | |
6da20161 | 644 | let protected = backup_dir.is_protected(); |
38a6cdda | 645 | |
42c2b5be TL |
646 | Ok(BackupInfo { |
647 | backup_dir, | |
648 | files, | |
649 | protected, | |
650 | }) | |
38a6cdda DM |
651 | } |
652 | ||
e1db0670 | 653 | pub fn sort_list(list: &mut [BackupInfo], ascendending: bool) { |
cded320e TL |
654 | if ascendending { |
655 | // oldest first | |
db87d93e | 656 | list.sort_unstable_by(|a, b| a.backup_dir.dir.time.cmp(&b.backup_dir.dir.time)); |
cded320e TL |
657 | } else { |
658 | // newest first | |
db87d93e | 659 | list.sort_unstable_by(|a, b| b.backup_dir.dir.time.cmp(&a.backup_dir.dir.time)); |
b3483782 DM |
660 | } |
661 | } | |
662 | ||
c9756b40 SR |
663 | pub fn is_finished(&self) -> bool { |
664 | // backup is considered unfinished if there is no manifest | |
42c2b5be | 665 | self.files.iter().any(|name| name == MANIFEST_BLOB_NAME) |
c9756b40 | 666 | } |
b3483782 | 667 | } |
c0977501 | 668 | |
cded320e TL |
669 | fn list_backup_files<P: ?Sized + nix::NixPath>( |
670 | dirfd: RawFd, | |
671 | path: &P, | |
672 | ) -> Result<Vec<String>, Error> { | |
c0977501 DM |
673 | let mut files = vec![]; |
674 | ||
25877d05 | 675 | proxmox_sys::fs::scandir(dirfd, path, &BACKUP_FILE_REGEX, |_, filename, file_type| { |
cded320e TL |
676 | if file_type != nix::dir::Type::File { |
677 | return Ok(()); | |
678 | } | |
c0977501 DM |
679 | files.push(filename.to_owned()); |
680 | Ok(()) | |
681 | })?; | |
682 | ||
683 | Ok(files) | |
684 | } |