file-restore: add 'extract' command for VM file restore
The data on the restore daemon is either encoded into a pxar archive, to provide the most accurate data for local restore, or encoded directly into a zip file (or written out unprocessed for files), depending on the 'pxar' argument to the 'extract' API call. Signed-off-by: Stefan Reiter <s.reiter@proxmox.com>
This commit is contained in:
committed by
Thomas Lamprecht
parent
1f03196c0b
commit
b13089cdf5
@ -14,6 +14,7 @@ use proxmox::api::{
|
||||
},
|
||||
};
|
||||
use pxar::accessor::aio::Accessor;
|
||||
use pxar::decoder::aio::Decoder;
|
||||
|
||||
use proxmox_backup::api2::{helpers, types::ArchiveEntry};
|
||||
use proxmox_backup::backup::{
|
||||
@ -21,7 +22,7 @@ use proxmox_backup::backup::{
|
||||
DirEntryAttribute, IndexFile, LocalDynamicReadAt, CATALOG_NAME,
|
||||
};
|
||||
use proxmox_backup::client::{BackupReader, RemoteChunkReader};
|
||||
use proxmox_backup::pxar::{create_zip, extract_sub_dir};
|
||||
use proxmox_backup::pxar::{create_zip, extract_sub_dir, extract_sub_dir_seq};
|
||||
use proxmox_backup::tools;
|
||||
|
||||
// use "pub" so rust doesn't complain about "unused" functions in the module
|
||||
@ -277,7 +278,11 @@ async fn list(
|
||||
description: "Print verbose information",
|
||||
optional: true,
|
||||
default: false,
|
||||
}
|
||||
},
|
||||
"driver": {
|
||||
type: BlockDriverType,
|
||||
optional: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
)]
|
||||
@ -314,20 +319,21 @@ async fn extract(
|
||||
}
|
||||
};
|
||||
|
||||
let client = connect(&repo)?;
|
||||
let client = BackupReader::start(
|
||||
client,
|
||||
crypt_config.clone(),
|
||||
repo.store(),
|
||||
&snapshot.group().backup_type(),
|
||||
&snapshot.group().backup_id(),
|
||||
snapshot.backup_time(),
|
||||
true,
|
||||
)
|
||||
.await?;
|
||||
let (manifest, _) = client.download_manifest().await?;
|
||||
|
||||
match path {
|
||||
ExtractPath::Pxar(archive_name, path) => {
|
||||
let client = connect(&repo)?;
|
||||
let client = BackupReader::start(
|
||||
client,
|
||||
crypt_config.clone(),
|
||||
repo.store(),
|
||||
&snapshot.group().backup_type(),
|
||||
&snapshot.group().backup_id(),
|
||||
snapshot.backup_time(),
|
||||
true,
|
||||
)
|
||||
.await?;
|
||||
let (manifest, _) = client.download_manifest().await?;
|
||||
let file_info = manifest.lookup_file_info(&archive_name)?;
|
||||
let index = client
|
||||
.download_dynamic_index(&manifest, &archive_name)
|
||||
@ -344,31 +350,33 @@ async fn extract(
|
||||
let archive_size = reader.archive_size();
|
||||
let reader = LocalDynamicReadAt::new(reader);
|
||||
let decoder = Accessor::new(reader, archive_size).await?;
|
||||
extract_to_target(decoder, &path, target, verbose).await?;
|
||||
}
|
||||
ExtractPath::VM(file, path) => {
|
||||
let details = SnapRestoreDetails {
|
||||
manifest,
|
||||
repo,
|
||||
snapshot,
|
||||
};
|
||||
let driver: Option<BlockDriverType> = match param.get("driver") {
|
||||
Some(drv) => Some(serde_json::from_value(drv.clone())?),
|
||||
None => None,
|
||||
};
|
||||
|
||||
let root = decoder.open_root().await?;
|
||||
let file = root
|
||||
.lookup(OsStr::from_bytes(&path))
|
||||
.await?
|
||||
.ok_or(format_err!("error opening '{:?}'", path))?;
|
||||
if let Some(mut target) = target {
|
||||
let reader = data_extract(driver, details, file, path.clone(), true).await?;
|
||||
let decoder = Decoder::from_tokio(reader).await?;
|
||||
extract_sub_dir_seq(&target, decoder, verbose).await?;
|
||||
|
||||
if let Some(target) = target {
|
||||
extract_sub_dir(target, decoder, OsStr::from_bytes(&path), verbose).await?;
|
||||
// we extracted a .pxarexclude-cli file auto-generated by the VM when encoding the
|
||||
// archive, this file is of no use for the user, so try to remove it
|
||||
target.push(".pxarexclude-cli");
|
||||
std::fs::remove_file(target).map_err(|e| {
|
||||
format_err!("unable to remove temporary .pxarexclude-cli file - {}", e)
|
||||
})?;
|
||||
} else {
|
||||
match file.kind() {
|
||||
pxar::EntryKind::File { .. } => {
|
||||
tokio::io::copy(&mut file.contents().await?, &mut tokio::io::stdout())
|
||||
.await?;
|
||||
}
|
||||
_ => {
|
||||
create_zip(
|
||||
tokio::io::stdout(),
|
||||
decoder,
|
||||
OsStr::from_bytes(&path),
|
||||
verbose,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
let mut reader = data_extract(driver, details, file, path.clone(), false).await?;
|
||||
tokio::io::copy(&mut reader, &mut tokio::io::stdout()).await?;
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
@ -379,6 +387,43 @@ async fn extract(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn extract_to_target<T>(
|
||||
decoder: Accessor<T>,
|
||||
path: &[u8],
|
||||
target: Option<PathBuf>,
|
||||
verbose: bool,
|
||||
) -> Result<(), Error>
|
||||
where
|
||||
T: pxar::accessor::ReadAt + Clone + Send + Sync + Unpin + 'static,
|
||||
{
|
||||
let root = decoder.open_root().await?;
|
||||
let file = root
|
||||
.lookup(OsStr::from_bytes(&path))
|
||||
.await?
|
||||
.ok_or_else(|| format_err!("error opening '{:?}'", path))?;
|
||||
|
||||
if let Some(target) = target {
|
||||
extract_sub_dir(target, decoder, OsStr::from_bytes(&path), verbose).await?;
|
||||
} else {
|
||||
match file.kind() {
|
||||
pxar::EntryKind::File { .. } => {
|
||||
tokio::io::copy(&mut file.contents().await?, &mut tokio::io::stdout()).await?;
|
||||
}
|
||||
_ => {
|
||||
create_zip(
|
||||
tokio::io::stdout(),
|
||||
decoder,
|
||||
OsStr::from_bytes(&path),
|
||||
verbose,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let list_cmd_def = CliCommand::new(&API_METHOD_LIST)
|
||||
.arg_param(&["snapshot", "path"])
|
||||
|
Reference in New Issue
Block a user