1 use std
::convert
::TryFrom
;
3 use std
::io
::{Write, Read, BufReader, Seek, SeekFrom}
;
4 use std
::os
::unix
::io
::AsRawFd
;
6 use std
::collections
::{HashSet, HashMap}
;
8 use anyhow
::{bail, format_err, Error}
;
9 use endian_trait
::Endian
;
25 tools
::fs
::read_subdir
,
29 file_formats
::MediaSetLabel
,
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
38 impl DatastoreContent
{
40 pub fn new() -> Self {
42 chunk_index
: HashMap
::new(),
43 snapshot_index
: HashMap
::new(),
50 /// Stores what chunks and snapshots are stored on a specific media,
51 /// including the file position.
53 /// We use a simple binary format to store data on disk.
54 pub struct MediaCatalog
{
56 uuid
: Uuid
, // BackupMedia uuid
62 current_archive
: Option
<(Uuid
, u64, String
)>, // (uuid, file_nr, store)
64 last_entry
: Option
<(Uuid
, u64)>,
66 content
: HashMap
<String
, DatastoreContent
>,
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];
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];
81 /// List media with catalogs
82 pub fn media_with_catalogs(base_path
: &Path
) -> Result
<HashSet
<Uuid
>, Error
> {
83 let mut catalogs
= HashSet
::new();
85 for entry
in read_subdir(libc
::AT_FDCWD
, base_path
)?
{
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
);
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");
105 /// Destroy the media catalog (remove all files)
106 pub fn destroy(base_path
: &Path
, uuid
: &Uuid
) -> Result
<(), Error
> {
108 let mut path
= base_path
.to_owned();
109 path
.push(uuid
.to_string());
110 path
.set_extension("log");
112 match std
::fs
::remove_file(path
) {
114 Err(err
) if err
.kind() == std
::io
::ErrorKind
::NotFound
=> Ok(()),
115 Err(err
) => Err(err
.into()),
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
;
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()
129 .owner(backup_user
.uid
)
130 .group(backup_user
.gid
);
132 create_path(base_path
, None
, Some(opts
))
133 .map_err(|err
: Error
| format_err
!("unable to create media catalog dir - {}", err
))?
;
137 /// Open a catalog database, load into memory
143 ) -> Result
<Self, Error
> {
145 let uuid
= &media_id
.label
.uuid
;
147 let mut path
= base_path
.to_owned();
148 path
.push(uuid
.to_string());
149 path
.set_extension("log");
151 let me
= proxmox
::try_block
!({
153 Self::create_basedir(base_path
)?
;
155 let mut file
= std
::fs
::OpenOptions
::new()
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
))?
;
168 log_to_stdout
: false,
169 current_archive
: None
,
171 content
: HashMap
::new(),
175 let found_magic_number
= me
.load_catalog(&mut file
, media_id
.media_set_label
.as_ref())?
;
177 if !found_magic_number
{
178 me
.pending
.extend(&Self::PROXMOX_BACKUP_MEDIA_CATALOG_MAGIC_1_1
);
182 me
.file
= Some(file
);
185 }).map_err(|err
: Error
| {
186 format_err
!("unable to open media catalog {:?} - {}", path
, err
)
192 /// Creates a temporary, empty catalog database
194 /// Creates a new catalog file using a ".tmp" file extension.
195 pub fn create_temporary_database(
199 ) -> Result
<Self, Error
> {
201 let uuid
= &media_id
.label
.uuid
;
203 let mut tmp_path
= base_path
.to_owned();
204 tmp_path
.push(uuid
.to_string());
205 tmp_path
.set_extension("tmp");
207 let me
= proxmox
::try_block
!({
209 Self::create_basedir(base_path
)?
;
211 let file
= std
::fs
::OpenOptions
::new()
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
))?
;
225 log_to_stdout
: false,
226 current_archive
: None
,
228 content
: HashMap
::new(),
232 me
.log_to_stdout
= log_to_stdout
;
234 me
.pending
.extend(&Self::PROXMOX_BACKUP_MEDIA_CATALOG_MAGIC_1_1
);
236 me
.register_label(&media_id
.label
.uuid
, 0, 0)?
;
238 if let Some(ref set
) = media_id
.media_set_label
{
239 me
.register_label(&set
.uuid
, set
.seq_nr
, 1)?
;
245 }).map_err(|err
: Error
| {
246 format_err
!("unable to create temporary media catalog {:?} - {}", tmp_path
, err
)
252 /// Commit or Abort a temporary catalog database
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(
260 ) -> Result
<(), Error
> {
262 let mut tmp_path
= base_path
.to_owned();
263 tmp_path
.push(uuid
.to_string());
264 tmp_path
.set_extension("tmp");
267 let mut catalog_path
= tmp_path
.clone();
268 catalog_path
.set_extension("log");
270 if let Err(err
) = std
::fs
::rename(&tmp_path
, &catalog_path
) {
271 bail
!("Atomic rename catalog {:?} failed - {}", catalog_path
, err
);
274 std
::fs
::remove_file(&tmp_path
)?
;
279 /// Returns the BackupMedia uuid
280 pub fn uuid(&self) -> &Uuid
{
284 /// Accessor to content list
285 pub fn content(&self) -> &HashMap
<String
, DatastoreContent
> {
289 /// Commit pending changes
291 /// This is necessary to store changes persistently.
293 /// Fixme: this should be atomic ...
294 pub fn commit(&mut self) -> Result
<(), Error
> {
296 if self.pending
.is_empty() {
301 Some(ref mut file
) => {
302 file
.write_all(&self.pending
)?
;
306 None
=> bail
!("media catalog not writable (opened read only)"),
309 self.pending
= Vec
::new();
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");
319 if self.pending
.len() > 1024*1024 {
325 /// Destroy existing catalog, opens a new one
330 ) -> Result
<Self, Error
> {
332 let uuid
= &media_id
.label
.uuid
;
334 let me
= Self::create_temporary_database(base_path
, &media_id
, log_to_stdout
)?
;
336 Self::finish_temporary_database(base_path
, uuid
, true)?
;
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
) {
345 Some(content
) => content
.snapshot_index
.contains_key(snapshot
),
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
) {
353 Some(content
) => content
.snapshot_index
.get(snapshot
).copied(),
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
) {
361 Some(content
) => content
.chunk_index
.contains_key(digest
),
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
) {
369 Some(content
) => content
.chunk_index
.get(digest
).copied(),
373 fn check_register_label(&self, file_number
: u64, uuid
: &Uuid
) -> Result
<(), Error
> {
375 if file_number
>= 2 {
376 bail
!("register label failed: got wrong file number ({} >= 2)", file_number
);
379 if file_number
== 0 && uuid
!= &self.uuid
{
380 bail
!("register label failed: uuid does not match");
383 if self.current_archive
.is_some() {
384 bail
!("register label failed: inside chunk archive");
387 let expected_file_number
= match self.last_entry
{
388 Some((_
, last_number
)) => last_number
+ 1,
392 if file_number
!= expected_file_number
{
393 bail
!("register label failed: got unexpected file number ({} < {})",
394 file_number
, expected_file_number
);
399 /// Register media labels (file 0 and 1)
400 pub fn register_label(
402 uuid
: &Uuid
, // Media/MediaSet Uuid
403 seq_nr
: u64, // onyl used for media set labels
405 ) -> Result
<(), Error
> {
407 self.check_register_label(file_number
, uuid
)?
;
409 if file_number
== 0 && seq_nr
!= 0 {
410 bail
!("register_label failed - seq_nr should be 0 - iternal error");
413 let entry
= LabelEntry
{
415 uuid
: *uuid
.as_bytes(),
419 if self.log_to_stdout
{
420 println
!("L|{}|{}", file_number
, uuid
.to_string());
423 self.pending
.push(b'L'
);
425 unsafe { self.pending.write_le_value(entry)?; }
427 self.last_entry
= Some((uuid
.clone(), file_number
));
434 /// Only valid after start_chunk_archive.
435 pub fn register_chunk(
438 ) -> Result
<(), Error
> {
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
),
445 if self.log_to_stdout
{
446 println
!("C|{}", proxmox
::tools
::digest_to_hex(digest
));
449 self.pending
.push(b'C'
);
450 self.pending
.extend(digest
);
452 match self.content
.get_mut(store
) {
453 None
=> bail
!("storage {} not registered - internal error", store
),
455 content
.chunk_index
.insert(*digest
, file_number
);
462 fn check_start_chunk_archive(&self, file_number
: u64) -> Result
<(), Error
> {
464 if self.current_archive
.is_some() {
465 bail
!("start_chunk_archive failed: already started");
469 bail
!("start_chunk_archive failed: got wrong file number ({} < 2)", file_number
);
472 let expect_min_file_number
= match self.last_entry
{
473 Some((_
, last_number
)) => last_number
+ 1,
477 if file_number
< expect_min_file_number
{
478 bail
!("start_chunk_archive: got unexpected file number ({} < {})",
479 file_number
, expect_min_file_number
);
485 /// Start a chunk archive section
486 pub fn start_chunk_archive(
488 uuid
: Uuid
, // Uuid form MediaContentHeader
491 ) -> Result
<(), Error
> {
493 self.check_start_chunk_archive(file_number
)?
;
495 let entry
= ChunkArchiveStart
{
497 uuid
: *uuid
.as_bytes(),
498 store_name_len
: u8::try_from(store
.len())?
,
501 if self.log_to_stdout
{
502 println
!("A|{}|{}|{}", file_number
, uuid
.to_string(), store
);
505 self.pending
.push(b'A'
);
507 unsafe { self.pending.write_le_value(entry)?; }
508 self.pending
.extend(store
.as_bytes());
510 self.content
.entry(store
.to_string()).or_insert(DatastoreContent
::new());
512 self.current_archive
= Some((uuid
, file_number
, store
.to_string()));
517 fn check_end_chunk_archive(&self, uuid
: &Uuid
, file_number
: u64) -> Result
<(), Error
> {
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");
525 if file_number
!= expected_file_number
{
526 bail
!("end_chunk_archive failed: got unexpected file number ({} != {})",
527 file_number
, expected_file_number
);
534 /// End a chunk archive section
535 pub fn end_chunk_archive(&mut self) -> Result
<(), Error
> {
537 match self.current_archive
.take() {
538 None
=> bail
!("end_chunk_archive failed: not started"),
539 Some((uuid
, file_number
, ..)) => {
541 let entry
= ChunkArchiveEnd
{
543 uuid
: *uuid
.as_bytes(),
546 if self.log_to_stdout
{
547 println
!("E|{}|{}\n", file_number
, uuid
.to_string());
550 self.pending
.push(b'E'
);
552 unsafe { self.pending.write_le_value(entry)?; }
554 self.last_entry
= Some((uuid
, file_number
));
561 fn check_register_snapshot(&self, file_number
: u64, snapshot
: &str) -> Result
<(), Error
> {
563 if self.current_archive
.is_some() {
564 bail
!("register_snapshot failed: inside chunk_archive");
568 bail
!("register_snapshot failed: got wrong file number ({} < 2)", file_number
);
571 let expect_min_file_number
= match self.last_entry
{
572 Some((_
, last_number
)) => last_number
+ 1,
576 if file_number
< expect_min_file_number
{
577 bail
!("register_snapshot failed: got unexpected file number ({} < {})",
578 file_number
, expect_min_file_number
);
581 if let Err(err
) = snapshot
.parse
::<BackupDir
>() {
582 bail
!("register_snapshot failed: unable to parse snapshot '{}' - {}", snapshot
, err
);
588 /// Register a snapshot
589 pub fn register_snapshot(
591 uuid
: Uuid
, // Uuid form MediaContentHeader
595 ) -> Result
<(), Error
> {
597 self.check_register_snapshot(file_number
, snapshot
)?
;
599 let entry
= SnapshotEntry
{
601 uuid
: *uuid
.as_bytes(),
602 store_name_len
: u8::try_from(store
.len())?
,
603 name_len
: u16::try_from(snapshot
.len())?
,
606 if self.log_to_stdout
{
607 println
!("S|{}|{}|{}:{}", file_number
, uuid
.to_string(), store
, snapshot
);
610 self.pending
.push(b'S'
);
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());
617 let content
= self.content
.entry(store
.to_string())
618 .or_insert(DatastoreContent
::new());
620 content
.snapshot_index
.insert(snapshot
.to_string(), file_number
);
622 self.last_entry
= Some((uuid
, file_number
));
630 media_set_label
: Option
<&MediaSetLabel
>,
631 ) -> Result
<bool
, Error
> {
633 let mut file
= BufReader
::new(file
);
634 let mut found_magic_number
= false;
637 let pos
= file
.seek(SeekFrom
::Current(0))?
;
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
),
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");
650 if magic
!= Self::PROXMOX_BACKUP_MEDIA_CATALOG_MAGIC_1_1
{
651 bail
!("wrong magic number");
653 found_magic_number
= true;
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
),
664 match entry_type
[0] {
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
),
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
),
675 content
.chunk_index
.insert(digest
, file_number
);
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;
685 let store
= file
.read_exact_allocated(store_name_len
)?
;
686 let store
= std
::str::from_utf8(&store
)?
;
688 self.check_start_chunk_archive(file_number
)?
;
690 self.content
.entry(store
.to_string())
691 .or_insert(DatastoreContent
::new());
693 self.current_archive
= Some((uuid
, file_number
, store
.to_string()));
696 let entry
: ChunkArchiveEnd
= unsafe { file.read_le_value()? }
;
697 let file_number
= entry
.file_number
;
698 let uuid
= Uuid
::from(entry
.uuid
);
700 self.check_end_chunk_archive(&uuid
, file_number
)?
;
702 self.current_archive
= None
;
703 self.last_entry
= Some((uuid
, file_number
));
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
);
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");
717 let store
= std
::str::from_utf8(&store
[..store_name_len
])?
;
719 let snapshot
= file
.read_exact_allocated(name_len
)?
;
720 let snapshot
= std
::str::from_utf8(&snapshot
)?
;
722 self.check_register_snapshot(file_number
, snapshot
)?
;
724 let content
= self.content
.entry(store
.to_string())
725 .or_insert(DatastoreContent
::new());
727 content
.snapshot_index
.insert(snapshot
.to_string(), file_number
);
729 self.last_entry
= Some((uuid
, file_number
));
732 let entry
: LabelEntry
= unsafe { file.read_le_value()? }
;
733 let file_number
= entry
.file_number
;
734 let uuid
= Uuid
::from(entry
.uuid
);
736 self.check_register_label(file_number
, &uuid
)?
;
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");
743 if set
.seq_nr
!= entry
.seq_nr
{
744 bail
!("got unexpected media set sequence number");
749 self.last_entry
= Some((uuid
, file_number
));
752 bail
!("unknown entry type '{}'", entry_type
[0]);
758 Ok(found_magic_number
)
762 /// Media set catalog
764 /// Catalog for multiple media.
765 pub struct MediaSetCatalog
{
766 catalog_list
: HashMap
<Uuid
, MediaCatalog
>,
769 impl MediaSetCatalog
{
771 /// Creates a new instance
772 pub fn new() -> Self {
774 catalog_list
: HashMap
::new(),
779 pub fn append_catalog(&mut self, catalog
: MediaCatalog
) -> Result
<(), Error
> {
781 if self.catalog_list
.get(&catalog
.uuid
).is_some() {
782 bail
!("MediaSetCatalog already contains media '{}'", catalog
.uuid
);
785 self.catalog_list
.insert(catalog
.uuid
.clone(), catalog
);
791 pub fn remove_catalog(&mut self, media_uuid
: &Uuid
) {
792 self.catalog_list
.remove(media_uuid
);
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
) {
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
) {
816 // Type definitions for internal binary catalog encoding
823 seq_nr
: u64, // only used for media set labels
828 struct ChunkArchiveStart
{
832 /* datastore name follows */
837 struct ChunkArchiveEnd
{
844 struct SnapshotEntry
{
849 /* datastore name, ':', snapshot name follows */