]>
Commit | Line | Data |
---|---|---|
42c2b5be | 1 | use std::collections::{HashMap, HashSet}; |
54552dda | 2 | use std::io::{self, Write}; |
4c7cc5b3 | 3 | use std::os::unix::io::AsRawFd; |
367f002e | 4 | use std::path::{Path, PathBuf}; |
42c2b5be | 5 | use std::sync::{Arc, Mutex}; |
367f002e | 6 | |
f7d4e4b5 | 7 | use anyhow::{bail, format_err, Error}; |
2c32fdde | 8 | use lazy_static::lazy_static; |
4c7cc5b3 | 9 | use nix::unistd::{unlinkat, UnlinkatFlags}; |
e4439025 | 10 | |
08f8a3e5 | 11 | use proxmox_human_byte::HumanByte; |
fef61684 DC |
12 | use proxmox_schema::ApiType; |
13 | ||
857f346c | 14 | use proxmox_sys::error::SysError; |
42c2b5be TL |
15 | use proxmox_sys::fs::{file_read_optional_string, replace_file, CreateOptions}; |
16 | use proxmox_sys::fs::{lock_dir_noblock, DirLockGuard}; | |
d5790a9f | 17 | use proxmox_sys::process_locker::ProcessLockSharedGuard; |
25877d05 | 18 | use proxmox_sys::WorkerTaskContext; |
d5790a9f | 19 | use proxmox_sys::{task_log, task_warn}; |
529de6c7 | 20 | |
fef61684 | 21 | use pbs_api_types::{ |
647186dd | 22 | Authid, BackupNamespace, BackupType, ChunkOrder, DataStoreConfig, DatastoreFSyncLevel, |
08f8a3e5 | 23 | DatastoreTuning, GarbageCollectionStatus, Operation, UPID, |
fef61684 | 24 | }; |
529de6c7 | 25 | |
524ed404 | 26 | use crate::backup_info::{BackupDir, BackupGroup, BackupGroupDeleteStats}; |
6d5d305d DM |
27 | use crate::chunk_store::ChunkStore; |
28 | use crate::dynamic_index::{DynamicIndexReader, DynamicIndexWriter}; | |
29 | use crate::fixed_index::{FixedIndexReader, FixedIndexWriter}; | |
5444b914 | 30 | use crate::hierarchy::{ListGroups, ListGroupsType, ListNamespaces, ListNamespacesRecursive}; |
6d5d305d | 31 | use crate::index::IndexFile; |
9ccf933b | 32 | use crate::manifest::{archive_type, ArchiveType}; |
857f346c | 33 | use crate::task_tracking::{self, update_active_operations}; |
42c2b5be | 34 | use crate::DataBlob; |
6d5d305d | 35 | |
367f002e | 36 | lazy_static! { |
42c2b5be TL |
37 | static ref DATASTORE_MAP: Mutex<HashMap<String, Arc<DataStoreImpl>>> = |
38 | Mutex::new(HashMap::new()); | |
b3483782 | 39 | } |
ff3d3100 | 40 | |
9751ef4b DC |
41 | /// checks if auth_id is owner, or, if owner is a token, if |
42 | /// auth_id is the user of the token | |
42c2b5be TL |
43 | pub fn check_backup_owner(owner: &Authid, auth_id: &Authid) -> Result<(), Error> { |
44 | let correct_owner = | |
45 | owner == auth_id || (owner.is_token() && &Authid::from(owner.user().clone()) == auth_id); | |
9751ef4b DC |
46 | if !correct_owner { |
47 | bail!("backup owner check failed ({} != {})", auth_id, owner); | |
48 | } | |
49 | Ok(()) | |
50 | } | |
51 | ||
e5064ba6 DM |
52 | /// Datastore Management |
53 | /// | |
54 | /// A Datastore can store severals backups, and provides the | |
55 | /// management interface for backup. | |
4bc84a65 | 56 | pub struct DataStoreImpl { |
1629d2ad | 57 | chunk_store: Arc<ChunkStore>, |
81b2a872 | 58 | gc_mutex: Mutex<()>, |
f2b99c34 | 59 | last_gc_status: Mutex<GarbageCollectionStatus>, |
0698f78d | 60 | verify_new: bool, |
fef61684 | 61 | chunk_order: ChunkOrder, |
51d900d1 | 62 | last_digest: Option<[u8; 32]>, |
647186dd | 63 | sync_level: DatastoreFSyncLevel, |
529de6c7 DM |
64 | } |
65 | ||
6da20161 WB |
66 | impl DataStoreImpl { |
67 | // This one just panics on everything | |
68 | #[doc(hidden)] | |
615a50c1 | 69 | pub(crate) unsafe fn new_test() -> Arc<Self> { |
6da20161 WB |
70 | Arc::new(Self { |
71 | chunk_store: Arc::new(unsafe { ChunkStore::panic_store() }), | |
72 | gc_mutex: Mutex::new(()), | |
73 | last_gc_status: Mutex::new(GarbageCollectionStatus::default()), | |
74 | verify_new: false, | |
c40a2f8b | 75 | chunk_order: Default::default(), |
51d900d1 | 76 | last_digest: None, |
647186dd | 77 | sync_level: Default::default(), |
6da20161 WB |
78 | }) |
79 | } | |
80 | } | |
81 | ||
4bc84a65 HL |
82 | pub struct DataStore { |
83 | inner: Arc<DataStoreImpl>, | |
84 | operation: Option<Operation>, | |
85 | } | |
86 | ||
87 | impl Clone for DataStore { | |
88 | fn clone(&self) -> Self { | |
89 | let mut new_operation = self.operation; | |
90 | if let Some(operation) = self.operation { | |
91 | if let Err(e) = update_active_operations(self.name(), operation, 1) { | |
92 | log::error!("could not update active operations - {}", e); | |
93 | new_operation = None; | |
94 | } | |
95 | } | |
96 | ||
97 | DataStore { | |
98 | inner: self.inner.clone(), | |
99 | operation: new_operation, | |
100 | } | |
101 | } | |
102 | } | |
103 | ||
104 | impl Drop for DataStore { | |
105 | fn drop(&mut self) { | |
106 | if let Some(operation) = self.operation { | |
9cba51ac HL |
107 | let mut last_task = false; |
108 | match update_active_operations(self.name(), operation, -1) { | |
109 | Err(e) => log::error!("could not update active operations - {}", e), | |
110 | Ok(updated_operations) => { | |
111 | last_task = updated_operations.read + updated_operations.write == 0; | |
112 | } | |
113 | } | |
114 | ||
115 | // remove datastore from cache iff | |
116 | // - last task finished, and | |
117 | // - datastore is in a maintenance mode that mandates it | |
118 | let remove_from_cache = last_task | |
119 | && pbs_config::datastore::config() | |
120 | .and_then(|(s, _)| s.lookup::<DataStoreConfig>("datastore", self.name())) | |
121 | .map_or(false, |c| { | |
122 | c.get_maintenance_mode().map_or(false, |m| m.is_offline()) | |
123 | }); | |
124 | ||
125 | if remove_from_cache { | |
126 | DATASTORE_MAP.lock().unwrap().remove(self.name()); | |
4bc84a65 HL |
127 | } |
128 | } | |
129 | } | |
130 | } | |
131 | ||
529de6c7 | 132 | impl DataStore { |
6da20161 WB |
133 | // This one just panics on everything |
134 | #[doc(hidden)] | |
615a50c1 | 135 | pub(crate) unsafe fn new_test() -> Arc<Self> { |
6da20161 WB |
136 | Arc::new(Self { |
137 | inner: unsafe { DataStoreImpl::new_test() }, | |
138 | operation: None, | |
139 | }) | |
140 | } | |
141 | ||
e9d2fc93 HL |
142 | pub fn lookup_datastore( |
143 | name: &str, | |
144 | operation: Option<Operation>, | |
145 | ) -> Result<Arc<DataStore>, Error> { | |
857f346c WB |
146 | // Avoid TOCTOU between checking maintenance mode and updating active operation counter, as |
147 | // we use it to decide whether it is okay to delete the datastore. | |
148 | let config_lock = pbs_config::datastore::lock_config()?; | |
149 | ||
51d900d1 TL |
150 | // we could use the ConfigVersionCache's generation for staleness detection, but we load |
151 | // the config anyway -> just use digest, additional benefit: manual changes get detected | |
152 | let (config, digest) = pbs_config::datastore::config()?; | |
e9d2fc93 | 153 | let config: DataStoreConfig = config.lookup("datastore", name)?; |
e9d2fc93 HL |
154 | |
155 | if let Some(maintenance_mode) = config.get_maintenance_mode() { | |
156 | if let Err(error) = maintenance_mode.check(operation) { | |
0408f60b | 157 | bail!("datastore '{name}' is in {error}"); |
e9d2fc93 HL |
158 | } |
159 | } | |
160 | ||
4bc84a65 HL |
161 | if let Some(operation) = operation { |
162 | update_active_operations(name, operation, 1)?; | |
163 | } | |
164 | ||
857f346c WB |
165 | // Our operation is registered, unlock the config. |
166 | drop(config_lock); | |
167 | ||
33a1ef7a TL |
168 | let mut datastore_cache = DATASTORE_MAP.lock().unwrap(); |
169 | let entry = datastore_cache.get(name); | |
2c32fdde | 170 | |
c7f7236b | 171 | // reuse chunk store so that we keep using the same process locker instance! |
0bd9c870 | 172 | let chunk_store = if let Some(datastore) = &entry { |
51d900d1 TL |
173 | let last_digest = datastore.last_digest.as_ref(); |
174 | if let Some(true) = last_digest.map(|last_digest| last_digest == &digest) { | |
4bc84a65 HL |
175 | return Ok(Arc::new(Self { |
176 | inner: Arc::clone(datastore), | |
177 | operation, | |
178 | })); | |
2c32fdde | 179 | } |
0bd9c870 DC |
180 | Arc::clone(&datastore.chunk_store) |
181 | } else { | |
647186dd DC |
182 | let tuning: DatastoreTuning = serde_json::from_value( |
183 | DatastoreTuning::API_SCHEMA | |
184 | .parse_property_string(config.tuning.as_deref().unwrap_or(""))?, | |
185 | )?; | |
186 | Arc::new(ChunkStore::open( | |
187 | name, | |
188 | &config.path, | |
189 | tuning.sync_level.unwrap_or_default(), | |
190 | )?) | |
0bd9c870 | 191 | }; |
2c32fdde | 192 | |
51d900d1 | 193 | let datastore = DataStore::with_store_and_config(chunk_store, config, Some(digest))?; |
f0a61124 DM |
194 | |
195 | let datastore = Arc::new(datastore); | |
33a1ef7a | 196 | datastore_cache.insert(name.to_string(), datastore.clone()); |
2c32fdde | 197 | |
4bc84a65 HL |
198 | Ok(Arc::new(Self { |
199 | inner: datastore, | |
200 | operation, | |
201 | })) | |
2c32fdde DM |
202 | } |
203 | ||
062cf75c | 204 | /// removes all datastores that are not configured anymore |
42c2b5be | 205 | pub fn remove_unused_datastores() -> Result<(), Error> { |
e7d4be9d | 206 | let (config, _digest) = pbs_config::datastore::config()?; |
062cf75c DC |
207 | |
208 | let mut map = DATASTORE_MAP.lock().unwrap(); | |
209 | // removes all elements that are not in the config | |
42c2b5be | 210 | map.retain(|key, _| config.sections.contains_key(key)); |
062cf75c DC |
211 | Ok(()) |
212 | } | |
213 | ||
9cba51ac HL |
214 | /// trigger clearing cache entry based on maintenance mode. Entry will only |
215 | /// be cleared iff there is no other task running, if there is, the end of the | |
216 | /// last running task will trigger the clearing of the cache entry. | |
217 | pub fn update_datastore_cache(name: &str) -> Result<(), Error> { | |
218 | let (config, _digest) = pbs_config::datastore::config()?; | |
219 | let datastore: DataStoreConfig = config.lookup("datastore", name)?; | |
220 | if datastore | |
221 | .get_maintenance_mode() | |
222 | .map_or(false, |m| m.is_offline()) | |
223 | { | |
224 | // the datastore drop handler does the checking if tasks are running and clears the | |
225 | // cache entry, so we just have to trigger it here | |
226 | let _ = DataStore::lookup_datastore(name, Some(Operation::Lookup)); | |
227 | } | |
228 | ||
229 | Ok(()) | |
230 | } | |
231 | ||
6da20161 | 232 | /// Open a raw database given a name and a path. |
c66fa32c TL |
233 | /// |
234 | /// # Safety | |
235 | /// See the safety section in `open_from_config` | |
519ca9d0 | 236 | pub unsafe fn open_path( |
6da20161 WB |
237 | name: &str, |
238 | path: impl AsRef<Path>, | |
239 | operation: Option<Operation>, | |
240 | ) -> Result<Arc<Self>, Error> { | |
241 | let path = path | |
242 | .as_ref() | |
243 | .to_str() | |
244 | .ok_or_else(|| format_err!("non-utf8 paths not supported"))? | |
245 | .to_owned(); | |
246 | unsafe { Self::open_from_config(DataStoreConfig::new(name.to_owned(), path), operation) } | |
247 | } | |
248 | ||
249 | /// Open a datastore given a raw configuration. | |
c66fa32c TL |
250 | /// |
251 | /// # Safety | |
74cad4a8 | 252 | /// There's no memory safety implication, but as this is opening a new ChunkStore it will |
c66fa32c TL |
253 | /// create a new process locker instance, potentially on the same path as existing safely |
254 | /// created ones. This is dangerous as dropping the reference of this and thus the underlying | |
255 | /// chunkstore's process locker will close all locks from our process on the config.path, | |
256 | /// breaking guarantees we need to uphold for safe long backup + GC interaction on newer/older | |
257 | /// process instances (from package update). | |
519ca9d0 | 258 | unsafe fn open_from_config( |
6da20161 WB |
259 | config: DataStoreConfig, |
260 | operation: Option<Operation>, | |
261 | ) -> Result<Arc<Self>, Error> { | |
262 | let name = config.name.clone(); | |
263 | ||
647186dd DC |
264 | let tuning: DatastoreTuning = serde_json::from_value( |
265 | DatastoreTuning::API_SCHEMA | |
266 | .parse_property_string(config.tuning.as_deref().unwrap_or(""))?, | |
267 | )?; | |
268 | let chunk_store = | |
269 | ChunkStore::open(&name, &config.path, tuning.sync_level.unwrap_or_default())?; | |
0bd9c870 DC |
270 | let inner = Arc::new(Self::with_store_and_config( |
271 | Arc::new(chunk_store), | |
272 | config, | |
51d900d1 | 273 | None, |
0bd9c870 | 274 | )?); |
6da20161 WB |
275 | |
276 | if let Some(operation) = operation { | |
277 | update_active_operations(&name, operation, 1)?; | |
278 | } | |
279 | ||
280 | Ok(Arc::new(Self { inner, operation })) | |
281 | } | |
282 | ||
283 | fn with_store_and_config( | |
0bd9c870 | 284 | chunk_store: Arc<ChunkStore>, |
118deb4d | 285 | config: DataStoreConfig, |
51d900d1 | 286 | last_digest: Option<[u8; 32]>, |
4bc84a65 | 287 | ) -> Result<DataStoreImpl, Error> { |
b683fd58 DC |
288 | let mut gc_status_path = chunk_store.base_path(); |
289 | gc_status_path.push(".gc-status"); | |
290 | ||
291 | let gc_status = if let Some(state) = file_read_optional_string(gc_status_path)? { | |
292 | match serde_json::from_str(&state) { | |
293 | Ok(state) => state, | |
294 | Err(err) => { | |
dce4b540 | 295 | log::error!("error reading gc-status: {}", err); |
b683fd58 DC |
296 | GarbageCollectionStatus::default() |
297 | } | |
298 | } | |
299 | } else { | |
300 | GarbageCollectionStatus::default() | |
301 | }; | |
f2b99c34 | 302 | |
fef61684 | 303 | let tuning: DatastoreTuning = serde_json::from_value( |
42c2b5be TL |
304 | DatastoreTuning::API_SCHEMA |
305 | .parse_property_string(config.tuning.as_deref().unwrap_or(""))?, | |
fef61684 | 306 | )?; |
fef61684 | 307 | |
4bc84a65 | 308 | Ok(DataStoreImpl { |
0bd9c870 | 309 | chunk_store, |
81b2a872 | 310 | gc_mutex: Mutex::new(()), |
f2b99c34 | 311 | last_gc_status: Mutex::new(gc_status), |
0698f78d | 312 | verify_new: config.verify_new.unwrap_or(false), |
c40a2f8b | 313 | chunk_order: tuning.chunk_order.unwrap_or_default(), |
51d900d1 | 314 | last_digest, |
647186dd | 315 | sync_level: tuning.sync_level.unwrap_or_default(), |
529de6c7 DM |
316 | }) |
317 | } | |
318 | ||
d59397e6 WB |
319 | pub fn get_chunk_iterator( |
320 | &self, | |
321 | ) -> Result< | |
25877d05 | 322 | impl Iterator<Item = (Result<proxmox_sys::fs::ReadDirEntry, Error>, usize, bool)>, |
42c2b5be | 323 | Error, |
d59397e6 | 324 | > { |
4bc84a65 | 325 | self.inner.chunk_store.get_chunk_iterator() |
d59397e6 WB |
326 | } |
327 | ||
42c2b5be TL |
328 | pub fn create_fixed_writer<P: AsRef<Path>>( |
329 | &self, | |
330 | filename: P, | |
331 | size: usize, | |
332 | chunk_size: usize, | |
333 | ) -> Result<FixedIndexWriter, Error> { | |
334 | let index = FixedIndexWriter::create( | |
335 | self.inner.chunk_store.clone(), | |
336 | filename.as_ref(), | |
337 | size, | |
338 | chunk_size, | |
339 | )?; | |
529de6c7 DM |
340 | |
341 | Ok(index) | |
342 | } | |
343 | ||
42c2b5be TL |
344 | pub fn open_fixed_reader<P: AsRef<Path>>( |
345 | &self, | |
346 | filename: P, | |
347 | ) -> Result<FixedIndexReader, Error> { | |
348 | let full_path = self.inner.chunk_store.relative_path(filename.as_ref()); | |
a7c72ad9 DM |
349 | |
350 | let index = FixedIndexReader::open(&full_path)?; | |
529de6c7 DM |
351 | |
352 | Ok(index) | |
353 | } | |
3d5c11e5 | 354 | |
93d5d779 | 355 | pub fn create_dynamic_writer<P: AsRef<Path>>( |
42c2b5be TL |
356 | &self, |
357 | filename: P, | |
93d5d779 | 358 | ) -> Result<DynamicIndexWriter, Error> { |
42c2b5be | 359 | let index = DynamicIndexWriter::create(self.inner.chunk_store.clone(), filename.as_ref())?; |
0433db19 DM |
360 | |
361 | Ok(index) | |
362 | } | |
ff3d3100 | 363 | |
42c2b5be TL |
364 | pub fn open_dynamic_reader<P: AsRef<Path>>( |
365 | &self, | |
366 | filename: P, | |
367 | ) -> Result<DynamicIndexReader, Error> { | |
368 | let full_path = self.inner.chunk_store.relative_path(filename.as_ref()); | |
d48a9955 DM |
369 | |
370 | let index = DynamicIndexReader::open(&full_path)?; | |
77703d95 DM |
371 | |
372 | Ok(index) | |
373 | } | |
374 | ||
5de2bced WB |
375 | pub fn open_index<P>(&self, filename: P) -> Result<Box<dyn IndexFile + Send>, Error> |
376 | where | |
377 | P: AsRef<Path>, | |
378 | { | |
379 | let filename = filename.as_ref(); | |
42c2b5be TL |
380 | let out: Box<dyn IndexFile + Send> = match archive_type(filename)? { |
381 | ArchiveType::DynamicIndex => Box::new(self.open_dynamic_reader(filename)?), | |
382 | ArchiveType::FixedIndex => Box::new(self.open_fixed_reader(filename)?), | |
383 | _ => bail!("cannot open index file of unknown type: {:?}", filename), | |
384 | }; | |
5de2bced WB |
385 | Ok(out) |
386 | } | |
387 | ||
1369bcdb | 388 | /// Fast index verification - only check if chunks exists |
28570d19 DM |
389 | pub fn fast_index_verification( |
390 | &self, | |
391 | index: &dyn IndexFile, | |
42c2b5be | 392 | checked: &mut HashSet<[u8; 32]>, |
28570d19 | 393 | ) -> Result<(), Error> { |
1369bcdb DM |
394 | for pos in 0..index.index_count() { |
395 | let info = index.chunk_info(pos).unwrap(); | |
28570d19 DM |
396 | if checked.contains(&info.digest) { |
397 | continue; | |
398 | } | |
399 | ||
42c2b5be TL |
400 | self.stat_chunk(&info.digest).map_err(|err| { |
401 | format_err!( | |
402 | "fast_index_verification error, stat_chunk {} failed - {}", | |
16f6766a | 403 | hex::encode(info.digest), |
42c2b5be TL |
404 | err, |
405 | ) | |
406 | })?; | |
28570d19 DM |
407 | |
408 | checked.insert(info.digest); | |
1369bcdb DM |
409 | } |
410 | ||
411 | Ok(()) | |
412 | } | |
413 | ||
60f9a6ea | 414 | pub fn name(&self) -> &str { |
4bc84a65 | 415 | self.inner.chunk_store.name() |
60f9a6ea DM |
416 | } |
417 | ||
ff3d3100 | 418 | pub fn base_path(&self) -> PathBuf { |
4bc84a65 | 419 | self.inner.chunk_store.base_path() |
ff3d3100 DM |
420 | } |
421 | ||
133d718f | 422 | /// Returns the absolute path for a backup namespace on this datastore |
8c74349b WB |
423 | pub fn namespace_path(&self, ns: &BackupNamespace) -> PathBuf { |
424 | let mut path = self.base_path(); | |
425 | path.reserve(ns.path_len()); | |
426 | for part in ns.components() { | |
427 | path.push("ns"); | |
428 | path.push(part); | |
429 | } | |
430 | path | |
431 | } | |
432 | ||
5444b914 WB |
433 | /// Returns the absolute path for a backup_type |
434 | pub fn type_path(&self, ns: &BackupNamespace, backup_type: BackupType) -> PathBuf { | |
435 | let mut full_path = self.namespace_path(ns); | |
436 | full_path.push(backup_type.to_string()); | |
437 | full_path | |
438 | } | |
439 | ||
41b373ec | 440 | /// Returns the absolute path for a backup_group |
133d718f WB |
441 | pub fn group_path( |
442 | &self, | |
443 | ns: &BackupNamespace, | |
444 | backup_group: &pbs_api_types::BackupGroup, | |
445 | ) -> PathBuf { | |
446 | let mut full_path = self.namespace_path(ns); | |
db87d93e | 447 | full_path.push(backup_group.to_string()); |
41b373ec DM |
448 | full_path |
449 | } | |
450 | ||
451 | /// Returns the absolute path for backup_dir | |
133d718f WB |
452 | pub fn snapshot_path( |
453 | &self, | |
454 | ns: &BackupNamespace, | |
455 | backup_dir: &pbs_api_types::BackupDir, | |
456 | ) -> PathBuf { | |
457 | let mut full_path = self.namespace_path(ns); | |
db87d93e | 458 | full_path.push(backup_dir.to_string()); |
41b373ec DM |
459 | full_path |
460 | } | |
461 | ||
dc3d716b TL |
462 | /// Create a backup namespace. |
463 | pub fn create_namespace( | |
464 | self: &Arc<Self>, | |
465 | parent: &BackupNamespace, | |
466 | name: String, | |
467 | ) -> Result<BackupNamespace, Error> { | |
fc99c279 | 468 | if !self.namespace_exists(parent) { |
dc3d716b TL |
469 | bail!("cannot create new namespace, parent {parent} doesn't already exists"); |
470 | } | |
471 | ||
472 | // construct ns before mkdir to enforce max-depth and name validity | |
473 | let ns = BackupNamespace::from_parent_ns(parent, name)?; | |
474 | ||
ca3f8757 | 475 | let mut ns_full_path = self.base_path(); |
dc3d716b TL |
476 | ns_full_path.push(ns.path()); |
477 | ||
478 | std::fs::create_dir_all(ns_full_path)?; | |
479 | ||
480 | Ok(ns) | |
481 | } | |
482 | ||
fc99c279 DC |
483 | /// Returns if the given namespace exists on the datastore |
484 | pub fn namespace_exists(&self, ns: &BackupNamespace) -> bool { | |
ca3f8757 | 485 | let mut path = self.base_path(); |
fc99c279 DC |
486 | path.push(ns.path()); |
487 | path.exists() | |
488 | } | |
489 | ||
4c7cc5b3 TL |
490 | /// Remove all backup groups of a single namespace level but not the namespace itself. |
491 | /// | |
492 | /// Does *not* descends into child-namespaces and doesn't remoes the namespace itself either. | |
493 | /// | |
494 | /// Returns true if all the groups were removed, and false if some were protected. | |
495 | pub fn remove_namespace_groups(self: &Arc<Self>, ns: &BackupNamespace) -> Result<bool, Error> { | |
496 | // FIXME: locking? The single groups/snapshots are already protected, so may not be | |
74cad4a8 | 497 | // necessary (depends on what we all allow to do with namespaces) |
4c7cc5b3 TL |
498 | log::info!("removing all groups in namespace {}:/{ns}", self.name()); |
499 | ||
500 | let mut removed_all_groups = true; | |
501 | ||
502 | for group in self.iter_backup_groups(ns.to_owned())? { | |
524ed404 CE |
503 | let delete_stats = group?.destroy()?; |
504 | removed_all_groups = removed_all_groups && delete_stats.all_removed(); | |
4c7cc5b3 TL |
505 | } |
506 | ||
507 | let base_file = std::fs::File::open(self.base_path())?; | |
508 | let base_fd = base_file.as_raw_fd(); | |
509 | for ty in BackupType::iter() { | |
510 | let mut ty_dir = ns.path(); | |
511 | ty_dir.push(ty.to_string()); | |
512 | // best effort only, but we probably should log the error | |
513 | if let Err(err) = unlinkat(Some(base_fd), &ty_dir, UnlinkatFlags::RemoveDir) { | |
11ca8343 | 514 | if err != nix::errno::Errno::ENOENT { |
4c7cc5b3 TL |
515 | log::error!("failed to remove backup type {ty} in {ns} - {err}"); |
516 | } | |
517 | } | |
518 | } | |
519 | ||
520 | Ok(removed_all_groups) | |
521 | } | |
522 | ||
d1f9ccea TL |
523 | /// Remove a complete backup namespace optionally including all it's, and child namespaces', |
524 | /// groups. If `removed_groups` is false this only prunes empty namespaces. | |
4c7cc5b3 | 525 | /// |
d1f9ccea TL |
526 | /// Returns true if everything requested, and false if some groups were protected or if some |
527 | /// namespaces weren't empty even though all groups were deleted (race with new backup) | |
4c7cc5b3 TL |
528 | pub fn remove_namespace_recursive( |
529 | self: &Arc<Self>, | |
530 | ns: &BackupNamespace, | |
d1f9ccea | 531 | delete_groups: bool, |
4c7cc5b3 | 532 | ) -> Result<bool, Error> { |
d1f9ccea TL |
533 | let store = self.name(); |
534 | let mut removed_all_requested = true; | |
535 | if delete_groups { | |
536 | log::info!("removing whole namespace recursively below {store}:/{ns}",); | |
537 | for ns in self.recursive_iter_backup_ns(ns.to_owned())? { | |
538 | let removed_ns_groups = self.remove_namespace_groups(&ns?)?; | |
539 | removed_all_requested = removed_all_requested && removed_ns_groups; | |
540 | } | |
541 | } else { | |
542 | log::info!("pruning empty namespace recursively below {store}:/{ns}"); | |
4c7cc5b3 TL |
543 | } |
544 | ||
545 | // now try to delete the actual namespaces, bottom up so that we can use safe rmdir that | |
546 | // will choke if a new backup/group appeared in the meantime (but not on an new empty NS) | |
547 | let mut children = self | |
548 | .recursive_iter_backup_ns(ns.to_owned())? | |
549 | .collect::<Result<Vec<BackupNamespace>, Error>>()?; | |
550 | ||
bc001e12 | 551 | children.sort_by_key(|b| std::cmp::Reverse(b.depth())); |
4c7cc5b3 TL |
552 | |
553 | let base_file = std::fs::File::open(self.base_path())?; | |
554 | let base_fd = base_file.as_raw_fd(); | |
555 | ||
556 | for ns in children.iter() { | |
557 | let mut ns_dir = ns.path(); | |
558 | ns_dir.push("ns"); | |
559 | let _ = unlinkat(Some(base_fd), &ns_dir, UnlinkatFlags::RemoveDir); | |
560 | ||
561 | if !ns.is_root() { | |
562 | match unlinkat(Some(base_fd), &ns.path(), UnlinkatFlags::RemoveDir) { | |
d1f9ccea | 563 | Ok(()) => log::debug!("removed namespace {ns}"), |
11ca8343 | 564 | Err(nix::errno::Errno::ENOENT) => { |
d1f9ccea TL |
565 | log::debug!("namespace {ns} already removed") |
566 | } | |
11ca8343 | 567 | Err(nix::errno::Errno::ENOTEMPTY) if !delete_groups => { |
3aafa613 | 568 | removed_all_requested = false; |
d1f9ccea TL |
569 | log::debug!("skip removal of non-empty namespace {ns}") |
570 | } | |
571 | Err(err) => { | |
572 | removed_all_requested = false; | |
573 | log::warn!("failed to remove namespace {ns} - {err}") | |
574 | } | |
4c7cc5b3 TL |
575 | } |
576 | } | |
577 | } | |
578 | ||
d1f9ccea | 579 | Ok(removed_all_requested) |
4c7cc5b3 TL |
580 | } |
581 | ||
f03649b8 TL |
582 | /// Remove a complete backup group including all snapshots. |
583 | /// | |
524ed404 CE |
584 | /// Returns `BackupGroupDeleteStats`, containing the number of deleted snapshots |
585 | /// and number of protected snaphsots, which therefore were not removed. | |
db87d93e | 586 | pub fn remove_backup_group( |
6da20161 | 587 | self: &Arc<Self>, |
133d718f | 588 | ns: &BackupNamespace, |
db87d93e | 589 | backup_group: &pbs_api_types::BackupGroup, |
524ed404 | 590 | ) -> Result<BackupGroupDeleteStats, Error> { |
133d718f | 591 | let backup_group = self.backup_group(ns.clone(), backup_group.clone()); |
db87d93e | 592 | |
f03649b8 | 593 | backup_group.destroy() |
4b4eba0b DM |
594 | } |
595 | ||
8f579717 | 596 | /// Remove a backup directory including all content |
db87d93e | 597 | pub fn remove_backup_dir( |
6da20161 | 598 | self: &Arc<Self>, |
133d718f | 599 | ns: &BackupNamespace, |
db87d93e WB |
600 | backup_dir: &pbs_api_types::BackupDir, |
601 | force: bool, | |
602 | ) -> Result<(), Error> { | |
133d718f | 603 | let backup_dir = self.backup_dir(ns.clone(), backup_dir.clone())?; |
db87d93e | 604 | |
f03649b8 | 605 | backup_dir.destroy(force) |
8f579717 DM |
606 | } |
607 | ||
41b373ec DM |
608 | /// Returns the time of the last successful backup |
609 | /// | |
610 | /// Or None if there is no backup in the group (or the group dir does not exist). | |
db87d93e | 611 | pub fn last_successful_backup( |
6da20161 | 612 | self: &Arc<Self>, |
133d718f | 613 | ns: &BackupNamespace, |
db87d93e WB |
614 | backup_group: &pbs_api_types::BackupGroup, |
615 | ) -> Result<Option<i64>, Error> { | |
133d718f | 616 | let backup_group = self.backup_group(ns.clone(), backup_group.clone()); |
db87d93e | 617 | |
4b77d300 | 618 | let group_path = backup_group.full_group_path(); |
41b373ec DM |
619 | |
620 | if group_path.exists() { | |
6da20161 | 621 | backup_group.last_successful_backup() |
41b373ec DM |
622 | } else { |
623 | Ok(None) | |
624 | } | |
625 | } | |
626 | ||
133d718f WB |
627 | /// Return the path of the 'owner' file. |
628 | fn owner_path(&self, ns: &BackupNamespace, group: &pbs_api_types::BackupGroup) -> PathBuf { | |
629 | self.group_path(ns, group).join("owner") | |
630 | } | |
631 | ||
54552dda DM |
632 | /// Returns the backup owner. |
633 | /// | |
e6dc35ac | 634 | /// The backup owner is the entity who first created the backup group. |
133d718f WB |
635 | pub fn get_owner( |
636 | &self, | |
637 | ns: &BackupNamespace, | |
638 | backup_group: &pbs_api_types::BackupGroup, | |
639 | ) -> Result<Authid, Error> { | |
640 | let full_path = self.owner_path(ns, backup_group); | |
25877d05 | 641 | let owner = proxmox_sys::fs::file_read_firstline(full_path)?; |
de6b0ea3 CE |
642 | owner |
643 | .trim_end() // remove trailing newline | |
644 | .parse() | |
fa86b05d | 645 | .map_err(|err| format_err!("parsing owner for {backup_group} failed: {err}")) |
54552dda DM |
646 | } |
647 | ||
db87d93e WB |
648 | pub fn owns_backup( |
649 | &self, | |
133d718f | 650 | ns: &BackupNamespace, |
db87d93e WB |
651 | backup_group: &pbs_api_types::BackupGroup, |
652 | auth_id: &Authid, | |
653 | ) -> Result<bool, Error> { | |
133d718f | 654 | let owner = self.get_owner(ns, backup_group)?; |
9751ef4b | 655 | |
8e0b852f | 656 | Ok(check_backup_owner(&owner, auth_id).is_ok()) |
9751ef4b DC |
657 | } |
658 | ||
54552dda | 659 | /// Set the backup owner. |
e7cb4dc5 WB |
660 | pub fn set_owner( |
661 | &self, | |
133d718f | 662 | ns: &BackupNamespace, |
db87d93e | 663 | backup_group: &pbs_api_types::BackupGroup, |
e6dc35ac | 664 | auth_id: &Authid, |
e7cb4dc5 WB |
665 | force: bool, |
666 | ) -> Result<(), Error> { | |
133d718f | 667 | let path = self.owner_path(ns, backup_group); |
54552dda DM |
668 | |
669 | let mut open_options = std::fs::OpenOptions::new(); | |
670 | open_options.write(true); | |
671 | open_options.truncate(true); | |
672 | ||
673 | if force { | |
674 | open_options.create(true); | |
675 | } else { | |
676 | open_options.create_new(true); | |
677 | } | |
678 | ||
42c2b5be TL |
679 | let mut file = open_options |
680 | .open(&path) | |
54552dda DM |
681 | .map_err(|err| format_err!("unable to create owner file {:?} - {}", path, err))?; |
682 | ||
e6dc35ac | 683 | writeln!(file, "{}", auth_id) |
54552dda DM |
684 | .map_err(|err| format_err!("unable to write owner file {:?} - {}", path, err))?; |
685 | ||
686 | Ok(()) | |
687 | } | |
688 | ||
1fc82c41 | 689 | /// Create (if it does not already exists) and lock a backup group |
54552dda DM |
690 | /// |
691 | /// And set the owner to 'userid'. If the group already exists, it returns the | |
692 | /// current owner (instead of setting the owner). | |
1fc82c41 | 693 | /// |
1ffe0301 | 694 | /// This also acquires an exclusive lock on the directory and returns the lock guard. |
e7cb4dc5 WB |
695 | pub fn create_locked_backup_group( |
696 | &self, | |
133d718f | 697 | ns: &BackupNamespace, |
db87d93e | 698 | backup_group: &pbs_api_types::BackupGroup, |
e6dc35ac FG |
699 | auth_id: &Authid, |
700 | ) -> Result<(Authid, DirLockGuard), Error> { | |
8731e40a | 701 | // create intermediate path first: |
44288184 | 702 | let mut full_path = self.base_path(); |
133d718f | 703 | for ns in ns.components() { |
8c74349b WB |
704 | full_path.push("ns"); |
705 | full_path.push(ns); | |
706 | } | |
db87d93e | 707 | full_path.push(backup_group.ty.as_str()); |
8731e40a WB |
708 | std::fs::create_dir_all(&full_path)?; |
709 | ||
db87d93e | 710 | full_path.push(&backup_group.id); |
54552dda DM |
711 | |
712 | // create the last component now | |
713 | match std::fs::create_dir(&full_path) { | |
714 | Ok(_) => { | |
42c2b5be TL |
715 | let guard = lock_dir_noblock( |
716 | &full_path, | |
717 | "backup group", | |
718 | "another backup is already running", | |
719 | )?; | |
133d718f WB |
720 | self.set_owner(ns, backup_group, auth_id, false)?; |
721 | let owner = self.get_owner(ns, backup_group)?; // just to be sure | |
1fc82c41 | 722 | Ok((owner, guard)) |
54552dda DM |
723 | } |
724 | Err(ref err) if err.kind() == io::ErrorKind::AlreadyExists => { | |
42c2b5be TL |
725 | let guard = lock_dir_noblock( |
726 | &full_path, | |
727 | "backup group", | |
728 | "another backup is already running", | |
729 | )?; | |
133d718f | 730 | let owner = self.get_owner(ns, backup_group)?; // just to be sure |
1fc82c41 | 731 | Ok((owner, guard)) |
54552dda DM |
732 | } |
733 | Err(err) => bail!("unable to create backup group {:?} - {}", full_path, err), | |
734 | } | |
735 | } | |
736 | ||
737 | /// Creates a new backup snapshot inside a BackupGroup | |
738 | /// | |
739 | /// The BackupGroup directory needs to exist. | |
42c2b5be TL |
740 | pub fn create_locked_backup_dir( |
741 | &self, | |
133d718f | 742 | ns: &BackupNamespace, |
db87d93e | 743 | backup_dir: &pbs_api_types::BackupDir, |
42c2b5be | 744 | ) -> Result<(PathBuf, bool, DirLockGuard), Error> { |
133d718f WB |
745 | let full_path = self.snapshot_path(ns, backup_dir); |
746 | let relative_path = full_path.strip_prefix(self.base_path()).map_err(|err| { | |
747 | format_err!( | |
748 | "failed to produce correct path for backup {backup_dir} in namespace {ns}: {err}" | |
749 | ) | |
750 | })?; | |
ff3d3100 | 751 | |
42c2b5be TL |
752 | let lock = || { |
753 | lock_dir_noblock( | |
754 | &full_path, | |
755 | "snapshot", | |
756 | "internal error - tried creating snapshot that's already in use", | |
757 | ) | |
758 | }; | |
f23f7543 | 759 | |
8731e40a | 760 | match std::fs::create_dir(&full_path) { |
133d718f | 761 | Ok(_) => Ok((relative_path.to_owned(), true, lock()?)), |
42c2b5be | 762 | Err(ref e) if e.kind() == io::ErrorKind::AlreadyExists => { |
133d718f | 763 | Ok((relative_path.to_owned(), false, lock()?)) |
42c2b5be TL |
764 | } |
765 | Err(e) => Err(e.into()), | |
8731e40a | 766 | } |
ff3d3100 DM |
767 | } |
768 | ||
90e38696 TL |
769 | /// Get a streaming iter over single-level backup namespaces of a datatstore |
770 | /// | |
771 | /// The iterated item is still a Result that can contain errors from rather unexptected FS or | |
772 | /// parsing errors. | |
773 | pub fn iter_backup_ns( | |
774 | self: &Arc<DataStore>, | |
775 | ns: BackupNamespace, | |
776 | ) -> Result<ListNamespaces, Error> { | |
777 | ListNamespaces::new(Arc::clone(self), ns) | |
778 | } | |
779 | ||
780 | /// Get a streaming iter over single-level backup namespaces of a datatstore, filtered by Ok | |
781 | /// | |
782 | /// The iterated item's result is already unwrapped, if it contained an error it will be | |
783 | /// logged. Can be useful in iterator chain commands | |
784 | pub fn iter_backup_ns_ok( | |
785 | self: &Arc<DataStore>, | |
786 | ns: BackupNamespace, | |
787 | ) -> Result<impl Iterator<Item = BackupNamespace> + 'static, Error> { | |
788 | let this = Arc::clone(self); | |
789 | Ok( | |
ca3f8757 | 790 | ListNamespaces::new(Arc::clone(self), ns)?.filter_map(move |ns| match ns { |
90e38696 TL |
791 | Ok(ns) => Some(ns), |
792 | Err(err) => { | |
793 | log::error!("list groups error on datastore {} - {}", this.name(), err); | |
794 | None | |
795 | } | |
796 | }), | |
797 | ) | |
798 | } | |
799 | ||
800 | /// Get a streaming iter over single-level backup namespaces of a datatstore | |
801 | /// | |
802 | /// The iterated item is still a Result that can contain errors from rather unexptected FS or | |
803 | /// parsing errors. | |
804 | pub fn recursive_iter_backup_ns( | |
805 | self: &Arc<DataStore>, | |
806 | ns: BackupNamespace, | |
807 | ) -> Result<ListNamespacesRecursive, Error> { | |
808 | ListNamespacesRecursive::new(Arc::clone(self), ns) | |
809 | } | |
810 | ||
811 | /// Get a streaming iter over single-level backup namespaces of a datatstore, filtered by Ok | |
812 | /// | |
813 | /// The iterated item's result is already unwrapped, if it contained an error it will be | |
814 | /// logged. Can be useful in iterator chain commands | |
815 | pub fn recursive_iter_backup_ns_ok( | |
816 | self: &Arc<DataStore>, | |
817 | ns: BackupNamespace, | |
15a92724 | 818 | max_depth: Option<usize>, |
90e38696 TL |
819 | ) -> Result<impl Iterator<Item = BackupNamespace> + 'static, Error> { |
820 | let this = Arc::clone(self); | |
15a92724 | 821 | Ok(if let Some(depth) = max_depth { |
ca3f8757 | 822 | ListNamespacesRecursive::new_max_depth(Arc::clone(self), ns, depth)? |
15a92724 | 823 | } else { |
ca3f8757 | 824 | ListNamespacesRecursive::new(Arc::clone(self), ns)? |
15a92724 TL |
825 | } |
826 | .filter_map(move |ns| match ns { | |
827 | Ok(ns) => Some(ns), | |
828 | Err(err) => { | |
829 | log::error!("list groups error on datastore {} - {}", this.name(), err); | |
830 | None | |
831 | } | |
832 | })) | |
90e38696 TL |
833 | } |
834 | ||
5444b914 WB |
835 | /// Get a streaming iter over top-level backup groups of a datatstore of a particular type. |
836 | /// | |
837 | /// The iterated item is still a Result that can contain errors from rather unexptected FS or | |
838 | /// parsing errors. | |
839 | pub fn iter_backup_type( | |
840 | self: &Arc<DataStore>, | |
841 | ns: BackupNamespace, | |
842 | ty: BackupType, | |
843 | ) -> Result<ListGroupsType, Error> { | |
844 | ListGroupsType::new(Arc::clone(self), ns, ty) | |
845 | } | |
846 | ||
847 | /// Get a streaming iter over top-level backup groups of a datatstore of a particular type, | |
848 | /// filtered by `Ok` results | |
849 | /// | |
850 | /// The iterated item's result is already unwrapped, if it contained an error it will be | |
851 | /// logged. Can be useful in iterator chain commands | |
852 | pub fn iter_backup_type_ok( | |
853 | self: &Arc<DataStore>, | |
854 | ns: BackupNamespace, | |
855 | ty: BackupType, | |
856 | ) -> Result<impl Iterator<Item = BackupGroup> + 'static, Error> { | |
857 | Ok(self.iter_backup_type(ns, ty)?.ok()) | |
858 | } | |
859 | ||
7b125de3 TL |
860 | /// Get a streaming iter over top-level backup groups of a datatstore |
861 | /// | |
862 | /// The iterated item is still a Result that can contain errors from rather unexptected FS or | |
863 | /// parsing errors. | |
8c74349b WB |
864 | pub fn iter_backup_groups( |
865 | self: &Arc<DataStore>, | |
866 | ns: BackupNamespace, | |
867 | ) -> Result<ListGroups, Error> { | |
868 | ListGroups::new(Arc::clone(self), ns) | |
7b125de3 TL |
869 | } |
870 | ||
871 | /// Get a streaming iter over top-level backup groups of a datatstore, filtered by Ok results | |
872 | /// | |
873 | /// The iterated item's result is already unwrapped, if it contained an error it will be | |
874 | /// logged. Can be useful in iterator chain commands | |
6da20161 WB |
875 | pub fn iter_backup_groups_ok( |
876 | self: &Arc<DataStore>, | |
8c74349b | 877 | ns: BackupNamespace, |
6da20161 | 878 | ) -> Result<impl Iterator<Item = BackupGroup> + 'static, Error> { |
5444b914 | 879 | Ok(self.iter_backup_groups(ns)?.ok()) |
7b125de3 TL |
880 | } |
881 | ||
c90dbb5c | 882 | /// Get a in-memory vector for all top-level backup groups of a datatstore |
7b125de3 TL |
883 | /// |
884 | /// NOTE: using the iterator directly is most often more efficient w.r.t. memory usage | |
8c74349b WB |
885 | pub fn list_backup_groups( |
886 | self: &Arc<DataStore>, | |
887 | ns: BackupNamespace, | |
888 | ) -> Result<Vec<BackupGroup>, Error> { | |
889 | ListGroups::new(Arc::clone(self), ns)?.collect() | |
c90dbb5c TL |
890 | } |
891 | ||
3d5c11e5 | 892 | pub fn list_images(&self) -> Result<Vec<PathBuf>, Error> { |
ff3d3100 | 893 | let base = self.base_path(); |
3d5c11e5 DM |
894 | |
895 | let mut list = vec![]; | |
896 | ||
95cea65b DM |
897 | use walkdir::WalkDir; |
898 | ||
a57413a5 | 899 | let walker = WalkDir::new(base).into_iter(); |
95cea65b DM |
900 | |
901 | // make sure we skip .chunks (and other hidden files to keep it simple) | |
902 | fn is_hidden(entry: &walkdir::DirEntry) -> bool { | |
42c2b5be TL |
903 | entry |
904 | .file_name() | |
95cea65b | 905 | .to_str() |
d8d8af98 | 906 | .map(|s| s.starts_with('.')) |
95cea65b DM |
907 | .unwrap_or(false) |
908 | } | |
c3b090ac | 909 | let handle_entry_err = |err: walkdir::Error| { |
5b16dffc TL |
910 | // first, extract the actual IO error and the affected path |
911 | let (inner, path) = match (err.io_error(), err.path()) { | |
912 | (None, _) => return Ok(()), // not an IO-error | |
913 | (Some(inner), Some(path)) => (inner, path), | |
914 | (Some(inner), None) => bail!("unexpected error on datastore traversal: {inner}"), | |
915 | }; | |
916 | if inner.kind() == io::ErrorKind::PermissionDenied { | |
0d69dcb4 | 917 | if err.depth() <= 1 && path.ends_with("lost+found") { |
8fcd709c | 918 | // allow skipping of (root-only) ext4 fsck-directory on EPERM .. |
5b16dffc | 919 | return Ok(()); |
c3b090ac | 920 | } |
8fcd709c FG |
921 | // .. but do not ignore EPERM in general, otherwise we might prune too many chunks. |
922 | // E.g., if users messed up with owner/perms on a rsync | |
5b16dffc | 923 | bail!("cannot continue garbage-collection safely, permission denied on: {path:?}"); |
9d1ba51d GG |
924 | } else if inner.kind() == io::ErrorKind::NotFound { |
925 | log::info!("ignoring vanished file: {path:?}"); | |
926 | return Ok(()); | |
5b16dffc TL |
927 | } else { |
928 | bail!("unexpected error on datastore traversal: {inner} - {path:?}"); | |
c3b090ac | 929 | } |
c3b090ac | 930 | }; |
95cea65b | 931 | for entry in walker.filter_entry(|e| !is_hidden(e)) { |
c3b090ac TL |
932 | let path = match entry { |
933 | Ok(entry) => entry.into_path(), | |
934 | Err(err) => { | |
935 | handle_entry_err(err)?; | |
42c2b5be TL |
936 | continue; |
937 | } | |
c3b090ac | 938 | }; |
1e8da0a7 | 939 | if let Ok(archive_type) = archive_type(&path) { |
42c2b5be TL |
940 | if archive_type == ArchiveType::FixedIndex |
941 | || archive_type == ArchiveType::DynamicIndex | |
942 | { | |
95cea65b | 943 | list.push(path); |
3d5c11e5 DM |
944 | } |
945 | } | |
946 | } | |
947 | ||
948 | Ok(list) | |
949 | } | |
950 | ||
a660978c DM |
951 | // mark chunks used by ``index`` as used |
952 | fn index_mark_used_chunks<I: IndexFile>( | |
953 | &self, | |
954 | index: I, | |
955 | file_name: &Path, // only used for error reporting | |
956 | status: &mut GarbageCollectionStatus, | |
c8449217 | 957 | worker: &dyn WorkerTaskContext, |
a660978c | 958 | ) -> Result<(), Error> { |
a660978c DM |
959 | status.index_file_count += 1; |
960 | status.index_data_bytes += index.index_bytes(); | |
961 | ||
962 | for pos in 0..index.index_count() { | |
f6b1d1cc | 963 | worker.check_abort()?; |
0fd55b08 | 964 | worker.fail_on_shutdown()?; |
a660978c | 965 | let digest = index.index_digest(pos).unwrap(); |
4bc84a65 | 966 | if !self.inner.chunk_store.cond_touch_chunk(digest, false)? { |
ca3f8757 | 967 | let hex = hex::encode(digest); |
c23192d3 | 968 | task_warn!( |
f6b1d1cc | 969 | worker, |
ca3f8757 | 970 | "warning: unable to access non-existent chunk {hex}, required by {file_name:?}" |
f6b1d1cc | 971 | ); |
fd192564 SR |
972 | |
973 | // touch any corresponding .bad files to keep them around, meaning if a chunk is | |
974 | // rewritten correctly they will be removed automatically, as well as if no index | |
975 | // file requires the chunk anymore (won't get to this loop then) | |
976 | for i in 0..=9 { | |
977 | let bad_ext = format!("{}.bad", i); | |
978 | let mut bad_path = PathBuf::new(); | |
979 | bad_path.push(self.chunk_path(digest).0); | |
980 | bad_path.set_extension(bad_ext); | |
4bc84a65 | 981 | self.inner.chunk_store.cond_touch_path(&bad_path, false)?; |
fd192564 | 982 | } |
a660978c DM |
983 | } |
984 | } | |
985 | Ok(()) | |
986 | } | |
987 | ||
f6b1d1cc WB |
988 | fn mark_used_chunks( |
989 | &self, | |
990 | status: &mut GarbageCollectionStatus, | |
c8449217 | 991 | worker: &dyn WorkerTaskContext, |
f6b1d1cc | 992 | ) -> Result<(), Error> { |
3d5c11e5 | 993 | let image_list = self.list_images()?; |
8317873c DM |
994 | let image_count = image_list.len(); |
995 | ||
8317873c DM |
996 | let mut last_percentage: usize = 0; |
997 | ||
cb4b721c FG |
998 | let mut strange_paths_count: u64 = 0; |
999 | ||
ea368a06 | 1000 | for (i, img) in image_list.into_iter().enumerate() { |
f6b1d1cc | 1001 | worker.check_abort()?; |
0fd55b08 | 1002 | worker.fail_on_shutdown()?; |
92da93b2 | 1003 | |
cb4b721c FG |
1004 | if let Some(backup_dir_path) = img.parent() { |
1005 | let backup_dir_path = backup_dir_path.strip_prefix(self.base_path())?; | |
1006 | if let Some(backup_dir_str) = backup_dir_path.to_str() { | |
161a8864 | 1007 | if pbs_api_types::parse_ns_and_snapshot(backup_dir_str).is_err() { |
cb4b721c FG |
1008 | strange_paths_count += 1; |
1009 | } | |
1010 | } | |
1011 | } | |
1012 | ||
efcac39d | 1013 | match std::fs::File::open(&img) { |
e0762002 | 1014 | Ok(file) => { |
788d82d9 | 1015 | if let Ok(archive_type) = archive_type(&img) { |
e0762002 | 1016 | if archive_type == ArchiveType::FixedIndex { |
788d82d9 | 1017 | let index = FixedIndexReader::new(file).map_err(|e| { |
efcac39d | 1018 | format_err!("can't read index '{}' - {}", img.to_string_lossy(), e) |
2f0b9235 | 1019 | })?; |
788d82d9 | 1020 | self.index_mark_used_chunks(index, &img, status, worker)?; |
e0762002 | 1021 | } else if archive_type == ArchiveType::DynamicIndex { |
788d82d9 | 1022 | let index = DynamicIndexReader::new(file).map_err(|e| { |
efcac39d | 1023 | format_err!("can't read index '{}' - {}", img.to_string_lossy(), e) |
2f0b9235 | 1024 | })?; |
788d82d9 | 1025 | self.index_mark_used_chunks(index, &img, status, worker)?; |
e0762002 DM |
1026 | } |
1027 | } | |
1028 | } | |
788d82d9 | 1029 | Err(err) if err.kind() == io::ErrorKind::NotFound => (), // ignore vanished files |
efcac39d | 1030 | Err(err) => bail!("can't open index {} - {}", img.to_string_lossy(), err), |
77703d95 | 1031 | } |
8317873c | 1032 | |
ea368a06 | 1033 | let percentage = (i + 1) * 100 / image_count; |
8317873c | 1034 | if percentage > last_percentage { |
c23192d3 | 1035 | task_log!( |
f6b1d1cc | 1036 | worker, |
7956877f | 1037 | "marked {}% ({} of {} index files)", |
f6b1d1cc | 1038 | percentage, |
ea368a06 | 1039 | i + 1, |
f6b1d1cc WB |
1040 | image_count, |
1041 | ); | |
8317873c DM |
1042 | last_percentage = percentage; |
1043 | } | |
3d5c11e5 DM |
1044 | } |
1045 | ||
cb4b721c | 1046 | if strange_paths_count > 0 { |
c23192d3 | 1047 | task_log!( |
cb4b721c FG |
1048 | worker, |
1049 | "found (and marked) {} index files outside of expected directory scheme", | |
1050 | strange_paths_count, | |
1051 | ); | |
1052 | } | |
1053 | ||
3d5c11e5 | 1054 | Ok(()) |
f2b99c34 DM |
1055 | } |
1056 | ||
1057 | pub fn last_gc_status(&self) -> GarbageCollectionStatus { | |
4bc84a65 | 1058 | self.inner.last_gc_status.lock().unwrap().clone() |
f2b99c34 | 1059 | } |
3d5c11e5 | 1060 | |
8545480a | 1061 | pub fn garbage_collection_running(&self) -> bool { |
1c3f1e7c | 1062 | self.inner.gc_mutex.try_lock().is_err() |
8545480a DM |
1063 | } |
1064 | ||
42c2b5be TL |
1065 | pub fn garbage_collection( |
1066 | &self, | |
1067 | worker: &dyn WorkerTaskContext, | |
1068 | upid: &UPID, | |
1069 | ) -> Result<(), Error> { | |
4bc84a65 | 1070 | if let Ok(ref mut _mutex) = self.inner.gc_mutex.try_lock() { |
c6772c92 TL |
1071 | // avoids that we run GC if an old daemon process has still a |
1072 | // running backup writer, which is not save as we have no "oldest | |
1073 | // writer" information and thus no safe atime cutoff | |
42c2b5be | 1074 | let _exclusive_lock = self.inner.chunk_store.try_exclusive_lock()?; |
43b13033 | 1075 | |
6ef1b649 | 1076 | let phase1_start_time = proxmox_time::epoch_i64(); |
42c2b5be TL |
1077 | let oldest_writer = self |
1078 | .inner | |
1079 | .chunk_store | |
1080 | .oldest_writer() | |
1081 | .unwrap_or(phase1_start_time); | |
11861a48 | 1082 | |
bc001e12 TL |
1083 | let mut gc_status = GarbageCollectionStatus { |
1084 | upid: Some(upid.to_string()), | |
1085 | ..Default::default() | |
1086 | }; | |
f6b1d1cc | 1087 | |
c23192d3 | 1088 | task_log!(worker, "Start GC phase1 (mark used chunks)"); |
f6b1d1cc WB |
1089 | |
1090 | self.mark_used_chunks(&mut gc_status, worker)?; | |
1091 | ||
c23192d3 | 1092 | task_log!(worker, "Start GC phase2 (sweep unused chunks)"); |
4bc84a65 | 1093 | self.inner.chunk_store.sweep_unused_chunks( |
f6b1d1cc WB |
1094 | oldest_writer, |
1095 | phase1_start_time, | |
1096 | &mut gc_status, | |
1097 | worker, | |
1098 | )?; | |
1099 | ||
c23192d3 | 1100 | task_log!( |
f6b1d1cc WB |
1101 | worker, |
1102 | "Removed garbage: {}", | |
1103 | HumanByte::from(gc_status.removed_bytes), | |
1104 | ); | |
c23192d3 | 1105 | task_log!(worker, "Removed chunks: {}", gc_status.removed_chunks); |
cf459b19 | 1106 | if gc_status.pending_bytes > 0 { |
c23192d3 | 1107 | task_log!( |
f6b1d1cc WB |
1108 | worker, |
1109 | "Pending removals: {} (in {} chunks)", | |
1110 | HumanByte::from(gc_status.pending_bytes), | |
1111 | gc_status.pending_chunks, | |
1112 | ); | |
cf459b19 | 1113 | } |
a9767cf7 | 1114 | if gc_status.removed_bad > 0 { |
c23192d3 | 1115 | task_log!(worker, "Removed bad chunks: {}", gc_status.removed_bad); |
a9767cf7 | 1116 | } |
cf459b19 | 1117 | |
b4fb2623 | 1118 | if gc_status.still_bad > 0 { |
c23192d3 | 1119 | task_log!(worker, "Leftover bad chunks: {}", gc_status.still_bad); |
b4fb2623 DM |
1120 | } |
1121 | ||
c23192d3 | 1122 | task_log!( |
f6b1d1cc WB |
1123 | worker, |
1124 | "Original data usage: {}", | |
1125 | HumanByte::from(gc_status.index_data_bytes), | |
1126 | ); | |
868c5852 DM |
1127 | |
1128 | if gc_status.index_data_bytes > 0 { | |
42c2b5be TL |
1129 | let comp_per = |
1130 | (gc_status.disk_bytes as f64 * 100.) / gc_status.index_data_bytes as f64; | |
c23192d3 | 1131 | task_log!( |
f6b1d1cc WB |
1132 | worker, |
1133 | "On-Disk usage: {} ({:.2}%)", | |
1134 | HumanByte::from(gc_status.disk_bytes), | |
1135 | comp_per, | |
1136 | ); | |
868c5852 DM |
1137 | } |
1138 | ||
c23192d3 | 1139 | task_log!(worker, "On-Disk chunks: {}", gc_status.disk_chunks); |
868c5852 | 1140 | |
d6373f35 | 1141 | let deduplication_factor = if gc_status.disk_bytes > 0 { |
42c2b5be | 1142 | (gc_status.index_data_bytes as f64) / (gc_status.disk_bytes as f64) |
d6373f35 DM |
1143 | } else { |
1144 | 1.0 | |
1145 | }; | |
1146 | ||
c23192d3 | 1147 | task_log!(worker, "Deduplication factor: {:.2}", deduplication_factor); |
d6373f35 | 1148 | |
868c5852 | 1149 | if gc_status.disk_chunks > 0 { |
42c2b5be | 1150 | let avg_chunk = gc_status.disk_bytes / (gc_status.disk_chunks as u64); |
c23192d3 | 1151 | task_log!(worker, "Average chunk size: {}", HumanByte::from(avg_chunk)); |
868c5852 | 1152 | } |
64e53b28 | 1153 | |
b683fd58 DC |
1154 | if let Ok(serialized) = serde_json::to_string(&gc_status) { |
1155 | let mut path = self.base_path(); | |
1156 | path.push(".gc-status"); | |
1157 | ||
21211748 | 1158 | let backup_user = pbs_config::backup_user()?; |
b683fd58 DC |
1159 | let mode = nix::sys::stat::Mode::from_bits_truncate(0o0644); |
1160 | // set the correct owner/group/permissions while saving file | |
1161 | // owner(rw) = backup, group(r)= backup | |
1162 | let options = CreateOptions::new() | |
1163 | .perm(mode) | |
1164 | .owner(backup_user.uid) | |
1165 | .group(backup_user.gid); | |
1166 | ||
1167 | // ignore errors | |
e0a19d33 | 1168 | let _ = replace_file(path, serialized.as_bytes(), options, false); |
b683fd58 DC |
1169 | } |
1170 | ||
4bc84a65 | 1171 | *self.inner.last_gc_status.lock().unwrap() = gc_status; |
64e53b28 | 1172 | } else { |
d4b59ae0 | 1173 | bail!("Start GC failed - (already running/locked)"); |
64e53b28 | 1174 | } |
3d5c11e5 DM |
1175 | |
1176 | Ok(()) | |
1177 | } | |
3b7ade9e | 1178 | |
ccc3896f | 1179 | pub fn try_shared_chunk_store_lock(&self) -> Result<ProcessLockSharedGuard, Error> { |
4bc84a65 | 1180 | self.inner.chunk_store.try_shared_lock() |
1cf5178a DM |
1181 | } |
1182 | ||
42c2b5be | 1183 | pub fn chunk_path(&self, digest: &[u8; 32]) -> (PathBuf, String) { |
4bc84a65 | 1184 | self.inner.chunk_store.chunk_path(digest) |
d48a9955 DM |
1185 | } |
1186 | ||
b298e9f1 | 1187 | pub fn cond_touch_chunk(&self, digest: &[u8; 32], assert_exists: bool) -> Result<bool, Error> { |
42c2b5be TL |
1188 | self.inner |
1189 | .chunk_store | |
b298e9f1 | 1190 | .cond_touch_chunk(digest, assert_exists) |
42c2b5be TL |
1191 | } |
1192 | ||
1193 | pub fn insert_chunk(&self, chunk: &DataBlob, digest: &[u8; 32]) -> Result<(bool, u64), Error> { | |
4bc84a65 | 1194 | self.inner.chunk_store.insert_chunk(chunk, digest) |
3b7ade9e | 1195 | } |
60f9a6ea | 1196 | |
7f394c80 | 1197 | pub fn stat_chunk(&self, digest: &[u8; 32]) -> Result<std::fs::Metadata, Error> { |
4bc84a65 | 1198 | let (chunk_path, _digest_str) = self.inner.chunk_store.chunk_path(digest); |
7f394c80 DC |
1199 | std::fs::metadata(chunk_path).map_err(Error::from) |
1200 | } | |
1201 | ||
39f18b30 | 1202 | pub fn load_chunk(&self, digest: &[u8; 32]) -> Result<DataBlob, Error> { |
4bc84a65 | 1203 | let (chunk_path, digest_str) = self.inner.chunk_store.chunk_path(digest); |
39f18b30 | 1204 | |
6ef1b649 | 1205 | proxmox_lang::try_block!({ |
39f18b30 DM |
1206 | let mut file = std::fs::File::open(&chunk_path)?; |
1207 | DataBlob::load_from_reader(&mut file) | |
42c2b5be TL |
1208 | }) |
1209 | .map_err(|err| { | |
1210 | format_err!( | |
1211 | "store '{}', unable to load chunk '{}' - {}", | |
1212 | self.name(), | |
1213 | digest_str, | |
1214 | err, | |
1215 | ) | |
1216 | }) | |
1a374fcf SR |
1217 | } |
1218 | ||
8292d3d2 | 1219 | /// Updates the protection status of the specified snapshot. |
42c2b5be | 1220 | pub fn update_protection(&self, backup_dir: &BackupDir, protection: bool) -> Result<(), Error> { |
6da20161 | 1221 | let full_path = backup_dir.full_path(); |
8292d3d2 | 1222 | |
c78437e3 FG |
1223 | if !full_path.exists() { |
1224 | bail!("snapshot {} does not exist!", backup_dir.dir()); | |
1225 | } | |
1226 | ||
8292d3d2 DC |
1227 | let _guard = lock_dir_noblock(&full_path, "snapshot", "possibly running or in use")?; |
1228 | ||
6da20161 | 1229 | let protected_path = backup_dir.protected_file(); |
8292d3d2 DC |
1230 | if protection { |
1231 | std::fs::File::create(protected_path) | |
1232 | .map_err(|err| format_err!("could not create protection file: {}", err))?; | |
1233 | } else if let Err(err) = std::fs::remove_file(protected_path) { | |
1234 | // ignore error for non-existing file | |
1235 | if err.kind() != std::io::ErrorKind::NotFound { | |
1236 | bail!("could not remove protection file: {}", err); | |
1237 | } | |
1238 | } | |
1239 | ||
1240 | Ok(()) | |
1241 | } | |
1242 | ||
0698f78d | 1243 | pub fn verify_new(&self) -> bool { |
4bc84a65 | 1244 | self.inner.verify_new |
0698f78d | 1245 | } |
4921a411 | 1246 | |
bc001e12 TL |
1247 | /// returns a list of chunks sorted by their inode number on disk chunks that couldn't get |
1248 | /// stat'ed are placed at the end of the list | |
4921a411 DC |
1249 | pub fn get_chunks_in_order<F, A>( |
1250 | &self, | |
e1db0670 | 1251 | index: &(dyn IndexFile + Send), |
4921a411 DC |
1252 | skip_chunk: F, |
1253 | check_abort: A, | |
1254 | ) -> Result<Vec<(usize, u64)>, Error> | |
1255 | where | |
1256 | F: Fn(&[u8; 32]) -> bool, | |
1257 | A: Fn(usize) -> Result<(), Error>, | |
1258 | { | |
1259 | let index_count = index.index_count(); | |
1260 | let mut chunk_list = Vec::with_capacity(index_count); | |
1261 | use std::os::unix::fs::MetadataExt; | |
1262 | for pos in 0..index_count { | |
1263 | check_abort(pos)?; | |
1264 | ||
1265 | let info = index.chunk_info(pos).unwrap(); | |
1266 | ||
1267 | if skip_chunk(&info.digest) { | |
1268 | continue; | |
1269 | } | |
1270 | ||
4bc84a65 | 1271 | let ino = match self.inner.chunk_order { |
fef61684 DC |
1272 | ChunkOrder::Inode => { |
1273 | match self.stat_chunk(&info.digest) { | |
1274 | Err(_) => u64::MAX, // could not stat, move to end of list | |
1275 | Ok(metadata) => metadata.ino(), | |
1276 | } | |
1277 | } | |
1278 | ChunkOrder::None => 0, | |
4921a411 DC |
1279 | }; |
1280 | ||
1281 | chunk_list.push((pos, ino)); | |
1282 | } | |
1283 | ||
4bc84a65 | 1284 | match self.inner.chunk_order { |
fef61684 DC |
1285 | // sorting by inode improves data locality, which makes it lots faster on spinners |
1286 | ChunkOrder::Inode => { | |
1287 | chunk_list.sort_unstable_by(|(_, ino_a), (_, ino_b)| ino_a.cmp(ino_b)) | |
1288 | } | |
1289 | ChunkOrder::None => {} | |
1290 | } | |
4921a411 DC |
1291 | |
1292 | Ok(chunk_list) | |
1293 | } | |
db87d93e | 1294 | |
6b0c6492 | 1295 | /// Open a backup group from this datastore. |
133d718f WB |
1296 | pub fn backup_group( |
1297 | self: &Arc<Self>, | |
1298 | ns: BackupNamespace, | |
1299 | group: pbs_api_types::BackupGroup, | |
1300 | ) -> BackupGroup { | |
ca3f8757 | 1301 | BackupGroup::new(Arc::clone(self), ns, group) |
db87d93e WB |
1302 | } |
1303 | ||
6b0c6492 | 1304 | /// Open a backup group from this datastore. |
8c74349b WB |
1305 | pub fn backup_group_from_parts<T>( |
1306 | self: &Arc<Self>, | |
1307 | ns: BackupNamespace, | |
1308 | ty: BackupType, | |
1309 | id: T, | |
1310 | ) -> BackupGroup | |
6b0c6492 WB |
1311 | where |
1312 | T: Into<String>, | |
1313 | { | |
133d718f | 1314 | self.backup_group(ns, (ty, id.into()).into()) |
6b0c6492 WB |
1315 | } |
1316 | ||
133d718f | 1317 | /* |
6b0c6492 WB |
1318 | /// Open a backup group from this datastore by backup group path such as `vm/100`. |
1319 | /// | |
1320 | /// Convenience method for `store.backup_group(path.parse()?)` | |
6da20161 | 1321 | pub fn backup_group_from_path(self: &Arc<Self>, path: &str) -> Result<BackupGroup, Error> { |
133d718f | 1322 | todo!("split out the namespace"); |
db87d93e | 1323 | } |
133d718f | 1324 | */ |
db87d93e | 1325 | |
6b0c6492 | 1326 | /// Open a snapshot (backup directory) from this datastore. |
133d718f WB |
1327 | pub fn backup_dir( |
1328 | self: &Arc<Self>, | |
1329 | ns: BackupNamespace, | |
1330 | dir: pbs_api_types::BackupDir, | |
1331 | ) -> Result<BackupDir, Error> { | |
1332 | BackupDir::with_group(self.backup_group(ns, dir.group), dir.time) | |
6b0c6492 WB |
1333 | } |
1334 | ||
1335 | /// Open a snapshot (backup directory) from this datastore. | |
db87d93e | 1336 | pub fn backup_dir_from_parts<T>( |
6da20161 | 1337 | self: &Arc<Self>, |
8c74349b | 1338 | ns: BackupNamespace, |
db87d93e WB |
1339 | ty: BackupType, |
1340 | id: T, | |
1341 | time: i64, | |
1342 | ) -> Result<BackupDir, Error> | |
1343 | where | |
1344 | T: Into<String>, | |
1345 | { | |
133d718f | 1346 | self.backup_dir(ns, (ty, id.into(), time).into()) |
db87d93e WB |
1347 | } |
1348 | ||
6b0c6492 | 1349 | /// Open a snapshot (backup directory) from this datastore with a cached rfc3339 time string. |
db87d93e | 1350 | pub fn backup_dir_with_rfc3339<T: Into<String>>( |
6da20161 | 1351 | self: &Arc<Self>, |
db87d93e WB |
1352 | group: BackupGroup, |
1353 | time_string: T, | |
1354 | ) -> Result<BackupDir, Error> { | |
1355 | BackupDir::with_rfc3339(group, time_string.into()) | |
1356 | } | |
1357 | ||
133d718f | 1358 | /* |
6b0c6492 | 1359 | /// Open a snapshot (backup directory) from this datastore by a snapshot path. |
6da20161 | 1360 | pub fn backup_dir_from_path(self: &Arc<Self>, path: &str) -> Result<BackupDir, Error> { |
133d718f | 1361 | todo!("split out the namespace"); |
db87d93e | 1362 | } |
133d718f | 1363 | */ |
647186dd DC |
1364 | |
1365 | /// Syncs the filesystem of the datastore if 'sync_level' is set to | |
1366 | /// [`DatastoreFSyncLevel::Filesystem`]. Uses syncfs(2). | |
1367 | pub fn try_ensure_sync_level(&self) -> Result<(), Error> { | |
1368 | if self.inner.sync_level != DatastoreFSyncLevel::Filesystem { | |
1369 | return Ok(()); | |
1370 | } | |
1371 | let file = std::fs::File::open(self.base_path())?; | |
1372 | let fd = file.as_raw_fd(); | |
7be0a3fd | 1373 | log::info!("syncing filesystem"); |
647186dd DC |
1374 | if unsafe { libc::syncfs(fd) } < 0 { |
1375 | bail!("error during syncfs: {}", std::io::Error::last_os_error()); | |
1376 | } | |
1377 | Ok(()) | |
1378 | } | |
857f346c WB |
1379 | |
1380 | /// Destroy a datastore. This requires that there are no active operations on the datastore. | |
1381 | /// | |
1382 | /// This is a synchronous operation and should be run in a worker-thread. | |
1383 | pub fn destroy( | |
1384 | name: &str, | |
1385 | destroy_data: bool, | |
1386 | worker: &dyn WorkerTaskContext, | |
1387 | ) -> Result<(), Error> { | |
1388 | let config_lock = pbs_config::datastore::lock_config()?; | |
1389 | ||
1390 | let (mut config, _digest) = pbs_config::datastore::config()?; | |
1391 | let mut datastore_config: DataStoreConfig = config.lookup("datastore", name)?; | |
1392 | ||
1393 | datastore_config.maintenance_mode = Some("type=delete".to_string()); | |
1394 | config.set_data(name, "datastore", &datastore_config)?; | |
1395 | pbs_config::datastore::save_config(&config)?; | |
1396 | drop(config_lock); | |
1397 | ||
1398 | let (operations, _lock) = task_tracking::get_active_operations_locked(name)?; | |
1399 | ||
1400 | if operations.read != 0 || operations.write != 0 { | |
1401 | bail!("datastore is currently in use"); | |
1402 | } | |
1403 | ||
1404 | let base = PathBuf::from(&datastore_config.path); | |
1405 | ||
1406 | let mut ok = true; | |
1407 | if destroy_data { | |
1408 | let remove = |subdir, ok: &mut bool| { | |
1409 | if let Err(err) = std::fs::remove_dir_all(base.join(subdir)) { | |
1410 | if err.kind() != io::ErrorKind::NotFound { | |
1411 | task_warn!(worker, "failed to remove {subdir:?} subdirectory: {err}"); | |
1412 | *ok = false; | |
1413 | } | |
1414 | } | |
1415 | }; | |
1416 | ||
1417 | task_log!(worker, "Deleting datastore data..."); | |
1418 | remove("ns", &mut ok); // ns first | |
1419 | remove("ct", &mut ok); | |
1420 | remove("vm", &mut ok); | |
1421 | remove("host", &mut ok); | |
1422 | ||
1423 | if ok { | |
1424 | if let Err(err) = std::fs::remove_file(base.join(".gc-status")) { | |
1425 | if err.kind() != io::ErrorKind::NotFound { | |
1426 | task_warn!(worker, "failed to remove .gc-status file: {err}"); | |
1427 | ok = false; | |
1428 | } | |
1429 | } | |
1430 | } | |
1431 | ||
1432 | // chunks get removed last and only if the backups were successfully deleted | |
1433 | if ok { | |
1434 | remove(".chunks", &mut ok); | |
1435 | } | |
1436 | } | |
1437 | ||
1438 | // now the config | |
1439 | if ok { | |
1440 | task_log!(worker, "Removing datastore from config..."); | |
1441 | let _lock = pbs_config::datastore::lock_config()?; | |
1442 | let _ = config.sections.remove(name); | |
1443 | pbs_config::datastore::save_config(&config)?; | |
1444 | } | |
1445 | ||
1446 | // finally the lock & toplevel directory | |
1447 | if destroy_data { | |
1448 | if ok { | |
1449 | if let Err(err) = std::fs::remove_file(base.join(".lock")) { | |
1450 | if err.kind() != io::ErrorKind::NotFound { | |
1451 | task_warn!(worker, "failed to remove .lock file: {err}"); | |
1452 | ok = false; | |
1453 | } | |
1454 | } | |
1455 | } | |
1456 | ||
1457 | if ok { | |
1458 | task_log!(worker, "Finished deleting data."); | |
1459 | ||
1460 | match std::fs::remove_dir(base) { | |
1461 | Ok(()) => task_log!(worker, "Removed empty datastore directory."), | |
1462 | Err(err) if err.kind() == io::ErrorKind::NotFound => { | |
1463 | // weird, but ok | |
1464 | } | |
1465 | Err(err) if err.is_errno(nix::errno::Errno::EBUSY) => { | |
1466 | task_warn!( | |
1467 | worker, | |
1468 | "Cannot delete datastore directory (is it a mount point?)." | |
1469 | ) | |
1470 | } | |
1471 | Err(err) if err.is_errno(nix::errno::Errno::ENOTEMPTY) => { | |
1472 | task_warn!(worker, "Datastore directory not empty, not deleting.") | |
1473 | } | |
1474 | Err(err) => { | |
1475 | task_warn!(worker, "Failed to remove datastore directory: {err}"); | |
1476 | } | |
1477 | } | |
1478 | } else { | |
1479 | task_log!(worker, "There were errors deleting data."); | |
1480 | } | |
1481 | } | |
1482 | ||
1483 | Ok(()) | |
1484 | } | |
529de6c7 | 1485 | } |