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