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