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