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
::HashMap
;
8 use anyhow
::{bail, format_err, Error}
;
9 use endian_trait
::Endian
;
31 // openssl::sha::sha256(b"Proxmox Backup Media Catalog v1.0")[0..8]
32 pub const PROXMOX_BACKUP_MEDIA_CATALOG_MAGIC_1_0
: [u8; 8] = [221, 29, 164, 1, 59, 69, 19, 40];
36 /// Stores what chunks and snapshots are stored on a specific media,
37 /// including the file position.
39 /// We use a simple binary format to store data on disk.
40 pub struct MediaCatalog
{
42 uuid
: Uuid
, // BackupMedia uuid
46 pub log_to_stdout
: bool
,
48 current_archive
: Option
<(Uuid
, u64)>,
50 last_entry
: Option
<(Uuid
, u64)>,
52 chunk_index
: HashMap
<[u8;32], u64>,
54 snapshot_index
: HashMap
<String
, u64>,
61 /// Test if a catalog exists
62 pub fn exists(base_path
: &Path
, uuid
: &Uuid
) -> bool
{
63 let mut path
= base_path
.to_owned();
64 path
.push(uuid
.to_string());
65 path
.set_extension("log");
69 /// Destroy the media catalog (remove all files)
70 pub fn destroy(base_path
: &Path
, uuid
: &Uuid
) -> Result
<(), Error
> {
72 let mut path
= base_path
.to_owned();
73 path
.push(uuid
.to_string());
74 path
.set_extension("log");
76 match std
::fs
::remove_file(path
) {
78 Err(err
) if err
.kind() == std
::io
::ErrorKind
::NotFound
=> Ok(()),
79 Err(err
) => Err(err
.into()),
83 fn create_basedir(base_path
: &Path
) -> Result
<(), Error
> {
84 let backup_user
= crate::backup
::backup_user()?
;
85 let mode
= nix
::sys
::stat
::Mode
::from_bits_truncate(0o0640);
86 let opts
= CreateOptions
::new()
88 .owner(backup_user
.uid
)
89 .group(backup_user
.gid
);
91 create_path(base_path
, None
, Some(opts
))
92 .map_err(|err
: Error
| format_err
!("unable to create media catalog dir - {}", err
))?
;
96 /// Open a catalog database, load into memory
102 ) -> Result
<Self, Error
> {
104 let mut path
= base_path
.to_owned();
105 path
.push(uuid
.to_string());
106 path
.set_extension("log");
108 let me
= proxmox
::try_block
!({
110 Self::create_basedir(base_path
)?
;
112 let mut file
= std
::fs
::OpenOptions
::new()
118 let backup_user
= crate::backup
::backup_user()?
;
119 fchown(file
.as_raw_fd(), Some(backup_user
.uid
), Some(backup_user
.gid
))
120 .map_err(|err
| format_err
!("fchown failed - {}", err
))?
;
125 log_to_stdout
: false,
126 current_archive
: None
,
128 chunk_index
: HashMap
::new(),
129 snapshot_index
: HashMap
::new(),
133 let found_magic_number
= me
.load_catalog(&mut file
)?
;
135 if !found_magic_number
{
136 me
.pending
.extend(&PROXMOX_BACKUP_MEDIA_CATALOG_MAGIC_1_0
);
140 me
.file
= Some(file
);
143 }).map_err(|err
: Error
| {
144 format_err
!("unable to open media catalog {:?} - {}", path
, err
)
150 /// Creates a temporary, empty catalog database
151 pub fn create_temporary_database(
155 ) -> Result
<Self, Error
> {
157 let uuid
= &media_id
.label
.uuid
;
159 let mut tmp_path
= base_path
.to_owned();
160 tmp_path
.push(uuid
.to_string());
161 tmp_path
.set_extension("tmp");
163 let me
= proxmox
::try_block
!({
165 Self::create_basedir(base_path
)?
;
167 let file
= std
::fs
::OpenOptions
::new()
174 let backup_user
= crate::backup
::backup_user()?
;
175 fchown(file
.as_raw_fd(), Some(backup_user
.uid
), Some(backup_user
.gid
))
176 .map_err(|err
| format_err
!("fchown failed - {}", err
))?
;
181 log_to_stdout
: false,
182 current_archive
: None
,
184 chunk_index
: HashMap
::new(),
185 snapshot_index
: HashMap
::new(),
189 me
.log_to_stdout
= log_to_stdout
;
191 me
.register_label(&media_id
.label
.uuid
, 0)?
;
193 if let Some(ref set
) = media_id
.media_set_label
{
194 me
.register_label(&set
.uuid
, 1)?
;
197 me
.pending
.extend(&PROXMOX_BACKUP_MEDIA_CATALOG_MAGIC_1_0
);
201 }).map_err(|err
: Error
| {
202 format_err
!("unable to create temporary media catalog {:?} - {}", tmp_path
, err
)
208 /// Commit or Abort a temporary catalog database
209 pub fn finish_temporary_database(
213 ) -> Result
<(), Error
> {
215 let mut tmp_path
= base_path
.to_owned();
216 tmp_path
.push(uuid
.to_string());
217 tmp_path
.set_extension("tmp");
220 let mut catalog_path
= tmp_path
.clone();
221 catalog_path
.set_extension("log");
223 if let Err(err
) = std
::fs
::rename(&tmp_path
, &catalog_path
) {
224 bail
!("Atomic rename catalog {:?} failed - {}", catalog_path
, err
);
227 std
::fs
::remove_file(&tmp_path
)?
;
232 /// Returns the BackupMedia uuid
233 pub fn uuid(&self) -> &Uuid
{
237 /// Accessor to content list
238 pub fn snapshot_index(&self) -> &HashMap
<String
, u64> {
242 /// Commit pending changes
244 /// This is necessary to store changes persistently.
246 /// Fixme: this should be atomic ...
247 pub fn commit(&mut self) -> Result
<(), Error
> {
249 if self.pending
.is_empty() {
254 Some(ref mut file
) => {
255 file
.write_all(&self.pending
)?
;
259 None
=> bail
!("media catalog not writable (opened read only)"),
262 self.pending
= Vec
::new();
267 /// Conditionally commit if in pending data is large (> 1Mb)
268 pub fn commit_if_large(&mut self) -> Result
<(), Error
> {
269 if self.pending
.len() > 1024*1024 {
275 /// Destroy existing catalog, opens a new one
280 ) -> Result
<Self, Error
> {
282 let uuid
= &media_id
.label
.uuid
;
284 let me
= Self::create_temporary_database(base_path
, &media_id
, log_to_stdout
)?
;
286 Self::finish_temporary_database(base_path
, uuid
, true)?
;
291 /// Test if the catalog already contain a snapshot
292 pub fn contains_snapshot(&self, snapshot
: &str) -> bool
{
293 self.snapshot_index
.contains_key(snapshot
)
296 /// Returns the chunk archive file number
297 pub fn lookup_snapshot(&self, snapshot
: &str) -> Option
<u64> {
298 self.snapshot_index
.get(snapshot
).map(|n
| *n
)
301 /// Test if the catalog already contain a chunk
302 pub fn contains_chunk(&self, digest
: &[u8;32]) -> bool
{
303 self.chunk_index
.contains_key(digest
)
306 /// Returns the chunk archive file number
307 pub fn lookup_chunk(&self, digest
: &[u8;32]) -> Option
<u64> {
308 self.chunk_index
.get(digest
).map(|n
| *n
)
311 fn check_register_label(&self, file_number
: u64) -> Result
<(), Error
> {
313 if file_number
>= 2 {
314 bail
!("register label failed: got wrong file number ({} >= 2)", file_number
);
317 if self.current_archive
.is_some() {
318 bail
!("register label failed: inside chunk archive");
321 let expected_file_number
= match self.last_entry
{
322 Some((_
, last_number
)) => last_number
+ 1,
326 if file_number
!= expected_file_number
{
327 bail
!("register label failed: got unexpected file number ({} < {})",
328 file_number
, expected_file_number
);
333 /// Register media labels (file 0 and 1)
334 pub fn register_label(
336 uuid
: &Uuid
, // Uuid form MediaContentHeader
338 ) -> Result
<(), Error
> {
340 self.check_register_label(file_number
)?
;
342 let entry
= LabelEntry
{
344 uuid
: *uuid
.as_bytes(),
347 if self.log_to_stdout
{
348 println
!("L|{}|{}", file_number
, uuid
.to_string());
351 self.pending
.push(b'L'
);
353 unsafe { self.pending.write_le_value(entry)?; }
355 self.last_entry
= Some((uuid
.clone(), file_number
));
362 /// Only valid after start_chunk_archive.
363 pub fn register_chunk(
366 ) -> Result
<(), Error
> {
368 let file_number
= match self.current_archive
{
369 None
=> bail
!("register_chunk failed: no archive started"),
370 Some((_
, file_number
)) => file_number
,
373 if self.log_to_stdout
{
374 println
!("C|{}", proxmox
::tools
::digest_to_hex(digest
));
377 self.pending
.push(b'C'
);
378 self.pending
.extend(digest
);
380 self.chunk_index
.insert(*digest
, file_number
);
385 fn check_start_chunk_archive(&self, file_number
: u64) -> Result
<(), Error
> {
387 if self.current_archive
.is_some() {
388 bail
!("start_chunk_archive failed: already started");
392 bail
!("start_chunk_archive failed: got wrong file number ({} < 2)", file_number
);
395 let expect_min_file_number
= match self.last_entry
{
396 Some((_
, last_number
)) => last_number
+ 1,
400 if file_number
< expect_min_file_number
{
401 bail
!("start_chunk_archive: got unexpected file number ({} < {})",
402 file_number
, expect_min_file_number
);
408 /// Start a chunk archive section
409 pub fn start_chunk_archive(
411 uuid
: Uuid
, // Uuid form MediaContentHeader
413 ) -> Result
<(), Error
> {
415 self.check_start_chunk_archive(file_number
)?
;
417 let entry
= ChunkArchiveStart
{
419 uuid
: *uuid
.as_bytes(),
422 if self.log_to_stdout
{
423 println
!("A|{}|{}", file_number
, uuid
.to_string());
426 self.pending
.push(b'A'
);
428 unsafe { self.pending.write_le_value(entry)?; }
430 self.current_archive
= Some((uuid
, file_number
));
435 fn check_end_chunk_archive(&self, uuid
: &Uuid
, file_number
: u64) -> Result
<(), Error
> {
437 match self.current_archive
{
438 None
=> bail
!("end_chunk archive failed: not started"),
439 Some((ref expected_uuid
, expected_file_number
)) => {
440 if uuid
!= expected_uuid
{
441 bail
!("end_chunk_archive failed: got unexpected uuid");
443 if file_number
!= expected_file_number
{
444 bail
!("end_chunk_archive failed: got unexpected file number ({} != {})",
445 file_number
, expected_file_number
);
453 /// End a chunk archive section
454 pub fn end_chunk_archive(&mut self) -> Result
<(), Error
> {
456 match self.current_archive
.take() {
457 None
=> bail
!("end_chunk_archive failed: not started"),
458 Some((uuid
, file_number
)) => {
460 let entry
= ChunkArchiveEnd
{
462 uuid
: *uuid
.as_bytes(),
465 if self.log_to_stdout
{
466 println
!("E|{}|{}\n", file_number
, uuid
.to_string());
469 self.pending
.push(b'E'
);
471 unsafe { self.pending.write_le_value(entry)?; }
473 self.last_entry
= Some((uuid
, file_number
));
480 fn check_register_snapshot(&self, file_number
: u64, snapshot
: &str) -> Result
<(), Error
> {
482 if self.current_archive
.is_some() {
483 bail
!("register_snapshot failed: inside chunk_archive");
487 bail
!("register_snapshot failed: got wrong file number ({} < 2)", file_number
);
490 let expect_min_file_number
= match self.last_entry
{
491 Some((_
, last_number
)) => last_number
+ 1,
495 if file_number
< expect_min_file_number
{
496 bail
!("register_snapshot failed: got unexpected file number ({} < {})",
497 file_number
, expect_min_file_number
);
500 if let Err(err
) = snapshot
.parse
::<BackupDir
>() {
501 bail
!("register_snapshot failed: unable to parse snapshot '{}' - {}", snapshot
, err
);
507 /// Register a snapshot
508 pub fn register_snapshot(
510 uuid
: Uuid
, // Uuid form MediaContentHeader
513 ) -> Result
<(), Error
> {
515 self.check_register_snapshot(file_number
, snapshot
)?
;
517 let entry
= SnapshotEntry
{
519 uuid
: *uuid
.as_bytes(),
520 name_len
: u16::try_from(snapshot
.len())?
,
523 if self.log_to_stdout
{
524 println
!("S|{}|{}|{}", file_number
, uuid
.to_string(), snapshot
);
527 self.pending
.push(b'S'
);
529 unsafe { self.pending.write_le_value(entry)?; }
530 self.pending
.extend(snapshot
.as_bytes());
532 self.snapshot_index
.insert(snapshot
.to_string(), file_number
);
534 self.last_entry
= Some((uuid
, file_number
));
539 fn load_catalog(&mut self, file
: &mut File
) -> Result
<bool
, Error
> {
541 let mut file
= BufReader
::new(file
);
542 let mut found_magic_number
= false;
545 let pos
= file
.seek(SeekFrom
::Current(0))?
;
547 if pos
== 0 { // read/check magic number
548 let mut magic
= [0u8; 8];
549 match file
.read_exact_or_eof(&mut magic
) {
550 Ok(false) => { /* EOF */ break; }
551 Ok(true) => { /* OK */ }
552 Err(err
) => bail
!("read failed - {}", err
),
554 if magic
!= PROXMOX_BACKUP_MEDIA_CATALOG_MAGIC_1_0
{
555 bail
!("wrong magic number");
557 found_magic_number
= true;
561 let mut entry_type
= [0u8; 1];
562 match file
.read_exact_or_eof(&mut entry_type
) {
563 Ok(false) => { /* EOF */ break; }
564 Ok(true) => { /* OK */ }
565 Err(err
) => bail
!("read failed - {}", err
),
568 match entry_type
[0] {
570 let file_number
= match self.current_archive
{
571 None
=> bail
!("register_chunk failed: no archive started"),
572 Some((_
, file_number
)) => file_number
,
574 let mut digest
= [0u8; 32];
575 file
.read_exact(&mut digest
)?
;
576 self.chunk_index
.insert(digest
, file_number
);
579 let entry
: ChunkArchiveStart
= unsafe { file.read_le_value()? }
;
580 let file_number
= entry
.file_number
;
581 let uuid
= Uuid
::from(entry
.uuid
);
583 self.check_start_chunk_archive(file_number
)?
;
585 self.current_archive
= Some((uuid
, file_number
));
588 let entry
: ChunkArchiveEnd
= unsafe { file.read_le_value()? }
;
589 let file_number
= entry
.file_number
;
590 let uuid
= Uuid
::from(entry
.uuid
);
592 self.check_end_chunk_archive(&uuid
, file_number
)?
;
594 self.current_archive
= None
;
595 self.last_entry
= Some((uuid
, file_number
));
598 let entry
: SnapshotEntry
= unsafe { file.read_le_value()? }
;
599 let file_number
= entry
.file_number
;
600 let name_len
= entry
.name_len
;
601 let uuid
= Uuid
::from(entry
.uuid
);
603 let snapshot
= file
.read_exact_allocated(name_len
.into())?
;
604 let snapshot
= std
::str::from_utf8(&snapshot
)?
;
606 self.check_register_snapshot(file_number
, snapshot
)?
;
608 self.snapshot_index
.insert(snapshot
.to_string(), file_number
);
610 self.last_entry
= Some((uuid
, file_number
));
613 let entry
: LabelEntry
= unsafe { file.read_le_value()? }
;
614 let file_number
= entry
.file_number
;
615 let uuid
= Uuid
::from(entry
.uuid
);
617 self.check_register_label(file_number
)?
;
619 self.last_entry
= Some((uuid
, file_number
));
622 bail
!("unknown entry type '{}'", entry_type
[0]);
628 Ok(found_magic_number
)
632 /// Media set catalog
634 /// Catalog for multiple media.
635 pub struct MediaSetCatalog
{
636 catalog_list
: HashMap
<Uuid
, MediaCatalog
>,
639 impl MediaSetCatalog
{
641 /// Creates a new instance
642 pub fn new() -> Self {
644 catalog_list
: HashMap
::new(),
649 pub fn append_catalog(&mut self, catalog
: MediaCatalog
) -> Result
<(), Error
> {
651 if self.catalog_list
.get(&catalog
.uuid
).is_some() {
652 bail
!("MediaSetCatalog already contains media '{}'", catalog
.uuid
);
655 self.catalog_list
.insert(catalog
.uuid
.clone(), catalog
);
661 pub fn remove_catalog(&mut self, media_uuid
: &Uuid
) {
662 self.catalog_list
.remove(media_uuid
);
665 /// Test if the catalog already contain a snapshot
666 pub fn contains_snapshot(&self, snapshot
: &str) -> bool
{
667 for catalog
in self.catalog_list
.values() {
668 if catalog
.contains_snapshot(snapshot
) {
675 /// Test if the catalog already contain a chunk
676 pub fn contains_chunk(&self, digest
: &[u8;32]) -> bool
{
677 for catalog
in self.catalog_list
.values() {
678 if catalog
.contains_chunk(digest
) {
686 // Type definitions for internal binary catalog encoding
697 struct ChunkArchiveStart
{
704 struct ChunkArchiveEnd
{
711 struct SnapshotEntry
{
715 /* snapshot name follows */