tape: implement encrypted backup - simple version
This is just a proof of concept, only storing the encryption key fingerprint inside the media-set label.
This commit is contained in:
parent
84cbdb35c4
commit
8a0046f519
@ -406,7 +406,7 @@ fn write_media_label(
|
|||||||
if let Some(ref pool) = pool {
|
if let Some(ref pool) = pool {
|
||||||
// assign media to pool by writing special media set label
|
// assign media to pool by writing special media set label
|
||||||
worker.log(format!("Label media '{}' for pool '{}'", label.label_text, pool));
|
worker.log(format!("Label media '{}' for pool '{}'", label.label_text, pool));
|
||||||
let set = MediaSetLabel::with_data(&pool, [0u8; 16].into(), 0, label.ctime);
|
let set = MediaSetLabel::with_data(&pool, [0u8; 16].into(), 0, label.ctime, None);
|
||||||
|
|
||||||
drive.write_media_set_label(&set)?;
|
drive.write_media_set_label(&set)?;
|
||||||
media_set_label = Some(set);
|
media_set_label = Some(set);
|
||||||
@ -493,6 +493,7 @@ pub async fn read_label(
|
|||||||
ctime: media_id.label.ctime,
|
ctime: media_id.label.ctime,
|
||||||
media_set_ctime: None,
|
media_set_ctime: None,
|
||||||
media_set_uuid: None,
|
media_set_uuid: None,
|
||||||
|
encryption_key_fingerprint: None,
|
||||||
pool: None,
|
pool: None,
|
||||||
seq_nr: None,
|
seq_nr: None,
|
||||||
};
|
};
|
||||||
@ -501,6 +502,10 @@ pub async fn read_label(
|
|||||||
flat.seq_nr = Some(set.seq_nr);
|
flat.seq_nr = Some(set.seq_nr);
|
||||||
flat.media_set_uuid = Some(set.uuid.to_string());
|
flat.media_set_uuid = Some(set.uuid.to_string());
|
||||||
flat.media_set_ctime = Some(set.ctime);
|
flat.media_set_ctime = Some(set.ctime);
|
||||||
|
flat.encryption_key_fingerprint = set
|
||||||
|
.encryption_key_fingerprint
|
||||||
|
.as_ref()
|
||||||
|
.map(|fp| crate::tools::format::as_fingerprint(fp.bytes()));
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(true) = inventorize {
|
if let Some(true) = inventorize {
|
||||||
@ -992,6 +997,9 @@ pub fn catalog_media(
|
|||||||
MediaCatalog::destroy(status_path, &media_id.label.uuid)?;
|
MediaCatalog::destroy(status_path, &media_id.label.uuid)?;
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
let encrypt_fingerprint = set.encryption_key_fingerprint.clone();
|
||||||
|
drive.set_encryption(encrypt_fingerprint)?;
|
||||||
|
|
||||||
set.pool.clone()
|
set.pool.clone()
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -74,6 +74,9 @@ pub struct MediaIdFlat {
|
|||||||
/// MediaSet Creation time stamp
|
/// MediaSet Creation time stamp
|
||||||
#[serde(skip_serializing_if="Option::is_none")]
|
#[serde(skip_serializing_if="Option::is_none")]
|
||||||
pub media_set_ctime: Option<i64>,
|
pub media_set_ctime: Option<i64>,
|
||||||
|
/// Encryption key fingerprint
|
||||||
|
#[serde(skip_serializing_if="Option::is_none")]
|
||||||
|
pub encryption_key_fingerprint: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[api()]
|
#[api()]
|
||||||
|
@ -422,6 +422,7 @@ async fn read_label(
|
|||||||
.column(ColumnConfig::new("pool"))
|
.column(ColumnConfig::new("pool"))
|
||||||
.column(ColumnConfig::new("media-set-uuid"))
|
.column(ColumnConfig::new("media-set-uuid"))
|
||||||
.column(ColumnConfig::new("media-set-ctime").renderer(render_epoch))
|
.column(ColumnConfig::new("media-set-ctime").renderer(render_epoch))
|
||||||
|
.column(ColumnConfig::new("encryption-key-fingerprint"))
|
||||||
;
|
;
|
||||||
|
|
||||||
format_and_print_result_full(&mut data, &info.returns, &output_format, &options);
|
format_and_print_result_full(&mut data, &info.returns, &output_format, &options);
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::os::unix::io::{AsRawFd, FromRawFd};
|
use std::os::unix::io::{AsRawFd, FromRawFd};
|
||||||
|
|
||||||
use anyhow::{bail, format_err, Error};
|
use anyhow::{bail, Error};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
use proxmox::{
|
use proxmox::{
|
||||||
|
@ -7,6 +7,8 @@ use bitflags::bitflags;
|
|||||||
|
|
||||||
use proxmox::tools::Uuid;
|
use proxmox::tools::Uuid;
|
||||||
|
|
||||||
|
use crate::backup::Fingerprint;
|
||||||
|
|
||||||
/// We use 256KB blocksize (always)
|
/// We use 256KB blocksize (always)
|
||||||
pub const PROXMOX_TAPE_BLOCK_SIZE: usize = 256*1024;
|
pub const PROXMOX_TAPE_BLOCK_SIZE: usize = 256*1024;
|
||||||
|
|
||||||
@ -185,16 +187,26 @@ pub struct MediaSetLabel {
|
|||||||
pub seq_nr: u64,
|
pub seq_nr: u64,
|
||||||
/// Creation time stamp
|
/// Creation time stamp
|
||||||
pub ctime: i64,
|
pub ctime: i64,
|
||||||
|
/// Encryption key finkerprint (if encryped)
|
||||||
|
#[serde(skip_serializing_if="Option::is_none")]
|
||||||
|
pub encryption_key_fingerprint: Option<Fingerprint>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MediaSetLabel {
|
impl MediaSetLabel {
|
||||||
|
|
||||||
pub fn with_data(pool: &str, uuid: Uuid, seq_nr: u64, ctime: i64) -> Self {
|
pub fn with_data(
|
||||||
|
pool: &str,
|
||||||
|
uuid: Uuid,
|
||||||
|
seq_nr: u64,
|
||||||
|
ctime: i64,
|
||||||
|
encryption_key_fingerprint: Option<Fingerprint>,
|
||||||
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
pool: pool.to_string(),
|
pool: pool.to_string(),
|
||||||
uuid,
|
uuid,
|
||||||
seq_nr,
|
seq_nr,
|
||||||
ctime,
|
ctime,
|
||||||
|
encryption_key_fingerprint,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -565,7 +565,7 @@ impl Inventory {
|
|||||||
|
|
||||||
let uuid = label.uuid.clone();
|
let uuid = label.uuid.clone();
|
||||||
|
|
||||||
let set = MediaSetLabel::with_data(pool, [0u8; 16].into(), 0, ctime);
|
let set = MediaSetLabel::with_data(pool, [0u8; 16].into(), 0, ctime, None);
|
||||||
|
|
||||||
self.store(MediaId { label, media_set_label: Some(set) }, false).unwrap();
|
self.store(MediaId { label, media_set_label: Some(set) }, false).unwrap();
|
||||||
|
|
||||||
|
@ -14,6 +14,7 @@ use ::serde::{Deserialize, Serialize};
|
|||||||
use proxmox::tools::Uuid;
|
use proxmox::tools::Uuid;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
backup::Fingerprint,
|
||||||
api2::types::{
|
api2::types::{
|
||||||
MediaStatus,
|
MediaStatus,
|
||||||
MediaLocation,
|
MediaLocation,
|
||||||
@ -44,6 +45,7 @@ pub struct MediaPool {
|
|||||||
media_set_policy: MediaSetPolicy,
|
media_set_policy: MediaSetPolicy,
|
||||||
retention: RetentionPolicy,
|
retention: RetentionPolicy,
|
||||||
use_offline_media: bool,
|
use_offline_media: bool,
|
||||||
|
encrypt_fingerprint: Option<Fingerprint>,
|
||||||
|
|
||||||
inventory: Inventory,
|
inventory: Inventory,
|
||||||
|
|
||||||
@ -59,6 +61,7 @@ impl MediaPool {
|
|||||||
media_set_policy: MediaSetPolicy,
|
media_set_policy: MediaSetPolicy,
|
||||||
retention: RetentionPolicy,
|
retention: RetentionPolicy,
|
||||||
use_offline_media: bool,
|
use_offline_media: bool,
|
||||||
|
encrypt_fingerprint: Option<Fingerprint>,
|
||||||
) -> Result<Self, Error> {
|
) -> Result<Self, Error> {
|
||||||
|
|
||||||
let inventory = Inventory::load(state_path)?;
|
let inventory = Inventory::load(state_path)?;
|
||||||
@ -75,6 +78,7 @@ impl MediaPool {
|
|||||||
use_offline_media,
|
use_offline_media,
|
||||||
inventory,
|
inventory,
|
||||||
current_media_set,
|
current_media_set,
|
||||||
|
encrypt_fingerprint,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -89,7 +93,19 @@ impl MediaPool {
|
|||||||
|
|
||||||
let retention = config.retention.clone().unwrap_or(String::from("keep")).parse()?;
|
let retention = config.retention.clone().unwrap_or(String::from("keep")).parse()?;
|
||||||
|
|
||||||
MediaPool::new(&config.name, state_path, allocation, retention, use_offline_media)
|
let encrypt_fingerprint = match config.encrypt {
|
||||||
|
Some(ref fingerprint) => Some(fingerprint.parse()?),
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
MediaPool::new(
|
||||||
|
&config.name,
|
||||||
|
state_path,
|
||||||
|
allocation,
|
||||||
|
retention,
|
||||||
|
use_offline_media,
|
||||||
|
encrypt_fingerprint,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the pool name
|
/// Returns the pool name
|
||||||
@ -97,6 +113,12 @@ impl MediaPool {
|
|||||||
&self.name
|
&self.name
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Retruns encryption settings
|
||||||
|
pub fn encrypt_fingerprint(&self) -> Option<Fingerprint> {
|
||||||
|
self.encrypt_fingerprint.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
fn compute_media_state(&self, media_id: &MediaId) -> (MediaStatus, MediaLocation) {
|
fn compute_media_state(&self, media_id: &MediaId) -> (MediaStatus, MediaLocation) {
|
||||||
|
|
||||||
let (status, location) = self.inventory.status_and_location(&media_id.label.uuid);
|
let (status, location) = self.inventory.status_and_location(&media_id.label.uuid);
|
||||||
@ -247,13 +269,49 @@ impl MediaPool {
|
|||||||
current_time > expire_time
|
current_time > expire_time
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// check if a location is considered on site
|
||||||
|
pub fn location_is_available(&self, location: &MediaLocation) -> bool {
|
||||||
|
match location {
|
||||||
|
MediaLocation::Online(_) => true,
|
||||||
|
MediaLocation::Offline => self.use_offline_media,
|
||||||
|
MediaLocation::Vault(_) => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_media_to_current_set(&mut self, mut media_id: MediaId, current_time: i64) -> Result<(), Error> {
|
||||||
|
|
||||||
|
let seq_nr = self.current_media_set.media_list().len() as u64;
|
||||||
|
|
||||||
|
let pool = self.name.clone();
|
||||||
|
|
||||||
|
let encrypt_fingerprint = self.encrypt_fingerprint();
|
||||||
|
|
||||||
|
let set = MediaSetLabel::with_data(
|
||||||
|
&pool,
|
||||||
|
self.current_media_set.uuid().clone(),
|
||||||
|
seq_nr,
|
||||||
|
current_time,
|
||||||
|
encrypt_fingerprint,
|
||||||
|
);
|
||||||
|
|
||||||
|
media_id.media_set_label = Some(set);
|
||||||
|
|
||||||
|
let uuid = media_id.label.uuid.clone();
|
||||||
|
|
||||||
|
let clear_media_status = true; // remove Full status
|
||||||
|
self.inventory.store(media_id, clear_media_status)?; // store persistently
|
||||||
|
|
||||||
|
self.current_media_set.add_media(uuid);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/// Allocates a writable media to the current media set
|
/// Allocates a writable media to the current media set
|
||||||
pub fn alloc_writable_media(&mut self, current_time: i64) -> Result<Uuid, Error> {
|
pub fn alloc_writable_media(&mut self, current_time: i64) -> Result<Uuid, Error> {
|
||||||
|
|
||||||
let last_is_writable = self.current_set_usable()?;
|
let last_is_writable = self.current_set_usable()?;
|
||||||
|
|
||||||
let pool = self.name.clone();
|
|
||||||
|
|
||||||
if last_is_writable {
|
if last_is_writable {
|
||||||
let last_uuid = self.current_media_set.last_media_uuid().unwrap();
|
let last_uuid = self.current_media_set.last_media_uuid().unwrap();
|
||||||
let media = self.lookup_media(last_uuid)?;
|
let media = self.lookup_media(last_uuid)?;
|
||||||
@ -262,88 +320,65 @@ impl MediaPool {
|
|||||||
|
|
||||||
// try to find empty media in pool, add to media set
|
// try to find empty media in pool, add to media set
|
||||||
|
|
||||||
let mut media_list = self.list_media();
|
let media_list = self.list_media();
|
||||||
|
|
||||||
let mut empty_media = Vec::new();
|
let mut empty_media = Vec::new();
|
||||||
for media in media_list.iter_mut() {
|
let mut used_media = Vec::new();
|
||||||
// already part of a media set?
|
|
||||||
if media.media_set_label().is_some() { continue; }
|
|
||||||
|
|
||||||
// check if media is on site
|
for media in media_list.into_iter() {
|
||||||
match media.location() {
|
if !self.location_is_available(media.location()) {
|
||||||
MediaLocation::Online(_) => { /* OK */ },
|
|
||||||
MediaLocation::Offline => {
|
|
||||||
if self.use_offline_media {
|
|
||||||
/* OK */
|
|
||||||
} else {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
},
|
// already part of a media set?
|
||||||
MediaLocation::Vault(_) => continue,
|
if media.media_set_label().is_some() {
|
||||||
}
|
used_media.push(media);
|
||||||
|
} else {
|
||||||
// only consider writable media
|
// only consider writable empty media
|
||||||
if media.status() != &MediaStatus::Writable { continue; }
|
if media.status() == &MediaStatus::Writable {
|
||||||
|
|
||||||
empty_media.push(media);
|
empty_media.push(media);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// sort empty_media, oldest media first
|
// sort empty_media, newest first -> oldest last
|
||||||
empty_media.sort_unstable_by_key(|media| media.label().ctime);
|
empty_media.sort_unstable_by(|a, b| b.label().ctime.cmp(&a.label().ctime));
|
||||||
|
|
||||||
if let Some(media) = empty_media.first_mut() {
|
if let Some(media) = empty_media.pop() {
|
||||||
// found empty media, add to media set an use it
|
// found empty media, add to media set an use it
|
||||||
let seq_nr = self.current_media_set.media_list().len() as u64;
|
let uuid = media.uuid().clone();
|
||||||
|
self.add_media_to_current_set(media.into_id(), current_time)?;
|
||||||
let set = MediaSetLabel::with_data(&pool, self.current_media_set.uuid().clone(), seq_nr, current_time);
|
return Ok(uuid);
|
||||||
|
|
||||||
media.set_media_set_label(set);
|
|
||||||
|
|
||||||
self.inventory.store(media.id().clone(), true)?; // store persistently
|
|
||||||
|
|
||||||
self.current_media_set.add_media(media.uuid().clone());
|
|
||||||
|
|
||||||
return Ok(media.uuid().clone());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
println!("no empty media in pool, try to reuse expired media");
|
println!("no empty media in pool, try to reuse expired media");
|
||||||
|
|
||||||
let mut expired_media = Vec::new();
|
let mut expired_media = Vec::new();
|
||||||
|
|
||||||
for media in media_list.into_iter() {
|
for media in used_media.into_iter() {
|
||||||
if let Some(set) = media.media_set_label() {
|
if let Some(set) = media.media_set_label() {
|
||||||
if &set.uuid == self.current_media_set.uuid() {
|
if &set.uuid == self.current_media_set.uuid() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.media_is_expired(&media, current_time) {
|
if self.media_is_expired(&media, current_time) {
|
||||||
println!("found expired media on media '{}'", media.label_text());
|
println!("found expired media on media '{}'", media.label_text());
|
||||||
expired_media.push(media);
|
expired_media.push(media);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// sort, oldest media first
|
// sort expired_media, newest first -> oldest last
|
||||||
expired_media.sort_unstable_by_key(|media| {
|
expired_media.sort_unstable_by(|a, b| {
|
||||||
match media.media_set_label() {
|
b.media_set_label().unwrap().ctime.cmp(&a.media_set_label().unwrap().ctime)
|
||||||
None => 0, // should not happen here
|
|
||||||
Some(set) => set.ctime,
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if let Some(media) = expired_media.first_mut() {
|
if let Some(media) = expired_media.pop() {
|
||||||
println!("reuse expired media '{}'", media.label_text());
|
println!("reuse expired media '{}'", media.label_text());
|
||||||
|
let uuid = media.uuid().clone();
|
||||||
let seq_nr = self.current_media_set.media_list().len() as u64;
|
self.add_media_to_current_set(media.into_id(), current_time)?;
|
||||||
let set = MediaSetLabel::with_data(&pool, self.current_media_set.uuid().clone(), seq_nr, current_time);
|
return Ok(uuid);
|
||||||
|
|
||||||
media.set_media_set_label(set);
|
|
||||||
|
|
||||||
let clear_media_status = true; // remove Full status
|
|
||||||
self.inventory.store(media.id().clone(), clear_media_status)?; // store persistently
|
|
||||||
|
|
||||||
self.current_media_set.add_media(media.uuid().clone());
|
|
||||||
|
|
||||||
return Ok(media.uuid().clone());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
println!("no expired media in pool, try to find unassigned/free media");
|
println!("no expired media in pool, try to find unassigned/free media");
|
||||||
@ -357,18 +392,9 @@ impl MediaPool {
|
|||||||
let (status, location) = self.compute_media_state(&media_id);
|
let (status, location) = self.compute_media_state(&media_id);
|
||||||
if media_id.media_set_label.is_some() { continue; } // should not happen
|
if media_id.media_set_label.is_some() { continue; } // should not happen
|
||||||
|
|
||||||
// check if media is on site
|
if !self.location_is_available(&location) {
|
||||||
match location {
|
|
||||||
MediaLocation::Online(_) => { /* OK */ },
|
|
||||||
MediaLocation::Offline => {
|
|
||||||
if self.use_offline_media {
|
|
||||||
/* OK */
|
|
||||||
} else {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
},
|
|
||||||
MediaLocation::Vault(_) => continue,
|
|
||||||
}
|
|
||||||
|
|
||||||
// only consider writable media
|
// only consider writable media
|
||||||
if status != MediaStatus::Writable { continue; }
|
if status != MediaStatus::Writable { continue; }
|
||||||
@ -376,23 +402,13 @@ impl MediaPool {
|
|||||||
free_media.push(media_id);
|
free_media.push(media_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(media) = free_media.first_mut() {
|
if let Some(media_id) = free_media.pop() {
|
||||||
println!("use free media '{}'", media.label.label_text);
|
println!("use free media '{}'", media_id.label.label_text);
|
||||||
|
let uuid = media_id.label.uuid.clone();
|
||||||
let seq_nr = self.current_media_set.media_list().len() as u64;
|
self.add_media_to_current_set(media_id, current_time)?;
|
||||||
let set = MediaSetLabel::with_data(&pool, self.current_media_set.uuid().clone(), seq_nr, current_time);
|
return Ok(uuid);
|
||||||
|
|
||||||
media.media_set_label = Some(set);
|
|
||||||
|
|
||||||
let clear_media_status = true; // remove Full status
|
|
||||||
self.inventory.store(media.clone(), clear_media_status)?; // store persistently
|
|
||||||
|
|
||||||
self.current_media_set.add_media(media.label.uuid.clone());
|
|
||||||
|
|
||||||
return Ok(media.label.uuid.clone());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
bail!("alloc writable media in pool '{}' failed: no usable media found", self.name());
|
bail!("alloc writable media in pool '{}' failed: no usable media found", self.name());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -537,6 +553,11 @@ impl BackupMedia {
|
|||||||
&self.id
|
&self.id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the media id, consumes self)
|
||||||
|
pub fn into_id(self) -> MediaId {
|
||||||
|
self.id
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns the media label (Barcode)
|
/// Returns the media label (Barcode)
|
||||||
pub fn label_text(&self) -> &str {
|
pub fn label_text(&self) -> &str {
|
||||||
&self.id.label.label_text
|
&self.id.label.label_text
|
||||||
|
@ -230,6 +230,15 @@ impl PoolWriter {
|
|||||||
media.id(),
|
media.id(),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
|
let encrypt_fingerprint = media
|
||||||
|
.media_set_label()
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.encryption_key_fingerprint
|
||||||
|
.clone();
|
||||||
|
|
||||||
|
drive.set_encryption(encrypt_fingerprint)?;
|
||||||
|
|
||||||
self.status = Some(PoolWriterState { drive, catalog, at_eom: false, bytes_written: 0 });
|
self.status = Some(PoolWriterState { drive, catalog, at_eom: false, bytes_written: 0 });
|
||||||
|
|
||||||
Ok(media_uuid)
|
Ok(media_uuid)
|
||||||
@ -457,6 +466,9 @@ fn update_media_set_label(
|
|||||||
bail!("got media with wrong media sequence number ({} != {}",
|
bail!("got media with wrong media sequence number ({} != {}",
|
||||||
new_set.seq_nr,media_set_label.seq_nr);
|
new_set.seq_nr,media_set_label.seq_nr);
|
||||||
}
|
}
|
||||||
|
if new_set.encryption_key_fingerprint != media_set_label.encryption_key_fingerprint {
|
||||||
|
bail!("detected changed encryption fingerprint - internal error");
|
||||||
|
}
|
||||||
media_catalog = MediaCatalog::open(status_path, &media_id.label.uuid, true, false)?;
|
media_catalog = MediaCatalog::open(status_path, &media_id.label.uuid, true, false)?;
|
||||||
} else {
|
} else {
|
||||||
worker.log(
|
worker.log(
|
||||||
|
Loading…
Reference in New Issue
Block a user