2020-12-07 07:27:15 +00:00
|
|
|
// Note: This is only for test an debug
|
|
|
|
|
|
|
|
use std::fs::File;
|
|
|
|
use std::io;
|
|
|
|
|
|
|
|
use anyhow::{bail, format_err, Error};
|
|
|
|
use serde::{Serialize, Deserialize};
|
|
|
|
|
|
|
|
use proxmox::tools::{
|
|
|
|
fs::{replace_file, CreateOptions},
|
|
|
|
};
|
|
|
|
|
|
|
|
use crate::{
|
|
|
|
tape::{
|
|
|
|
TapeWrite,
|
|
|
|
TapeRead,
|
|
|
|
changer::MediaChange,
|
|
|
|
drive::{
|
|
|
|
VirtualTapeDrive,
|
|
|
|
TapeDriver,
|
|
|
|
},
|
|
|
|
file_formats::{
|
|
|
|
MediaSetLabel,
|
|
|
|
MediaContentHeader,
|
|
|
|
PROXMOX_BACKUP_MEDIA_SET_LABEL_MAGIC_1_0,
|
|
|
|
},
|
|
|
|
helpers::{
|
|
|
|
EmulateTapeReader,
|
|
|
|
EmulateTapeWriter,
|
|
|
|
BlockedReader,
|
|
|
|
BlockedWriter,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
};
|
|
|
|
|
|
|
|
impl VirtualTapeDrive {
|
|
|
|
|
|
|
|
/// This needs to lock the drive
|
|
|
|
pub fn open(&self) -> Result<VirtualTapeHandle, Error> {
|
|
|
|
let mut lock_path = std::path::PathBuf::from(&self.path);
|
|
|
|
lock_path.push(".drive.lck");
|
|
|
|
|
|
|
|
let timeout = std::time::Duration::new(10, 0);
|
|
|
|
let lock = proxmox::tools::fs::open_file_locked(&lock_path, timeout, true)?;
|
|
|
|
|
|
|
|
Ok(VirtualTapeHandle {
|
|
|
|
_lock: lock,
|
|
|
|
max_size: self.max_size.unwrap_or(64*1024*1024),
|
|
|
|
path: std::path::PathBuf::from(&self.path),
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Serialize,Deserialize)]
|
|
|
|
struct VirtualTapeStatus {
|
|
|
|
name: String,
|
|
|
|
pos: usize,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Serialize,Deserialize)]
|
|
|
|
struct VirtualDriveStatus {
|
|
|
|
current_tape: Option<VirtualTapeStatus>,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Serialize,Deserialize)]
|
|
|
|
struct TapeIndex {
|
|
|
|
files: usize,
|
|
|
|
}
|
|
|
|
|
|
|
|
pub struct VirtualTapeHandle {
|
|
|
|
path: std::path::PathBuf,
|
|
|
|
max_size: usize,
|
|
|
|
_lock: File,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl VirtualTapeHandle {
|
|
|
|
|
|
|
|
pub fn insert_tape(&self, _tape_filename: &str) {
|
|
|
|
unimplemented!();
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn eject_tape(&self) {
|
|
|
|
unimplemented!();
|
|
|
|
}
|
|
|
|
|
|
|
|
fn status_file_path(&self) -> std::path::PathBuf {
|
|
|
|
let mut path = self.path.clone();
|
|
|
|
path.push("drive-status.json");
|
|
|
|
path
|
|
|
|
}
|
|
|
|
|
|
|
|
fn tape_index_path(&self, tape_name: &str) -> std::path::PathBuf {
|
|
|
|
let mut path = self.path.clone();
|
|
|
|
path.push(format!("tape-{}.json", tape_name));
|
|
|
|
path
|
|
|
|
}
|
|
|
|
|
|
|
|
fn tape_file_path(&self, tape_name: &str, pos: usize) -> std::path::PathBuf {
|
|
|
|
let mut path = self.path.clone();
|
|
|
|
path.push(format!("tapefile-{}-{}.json", pos, tape_name));
|
|
|
|
path
|
|
|
|
}
|
|
|
|
|
|
|
|
fn load_tape_index(&self, tape_name: &str) -> Result<TapeIndex, Error> {
|
|
|
|
let path = self.tape_index_path(tape_name);
|
|
|
|
let raw = proxmox::tools::fs::file_get_contents(&path)?;
|
|
|
|
if raw.is_empty() {
|
|
|
|
return Ok(TapeIndex { files: 0 });
|
|
|
|
}
|
|
|
|
let data: TapeIndex = serde_json::from_slice(&raw)?;
|
|
|
|
Ok(data)
|
|
|
|
}
|
|
|
|
|
|
|
|
fn store_tape_index(&self, tape_name: &str, index: &TapeIndex) -> Result<(), Error> {
|
|
|
|
let path = self.tape_index_path(tape_name);
|
|
|
|
let raw = serde_json::to_string_pretty(&serde_json::to_value(index)?)?;
|
|
|
|
|
|
|
|
let options = CreateOptions::new();
|
|
|
|
replace_file(&path, raw.as_bytes(), options)?;
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
fn truncate_tape(&self, tape_name: &str, pos: usize) -> Result<usize, Error> {
|
|
|
|
let mut index = self.load_tape_index(tape_name)?;
|
|
|
|
|
|
|
|
if index.files <= pos {
|
|
|
|
return Ok(index.files)
|
|
|
|
}
|
|
|
|
|
|
|
|
for i in pos..index.files {
|
|
|
|
let path = self.tape_file_path(tape_name, i);
|
|
|
|
let _ = std::fs::remove_file(path);
|
|
|
|
}
|
|
|
|
|
|
|
|
index.files = pos;
|
|
|
|
|
|
|
|
self.store_tape_index(tape_name, &index)?;
|
|
|
|
|
|
|
|
Ok(index.files)
|
|
|
|
}
|
|
|
|
|
|
|
|
fn load_status(&self) -> Result<VirtualDriveStatus, Error> {
|
|
|
|
let path = self.status_file_path();
|
|
|
|
|
|
|
|
let default = serde_json::to_value(VirtualDriveStatus {
|
|
|
|
current_tape: None,
|
|
|
|
})?;
|
|
|
|
|
|
|
|
let data = proxmox::tools::fs::file_get_json(&path, Some(default))?;
|
|
|
|
let status: VirtualDriveStatus = serde_json::from_value(data)?;
|
|
|
|
Ok(status)
|
|
|
|
}
|
|
|
|
|
|
|
|
fn store_status(&self, status: &VirtualDriveStatus) -> Result<(), Error> {
|
|
|
|
let path = self.status_file_path();
|
|
|
|
let raw = serde_json::to_string_pretty(&serde_json::to_value(status)?)?;
|
|
|
|
|
|
|
|
let options = CreateOptions::new();
|
|
|
|
replace_file(&path, raw.as_bytes(), options)?;
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl TapeDriver for VirtualTapeHandle {
|
|
|
|
|
|
|
|
fn sync(&mut self) -> Result<(), Error> {
|
|
|
|
Ok(()) // do nothing for now
|
|
|
|
}
|
|
|
|
|
2020-12-18 06:44:50 +00:00
|
|
|
fn current_file_number(&mut self) -> Result<u64, Error> {
|
2020-12-07 07:27:15 +00:00
|
|
|
let status = self.load_status()
|
|
|
|
.map_err(|err| format_err!("current_file_number failed: {}", err.to_string()))?;
|
|
|
|
|
|
|
|
match status.current_tape {
|
2020-12-18 06:44:50 +00:00
|
|
|
Some(VirtualTapeStatus { pos, .. }) => { Ok(pos as u64)},
|
2020-12-07 07:27:15 +00:00
|
|
|
None => bail!("current_file_number failed: drive is empty (no tape loaded)."),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn read_next_file(&mut self) -> Result<Option<Box<dyn TapeRead>>, io::Error> {
|
|
|
|
let mut status = self.load_status()
|
|
|
|
.map_err(|err| io::Error::new(io::ErrorKind::Other, err.to_string()))?;
|
|
|
|
|
|
|
|
match status.current_tape {
|
|
|
|
Some(VirtualTapeStatus { ref name, ref mut pos }) => {
|
|
|
|
|
|
|
|
let index = self.load_tape_index(name)
|
|
|
|
.map_err(|err| io::Error::new(io::ErrorKind::Other, err.to_string()))?;
|
|
|
|
|
|
|
|
if *pos >= index.files {
|
|
|
|
return Ok(None); // EOM
|
|
|
|
}
|
|
|
|
|
|
|
|
let path = self.tape_file_path(name, *pos);
|
|
|
|
let file = std::fs::OpenOptions::new()
|
|
|
|
.read(true)
|
|
|
|
.open(path)?;
|
|
|
|
|
|
|
|
*pos += 1;
|
|
|
|
self.store_status(&status)
|
|
|
|
.map_err(|err| io::Error::new(io::ErrorKind::Other, err.to_string()))?;
|
|
|
|
|
|
|
|
let reader = Box::new(file);
|
|
|
|
let reader = Box::new(EmulateTapeReader::new(reader));
|
|
|
|
|
|
|
|
match BlockedReader::open(reader)? {
|
|
|
|
Some(reader) => Ok(Some(Box::new(reader))),
|
|
|
|
None => Ok(None),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
None => proxmox::io_bail!("drive is empty (no tape loaded)."),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn write_file(&mut self) -> Result<Box<dyn TapeWrite>, io::Error> {
|
|
|
|
let mut status = self.load_status()
|
|
|
|
.map_err(|err| io::Error::new(io::ErrorKind::Other, err.to_string()))?;
|
|
|
|
|
|
|
|
match status.current_tape {
|
|
|
|
Some(VirtualTapeStatus { ref name, ref mut pos }) => {
|
|
|
|
|
|
|
|
let mut index = self.load_tape_index(name)
|
|
|
|
.map_err(|err| io::Error::new(io::ErrorKind::Other, err.to_string()))?;
|
|
|
|
|
|
|
|
for i in *pos..index.files {
|
|
|
|
let path = self.tape_file_path(name, i);
|
|
|
|
let _ = std::fs::remove_file(path);
|
|
|
|
}
|
|
|
|
|
|
|
|
let mut used_space = 0;
|
|
|
|
for i in 0..*pos {
|
|
|
|
let path = self.tape_file_path(name, i);
|
|
|
|
used_space += path.metadata()?.len() as usize;
|
|
|
|
|
|
|
|
}
|
|
|
|
index.files = *pos + 1;
|
|
|
|
|
|
|
|
self.store_tape_index(name, &index)
|
|
|
|
.map_err(|err| io::Error::new(io::ErrorKind::Other, err.to_string()))?;
|
|
|
|
|
|
|
|
let path = self.tape_file_path(name, *pos);
|
|
|
|
let file = std::fs::OpenOptions::new()
|
|
|
|
.write(true)
|
|
|
|
.create(true)
|
|
|
|
.truncate(true)
|
|
|
|
.open(path)?;
|
|
|
|
|
|
|
|
*pos = index.files;
|
|
|
|
|
|
|
|
self.store_status(&status)
|
|
|
|
.map_err(|err| io::Error::new(io::ErrorKind::Other, err.to_string()))?;
|
|
|
|
|
|
|
|
let mut free_space = 0;
|
|
|
|
if used_space < self.max_size {
|
|
|
|
free_space = self.max_size - used_space;
|
|
|
|
}
|
|
|
|
|
|
|
|
let writer = Box::new(file);
|
|
|
|
let writer = Box::new(EmulateTapeWriter::new(writer, free_space));
|
|
|
|
let writer = Box::new(BlockedWriter::new(writer));
|
|
|
|
|
|
|
|
Ok(writer)
|
|
|
|
}
|
|
|
|
None => proxmox::io_bail!("drive is empty (no tape loaded)."),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn move_to_eom(&mut self) -> Result<(), Error> {
|
|
|
|
let mut status = self.load_status()?;
|
|
|
|
match status.current_tape {
|
|
|
|
Some(VirtualTapeStatus { ref name, ref mut pos }) => {
|
|
|
|
|
|
|
|
let index = self.load_tape_index(name)
|
|
|
|
.map_err(|err| io::Error::new(io::ErrorKind::Other, err.to_string()))?;
|
|
|
|
|
|
|
|
*pos = index.files;
|
|
|
|
self.store_status(&status)
|
|
|
|
.map_err(|err| io::Error::new(io::ErrorKind::Other, err.to_string()))?;
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
None => bail!("drive is empty (no tape loaded)."),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn rewind(&mut self) -> Result<(), Error> {
|
|
|
|
let mut status = self.load_status()?;
|
|
|
|
match status.current_tape {
|
|
|
|
Some(ref mut tape_status) => {
|
|
|
|
tape_status.pos = 0;
|
|
|
|
self.store_status(&status)?;
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
None => bail!("drive is empty (no tape loaded)."),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn erase_media(&mut self, _fast: bool) -> Result<(), Error> {
|
|
|
|
let mut status = self.load_status()?;
|
|
|
|
match status.current_tape {
|
|
|
|
Some(VirtualTapeStatus { ref name, ref mut pos }) => {
|
|
|
|
*pos = self.truncate_tape(name, 0)?;
|
|
|
|
self.store_status(&status)?;
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
None => bail!("drive is empty (no tape loaded)."),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-12-16 12:27:53 +00:00
|
|
|
fn write_media_set_label(&mut self, media_set_label: &MediaSetLabel) -> Result<(), Error> {
|
2020-12-07 07:27:15 +00:00
|
|
|
|
|
|
|
let mut status = self.load_status()?;
|
|
|
|
match status.current_tape {
|
|
|
|
Some(VirtualTapeStatus { ref name, ref mut pos }) => {
|
|
|
|
*pos = self.truncate_tape(name, 1)?;
|
|
|
|
let pos = *pos;
|
|
|
|
self.store_status(&status)?;
|
|
|
|
|
|
|
|
if pos == 0 {
|
|
|
|
bail!("media is empty (no label).");
|
|
|
|
}
|
|
|
|
if pos != 1 {
|
|
|
|
bail!("write_media_set_label: truncate failed - got wrong pos '{}'", pos);
|
|
|
|
}
|
|
|
|
|
|
|
|
let raw = serde_json::to_string_pretty(&serde_json::to_value(media_set_label)?)?;
|
|
|
|
let header = MediaContentHeader::new(PROXMOX_BACKUP_MEDIA_SET_LABEL_MAGIC_1_0, raw.len() as u32);
|
|
|
|
|
|
|
|
{
|
|
|
|
let mut writer = self.write_file()?;
|
|
|
|
writer.write_header(&header, raw.as_bytes())?;
|
|
|
|
writer.finish(false)?;
|
|
|
|
}
|
|
|
|
|
2020-12-16 12:27:53 +00:00
|
|
|
Ok(())
|
2020-12-07 07:27:15 +00:00
|
|
|
}
|
|
|
|
None => bail!("drive is empty (no tape loaded)."),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn eject_media(&mut self) -> Result<(), Error> {
|
|
|
|
let status = VirtualDriveStatus {
|
|
|
|
current_tape: None,
|
|
|
|
};
|
|
|
|
self.store_status(&status)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl MediaChange for VirtualTapeHandle {
|
|
|
|
|
|
|
|
/// Try to load media
|
|
|
|
///
|
|
|
|
/// We automatically create an empty virtual tape here (if it does
|
|
|
|
/// not exist already)
|
|
|
|
fn load_media(&mut self, label: &str) -> Result<(), Error> {
|
|
|
|
let name = format!("tape-{}.json", label);
|
|
|
|
let mut path = self.path.clone();
|
|
|
|
path.push(&name);
|
|
|
|
if !path.exists() {
|
|
|
|
eprintln!("unable to find tape {} - creating file {:?}", label, path);
|
|
|
|
let index = TapeIndex { files: 0 };
|
|
|
|
self.store_tape_index(label, &index)?;
|
|
|
|
}
|
|
|
|
|
|
|
|
let status = VirtualDriveStatus {
|
|
|
|
current_tape: Some(VirtualTapeStatus {
|
|
|
|
name: label.to_string(),
|
|
|
|
pos: 0,
|
|
|
|
}),
|
|
|
|
};
|
|
|
|
self.store_status(&status)
|
|
|
|
}
|
|
|
|
|
|
|
|
fn unload_media(&mut self) -> Result<(), Error> {
|
|
|
|
self.eject_media()?;
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
fn eject_on_unload(&self) -> bool {
|
|
|
|
true
|
|
|
|
}
|
|
|
|
|
|
|
|
fn list_media_changer_ids(&self) -> Result<Vec<String>, Error> {
|
|
|
|
let mut list = Vec::new();
|
|
|
|
for entry in std::fs::read_dir(&self.path)? {
|
|
|
|
let entry = entry?;
|
|
|
|
let path = entry.path();
|
|
|
|
if path.is_file() && path.extension() == Some(std::ffi::OsStr::new("json")) {
|
|
|
|
if let Some(name) = path.file_stem() {
|
|
|
|
if let Some(name) = name.to_str() {
|
|
|
|
if name.starts_with("tape-") {
|
|
|
|
list.push(name[5..].to_string());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
Ok(list)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl MediaChange for VirtualTapeDrive {
|
|
|
|
|
|
|
|
fn load_media(&mut self, changer_id: &str) -> Result<(), Error> {
|
|
|
|
let mut handle = self.open()?;
|
|
|
|
handle.load_media(changer_id)
|
|
|
|
}
|
|
|
|
|
|
|
|
fn unload_media(&mut self) -> Result<(), Error> {
|
|
|
|
let mut handle = self.open()?;
|
|
|
|
handle.eject_media()?;
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
fn eject_on_unload(&self) -> bool {
|
|
|
|
true
|
|
|
|
}
|
|
|
|
|
|
|
|
fn list_media_changer_ids(&self) -> Result<Vec<String>, Error> {
|
|
|
|
let handle = self.open()?;
|
|
|
|
handle.list_media_changer_ids()
|
|
|
|
}
|
|
|
|
}
|