1 use std
::collections
::HashSet
;
4 use anyhow
::{bail, Error}
;
8 api
::section_config
::SectionConfigData
,
17 MAX_CHUNK_ARCHIVE_SIZE
,
23 SnapshotChunkIterator
,
28 tape_write_snapshot_archive
,
29 request_and_load_media
,
34 struct PoolWriterState
{
35 drive
: Box
<dyn TapeDriver
>,
36 catalog
: MediaCatalog
,
37 // tell if we already moved to EOM
39 // bytes written after the last tape fush/sync
43 impl PoolWriterState
{
45 fn commit(&mut self) -> Result
<(), Error
> {
46 self.drive
.sync()?
; // sync all data to the tape
47 self.catalog
.commit()?
; // then commit the catalog
48 self.bytes_written
= 0;
53 /// Helper to manage a backup job, writing several tapes of a pool
54 pub struct PoolWriter
{
57 status
: Option
<PoolWriterState
>,
58 media_set_catalog
: MediaSetCatalog
,
63 pub fn new(mut pool
: MediaPool
, drive_name
: &str) -> Result
<Self, Error
> {
65 let current_time
= proxmox
::tools
::time
::epoch_i64();
67 pool
.start_write_session(current_time
)?
;
69 let mut media_set_catalog
= MediaSetCatalog
::new();
71 // load all catalogs read-only at start
72 for media_uuid
in pool
.current_media_list()?
{
73 let media_catalog
= MediaCatalog
::open(
74 Path
::new(TAPE_STATUS_DIR
),
79 media_set_catalog
.append_catalog(media_catalog
)?
;
84 drive_name
: drive_name
.to_string(),
90 pub fn pool(&mut self) -> &mut MediaPool
{
94 /// Set media status to FULL (persistent - stores pool status)
95 pub fn set_media_status_full(&mut self, uuid
: &Uuid
) -> Result
<(), Error
> {
96 self.pool
.set_media_status_full(&uuid
)?
;
100 pub fn contains_snapshot(&self, snapshot
: &str) -> bool
{
101 if let Some(PoolWriterState { ref catalog, .. }
) = self.status
{
102 if catalog
.contains_snapshot(snapshot
) {
106 self.media_set_catalog
.contains_snapshot(snapshot
)
109 /// commit changes to tape and catalog
111 /// This is done automatically during a backupsession, but needs to
112 /// be called explicitly before dropping the PoolWriter
113 pub fn commit(&mut self) -> Result
<(), Error
> {
114 if let Some(ref mut status
) = self.status
{
120 /// Load a writable media into the drive
121 pub fn load_writable_media(&mut self) -> Result
<Uuid
, Error
> {
122 let last_media_uuid
= match self.status
{
123 Some(PoolWriterState { ref catalog, .. }
) => Some(catalog
.uuid().clone()),
127 let current_time
= proxmox
::tools
::time
::epoch_i64();
128 let media_uuid
= self.pool
.alloc_writable_media(current_time
)?
;
130 let media
= self.pool
.lookup_media(&media_uuid
).unwrap();
132 let media_changed
= match last_media_uuid
{
133 Some(ref last_media_uuid
) => last_media_uuid
!= &media_uuid
,
138 return Ok(media_uuid
);
141 // remove read-only catalog (we store a writable version in status)
142 self.media_set_catalog
.remove_catalog(&media_uuid
);
144 if let Some(PoolWriterState {mut drive, catalog, .. }
) = self.status
.take() {
145 self.media_set_catalog
.append_catalog(catalog
)?
;
146 drive
.eject_media()?
;
149 let (drive_config
, _digest
) = crate::config
::drive
::config()?
;
150 let (drive
, catalog
) = drive_load_and_label_media(&drive_config
, &self.drive_name
, &media
.id())?
;
151 self.status
= Some(PoolWriterState { drive, catalog, at_eom: false, bytes_written: 0 }
);
156 /// uuid of currently loaded BackupMedia
157 pub fn current_media_uuid(&self) -> Result
<&Uuid
, Error
> {
159 Some(PoolWriterState { ref catalog, ..}
) => Ok(catalog
.uuid()),
160 None
=> bail
!("PoolWriter - no media loaded"),
164 /// Move to EOM (if not aleady there), then creates a new snapshot
165 /// archive writing specified files (as .pxar) into it. On
166 /// success, this return 'Ok(true)' and the media catalog gets
169 /// Please note that this may fail when there is not enough space
170 /// on the media (return value 'Ok(false, _)'). In that case, the
171 /// archive is marked incomplete, and we do not use it. The caller
172 /// should mark the media as full and try again using another
174 pub fn append_snapshot_archive(
176 snapshot_reader
: &SnapshotReader
,
177 ) -> Result
<(bool
, usize), Error
> {
179 let status
= match self.status
{
180 Some(ref mut status
) => status
,
181 None
=> bail
!("PoolWriter - no media loaded"),
185 status
.drive
.move_to_eom()?
;
186 status
.at_eom
= true;
189 let current_file_number
= status
.drive
.current_file_number()?
;
190 if current_file_number
< 2 {
191 bail
!("got strange file position number from drive ({})", current_file_number
);
194 let (done
, bytes_written
) = {
195 let mut writer
: Box
<dyn TapeWrite
> = status
.drive
.write_file()?
;
197 match tape_write_snapshot_archive(writer
.as_mut(), snapshot_reader
)?
{
198 Some(content_uuid
) => {
199 status
.catalog
.register_snapshot(
202 &snapshot_reader
.snapshot().to_string(),
204 (true, writer
.bytes_written())
206 None
=> (false, writer
.bytes_written()),
210 status
.bytes_written
+= bytes_written
;
212 let request_sync
= if status
.bytes_written
>= COMMIT_BLOCK_SIZE { true }
else { false }
;
214 if !done
|| request_sync
{
218 Ok((done
, bytes_written
))
221 /// Move to EOM (if not aleady there), then creates a new chunk
222 /// archive and writes chunks from 'chunk_iter'. This stops when
223 /// it detect LEOM or when we reach max archive size
224 /// (4GB). Written chunks are registered in the media catalog.
225 pub fn append_chunk_archive(
227 datastore
: &DataStore
,
228 chunk_iter
: &mut std
::iter
::Peekable
<SnapshotChunkIterator
>,
229 ) -> Result
<(bool
, usize), Error
> {
231 let status
= match self.status
{
232 Some(ref mut status
) => status
,
233 None
=> bail
!("PoolWriter - no media loaded"),
237 status
.drive
.move_to_eom()?
;
238 status
.at_eom
= true;
241 let current_file_number
= status
.drive
.current_file_number()?
;
242 if current_file_number
< 2 {
243 bail
!("got strange file position number from drive ({})", current_file_number
);
245 let writer
= status
.drive
.write_file()?
;
247 let (saved_chunks
, content_uuid
, leom
, bytes_written
) = write_chunk_archive(
251 &self.media_set_catalog
,
253 MAX_CHUNK_ARCHIVE_SIZE
,
256 status
.bytes_written
+= bytes_written
;
258 let request_sync
= if status
.bytes_written
>= COMMIT_BLOCK_SIZE { true }
else { false }
;
260 // register chunks in media_catalog
261 status
.catalog
.start_chunk_archive(content_uuid
, current_file_number
)?
;
262 for digest
in saved_chunks
{
263 status
.catalog
.register_chunk(&digest
)?
;
265 status
.catalog
.end_chunk_archive()?
;
267 if leom
|| request_sync
{
271 Ok((leom
, bytes_written
))
275 /// write up to <max_size> of chunks
276 fn write_chunk_archive
<'a
>(
277 writer
: Box
<dyn 'a
+ TapeWrite
>,
278 datastore
: &DataStore
,
279 chunk_iter
: &mut std
::iter
::Peekable
<SnapshotChunkIterator
>,
280 media_set_catalog
: &MediaSetCatalog
,
281 media_catalog
: &MediaCatalog
,
283 ) -> Result
<(Vec
<[u8;32]>, Uuid
, bool
, usize), Error
> {
285 let (mut writer
, content_uuid
) = ChunkArchiveWriter
::new(writer
, true)?
;
287 let mut chunk_index
: HashSet
<[u8;32]> = HashSet
::new();
289 // we want to get the chunk list in correct order
290 let mut chunk_list
: Vec
<[u8;32]> = Vec
::new();
292 let mut leom
= false;
295 let digest
= match chunk_iter
.next() {
297 Some(digest
) => digest?
,
299 if media_catalog
.contains_chunk(&digest
)
300 || chunk_index
.contains(&digest
)
301 || media_set_catalog
.contains_chunk(&digest
)
306 let blob
= datastore
.load_chunk(&digest
)?
;
307 println
!("CHUNK {} size {}", proxmox
::tools
::digest_to_hex(&digest
), blob
.raw_size());
309 match writer
.try_write_chunk(&digest
, &blob
) {
311 chunk_index
.insert(digest
);
312 chunk_list
.push(digest
);
318 Err(err
) => bail
!("write chunk failed - {}", err
),
321 if writer
.bytes_written() > max_size
{
322 println
!("Chunk Archive max size reached, closing archive");
329 Ok((chunk_list
, content_uuid
, leom
, writer
.bytes_written()))
332 // Requests and load 'media' into the drive. Then compare the media
333 // set label. If the tabe is empty, or the existing set label does not
334 // match the expected media set, overwrite the media set label.
335 fn drive_load_and_label_media(
336 drive_config
: &SectionConfigData
,
339 ) -> Result
<(Box
<dyn TapeDriver
>, MediaCatalog
), Error
> {
341 let (mut tmp_drive
, info
) =
342 request_and_load_media(&drive_config
, &drive_name
, &media_id
.label
)?
;
346 let new_set
= match media_id
.media_set_label
{
348 bail
!("got media without media set - internal error");
350 Some(ref set
) => set
,
353 let status_path
= Path
::new(TAPE_STATUS_DIR
);
355 match &info
.media_set_label
{
357 println
!("wrinting new media set label");
358 tmp_drive
.write_media_set_label(new_set
)?
;
362 media_set_label
: Some(new_set
.clone()),
364 media_catalog
= MediaCatalog
::overwrite(status_path
, &info
, true)?
;
366 Some(media_set_label
) => {
367 if new_set
.uuid
== media_set_label
.uuid
{
368 if new_set
.seq_nr
!= media_set_label
.seq_nr
{
369 bail
!("got media with wrong media sequence number ({} != {}",
370 new_set
.seq_nr
,media_set_label
.seq_nr
);
372 media_catalog
= MediaCatalog
::open(status_path
, &media_id
.label
.uuid
, true, false)?
;
374 println
!("wrinting new media set label (overwrite '{}/{}')",
375 media_set_label
.uuid
.to_string(), media_set_label
.seq_nr
);
377 tmp_drive
.write_media_set_label(new_set
)?
;
381 media_set_label
: Some(new_set
.clone()),
383 media_catalog
= MediaCatalog
::overwrite(status_path
, &info
, true)?
;
388 // todo: verify last content/media_catalog somehow?
389 tmp_drive
.move_to_eom()?
;
391 Ok((tmp_drive
, media_catalog
))