]>
Commit | Line | Data |
---|---|---|
205e1876 DM |
1 | use std::convert::TryFrom; |
2 | use std::fs::File; | |
3 | use std::io::{Write, Read, BufReader}; | |
4 | use std::path::Path; | |
5 | use std::collections::HashMap; | |
6 | ||
7 | use anyhow::{bail, format_err, Error}; | |
8 | use endian_trait::Endian; | |
9 | ||
10 | use proxmox::tools::{ | |
11 | Uuid, | |
12 | fs::{ | |
13 | fchown, | |
14 | create_path, | |
15 | CreateOptions, | |
16 | }, | |
17 | io::{ | |
18 | WriteExt, | |
19 | ReadExt, | |
20 | }, | |
21 | }; | |
22 | ||
23 | use 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. | |
34 | pub 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 | ||
53 | impl 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. | |
607 | pub struct MediaSetCatalog { | |
608 | catalog_list: HashMap<Uuid, MediaCatalog>, | |
609 | } | |
610 | ||
611 | impl 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)] | |
662 | pub struct LabelEntry { | |
663 | file_number: u64, | |
664 | uuid: [u8;16], | |
665 | } | |
666 | ||
667 | #[derive(Endian)] | |
668 | #[repr(C)] | |
669 | pub struct ChunkArchiveStart { | |
670 | file_number: u64, | |
671 | uuid: [u8;16], | |
672 | } | |
673 | ||
674 | #[derive(Endian)] | |
675 | #[repr(C)] | |
676 | pub struct ChunkArchiveEnd{ | |
677 | file_number: u64, | |
678 | uuid: [u8;16], | |
679 | } | |
680 | ||
681 | #[derive(Endian)] | |
682 | #[repr(C)] | |
683 | pub struct SnapshotEntry{ | |
684 | file_number: u64, | |
685 | uuid: [u8;16], | |
686 | name_len: u16, | |
687 | /* snapshot name follows */ | |
688 | } |