1 use std
::convert
::TryFrom
;
3 use std
::io
::{Write, Read, BufReader, Seek, SeekFrom}
;
4 use std
::os
::unix
::io
::AsRawFd
;
5 use std
::path
::{PathBuf, Path}
;
6 use std
::collections
::{HashSet, HashMap}
;
8 use anyhow
::{bail, format_err, Error}
;
9 use endian_trait
::Endian
;
11 use pbs_tools
::fs
::read_subdir
;
12 use pbs_datastore
::backup_info
::BackupDir
;
14 use proxmox
::tools
::fs
::{
19 use proxmox_io
::{WriteExt, ReadExt}
;
20 use proxmox_uuid
::Uuid
;
25 file_formats
::MediaSetLabel
,
29 pub struct DatastoreContent
{
30 pub snapshot_index
: HashMap
<String
, u64>, // snapshot => file_nr
31 pub chunk_index
: HashMap
<[u8;32], u64>, // chunk => file_nr
34 impl DatastoreContent
{
36 pub fn new() -> Self {
38 chunk_index
: HashMap
::new(),
39 snapshot_index
: HashMap
::new(),
46 /// Stores what chunks and snapshots are stored on a specific media,
47 /// including the file position.
49 /// We use a simple binary format to store data on disk.
50 pub struct MediaCatalog
{
52 uuid
: Uuid
, // BackupMedia uuid
58 current_archive
: Option
<(Uuid
, u64, String
)>, // (uuid, file_nr, store)
60 last_entry
: Option
<(Uuid
, u64)>,
62 content
: HashMap
<String
, DatastoreContent
>,
69 /// Magic number for media catalog files.
70 // openssl::sha::sha256(b"Proxmox Backup Media Catalog v1.0")[0..8]
71 // Note: this version did not store datastore names (not supported anymore)
72 pub const PROXMOX_BACKUP_MEDIA_CATALOG_MAGIC_1_0
: [u8; 8] = [221, 29, 164, 1, 59, 69, 19, 40];
74 // openssl::sha::sha256(b"Proxmox Backup Media Catalog v1.1")[0..8]
75 pub const PROXMOX_BACKUP_MEDIA_CATALOG_MAGIC_1_1
: [u8; 8] = [76, 142, 232, 193, 32, 168, 137, 113];
77 /// List media with catalogs
78 pub fn media_with_catalogs(base_path
: &Path
) -> Result
<HashSet
<Uuid
>, Error
> {
79 let mut catalogs
= HashSet
::new();
81 for entry
in read_subdir(libc
::AT_FDCWD
, base_path
)?
{
83 let name
= unsafe { entry.file_name_utf8_unchecked() }
;
84 if !name
.ends_with(".log") { continue; }
85 if let Ok(uuid
) = Uuid
::parse_str(&name
[..(name
.len()-4)]) {
86 catalogs
.insert(uuid
);
93 pub fn catalog_path(base_path
: &Path
, uuid
: &Uuid
) -> PathBuf
{
94 let mut path
= base_path
.to_owned();
95 path
.push(uuid
.to_string());
96 path
.set_extension("log");
100 fn tmp_catalog_path(base_path
: &Path
, uuid
: &Uuid
) -> PathBuf
{
101 let mut path
= base_path
.to_owned();
102 path
.push(uuid
.to_string());
103 path
.set_extension("tmp");
107 /// Test if a catalog exists
108 pub fn exists(base_path
: &Path
, uuid
: &Uuid
) -> bool
{
109 Self::catalog_path(base_path
, uuid
).exists()
112 /// Destroy the media catalog (remove all files)
113 pub fn destroy(base_path
: &Path
, uuid
: &Uuid
) -> Result
<(), Error
> {
115 let path
= Self::catalog_path(base_path
, uuid
);
117 match std
::fs
::remove_file(path
) {
119 Err(err
) if err
.kind() == std
::io
::ErrorKind
::NotFound
=> Ok(()),
120 Err(err
) => Err(err
.into()),
124 /// Destroy the media catalog if media_set uuid does not match
125 pub fn destroy_unrelated_catalog(
128 ) -> Result
<(), Error
> {
130 let uuid
= &media_id
.label
.uuid
;
132 let path
= Self::catalog_path(base_path
, uuid
);
134 let file
= match std
::fs
::OpenOptions
::new().read(true).open(&path
) {
136 Err(ref err
) if err
.kind() == std
::io
::ErrorKind
::NotFound
=> {
139 Err(err
) => return Err(err
.into()),
142 let mut file
= BufReader
::new(file
);
144 let expected_media_set_id
= match media_id
.media_set_label
{
146 std
::fs
::remove_file(path
)?
;
149 Some(ref set
) => &set
.uuid
,
152 let (found_magic_number
, media_uuid
, media_set_uuid
) =
153 Self::parse_catalog_header(&mut file
)?
;
155 if !found_magic_number
{
159 if let Some(ref media_uuid
) = media_uuid
{
160 if media_uuid
!= uuid
{
161 std
::fs
::remove_file(path
)?
;
166 if let Some(ref media_set_uuid
) = media_set_uuid
{
167 if media_set_uuid
!= expected_media_set_id
{
168 std
::fs
::remove_file(path
)?
;
175 /// Enable/Disable logging to stdout (disabled by default)
176 pub fn log_to_stdout(&mut self, enable
: bool
) {
177 self.log_to_stdout
= enable
;
180 fn create_basedir(base_path
: &Path
) -> Result
<(), Error
> {
181 let backup_user
= pbs_config
::backup_user()?
;
182 let mode
= nix
::sys
::stat
::Mode
::from_bits_truncate(0o0640);
183 let opts
= CreateOptions
::new()
185 .owner(backup_user
.uid
)
186 .group(backup_user
.gid
);
188 create_path(base_path
, None
, Some(opts
))
189 .map_err(|err
: Error
| format_err
!("unable to create media catalog dir - {}", err
))?
;
193 /// Open a catalog database, load into memory
199 ) -> Result
<Self, Error
> {
201 let uuid
= &media_id
.label
.uuid
;
203 let path
= Self::catalog_path(base_path
, uuid
);
205 let me
= proxmox_lang
::try_block
!({
207 Self::create_basedir(base_path
)?
;
209 let mut file
= std
::fs
::OpenOptions
::new()
215 let backup_user
= pbs_config
::backup_user()?
;
216 fchown(file
.as_raw_fd(), Some(backup_user
.uid
), Some(backup_user
.gid
))
217 .map_err(|err
| format_err
!("fchown failed - {}", err
))?
;
222 log_to_stdout
: false,
223 current_archive
: None
,
225 content
: HashMap
::new(),
229 // Note: lock file, to get a consistent view with load_catalog
230 nix
::fcntl
::flock(file
.as_raw_fd(), nix
::fcntl
::FlockArg
::LockExclusive
)?
;
231 let result
= me
.load_catalog(&mut file
, media_id
.media_set_label
.as_ref());
232 nix
::fcntl
::flock(file
.as_raw_fd(), nix
::fcntl
::FlockArg
::Unlock
)?
;
234 let (found_magic_number
, _
) = result?
;
236 if !found_magic_number
{
237 me
.pending
.extend(&Self::PROXMOX_BACKUP_MEDIA_CATALOG_MAGIC_1_1
);
241 me
.file
= Some(file
);
244 }).map_err(|err
: Error
| {
245 format_err
!("unable to open media catalog {:?} - {}", path
, err
)
251 /// Creates a temporary empty catalog file
252 pub fn create_temporary_database_file(
255 ) -> Result
<File
, Error
> {
257 Self::create_basedir(base_path
)?
;
259 let tmp_path
= Self::tmp_catalog_path(base_path
, uuid
);
261 let file
= std
::fs
::OpenOptions
::new()
269 // We cannot use chown inside test environment (no permissions)
273 let backup_user
= pbs_config
::backup_user()?
;
274 fchown(file
.as_raw_fd(), Some(backup_user
.uid
), Some(backup_user
.gid
))
275 .map_err(|err
| format_err
!("fchown failed - {}", err
))?
;
280 /// Creates a temporary, empty catalog database
282 /// Creates a new catalog file using a ".tmp" file extension.
283 pub fn create_temporary_database(
287 ) -> Result
<Self, Error
> {
289 let uuid
= &media_id
.label
.uuid
;
291 let tmp_path
= Self::tmp_catalog_path(base_path
, uuid
);
293 let me
= proxmox_lang
::try_block
!({
295 let file
= Self::create_temporary_database_file(base_path
, uuid
)?
;
300 log_to_stdout
: false,
301 current_archive
: None
,
303 content
: HashMap
::new(),
307 me
.log_to_stdout
= log_to_stdout
;
309 me
.pending
.extend(&Self::PROXMOX_BACKUP_MEDIA_CATALOG_MAGIC_1_1
);
311 me
.register_label(&media_id
.label
.uuid
, 0, 0)?
;
313 if let Some(ref set
) = media_id
.media_set_label
{
314 me
.register_label(&set
.uuid
, set
.seq_nr
, 1)?
;
320 }).map_err(|err
: Error
| {
321 format_err
!("unable to create temporary media catalog {:?} - {}", tmp_path
, err
)
327 /// Commit or Abort a temporary catalog database
329 /// With commit set, we rename the ".tmp" file extension to
330 /// ".log". When commit is false, we remove the ".tmp" file.
331 pub fn finish_temporary_database(
335 ) -> Result
<(), Error
> {
337 let tmp_path
= Self::tmp_catalog_path(base_path
, uuid
);
340 let mut catalog_path
= tmp_path
.clone();
341 catalog_path
.set_extension("log");
343 if let Err(err
) = std
::fs
::rename(&tmp_path
, &catalog_path
) {
344 bail
!("Atomic rename catalog {:?} failed - {}", catalog_path
, err
);
347 std
::fs
::remove_file(&tmp_path
)?
;
352 /// Returns the BackupMedia uuid
353 pub fn uuid(&self) -> &Uuid
{
357 /// Accessor to content list
358 pub fn content(&self) -> &HashMap
<String
, DatastoreContent
> {
362 /// Commit pending changes
364 /// This is necessary to store changes persistently.
366 /// Fixme: this should be atomic ...
367 pub fn commit(&mut self) -> Result
<(), Error
> {
369 if self.pending
.is_empty() {
374 Some(ref mut file
) => {
375 let pending
= &self.pending
;
376 // Note: lock file, to get a consistent view with load_catalog
377 nix
::fcntl
::flock(file
.as_raw_fd(), nix
::fcntl
::FlockArg
::LockExclusive
)?
;
378 let result
: Result
<(), Error
> = proxmox_lang
::try_block
!({
379 file
.write_all(pending
)?
;
384 nix
::fcntl
::flock(file
.as_raw_fd(), nix
::fcntl
::FlockArg
::Unlock
)?
;
388 None
=> bail
!("media catalog not writable (opened read only)"),
391 self.pending
= Vec
::new();
396 /// Conditionally commit if in pending data is large (> 1Mb)
397 pub fn commit_if_large(&mut self) -> Result
<(), Error
> {
398 if self.current_archive
.is_some() {
399 bail
!("can't commit catalog in the middle of an chunk archive");
401 if self.pending
.len() > 1024*1024 {
407 /// Destroy existing catalog, opens a new one
412 ) -> Result
<Self, Error
> {
414 let uuid
= &media_id
.label
.uuid
;
416 let me
= Self::create_temporary_database(base_path
, &media_id
, log_to_stdout
)?
;
418 Self::finish_temporary_database(base_path
, uuid
, true)?
;
423 /// Test if the catalog already contain a snapshot
424 pub fn contains_snapshot(&self, store
: &str, snapshot
: &str) -> bool
{
425 match self.content
.get(store
) {
427 Some(content
) => content
.snapshot_index
.contains_key(snapshot
),
431 /// Returns the snapshot archive file number
432 pub fn lookup_snapshot(&self, store
: &str, snapshot
: &str) -> Option
<u64> {
433 match self.content
.get(store
) {
435 Some(content
) => content
.snapshot_index
.get(snapshot
).copied(),
439 /// Test if the catalog already contain a chunk
440 pub fn contains_chunk(&self, store
: &str, digest
: &[u8;32]) -> bool
{
441 match self.content
.get(store
) {
443 Some(content
) => content
.chunk_index
.contains_key(digest
),
447 /// Returns the chunk archive file number
448 pub fn lookup_chunk(&self, store
: &str, digest
: &[u8;32]) -> Option
<u64> {
449 match self.content
.get(store
) {
451 Some(content
) => content
.chunk_index
.get(digest
).copied(),
455 fn check_register_label(&self, file_number
: u64, uuid
: &Uuid
) -> Result
<(), Error
> {
457 if file_number
>= 2 {
458 bail
!("register label failed: got wrong file number ({} >= 2)", file_number
);
461 if file_number
== 0 && uuid
!= &self.uuid
{
462 bail
!("register label failed: uuid does not match");
465 if self.current_archive
.is_some() {
466 bail
!("register label failed: inside chunk archive");
469 let expected_file_number
= match self.last_entry
{
470 Some((_
, last_number
)) => last_number
+ 1,
474 if file_number
!= expected_file_number
{
475 bail
!("register label failed: got unexpected file number ({} < {})",
476 file_number
, expected_file_number
);
481 /// Register media labels (file 0 and 1)
482 pub fn register_label(
484 uuid
: &Uuid
, // Media/MediaSet Uuid
485 seq_nr
: u64, // onyl used for media set labels
487 ) -> Result
<(), Error
> {
489 self.check_register_label(file_number
, uuid
)?
;
491 if file_number
== 0 && seq_nr
!= 0 {
492 bail
!("register_label failed - seq_nr should be 0 - iternal error");
495 let entry
= LabelEntry
{
497 uuid
: *uuid
.as_bytes(),
501 if self.log_to_stdout
{
502 println
!("L|{}|{}", file_number
, uuid
.to_string());
505 self.pending
.push(b'L'
);
507 unsafe { self.pending.write_le_value(entry)?; }
509 self.last_entry
= Some((uuid
.clone(), file_number
));
514 /// Register a chunk archive
515 pub fn register_chunk_archive(
517 uuid
: Uuid
, // Uuid form MediaContentHeader
520 chunk_list
: &[[u8; 32]],
521 ) -> Result
<(), Error
> {
522 self.start_chunk_archive(uuid
, file_number
, store
)?
;
523 for digest
in chunk_list
{
524 self.register_chunk(digest
)?
;
526 self.end_chunk_archive()?
;
532 /// Only valid after start_chunk_archive.
536 ) -> Result
<(), Error
> {
538 let (file_number
, store
) = match self.current_archive
{
539 None
=> bail
!("register_chunk failed: no archive started"),
540 Some((_
, file_number
, ref store
)) => (file_number
, store
),
543 if self.log_to_stdout
{
544 println
!("C|{}", proxmox
::tools
::digest_to_hex(digest
));
547 self.pending
.push(b'C'
);
548 self.pending
.extend(digest
);
550 match self.content
.get_mut(store
) {
551 None
=> bail
!("storage {} not registered - internal error", store
),
553 content
.chunk_index
.insert(*digest
, file_number
);
560 fn check_start_chunk_archive(&self, file_number
: u64) -> Result
<(), Error
> {
562 if self.current_archive
.is_some() {
563 bail
!("start_chunk_archive failed: already started");
567 bail
!("start_chunk_archive failed: got wrong file number ({} < 2)", file_number
);
570 let expect_min_file_number
= match self.last_entry
{
571 Some((_
, last_number
)) => last_number
+ 1,
575 if file_number
< expect_min_file_number
{
576 bail
!("start_chunk_archive: got unexpected file number ({} < {})",
577 file_number
, expect_min_file_number
);
583 /// Start a chunk archive section
584 fn start_chunk_archive(
586 uuid
: Uuid
, // Uuid form MediaContentHeader
589 ) -> Result
<(), Error
> {
591 self.check_start_chunk_archive(file_number
)?
;
593 let entry
= ChunkArchiveStart
{
595 uuid
: *uuid
.as_bytes(),
596 store_name_len
: u8::try_from(store
.len())?
,
599 if self.log_to_stdout
{
600 println
!("A|{}|{}|{}", file_number
, uuid
.to_string(), store
);
603 self.pending
.push(b'A'
);
605 unsafe { self.pending.write_le_value(entry)?; }
606 self.pending
.extend(store
.as_bytes());
608 self.content
.entry(store
.to_string()).or_insert(DatastoreContent
::new());
610 self.current_archive
= Some((uuid
, file_number
, store
.to_string()));
615 fn check_end_chunk_archive(&self, uuid
: &Uuid
, file_number
: u64) -> Result
<(), Error
> {
617 match self.current_archive
{
618 None
=> bail
!("end_chunk archive failed: not started"),
619 Some((ref expected_uuid
, expected_file_number
, ..)) => {
620 if uuid
!= expected_uuid
{
621 bail
!("end_chunk_archive failed: got unexpected uuid");
623 if file_number
!= expected_file_number
{
624 bail
!("end_chunk_archive failed: got unexpected file number ({} != {})",
625 file_number
, expected_file_number
);
632 /// End a chunk archive section
633 fn end_chunk_archive(&mut self) -> Result
<(), Error
> {
635 match self.current_archive
.take() {
636 None
=> bail
!("end_chunk_archive failed: not started"),
637 Some((uuid
, file_number
, ..)) => {
639 let entry
= ChunkArchiveEnd
{
641 uuid
: *uuid
.as_bytes(),
644 if self.log_to_stdout
{
645 println
!("E|{}|{}\n", file_number
, uuid
.to_string());
648 self.pending
.push(b'E'
);
650 unsafe { self.pending.write_le_value(entry)?; }
652 self.last_entry
= Some((uuid
, file_number
));
659 fn check_register_snapshot(&self, file_number
: u64, snapshot
: &str) -> Result
<(), Error
> {
661 if self.current_archive
.is_some() {
662 bail
!("register_snapshot failed: inside chunk_archive");
666 bail
!("register_snapshot failed: got wrong file number ({} < 2)", file_number
);
669 let expect_min_file_number
= match self.last_entry
{
670 Some((_
, last_number
)) => last_number
+ 1,
674 if file_number
< expect_min_file_number
{
675 bail
!("register_snapshot failed: got unexpected file number ({} < {})",
676 file_number
, expect_min_file_number
);
679 if let Err(err
) = snapshot
.parse
::<BackupDir
>() {
680 bail
!("register_snapshot failed: unable to parse snapshot '{}' - {}", snapshot
, err
);
686 /// Register a snapshot
687 pub fn register_snapshot(
689 uuid
: Uuid
, // Uuid form MediaContentHeader
693 ) -> Result
<(), Error
> {
695 self.check_register_snapshot(file_number
, snapshot
)?
;
697 let entry
= SnapshotEntry
{
699 uuid
: *uuid
.as_bytes(),
700 store_name_len
: u8::try_from(store
.len())?
,
701 name_len
: u16::try_from(snapshot
.len())?
,
704 if self.log_to_stdout
{
705 println
!("S|{}|{}|{}:{}", file_number
, uuid
.to_string(), store
, snapshot
);
708 self.pending
.push(b'S'
);
710 unsafe { self.pending.write_le_value(entry)?; }
711 self.pending
.extend(store
.as_bytes());
712 self.pending
.push(b'
:'
);
713 self.pending
.extend(snapshot
.as_bytes());
715 let content
= self.content
.entry(store
.to_string())
716 .or_insert(DatastoreContent
::new());
718 content
.snapshot_index
.insert(snapshot
.to_string(), file_number
);
720 self.last_entry
= Some((uuid
, file_number
));
725 /// Parse the catalog header
726 pub fn parse_catalog_header
<R
: Read
>(
728 ) -> Result
<(bool
, Option
<Uuid
>, Option
<Uuid
>), Error
> {
730 // read/check magic number
731 let mut magic
= [0u8; 8];
732 if !reader
.read_exact_or_eof(&mut magic
)?
{
734 return Ok((false, None
, None
));
737 if magic
== Self::PROXMOX_BACKUP_MEDIA_CATALOG_MAGIC_1_0
{
738 // only use in unreleased versions
739 bail
!("old catalog format (v1.0) is no longer supported");
741 if magic
!= Self::PROXMOX_BACKUP_MEDIA_CATALOG_MAGIC_1_1
{
742 bail
!("wrong magic number");
745 let mut entry_type
= [0u8; 1];
746 if !reader
.read_exact_or_eof(&mut entry_type
)?
{
748 return Ok((true, None
, None
));
751 if entry_type
[0] != b'L'
{
752 bail
!("got unexpected entry type");
755 let entry0
: LabelEntry
= unsafe { reader.read_le_value()? }
;
757 let mut entry_type
= [0u8; 1];
758 if !reader
.read_exact_or_eof(&mut entry_type
)?
{
760 return Ok((true, Some(entry0
.uuid
.into()), None
));
763 if entry_type
[0] != b'L'
{
764 bail
!("got unexpected entry type");
767 let entry1
: LabelEntry
= unsafe { reader.read_le_value()? }
;
769 Ok((true, Some(entry0
.uuid
.into()), Some(entry1
.uuid
.into())))
775 media_set_label
: Option
<&MediaSetLabel
>,
776 ) -> Result
<(bool
, Option
<Uuid
>), Error
> {
778 let mut file
= BufReader
::new(file
);
779 let mut found_magic_number
= false;
780 let mut media_set_uuid
= None
;
783 let pos
= file
.seek(SeekFrom
::Current(0))?
; // get current pos
785 if pos
== 0 { // read/check magic number
786 let mut magic
= [0u8; 8];
787 match file
.read_exact_or_eof(&mut magic
) {
788 Ok(false) => { /* EOF */ break; }
789 Ok(true) => { /* OK */ }
790 Err(err
) => bail
!("read failed - {}", err
),
792 if magic
== Self::PROXMOX_BACKUP_MEDIA_CATALOG_MAGIC_1_0
{
793 // only use in unreleased versions
794 bail
!("old catalog format (v1.0) is no longer supported");
796 if magic
!= Self::PROXMOX_BACKUP_MEDIA_CATALOG_MAGIC_1_1
{
797 bail
!("wrong magic number");
799 found_magic_number
= true;
803 let mut entry_type
= [0u8; 1];
804 match file
.read_exact_or_eof(&mut entry_type
) {
805 Ok(false) => { /* EOF */ break; }
806 Ok(true) => { /* OK */ }
807 Err(err
) => bail
!("read failed - {}", err
),
810 match entry_type
[0] {
812 let (file_number
, store
) = match self.current_archive
{
813 None
=> bail
!("register_chunk failed: no archive started"),
814 Some((_
, file_number
, ref store
)) => (file_number
, store
),
816 let mut digest
= [0u8; 32];
817 file
.read_exact(&mut digest
)?
;
818 match self.content
.get_mut(store
) {
819 None
=> bail
!("storage {} not registered - internal error", store
),
821 content
.chunk_index
.insert(digest
, file_number
);
826 let entry
: ChunkArchiveStart
= unsafe { file.read_le_value()? }
;
827 let file_number
= entry
.file_number
;
828 let uuid
= Uuid
::from(entry
.uuid
);
829 let store_name_len
= entry
.store_name_len
as usize;
831 let store
= file
.read_exact_allocated(store_name_len
)?
;
832 let store
= std
::str::from_utf8(&store
)?
;
834 self.check_start_chunk_archive(file_number
)?
;
836 self.content
.entry(store
.to_string())
837 .or_insert(DatastoreContent
::new());
839 self.current_archive
= Some((uuid
, file_number
, store
.to_string()));
842 let entry
: ChunkArchiveEnd
= unsafe { file.read_le_value()? }
;
843 let file_number
= entry
.file_number
;
844 let uuid
= Uuid
::from(entry
.uuid
);
846 self.check_end_chunk_archive(&uuid
, file_number
)?
;
848 self.current_archive
= None
;
849 self.last_entry
= Some((uuid
, file_number
));
852 let entry
: SnapshotEntry
= unsafe { file.read_le_value()? }
;
853 let file_number
= entry
.file_number
;
854 let store_name_len
= entry
.store_name_len
as usize;
855 let name_len
= entry
.name_len
as usize;
856 let uuid
= Uuid
::from(entry
.uuid
);
858 let store
= file
.read_exact_allocated(store_name_len
+ 1)?
;
859 if store
[store_name_len
] != b'
:'
{
860 bail
!("parse-error: missing separator in SnapshotEntry");
863 let store
= std
::str::from_utf8(&store
[..store_name_len
])?
;
865 let snapshot
= file
.read_exact_allocated(name_len
)?
;
866 let snapshot
= std
::str::from_utf8(&snapshot
)?
;
868 self.check_register_snapshot(file_number
, snapshot
)?
;
870 let content
= self.content
.entry(store
.to_string())
871 .or_insert(DatastoreContent
::new());
873 content
.snapshot_index
.insert(snapshot
.to_string(), file_number
);
875 self.last_entry
= Some((uuid
, file_number
));
878 let entry
: LabelEntry
= unsafe { file.read_le_value()? }
;
879 let file_number
= entry
.file_number
;
880 let uuid
= Uuid
::from(entry
.uuid
);
882 self.check_register_label(file_number
, &uuid
)?
;
884 if file_number
== 1 {
885 if let Some(set
) = media_set_label
{
886 if set
.uuid
!= uuid
{
887 bail
!("got unexpected media set uuid");
889 if set
.seq_nr
!= entry
.seq_nr
{
890 bail
!("got unexpected media set sequence number");
893 media_set_uuid
= Some(uuid
.clone());
896 self.last_entry
= Some((uuid
, file_number
));
899 bail
!("unknown entry type '{}'", entry_type
[0]);
905 Ok((found_magic_number
, media_set_uuid
))
909 /// Media set catalog
911 /// Catalog for multiple media.
912 pub struct MediaSetCatalog
{
913 catalog_list
: HashMap
<Uuid
, MediaCatalog
>,
916 impl MediaSetCatalog
{
918 /// Creates a new instance
919 pub fn new() -> Self {
921 catalog_list
: HashMap
::new(),
926 pub fn append_catalog(&mut self, catalog
: MediaCatalog
) -> Result
<(), Error
> {
928 if self.catalog_list
.get(&catalog
.uuid
).is_some() {
929 bail
!("MediaSetCatalog already contains media '{}'", catalog
.uuid
);
932 self.catalog_list
.insert(catalog
.uuid
.clone(), catalog
);
938 pub fn remove_catalog(&mut self, media_uuid
: &Uuid
) {
939 self.catalog_list
.remove(media_uuid
);
942 /// Test if the catalog already contain a snapshot
943 pub fn contains_snapshot(&self, store
: &str, snapshot
: &str) -> bool
{
944 for catalog
in self.catalog_list
.values() {
945 if catalog
.contains_snapshot(store
, snapshot
) {
952 /// Returns the media uuid and snapshot archive file number
953 pub fn lookup_snapshot(&self, store
: &str, snapshot
: &str) -> Option
<(&Uuid
, u64)> {
954 for (uuid
, catalog
) in self.catalog_list
.iter() {
955 if let Some(nr
) = catalog
.lookup_snapshot(store
, snapshot
) {
956 return Some((uuid
, nr
));
962 /// Test if the catalog already contain a chunk
963 pub fn contains_chunk(&self, store
: &str, digest
: &[u8;32]) -> bool
{
964 for catalog
in self.catalog_list
.values() {
965 if catalog
.contains_chunk(store
, digest
) {
972 /// Returns the media uuid and chunk archive file number
973 pub fn lookup_chunk(&self, store
: &str, digest
: &[u8;32]) -> Option
<(&Uuid
, u64)> {
974 for (uuid
, catalog
) in self.catalog_list
.iter() {
975 if let Some(nr
) = catalog
.lookup_chunk(store
, digest
) {
976 return Some((uuid
, nr
));
983 // Type definitions for internal binary catalog encoding
990 seq_nr
: u64, // only used for media set labels
995 struct ChunkArchiveStart
{
999 /* datastore name follows */
1004 struct ChunkArchiveEnd
{
1011 struct SnapshotEntry
{
1016 /* datastore name, ':', snapshot name follows */