]> git.proxmox.com Git - proxmox-backup.git/blame - src/tape/media_catalog.rs
clippy: use copied/cloned instead of map
[proxmox-backup.git] / src / tape / media_catalog.rs
CommitLineData
205e1876
DM
1use std::convert::TryFrom;
2use std::fs::File;
42298d58
DM
3use std::io::{Write, Read, BufReader, Seek, SeekFrom};
4use std::os::unix::io::AsRawFd;
205e1876 5use std::path::Path;
0bf1c314 6use std::collections::{HashSet, HashMap};
205e1876
DM
7
8use anyhow::{bail, format_err, Error};
9use endian_trait::Endian;
10
11use proxmox::tools::{
12 Uuid,
13 fs::{
14 fchown,
15 create_path,
16 CreateOptions,
17 },
18 io::{
19 WriteExt,
20 ReadExt,
21 },
22};
23
24use crate::{
0bf1c314 25 tools::fs::read_subdir,
205e1876 26 backup::BackupDir,
fe6c1938
DM
27 tape::{
28 MediaId,
29 },
205e1876
DM
30};
31
42298d58
DM
32// openssl::sha::sha256(b"Proxmox Backup Media Catalog v1.0")[0..8]
33pub const PROXMOX_BACKUP_MEDIA_CATALOG_MAGIC_1_0: [u8; 8] = [221, 29, 164, 1, 59, 69, 19, 40];
34
205e1876
DM
35/// The Media Catalog
36///
37/// Stores what chunks and snapshots are stored on a specific media,
38/// including the file position.
39///
40/// We use a simple binary format to store data on disk.
41pub struct MediaCatalog {
42
43 uuid: Uuid, // BackupMedia uuid
44
45 file: Option<File>,
46
3fbf2d2f 47 log_to_stdout: bool,
205e1876
DM
48
49 current_archive: Option<(Uuid, u64)>,
50
51 last_entry: Option<(Uuid, u64)>,
52
53 chunk_index: HashMap<[u8;32], u64>,
54
55 snapshot_index: HashMap<String, u64>,
56
57 pending: Vec<u8>,
58}
59
60impl MediaCatalog {
61
0bf1c314
DM
62 /// List media with catalogs
63 pub fn media_with_catalogs(base_path: &Path) -> Result<HashSet<Uuid>, Error> {
64 let mut catalogs = HashSet::new();
65
66 for entry in read_subdir(libc::AT_FDCWD, base_path)? {
67 let entry = entry?;
68 let name = unsafe { entry.file_name_utf8_unchecked() };
69 if !name.ends_with(".log") { continue; }
70 if let Ok(uuid) = Uuid::parse_str(&name[..(name.len()-4)]) {
71 catalogs.insert(uuid);
72 }
73 }
74
75 Ok(catalogs)
76 }
77
205e1876
DM
78 /// Test if a catalog exists
79 pub fn exists(base_path: &Path, uuid: &Uuid) -> bool {
80 let mut path = base_path.to_owned();
81 path.push(uuid.to_string());
82 path.set_extension("log");
83 path.exists()
84 }
85
86 /// Destroy the media catalog (remove all files)
87 pub fn destroy(base_path: &Path, uuid: &Uuid) -> Result<(), Error> {
88
89 let mut path = base_path.to_owned();
90 path.push(uuid.to_string());
91 path.set_extension("log");
92
93 match std::fs::remove_file(path) {
94 Ok(()) => Ok(()),
95 Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(()),
96 Err(err) => Err(err.into()),
97 }
98 }
99
3fbf2d2f
DM
100 /// Enable/Disable logging to stdout (disabled by default)
101 pub fn log_to_stdout(&mut self, enable: bool) {
102 self.log_to_stdout = enable;
103 }
104
205e1876
DM
105 fn create_basedir(base_path: &Path) -> Result<(), Error> {
106 let backup_user = crate::backup::backup_user()?;
107 let mode = nix::sys::stat::Mode::from_bits_truncate(0o0640);
108 let opts = CreateOptions::new()
109 .perm(mode)
110 .owner(backup_user.uid)
111 .group(backup_user.gid);
112
113 create_path(base_path, None, Some(opts))
114 .map_err(|err: Error| format_err!("unable to create media catalog dir - {}", err))?;
115 Ok(())
116 }
117
118 /// Open a catalog database, load into memory
119 pub fn open(
120 base_path: &Path,
121 uuid: &Uuid,
122 write: bool,
123 create: bool,
124 ) -> Result<Self, Error> {
125
126 let mut path = base_path.to_owned();
127 path.push(uuid.to_string());
128 path.set_extension("log");
129
130 let me = proxmox::try_block!({
131
132 Self::create_basedir(base_path)?;
133
134 let mut file = std::fs::OpenOptions::new()
135 .read(true)
136 .write(write)
137 .create(create)
138 .open(&path)?;
139
205e1876 140 let backup_user = crate::backup::backup_user()?;
42298d58
DM
141 fchown(file.as_raw_fd(), Some(backup_user.uid), Some(backup_user.gid))
142 .map_err(|err| format_err!("fchown failed - {}", err))?;
205e1876
DM
143
144 let mut me = Self {
145 uuid: uuid.clone(),
146 file: None,
147 log_to_stdout: false,
148 current_archive: None,
149 last_entry: None,
150 chunk_index: HashMap::new(),
151 snapshot_index: HashMap::new(),
152 pending: Vec::new(),
153 };
154
42298d58
DM
155 let found_magic_number = me.load_catalog(&mut file)?;
156
157 if !found_magic_number {
158 me.pending.extend(&PROXMOX_BACKUP_MEDIA_CATALOG_MAGIC_1_0);
159 }
205e1876
DM
160
161 if write {
162 me.file = Some(file);
163 }
164 Ok(me)
165 }).map_err(|err: Error| {
166 format_err!("unable to open media catalog {:?} - {}", path, err)
167 })?;
168
169 Ok(me)
170 }
171
172 /// Creates a temporary, empty catalog database
3fbf2d2f
DM
173 ///
174 /// Creates a new catalog file using a ".tmp" file extension.
205e1876
DM
175 pub fn create_temporary_database(
176 base_path: &Path,
fe6c1938 177 media_id: &MediaId,
205e1876
DM
178 log_to_stdout: bool,
179 ) -> Result<Self, Error> {
180
fe6c1938 181 let uuid = &media_id.label.uuid;
205e1876
DM
182
183 let mut tmp_path = base_path.to_owned();
184 tmp_path.push(uuid.to_string());
185 tmp_path.set_extension("tmp");
186
42298d58 187 let me = proxmox::try_block!({
205e1876 188
42298d58 189 Self::create_basedir(base_path)?;
205e1876 190
42298d58
DM
191 let file = std::fs::OpenOptions::new()
192 .read(true)
193 .write(true)
194 .create(true)
195 .truncate(true)
196 .open(&tmp_path)?;
205e1876 197
42298d58
DM
198 let backup_user = crate::backup::backup_user()?;
199 fchown(file.as_raw_fd(), Some(backup_user.uid), Some(backup_user.gid))
200 .map_err(|err| format_err!("fchown failed - {}", err))?;
205e1876 201
42298d58
DM
202 let mut me = Self {
203 uuid: uuid.clone(),
204 file: Some(file),
205 log_to_stdout: false,
206 current_archive: None,
207 last_entry: None,
208 chunk_index: HashMap::new(),
209 snapshot_index: HashMap::new(),
210 pending: Vec::new(),
211 };
205e1876 212
42298d58 213 me.log_to_stdout = log_to_stdout;
205e1876 214
76b15a03
DM
215 me.pending.extend(&PROXMOX_BACKUP_MEDIA_CATALOG_MAGIC_1_0);
216
fe6c1938 217 me.register_label(&media_id.label.uuid, 0)?;
42298d58 218
fe6c1938
DM
219 if let Some(ref set) = media_id.media_set_label {
220 me.register_label(&set.uuid, 1)?;
42298d58 221 }
205e1876 222
42298d58
DM
223 me.commit()?;
224
225 Ok(me)
226 }).map_err(|err: Error| {
227 format_err!("unable to create temporary media catalog {:?} - {}", tmp_path, err)
228 })?;
205e1876
DM
229
230 Ok(me)
231 }
232
233 /// Commit or Abort a temporary catalog database
3fbf2d2f
DM
234 ///
235 /// With commit set, we rename the ".tmp" file extension to
236 /// ".log". When commit is false, we remove the ".tmp" file.
205e1876
DM
237 pub fn finish_temporary_database(
238 base_path: &Path,
239 uuid: &Uuid,
240 commit: bool,
241 ) -> Result<(), Error> {
242
243 let mut tmp_path = base_path.to_owned();
244 tmp_path.push(uuid.to_string());
245 tmp_path.set_extension("tmp");
246
247 if commit {
248 let mut catalog_path = tmp_path.clone();
249 catalog_path.set_extension("log");
250
251 if let Err(err) = std::fs::rename(&tmp_path, &catalog_path) {
252 bail!("Atomic rename catalog {:?} failed - {}", catalog_path, err);
253 }
254 } else {
255 std::fs::remove_file(&tmp_path)?;
256 }
257 Ok(())
258 }
259
260 /// Returns the BackupMedia uuid
261 pub fn uuid(&self) -> &Uuid {
262 &self.uuid
263 }
264
265 /// Accessor to content list
266 pub fn snapshot_index(&self) -> &HashMap<String, u64> {
267 &self.snapshot_index
268 }
269
270 /// Commit pending changes
271 ///
272 /// This is necessary to store changes persistently.
273 ///
274 /// Fixme: this should be atomic ...
275 pub fn commit(&mut self) -> Result<(), Error> {
276
277 if self.pending.is_empty() {
278 return Ok(());
279 }
280
281 match self.file {
282 Some(ref mut file) => {
283 file.write_all(&self.pending)?;
284 file.flush()?;
285 file.sync_data()?;
286 }
287 None => bail!("media catalog not writable (opened read only)"),
288 }
289
290 self.pending = Vec::new();
291
292 Ok(())
293 }
294
295 /// Conditionally commit if in pending data is large (> 1Mb)
296 pub fn commit_if_large(&mut self) -> Result<(), Error> {
297 if self.pending.len() > 1024*1024 {
298 self.commit()?;
299 }
300 Ok(())
301 }
302
303 /// Destroy existing catalog, opens a new one
304 pub fn overwrite(
305 base_path: &Path,
fe6c1938 306 media_id: &MediaId,
205e1876
DM
307 log_to_stdout: bool,
308 ) -> Result<Self, Error> {
309
fe6c1938 310 let uuid = &media_id.label.uuid;
205e1876 311
fe6c1938 312 let me = Self::create_temporary_database(base_path, &media_id, log_to_stdout)?;
205e1876
DM
313
314 Self::finish_temporary_database(base_path, uuid, true)?;
315
316 Ok(me)
317 }
318
319 /// Test if the catalog already contain a snapshot
320 pub fn contains_snapshot(&self, snapshot: &str) -> bool {
321 self.snapshot_index.contains_key(snapshot)
322 }
323
324 /// Returns the chunk archive file number
325 pub fn lookup_snapshot(&self, snapshot: &str) -> Option<u64> {
a375df6f 326 self.snapshot_index.get(snapshot).copied()
205e1876
DM
327 }
328
329 /// Test if the catalog already contain a chunk
330 pub fn contains_chunk(&self, digest: &[u8;32]) -> bool {
331 self.chunk_index.contains_key(digest)
332 }
333
334 /// Returns the chunk archive file number
335 pub fn lookup_chunk(&self, digest: &[u8;32]) -> Option<u64> {
a375df6f 336 self.chunk_index.get(digest).copied()
205e1876
DM
337 }
338
339 fn check_register_label(&self, file_number: u64) -> Result<(), Error> {
340
341 if file_number >= 2 {
342 bail!("register label failed: got wrong file number ({} >= 2)", file_number);
343 }
344
345 if self.current_archive.is_some() {
346 bail!("register label failed: inside chunk archive");
347 }
348
349 let expected_file_number = match self.last_entry {
350 Some((_, last_number)) => last_number + 1,
351 None => 0,
352 };
353
354 if file_number != expected_file_number {
355 bail!("register label failed: got unexpected file number ({} < {})",
356 file_number, expected_file_number);
357 }
358 Ok(())
359 }
360
361 /// Register media labels (file 0 and 1)
362 pub fn register_label(
363 &mut self,
364 uuid: &Uuid, // Uuid form MediaContentHeader
365 file_number: u64,
366 ) -> Result<(), Error> {
367
368 self.check_register_label(file_number)?;
369
370 let entry = LabelEntry {
371 file_number,
372 uuid: *uuid.as_bytes(),
373 };
374
375 if self.log_to_stdout {
376 println!("L|{}|{}", file_number, uuid.to_string());
377 }
378
379 self.pending.push(b'L');
380
381 unsafe { self.pending.write_le_value(entry)?; }
382
383 self.last_entry = Some((uuid.clone(), file_number));
384
385 Ok(())
386 }
387
388 /// Register a chunk
389 ///
390 /// Only valid after start_chunk_archive.
391 pub fn register_chunk(
392 &mut self,
393 digest: &[u8;32],
394 ) -> Result<(), Error> {
395
396 let file_number = match self.current_archive {
397 None => bail!("register_chunk failed: no archive started"),
398 Some((_, file_number)) => file_number,
399 };
400
401 if self.log_to_stdout {
402 println!("C|{}", proxmox::tools::digest_to_hex(digest));
403 }
404
405 self.pending.push(b'C');
406 self.pending.extend(digest);
407
408 self.chunk_index.insert(*digest, file_number);
409
410 Ok(())
411 }
412
413 fn check_start_chunk_archive(&self, file_number: u64) -> Result<(), Error> {
414
415 if self.current_archive.is_some() {
416 bail!("start_chunk_archive failed: already started");
417 }
418
419 if file_number < 2 {
420 bail!("start_chunk_archive failed: got wrong file number ({} < 2)", file_number);
421 }
422
423 let expect_min_file_number = match self.last_entry {
424 Some((_, last_number)) => last_number + 1,
425 None => 0,
426 };
427
428 if file_number < expect_min_file_number {
429 bail!("start_chunk_archive: got unexpected file number ({} < {})",
430 file_number, expect_min_file_number);
431 }
432
433 Ok(())
434 }
435
436 /// Start a chunk archive section
437 pub fn start_chunk_archive(
438 &mut self,
439 uuid: Uuid, // Uuid form MediaContentHeader
440 file_number: u64,
441 ) -> Result<(), Error> {
442
443 self.check_start_chunk_archive(file_number)?;
444
445 let entry = ChunkArchiveStart {
446 file_number,
447 uuid: *uuid.as_bytes(),
448 };
449
450 if self.log_to_stdout {
451 println!("A|{}|{}", file_number, uuid.to_string());
452 }
453
454 self.pending.push(b'A');
455
456 unsafe { self.pending.write_le_value(entry)?; }
457
458 self.current_archive = Some((uuid, file_number));
459
460 Ok(())
461 }
462
463 fn check_end_chunk_archive(&self, uuid: &Uuid, file_number: u64) -> Result<(), Error> {
464
465 match self.current_archive {
466 None => bail!("end_chunk archive failed: not started"),
467 Some((ref expected_uuid, expected_file_number)) => {
468 if uuid != expected_uuid {
469 bail!("end_chunk_archive failed: got unexpected uuid");
470 }
471 if file_number != expected_file_number {
472 bail!("end_chunk_archive failed: got unexpected file number ({} != {})",
473 file_number, expected_file_number);
474 }
475 }
476 }
477
478 Ok(())
479 }
480
481 /// End a chunk archive section
482 pub fn end_chunk_archive(&mut self) -> Result<(), Error> {
483
484 match self.current_archive.take() {
485 None => bail!("end_chunk_archive failed: not started"),
486 Some((uuid, file_number)) => {
487
488 let entry = ChunkArchiveEnd {
489 file_number,
490 uuid: *uuid.as_bytes(),
491 };
492
493 if self.log_to_stdout {
494 println!("E|{}|{}\n", file_number, uuid.to_string());
495 }
496
497 self.pending.push(b'E');
498
499 unsafe { self.pending.write_le_value(entry)?; }
500
501 self.last_entry = Some((uuid, file_number));
502 }
503 }
504
505 Ok(())
506 }
507
508 fn check_register_snapshot(&self, file_number: u64, snapshot: &str) -> Result<(), Error> {
509
510 if self.current_archive.is_some() {
511 bail!("register_snapshot failed: inside chunk_archive");
512 }
513
514 if file_number < 2 {
515 bail!("register_snapshot failed: got wrong file number ({} < 2)", file_number);
516 }
517
518 let expect_min_file_number = match self.last_entry {
519 Some((_, last_number)) => last_number + 1,
520 None => 0,
521 };
522
523 if file_number < expect_min_file_number {
524 bail!("register_snapshot failed: got unexpected file number ({} < {})",
525 file_number, expect_min_file_number);
526 }
527
528 if let Err(err) = snapshot.parse::<BackupDir>() {
529 bail!("register_snapshot failed: unable to parse snapshot '{}' - {}", snapshot, err);
530 }
531
532 Ok(())
533 }
534
535 /// Register a snapshot
536 pub fn register_snapshot(
537 &mut self,
538 uuid: Uuid, // Uuid form MediaContentHeader
539 file_number: u64,
540 snapshot: &str,
541 ) -> Result<(), Error> {
542
543 self.check_register_snapshot(file_number, snapshot)?;
544
545 let entry = SnapshotEntry {
546 file_number,
547 uuid: *uuid.as_bytes(),
548 name_len: u16::try_from(snapshot.len())?,
549 };
550
551 if self.log_to_stdout {
552 println!("S|{}|{}|{}", file_number, uuid.to_string(), snapshot);
553 }
554
555 self.pending.push(b'S');
556
557 unsafe { self.pending.write_le_value(entry)?; }
558 self.pending.extend(snapshot.as_bytes());
559
560 self.snapshot_index.insert(snapshot.to_string(), file_number);
561
562 self.last_entry = Some((uuid, file_number));
563
564 Ok(())
565 }
566
42298d58 567 fn load_catalog(&mut self, file: &mut File) -> Result<bool, Error> {
205e1876
DM
568
569 let mut file = BufReader::new(file);
42298d58 570 let mut found_magic_number = false;
205e1876
DM
571
572 loop {
42298d58
DM
573 let pos = file.seek(SeekFrom::Current(0))?;
574
575 if pos == 0 { // read/check magic number
576 let mut magic = [0u8; 8];
577 match file.read_exact_or_eof(&mut magic) {
578 Ok(false) => { /* EOF */ break; }
579 Ok(true) => { /* OK */ }
580 Err(err) => bail!("read failed - {}", err),
581 }
582 if magic != PROXMOX_BACKUP_MEDIA_CATALOG_MAGIC_1_0 {
583 bail!("wrong magic number");
584 }
585 found_magic_number = true;
586 continue;
587 }
588
205e1876
DM
589 let mut entry_type = [0u8; 1];
590 match file.read_exact_or_eof(&mut entry_type) {
591 Ok(false) => { /* EOF */ break; }
592 Ok(true) => { /* OK */ }
593 Err(err) => bail!("read failed - {}", err),
594 }
595
596 match entry_type[0] {
597 b'C' => {
598 let file_number = match self.current_archive {
599 None => bail!("register_chunk failed: no archive started"),
600 Some((_, file_number)) => file_number,
601 };
602 let mut digest = [0u8; 32];
603 file.read_exact(&mut digest)?;
604 self.chunk_index.insert(digest, file_number);
605 }
606 b'A' => {
607 let entry: ChunkArchiveStart = unsafe { file.read_le_value()? };
608 let file_number = entry.file_number;
609 let uuid = Uuid::from(entry.uuid);
610
611 self.check_start_chunk_archive(file_number)?;
612
613 self.current_archive = Some((uuid, file_number));
614 }
615 b'E' => {
616 let entry: ChunkArchiveEnd = unsafe { file.read_le_value()? };
617 let file_number = entry.file_number;
618 let uuid = Uuid::from(entry.uuid);
619
620 self.check_end_chunk_archive(&uuid, file_number)?;
621
622 self.current_archive = None;
623 self.last_entry = Some((uuid, file_number));
624 }
625 b'S' => {
626 let entry: SnapshotEntry = unsafe { file.read_le_value()? };
627 let file_number = entry.file_number;
628 let name_len = entry.name_len;
629 let uuid = Uuid::from(entry.uuid);
630
631 let snapshot = file.read_exact_allocated(name_len.into())?;
632 let snapshot = std::str::from_utf8(&snapshot)?;
633
634 self.check_register_snapshot(file_number, snapshot)?;
635
636 self.snapshot_index.insert(snapshot.to_string(), file_number);
637
638 self.last_entry = Some((uuid, file_number));
639 }
640 b'L' => {
641 let entry: LabelEntry = unsafe { file.read_le_value()? };
642 let file_number = entry.file_number;
643 let uuid = Uuid::from(entry.uuid);
644
645 self.check_register_label(file_number)?;
646
647 self.last_entry = Some((uuid, file_number));
648 }
649 _ => {
650 bail!("unknown entry type '{}'", entry_type[0]);
651 }
652 }
653
654 }
655
42298d58 656 Ok(found_magic_number)
205e1876
DM
657 }
658}
659
660/// Media set catalog
661///
662/// Catalog for multiple media.
663pub struct MediaSetCatalog {
664 catalog_list: HashMap<Uuid, MediaCatalog>,
665}
666
667impl MediaSetCatalog {
668
669 /// Creates a new instance
670 pub fn new() -> Self {
671 Self {
672 catalog_list: HashMap::new(),
673 }
674 }
675
676 /// Add a catalog
677 pub fn append_catalog(&mut self, catalog: MediaCatalog) -> Result<(), Error> {
678
679 if self.catalog_list.get(&catalog.uuid).is_some() {
680 bail!("MediaSetCatalog already contains media '{}'", catalog.uuid);
681 }
682
683 self.catalog_list.insert(catalog.uuid.clone(), catalog);
684
685 Ok(())
686 }
687
688 /// Remove a catalog
689 pub fn remove_catalog(&mut self, media_uuid: &Uuid) {
690 self.catalog_list.remove(media_uuid);
691 }
692
693 /// Test if the catalog already contain a snapshot
694 pub fn contains_snapshot(&self, snapshot: &str) -> bool {
695 for catalog in self.catalog_list.values() {
696 if catalog.contains_snapshot(snapshot) {
697 return true;
698 }
699 }
700 false
701 }
702
703 /// Test if the catalog already contain a chunk
704 pub fn contains_chunk(&self, digest: &[u8;32]) -> bool {
705 for catalog in self.catalog_list.values() {
706 if catalog.contains_chunk(digest) {
707 return true;
708 }
709 }
710 false
711 }
712}
713
714// Type definitions for internal binary catalog encoding
715
716#[derive(Endian)]
717#[repr(C)]
9839d3f7 718struct LabelEntry {
205e1876
DM
719 file_number: u64,
720 uuid: [u8;16],
721}
722
723#[derive(Endian)]
724#[repr(C)]
9839d3f7 725struct ChunkArchiveStart {
205e1876
DM
726 file_number: u64,
727 uuid: [u8;16],
728}
729
730#[derive(Endian)]
731#[repr(C)]
9839d3f7 732struct ChunkArchiveEnd{
205e1876
DM
733 file_number: u64,
734 uuid: [u8;16],
735}
736
737#[derive(Endian)]
738#[repr(C)]
9839d3f7 739struct SnapshotEntry{
205e1876
DM
740 file_number: u64,
741 uuid: [u8;16],
742 name_len: u16,
743 /* snapshot name follows */
744}