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