8a0046f519
This is just a proof of concept, only storing the encryption key fingerprint inside the media-set label.
750 lines
23 KiB
Rust
750 lines
23 KiB
Rust
//! Backup media Inventory
|
|
//!
|
|
//! The Inventory persistently stores the list of known backup
|
|
//! media. A backup media is identified by its 'MediaId', which is the
|
|
//! MediaLabel/MediaSetLabel combination.
|
|
|
|
use std::collections::{HashMap, BTreeMap};
|
|
use std::path::{Path, PathBuf};
|
|
|
|
use anyhow::{bail, Error};
|
|
use serde::{Serialize, Deserialize};
|
|
use serde_json::json;
|
|
|
|
use proxmox::tools::{
|
|
Uuid,
|
|
fs::{
|
|
open_file_locked,
|
|
replace_file,
|
|
file_get_json,
|
|
CreateOptions,
|
|
},
|
|
};
|
|
|
|
use crate::{
|
|
tools::systemd::time::compute_next_event,
|
|
api2::types::{
|
|
MediaSetPolicy,
|
|
RetentionPolicy,
|
|
MediaStatus,
|
|
MediaLocation,
|
|
},
|
|
tape::{
|
|
TAPE_STATUS_DIR,
|
|
OnlineStatusMap,
|
|
MediaSet,
|
|
file_formats::{
|
|
MediaLabel,
|
|
MediaSetLabel,
|
|
},
|
|
},
|
|
};
|
|
|
|
/// Unique Media Identifier
|
|
///
|
|
/// This combines the label and media set label.
|
|
#[derive(Debug,Serialize,Deserialize,Clone)]
|
|
pub struct MediaId {
|
|
pub label: MediaLabel,
|
|
#[serde(skip_serializing_if="Option::is_none")]
|
|
pub media_set_label: Option<MediaSetLabel>,
|
|
}
|
|
|
|
|
|
#[derive(Serialize,Deserialize)]
|
|
struct MediaStateEntry {
|
|
id: MediaId,
|
|
#[serde(skip_serializing_if="Option::is_none")]
|
|
location: Option<MediaLocation>,
|
|
#[serde(skip_serializing_if="Option::is_none")]
|
|
status: Option<MediaStatus>,
|
|
}
|
|
|
|
/// Media Inventory
|
|
pub struct Inventory {
|
|
map: BTreeMap<Uuid, MediaStateEntry>,
|
|
|
|
inventory_path: PathBuf,
|
|
lockfile_path: PathBuf,
|
|
|
|
// helpers
|
|
media_set_start_times: HashMap<Uuid, i64>
|
|
}
|
|
|
|
impl Inventory {
|
|
|
|
pub const MEDIA_INVENTORY_FILENAME: &'static str = "inventory.json";
|
|
pub const MEDIA_INVENTORY_LOCKFILE: &'static str = ".inventory.lck";
|
|
|
|
fn new(base_path: &Path) -> Self {
|
|
|
|
let mut inventory_path = base_path.to_owned();
|
|
inventory_path.push(Self::MEDIA_INVENTORY_FILENAME);
|
|
|
|
let mut lockfile_path = base_path.to_owned();
|
|
lockfile_path.push(Self::MEDIA_INVENTORY_LOCKFILE);
|
|
|
|
Self {
|
|
map: BTreeMap::new(),
|
|
media_set_start_times: HashMap::new(),
|
|
inventory_path,
|
|
lockfile_path,
|
|
}
|
|
}
|
|
|
|
pub fn load(base_path: &Path) -> Result<Self, Error> {
|
|
let mut me = Self::new(base_path);
|
|
me.reload()?;
|
|
Ok(me)
|
|
}
|
|
|
|
/// Reload the database
|
|
pub fn reload(&mut self) -> Result<(), Error> {
|
|
self.map = Self::load_media_db(&self.inventory_path)?;
|
|
self.update_helpers();
|
|
Ok(())
|
|
}
|
|
|
|
fn update_helpers(&mut self) {
|
|
|
|
// recompute media_set_start_times
|
|
|
|
let mut set_start_times = HashMap::new();
|
|
|
|
for entry in self.map.values() {
|
|
let set = match &entry.id.media_set_label {
|
|
None => continue,
|
|
Some(set) => set,
|
|
};
|
|
if set.seq_nr == 0 {
|
|
set_start_times.insert(set.uuid.clone(), set.ctime);
|
|
}
|
|
}
|
|
|
|
self.media_set_start_times = set_start_times;
|
|
}
|
|
|
|
/// Lock the database
|
|
pub fn lock(&self) -> Result<std::fs::File, Error> {
|
|
open_file_locked(&self.lockfile_path, std::time::Duration::new(10, 0), true)
|
|
}
|
|
|
|
fn load_media_db(path: &Path) -> Result<BTreeMap<Uuid, MediaStateEntry>, Error> {
|
|
|
|
let data = file_get_json(path, Some(json!([])))?;
|
|
let media_list: Vec<MediaStateEntry> = serde_json::from_value(data)?;
|
|
|
|
let mut map = BTreeMap::new();
|
|
for entry in media_list.into_iter() {
|
|
map.insert(entry.id.label.uuid.clone(), entry);
|
|
}
|
|
|
|
Ok(map)
|
|
}
|
|
|
|
fn replace_file(&self) -> Result<(), Error> {
|
|
let list: Vec<&MediaStateEntry> = self.map.values().collect();
|
|
let raw = serde_json::to_string_pretty(&serde_json::to_value(list)?)?;
|
|
|
|
let backup_user = crate::backup::backup_user()?;
|
|
let mode = nix::sys::stat::Mode::from_bits_truncate(0o0640);
|
|
let options = CreateOptions::new()
|
|
.perm(mode)
|
|
.owner(backup_user.uid)
|
|
.group(backup_user.gid);
|
|
|
|
replace_file(&self.inventory_path, raw.as_bytes(), options)?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Stores a single MediaID persistently
|
|
pub fn store(
|
|
&mut self,
|
|
mut media_id: MediaId,
|
|
clear_media_status: bool,
|
|
) -> Result<(), Error> {
|
|
let _lock = self.lock()?;
|
|
self.map = Self::load_media_db(&self.inventory_path)?;
|
|
|
|
let uuid = media_id.label.uuid.clone();
|
|
|
|
if let Some(previous) = self.map.remove(&media_id.label.uuid) {
|
|
// do not overwrite unsaved pool assignments
|
|
if media_id.media_set_label.is_none() {
|
|
if let Some(ref set) = previous.id.media_set_label {
|
|
if set.uuid.as_ref() == [0u8;16] {
|
|
media_id.media_set_label = Some(set.clone());
|
|
}
|
|
}
|
|
}
|
|
let entry = MediaStateEntry {
|
|
id: media_id,
|
|
location: previous.location,
|
|
status: if clear_media_status {
|
|
None
|
|
} else {
|
|
previous.status
|
|
},
|
|
};
|
|
self.map.insert(uuid, entry);
|
|
} else {
|
|
let entry = MediaStateEntry { id: media_id, location: None, status: None };
|
|
self.map.insert(uuid, entry);
|
|
}
|
|
|
|
self.update_helpers();
|
|
self.replace_file()?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Remove a single media persistently
|
|
pub fn remove_media(&mut self, uuid: &Uuid) -> Result<(), Error> {
|
|
let _lock = self.lock()?;
|
|
self.map = Self::load_media_db(&self.inventory_path)?;
|
|
self.map.remove(uuid);
|
|
self.update_helpers();
|
|
self.replace_file()?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Lookup media
|
|
pub fn lookup_media(&self, uuid: &Uuid) -> Option<&MediaId> {
|
|
self.map.get(uuid).map(|entry| &entry.id)
|
|
}
|
|
|
|
/// find media by label_text
|
|
pub fn find_media_by_label_text(&self, label_text: &str) -> Option<&MediaId> {
|
|
for (_uuid, entry) in &self.map {
|
|
if entry.id.label.label_text == label_text {
|
|
return Some(&entry.id);
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
/// Lookup media pool
|
|
///
|
|
/// Returns (pool, is_empty)
|
|
pub fn lookup_media_pool(&self, uuid: &Uuid) -> Option<(&str, bool)> {
|
|
match self.map.get(uuid) {
|
|
None => None,
|
|
Some(entry) => {
|
|
match entry.id.media_set_label {
|
|
None => None, // not assigned to any pool
|
|
Some(ref set) => {
|
|
let is_empty = set.uuid.as_ref() == [0u8;16];
|
|
Some((&set.pool, is_empty))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// List all media assigned to the pool
|
|
pub fn list_pool_media(&self, pool: &str) -> Vec<MediaId> {
|
|
let mut list = Vec::new();
|
|
|
|
for (_uuid, entry) in &self.map {
|
|
match entry.id.media_set_label {
|
|
None => continue, // not assigned to any pool
|
|
Some(ref set) => {
|
|
if set.pool != pool {
|
|
continue; // belong to another pool
|
|
}
|
|
|
|
if set.uuid.as_ref() == [0u8;16] { // should we do this??
|
|
list.push(MediaId {
|
|
label: entry.id.label.clone(),
|
|
media_set_label: None,
|
|
})
|
|
} else {
|
|
list.push(entry.id.clone());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
list
|
|
}
|
|
|
|
/// List all used media
|
|
pub fn list_used_media(&self) -> Vec<MediaId> {
|
|
let mut list = Vec::new();
|
|
|
|
for (_uuid, entry) in &self.map {
|
|
match entry.id.media_set_label {
|
|
None => continue, // not assigned to any pool
|
|
Some(ref set) => {
|
|
if set.uuid.as_ref() != [0u8;16] {
|
|
list.push(entry.id.clone());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
list
|
|
}
|
|
|
|
/// List media not assigned to any pool
|
|
pub fn list_unassigned_media(&self) -> Vec<MediaId> {
|
|
let mut list = Vec::new();
|
|
|
|
for (_uuid, entry) in &self.map {
|
|
if entry.id.media_set_label.is_none() {
|
|
list.push(entry.id.clone());
|
|
}
|
|
}
|
|
|
|
list
|
|
}
|
|
|
|
pub fn media_set_start_time(&self, media_set_uuid: &Uuid) -> Option<i64> {
|
|
self.media_set_start_times.get(media_set_uuid).map(|t| *t)
|
|
}
|
|
|
|
/// Lookup media set pool
|
|
pub fn lookup_media_set_pool(&self, media_set_uuid: &Uuid) -> Result<String, Error> {
|
|
|
|
let mut last_pool = None;
|
|
|
|
for entry in self.map.values() {
|
|
match entry.id.media_set_label {
|
|
None => continue,
|
|
Some(MediaSetLabel { ref uuid, .. }) => {
|
|
if uuid != media_set_uuid {
|
|
continue;
|
|
}
|
|
if let Some((pool, _)) = self.lookup_media_pool(&entry.id.label.uuid) {
|
|
if let Some(last_pool) = last_pool {
|
|
if last_pool != pool {
|
|
bail!("detected media set with inconsistent pool assignment - internal error");
|
|
}
|
|
} else {
|
|
last_pool = Some(pool);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
match last_pool {
|
|
Some(pool) => Ok(pool.to_string()),
|
|
None => bail!("media set {} is incomplete - unable to lookup pool"),
|
|
}
|
|
}
|
|
|
|
/// Compute a single media sets
|
|
pub fn compute_media_set_members(&self, media_set_uuid: &Uuid) -> Result<MediaSet, Error> {
|
|
|
|
let mut set = MediaSet::with_data(media_set_uuid.clone(), Vec::new());
|
|
|
|
for entry in self.map.values() {
|
|
match entry.id.media_set_label {
|
|
None => continue,
|
|
Some(MediaSetLabel { seq_nr, ref uuid, .. }) => {
|
|
if uuid != media_set_uuid {
|
|
continue;
|
|
}
|
|
set.insert_media(entry.id.label.uuid.clone(), seq_nr)?;
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(set)
|
|
}
|
|
|
|
/// Compute all media sets
|
|
pub fn compute_media_set_list(&self) -> Result<HashMap<Uuid, MediaSet>, Error> {
|
|
|
|
let mut set_map: HashMap<Uuid, MediaSet> = HashMap::new();
|
|
|
|
for entry in self.map.values() {
|
|
match entry.id.media_set_label {
|
|
None => continue,
|
|
Some(MediaSetLabel { seq_nr, ref uuid, .. }) => {
|
|
|
|
let set = set_map.entry(uuid.clone()).or_insert_with(|| {
|
|
MediaSet::with_data(uuid.clone(), Vec::new())
|
|
});
|
|
|
|
set.insert_media(entry.id.label.uuid.clone(), seq_nr)?;
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(set_map)
|
|
}
|
|
|
|
/// Returns the latest media set for a pool
|
|
pub fn latest_media_set(&self, pool: &str) -> Option<Uuid> {
|
|
|
|
let mut last_set: Option<(Uuid, i64)> = None;
|
|
|
|
let set_list = self.map.values()
|
|
.filter_map(|entry| entry.id.media_set_label.as_ref())
|
|
.filter(|set| &set.pool == &pool && set.uuid.as_ref() != [0u8;16]);
|
|
|
|
for set in set_list {
|
|
match last_set {
|
|
None => {
|
|
last_set = Some((set.uuid.clone(), set.ctime));
|
|
}
|
|
Some((_, last_ctime)) => {
|
|
if set.ctime > last_ctime {
|
|
last_set = Some((set.uuid.clone(), set.ctime));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
let (uuid, ctime) = match last_set {
|
|
None => return None,
|
|
Some((uuid, ctime)) => (uuid, ctime),
|
|
};
|
|
|
|
// consistency check - must be the only set with that ctime
|
|
let set_list = self.map.values()
|
|
.filter_map(|entry| entry.id.media_set_label.as_ref())
|
|
.filter(|set| &set.pool == &pool && set.uuid.as_ref() != [0u8;16]);
|
|
|
|
for set in set_list {
|
|
if set.uuid != uuid && set.ctime >= ctime { // should not happen
|
|
eprintln!("latest_media_set: found set with equal ctime ({}, {})", set.uuid, uuid);
|
|
return None;
|
|
}
|
|
}
|
|
|
|
Some(uuid)
|
|
}
|
|
|
|
// Test if there is a media set (in the same pool) newer than this one.
|
|
// Return the ctime of the nearest media set
|
|
fn media_set_next_start_time(&self, media_set_uuid: &Uuid) -> Option<i64> {
|
|
|
|
let (pool, ctime) = match self.map.values()
|
|
.filter_map(|entry| entry.id.media_set_label.as_ref())
|
|
.find_map(|set| {
|
|
if &set.uuid == media_set_uuid {
|
|
Some((set.pool.clone(), set.ctime))
|
|
} else {
|
|
None
|
|
}
|
|
}) {
|
|
Some((pool, ctime)) => (pool, ctime),
|
|
None => return None,
|
|
};
|
|
|
|
let set_list = self.map.values()
|
|
.filter_map(|entry| entry.id.media_set_label.as_ref())
|
|
.filter(|set| (&set.uuid != media_set_uuid) && (&set.pool == &pool));
|
|
|
|
let mut next_ctime = None;
|
|
|
|
for set in set_list {
|
|
if set.ctime > ctime {
|
|
match next_ctime {
|
|
None => {
|
|
next_ctime = Some(set.ctime);
|
|
}
|
|
Some(last_next_ctime) => {
|
|
if set.ctime < last_next_ctime {
|
|
next_ctime = Some(set.ctime);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
next_ctime
|
|
}
|
|
|
|
pub fn media_expire_time(
|
|
&self,
|
|
media: &MediaId,
|
|
media_set_policy: &MediaSetPolicy,
|
|
retention_policy: &RetentionPolicy,
|
|
) -> i64 {
|
|
|
|
if let RetentionPolicy::KeepForever = retention_policy {
|
|
return i64::MAX;
|
|
}
|
|
|
|
let set = match media.media_set_label {
|
|
None => return i64::MAX,
|
|
Some(ref set) => set,
|
|
};
|
|
|
|
let set_start_time = match self.media_set_start_time(&set.uuid) {
|
|
None => {
|
|
// missing information, use ctime from this
|
|
// set (always greater than ctime from seq_nr 0)
|
|
set.ctime
|
|
}
|
|
Some(time) => time,
|
|
};
|
|
|
|
let max_use_time = match media_set_policy {
|
|
MediaSetPolicy::ContinueCurrent => {
|
|
match self.media_set_next_start_time(&set.uuid) {
|
|
Some(next_start_time) => next_start_time,
|
|
None => return i64::MAX,
|
|
}
|
|
}
|
|
MediaSetPolicy::AlwaysCreate => {
|
|
set_start_time + 1
|
|
}
|
|
MediaSetPolicy::CreateAt(ref event) => {
|
|
match compute_next_event(event, set_start_time, false) {
|
|
Ok(Some(next)) => next,
|
|
Ok(None) | Err(_) => return i64::MAX,
|
|
}
|
|
}
|
|
};
|
|
|
|
match retention_policy {
|
|
RetentionPolicy::KeepForever => i64::MAX,
|
|
RetentionPolicy::OverwriteAlways => max_use_time,
|
|
RetentionPolicy::ProtectFor(time_span) => {
|
|
let seconds = f64::from(time_span.clone()) as i64;
|
|
max_use_time + seconds
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Generate a human readable name for the media set
|
|
///
|
|
/// The template can include strftime time format specifications.
|
|
pub fn generate_media_set_name(
|
|
&self,
|
|
media_set_uuid: &Uuid,
|
|
template: Option<String>,
|
|
) -> Result<String, Error> {
|
|
|
|
if let Some(ctime) = self.media_set_start_time(media_set_uuid) {
|
|
let mut template = template.unwrap_or(String::from("%c"));
|
|
template = template.replace("%id%", &media_set_uuid.to_string());
|
|
proxmox::tools::time::strftime_local(&template, ctime)
|
|
} else {
|
|
// We don't know the set start time, so we cannot use the template
|
|
Ok(media_set_uuid.to_string())
|
|
}
|
|
}
|
|
|
|
// Helpers to simplify testing
|
|
|
|
/// Genreate and insert a new free tape (test helper)
|
|
pub fn generate_free_tape(&mut self, label_text: &str, ctime: i64) -> Uuid {
|
|
|
|
let label = MediaLabel {
|
|
label_text: label_text.to_string(),
|
|
uuid: Uuid::generate(),
|
|
ctime,
|
|
};
|
|
let uuid = label.uuid.clone();
|
|
|
|
self.store(MediaId { label, media_set_label: None }, false).unwrap();
|
|
|
|
uuid
|
|
}
|
|
|
|
/// Genreate and insert a new tape assigned to a specific pool
|
|
/// (test helper)
|
|
pub fn generate_assigned_tape(
|
|
&mut self,
|
|
label_text: &str,
|
|
pool: &str,
|
|
ctime: i64,
|
|
) -> Uuid {
|
|
|
|
let label = MediaLabel {
|
|
label_text: label_text.to_string(),
|
|
uuid: Uuid::generate(),
|
|
ctime,
|
|
};
|
|
|
|
let uuid = label.uuid.clone();
|
|
|
|
let set = MediaSetLabel::with_data(pool, [0u8; 16].into(), 0, ctime, None);
|
|
|
|
self.store(MediaId { label, media_set_label: Some(set) }, false).unwrap();
|
|
|
|
uuid
|
|
}
|
|
|
|
/// Genreate and insert a used tape (test helper)
|
|
pub fn generate_used_tape(
|
|
&mut self,
|
|
label_text: &str,
|
|
set: MediaSetLabel,
|
|
ctime: i64,
|
|
) -> Uuid {
|
|
let label = MediaLabel {
|
|
label_text: label_text.to_string(),
|
|
uuid: Uuid::generate(),
|
|
ctime,
|
|
};
|
|
let uuid = label.uuid.clone();
|
|
|
|
self.store(MediaId { label, media_set_label: Some(set) }, false).unwrap();
|
|
|
|
uuid
|
|
}
|
|
}
|
|
|
|
// Status/location handling
|
|
impl Inventory {
|
|
|
|
/// Returns status and location with reasonable defaults.
|
|
///
|
|
/// Default status is 'MediaStatus::Unknown'.
|
|
/// Default location is 'MediaLocation::Offline'.
|
|
pub fn status_and_location(&self, uuid: &Uuid) -> (MediaStatus, MediaLocation) {
|
|
|
|
match self.map.get(uuid) {
|
|
None => {
|
|
// no info stored - assume media is writable/offline
|
|
(MediaStatus::Unknown, MediaLocation::Offline)
|
|
}
|
|
Some(entry) => {
|
|
let location = entry.location.clone().unwrap_or(MediaLocation::Offline);
|
|
let status = entry.status.unwrap_or(MediaStatus::Unknown);
|
|
(status, location)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Lock database, reload database, set status, store database
|
|
fn set_media_status(&mut self, uuid: &Uuid, status: Option<MediaStatus>) -> Result<(), Error> {
|
|
let _lock = self.lock()?;
|
|
self.map = Self::load_media_db(&self.inventory_path)?;
|
|
if let Some(entry) = self.map.get_mut(uuid) {
|
|
entry.status = status;
|
|
self.update_helpers();
|
|
self.replace_file()?;
|
|
Ok(())
|
|
} else {
|
|
bail!("no such media '{}'", uuid);
|
|
}
|
|
}
|
|
|
|
/// Lock database, reload database, set status to Full, store database
|
|
pub fn set_media_status_full(&mut self, uuid: &Uuid) -> Result<(), Error> {
|
|
self.set_media_status(uuid, Some(MediaStatus::Full))
|
|
}
|
|
|
|
/// Lock database, reload database, set status to Damaged, store database
|
|
pub fn set_media_status_damaged(&mut self, uuid: &Uuid) -> Result<(), Error> {
|
|
self.set_media_status(uuid, Some(MediaStatus::Damaged))
|
|
}
|
|
|
|
/// Lock database, reload database, set status to None, store database
|
|
pub fn clear_media_status(&mut self, uuid: &Uuid) -> Result<(), Error> {
|
|
self.set_media_status(uuid, None)
|
|
}
|
|
|
|
// Lock database, reload database, set location, store database
|
|
fn set_media_location(&mut self, uuid: &Uuid, location: Option<MediaLocation>) -> Result<(), Error> {
|
|
let _lock = self.lock()?;
|
|
self.map = Self::load_media_db(&self.inventory_path)?;
|
|
if let Some(entry) = self.map.get_mut(uuid) {
|
|
entry.location = location;
|
|
self.update_helpers();
|
|
self.replace_file()?;
|
|
Ok(())
|
|
} else {
|
|
bail!("no such media '{}'", uuid);
|
|
}
|
|
}
|
|
|
|
/// Lock database, reload database, set location to vault, store database
|
|
pub fn set_media_location_vault(&mut self, uuid: &Uuid, vault: &str) -> Result<(), Error> {
|
|
self.set_media_location(uuid, Some(MediaLocation::Vault(vault.to_string())))
|
|
}
|
|
|
|
/// Lock database, reload database, set location to offline, store database
|
|
pub fn set_media_location_offline(&mut self, uuid: &Uuid) -> Result<(), Error> {
|
|
self.set_media_location(uuid, Some(MediaLocation::Offline))
|
|
}
|
|
|
|
/// Update online status
|
|
pub fn update_online_status(&mut self, online_map: &OnlineStatusMap) -> Result<(), Error> {
|
|
let _lock = self.lock()?;
|
|
self.map = Self::load_media_db(&self.inventory_path)?;
|
|
|
|
for (uuid, entry) in self.map.iter_mut() {
|
|
if let Some(changer_name) = online_map.lookup_changer(uuid) {
|
|
entry.location = Some(MediaLocation::Online(changer_name.to_string()));
|
|
} else {
|
|
if let Some(MediaLocation::Online(ref changer_name)) = entry.location {
|
|
match online_map.online_map(changer_name) {
|
|
None => {
|
|
// no such changer device
|
|
entry.location = Some(MediaLocation::Offline);
|
|
}
|
|
Some(None) => {
|
|
// got no info - do nothing
|
|
}
|
|
Some(Some(_)) => {
|
|
// media changer changed
|
|
entry.location = Some(MediaLocation::Offline);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
self.update_helpers();
|
|
self.replace_file()?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
}
|
|
|
|
// shell completion helper
|
|
|
|
/// List of known media uuids
|
|
pub fn complete_media_uuid(
|
|
_arg: &str,
|
|
_param: &HashMap<String, String>,
|
|
) -> Vec<String> {
|
|
|
|
let inventory = match Inventory::load(Path::new(TAPE_STATUS_DIR)) {
|
|
Ok(inventory) => inventory,
|
|
Err(_) => return Vec::new(),
|
|
};
|
|
|
|
inventory.map.keys().map(|uuid| uuid.to_string()).collect()
|
|
}
|
|
|
|
/// List of known media sets
|
|
pub fn complete_media_set_uuid(
|
|
_arg: &str,
|
|
_param: &HashMap<String, String>,
|
|
) -> Vec<String> {
|
|
|
|
let inventory = match Inventory::load(Path::new(TAPE_STATUS_DIR)) {
|
|
Ok(inventory) => inventory,
|
|
Err(_) => return Vec::new(),
|
|
};
|
|
|
|
inventory.map.values()
|
|
.filter_map(|entry| entry.id.media_set_label.as_ref())
|
|
.map(|set| set.uuid.to_string()).collect()
|
|
}
|
|
|
|
/// List of known media labels (barcodes)
|
|
pub fn complete_media_label_text(
|
|
_arg: &str,
|
|
_param: &HashMap<String, String>,
|
|
) -> Vec<String> {
|
|
|
|
let inventory = match Inventory::load(Path::new(TAPE_STATUS_DIR)) {
|
|
Ok(inventory) => inventory,
|
|
Err(_) => return Vec::new(),
|
|
};
|
|
|
|
inventory.map.values().map(|entry| entry.id.label.label_text.clone()).collect()
|
|
}
|