split proxmox-file-restore into its own crate
This also moves a couple of required utilities such as logrotate and some file descriptor methods to pbs-tools. Note that the logrotate usage and run-dir handling should be improved to work as a regular user as this *should* (IMHO) be a regular unprivileged command (including running qemu given the kvm privileges...) Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
This commit is contained in:
511
proxmox-file-restore/src/main.rs
Normal file
511
proxmox-file-restore/src/main.rs
Normal file
@ -0,0 +1,511 @@
|
||||
use std::ffi::OsStr;
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{bail, format_err, Error};
|
||||
use serde_json::{json, Value};
|
||||
|
||||
use proxmox::api::{
|
||||
api,
|
||||
cli::{
|
||||
default_table_format_options, format_and_print_result_full, get_output_format,
|
||||
run_cli_command, CliCommand, CliCommandMap, CliEnvironment, ColumnConfig, OUTPUT_FORMAT,
|
||||
},
|
||||
};
|
||||
use proxmox::tools::fs::{create_path, CreateOptions};
|
||||
use pxar::accessor::aio::Accessor;
|
||||
use pxar::decoder::aio::Decoder;
|
||||
|
||||
use pbs_api_types::CryptMode;
|
||||
use pbs_datastore::{CryptConfig, CATALOG_NAME};
|
||||
use pbs_datastore::backup_info::BackupDir;
|
||||
use pbs_datastore::catalog::{ArchiveEntry, CatalogReader, DirEntryAttribute};
|
||||
use pbs_datastore::dynamic_index::{BufferedDynamicReader, LocalDynamicReadAt};
|
||||
use pbs_datastore::index::IndexFile;
|
||||
use pbs_datastore::key_derivation::decrypt_key;
|
||||
use pbs_client::{BackupReader, RemoteChunkReader};
|
||||
use pbs_client::pxar::{create_zip, extract_sub_dir, extract_sub_dir_seq};
|
||||
use pbs_client::tools::{
|
||||
complete_group_or_snapshot, complete_repository, connect, extract_repository_from_value,
|
||||
key_source::{
|
||||
crypto_parameters_keep_fd, format_key_source, get_encryption_key_password, KEYFD_SCHEMA,
|
||||
KEYFILE_SCHEMA,
|
||||
},
|
||||
REPO_URL_SCHEMA,
|
||||
};
|
||||
|
||||
pub mod block_driver;
|
||||
pub use block_driver::*;
|
||||
|
||||
pub mod cpio;
|
||||
|
||||
mod qemu_helper;
|
||||
mod block_driver_qemu;
|
||||
|
||||
enum ExtractPath {
|
||||
ListArchives,
|
||||
Pxar(String, Vec<u8>),
|
||||
VM(String, Vec<u8>),
|
||||
}
|
||||
|
||||
fn parse_path(path: String, base64: bool) -> Result<ExtractPath, Error> {
|
||||
let mut bytes = if base64 {
|
||||
base64::decode(&path)
|
||||
.map_err(|err| format_err!("Failed base64-decoding path '{}' - {}", path, err))?
|
||||
} else {
|
||||
path.into_bytes()
|
||||
};
|
||||
|
||||
if bytes == b"/" {
|
||||
return Ok(ExtractPath::ListArchives);
|
||||
}
|
||||
|
||||
while !bytes.is_empty() && bytes[0] == b'/' {
|
||||
bytes.remove(0);
|
||||
}
|
||||
|
||||
let (file, path) = {
|
||||
let slash_pos = bytes.iter().position(|c| *c == b'/').unwrap_or(bytes.len());
|
||||
let path = bytes.split_off(slash_pos);
|
||||
let file = String::from_utf8(bytes)?;
|
||||
(file, path)
|
||||
};
|
||||
|
||||
if file.ends_with(".pxar.didx") {
|
||||
Ok(ExtractPath::Pxar(file, path))
|
||||
} else if file.ends_with(".img.fidx") {
|
||||
Ok(ExtractPath::VM(file, path))
|
||||
} else {
|
||||
bail!("'{}' is not supported for file-restore", file);
|
||||
}
|
||||
}
|
||||
|
||||
fn keyfile_path(param: &Value) -> Option<String> {
|
||||
if let Some(Value::String(keyfile)) = param.get("keyfile") {
|
||||
return Some(keyfile.to_owned());
|
||||
}
|
||||
|
||||
if let Some(Value::Number(keyfd)) = param.get("keyfd") {
|
||||
return Some(format!("/dev/fd/{}", keyfd));
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
#[api(
|
||||
input: {
|
||||
properties: {
|
||||
repository: {
|
||||
schema: REPO_URL_SCHEMA,
|
||||
optional: true,
|
||||
},
|
||||
snapshot: {
|
||||
type: String,
|
||||
description: "Group/Snapshot path.",
|
||||
},
|
||||
"path": {
|
||||
description: "Path to restore. Directories will be restored as .zip files.",
|
||||
type: String,
|
||||
},
|
||||
"base64": {
|
||||
type: Boolean,
|
||||
description: "If set, 'path' will be interpreted as base64 encoded.",
|
||||
optional: true,
|
||||
default: false,
|
||||
},
|
||||
keyfile: {
|
||||
schema: KEYFILE_SCHEMA,
|
||||
optional: true,
|
||||
},
|
||||
"keyfd": {
|
||||
schema: KEYFD_SCHEMA,
|
||||
optional: true,
|
||||
},
|
||||
"crypt-mode": {
|
||||
type: CryptMode,
|
||||
optional: true,
|
||||
},
|
||||
"driver": {
|
||||
type: BlockDriverType,
|
||||
optional: true,
|
||||
},
|
||||
"output-format": {
|
||||
schema: OUTPUT_FORMAT,
|
||||
optional: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
returns: {
|
||||
description: "A list of elements under the given path",
|
||||
type: Array,
|
||||
items: {
|
||||
type: ArchiveEntry,
|
||||
}
|
||||
}
|
||||
)]
|
||||
/// List a directory from a backup snapshot.
|
||||
async fn list(
|
||||
snapshot: String,
|
||||
path: String,
|
||||
base64: bool,
|
||||
param: Value,
|
||||
) -> Result<(), Error> {
|
||||
let repo = extract_repository_from_value(¶m)?;
|
||||
let snapshot: BackupDir = snapshot.parse()?;
|
||||
let path = parse_path(path, base64)?;
|
||||
|
||||
let keyfile = keyfile_path(¶m);
|
||||
let crypto = crypto_parameters_keep_fd(¶m)?;
|
||||
let crypt_config = match crypto.enc_key {
|
||||
None => None,
|
||||
Some(ref key) => {
|
||||
let (key, _, _) =
|
||||
decrypt_key(&key.key, &get_encryption_key_password).map_err(|err| {
|
||||
eprintln!("{}", format_key_source(&key.source, "encryption"));
|
||||
err
|
||||
})?;
|
||||
Some(Arc::new(CryptConfig::new(key)?))
|
||||
}
|
||||
};
|
||||
|
||||
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?;
|
||||
manifest.check_fingerprint(crypt_config.as_ref().map(Arc::as_ref))?;
|
||||
|
||||
let result = match path {
|
||||
ExtractPath::ListArchives => {
|
||||
let mut entries = vec![];
|
||||
for file in manifest.files() {
|
||||
if !file.filename.ends_with(".pxar.didx") && !file.filename.ends_with(".img.fidx") {
|
||||
continue;
|
||||
}
|
||||
let path = format!("/{}", file.filename);
|
||||
let attr = if file.filename.ends_with(".pxar.didx") {
|
||||
// a pxar file is a file archive, so it's root is also a directory root
|
||||
Some(&DirEntryAttribute::Directory { start: 0 })
|
||||
} else {
|
||||
None
|
||||
};
|
||||
entries.push(ArchiveEntry::new_with_size(path.as_bytes(), attr, Some(file.size)));
|
||||
}
|
||||
|
||||
Ok(entries)
|
||||
}
|
||||
ExtractPath::Pxar(file, mut path) => {
|
||||
let index = client
|
||||
.download_dynamic_index(&manifest, CATALOG_NAME)
|
||||
.await?;
|
||||
let most_used = index.find_most_used_chunks(8);
|
||||
let file_info = manifest.lookup_file_info(&CATALOG_NAME)?;
|
||||
let chunk_reader = RemoteChunkReader::new(
|
||||
client.clone(),
|
||||
crypt_config,
|
||||
file_info.chunk_crypt_mode(),
|
||||
most_used,
|
||||
);
|
||||
let reader = BufferedDynamicReader::new(index, chunk_reader);
|
||||
let mut catalog_reader = CatalogReader::new(reader);
|
||||
|
||||
let mut fullpath = file.into_bytes();
|
||||
fullpath.append(&mut path);
|
||||
|
||||
catalog_reader.list_dir_contents(&fullpath)
|
||||
}
|
||||
ExtractPath::VM(file, path) => {
|
||||
let details = SnapRestoreDetails {
|
||||
manifest,
|
||||
repo,
|
||||
snapshot,
|
||||
keyfile,
|
||||
};
|
||||
let driver: Option<BlockDriverType> = match param.get("driver") {
|
||||
Some(drv) => Some(serde_json::from_value(drv.clone())?),
|
||||
None => None,
|
||||
};
|
||||
data_list(driver, details, file, path).await
|
||||
}
|
||||
}?;
|
||||
|
||||
let options = default_table_format_options()
|
||||
.sortby("type", false)
|
||||
.sortby("text", false)
|
||||
.column(ColumnConfig::new("type"))
|
||||
.column(ColumnConfig::new("text").header("name"))
|
||||
.column(ColumnConfig::new("mtime").header("last modified"))
|
||||
.column(ColumnConfig::new("size"));
|
||||
|
||||
let output_format = get_output_format(¶m);
|
||||
format_and_print_result_full(
|
||||
&mut json!(result),
|
||||
&API_METHOD_LIST.returns,
|
||||
&output_format,
|
||||
&options,
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[api(
|
||||
input: {
|
||||
properties: {
|
||||
repository: {
|
||||
schema: REPO_URL_SCHEMA,
|
||||
optional: true,
|
||||
},
|
||||
snapshot: {
|
||||
type: String,
|
||||
description: "Group/Snapshot path.",
|
||||
},
|
||||
"path": {
|
||||
description: "Path to restore. Directories will be restored as .zip files if extracted to stdout.",
|
||||
type: String,
|
||||
},
|
||||
"base64": {
|
||||
type: Boolean,
|
||||
description: "If set, 'path' will be interpreted as base64 encoded.",
|
||||
optional: true,
|
||||
default: false,
|
||||
},
|
||||
target: {
|
||||
type: String,
|
||||
optional: true,
|
||||
description: "Target directory path. Use '-' to write to standard output.",
|
||||
},
|
||||
keyfile: {
|
||||
schema: KEYFILE_SCHEMA,
|
||||
optional: true,
|
||||
},
|
||||
"keyfd": {
|
||||
schema: KEYFD_SCHEMA,
|
||||
optional: true,
|
||||
},
|
||||
"crypt-mode": {
|
||||
type: CryptMode,
|
||||
optional: true,
|
||||
},
|
||||
verbose: {
|
||||
type: Boolean,
|
||||
description: "Print verbose information",
|
||||
optional: true,
|
||||
default: false,
|
||||
},
|
||||
"driver": {
|
||||
type: BlockDriverType,
|
||||
optional: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
)]
|
||||
/// Restore files from a backup snapshot.
|
||||
async fn extract(
|
||||
snapshot: String,
|
||||
path: String,
|
||||
base64: bool,
|
||||
target: Option<String>,
|
||||
verbose: bool,
|
||||
param: Value,
|
||||
) -> Result<(), Error> {
|
||||
let repo = extract_repository_from_value(¶m)?;
|
||||
let snapshot: BackupDir = snapshot.parse()?;
|
||||
let orig_path = path;
|
||||
let path = parse_path(orig_path.clone(), base64)?;
|
||||
|
||||
let target = match target {
|
||||
Some(target) if target == "-" => None,
|
||||
Some(target) => Some(PathBuf::from(target)),
|
||||
None => Some(std::env::current_dir()?),
|
||||
};
|
||||
|
||||
let keyfile = keyfile_path(¶m);
|
||||
let crypto = crypto_parameters_keep_fd(¶m)?;
|
||||
let crypt_config = match crypto.enc_key {
|
||||
None => None,
|
||||
Some(ref key) => {
|
||||
let (key, _, _) =
|
||||
decrypt_key(&key.key, &get_encryption_key_password).map_err(|err| {
|
||||
eprintln!("{}", format_key_source(&key.source, "encryption"));
|
||||
err
|
||||
})?;
|
||||
Some(Arc::new(CryptConfig::new(key)?))
|
||||
}
|
||||
};
|
||||
|
||||
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 file_info = manifest.lookup_file_info(&archive_name)?;
|
||||
let index = client
|
||||
.download_dynamic_index(&manifest, &archive_name)
|
||||
.await?;
|
||||
let most_used = index.find_most_used_chunks(8);
|
||||
let chunk_reader = RemoteChunkReader::new(
|
||||
client.clone(),
|
||||
crypt_config,
|
||||
file_info.chunk_crypt_mode(),
|
||||
most_used,
|
||||
);
|
||||
let reader = BufferedDynamicReader::new(index, chunk_reader);
|
||||
|
||||
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,
|
||||
keyfile,
|
||||
};
|
||||
let driver: Option<BlockDriverType> = match param.get("driver") {
|
||||
Some(drv) => Some(serde_json::from_value(drv.clone())?),
|
||||
None => None,
|
||||
};
|
||||
|
||||
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?;
|
||||
|
||||
// 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 {
|
||||
let mut reader = data_extract(driver, details, file, path.clone(), false).await?;
|
||||
tokio::io::copy(&mut reader, &mut tokio::io::stdout()).await?;
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
bail!("cannot extract '{}'", orig_path);
|
||||
}
|
||||
}
|
||||
|
||||
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 path = if path.is_empty() { b"/" } else { path };
|
||||
|
||||
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"])
|
||||
.completion_cb("repository", complete_repository)
|
||||
.completion_cb("snapshot", complete_group_or_snapshot);
|
||||
|
||||
let restore_cmd_def = CliCommand::new(&API_METHOD_EXTRACT)
|
||||
.arg_param(&["snapshot", "path", "target"])
|
||||
.completion_cb("repository", complete_repository)
|
||||
.completion_cb("snapshot", complete_group_or_snapshot)
|
||||
.completion_cb("target", pbs_tools::fs::complete_file_name);
|
||||
|
||||
let status_cmd_def = CliCommand::new(&API_METHOD_STATUS);
|
||||
let stop_cmd_def = CliCommand::new(&API_METHOD_STOP)
|
||||
.arg_param(&["name"])
|
||||
.completion_cb("name", complete_block_driver_ids);
|
||||
|
||||
let cmd_def = CliCommandMap::new()
|
||||
.insert("list", list_cmd_def)
|
||||
.insert("extract", restore_cmd_def)
|
||||
.insert("status", status_cmd_def)
|
||||
.insert("stop", stop_cmd_def);
|
||||
|
||||
let rpcenv = CliEnvironment::new();
|
||||
run_cli_command(
|
||||
cmd_def,
|
||||
rpcenv,
|
||||
Some(|future| pbs_runtime::main(future)),
|
||||
);
|
||||
}
|
||||
|
||||
/// Returns a runtime dir owned by the current user.
|
||||
/// Note that XDG_RUNTIME_DIR is not always available, especially for non-login users like
|
||||
/// "www-data", so we use a custom one in /run/proxmox-backup/<uid> instead.
|
||||
pub fn get_user_run_dir() -> Result<std::path::PathBuf, Error> {
|
||||
let uid = nix::unistd::Uid::current();
|
||||
let mut path: std::path::PathBuf = pbs_buildcfg::PROXMOX_BACKUP_RUN_DIR.into();
|
||||
path.push(uid.to_string());
|
||||
create_run_dir()?;
|
||||
std::fs::create_dir_all(&path)?;
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
/// FIXME: proxmox-file-restore should not depend on this!
|
||||
fn create_run_dir() -> Result<(), Error> {
|
||||
let backup_user = backup_user()?;
|
||||
let opts = CreateOptions::new()
|
||||
.owner(backup_user.uid)
|
||||
.group(backup_user.gid);
|
||||
let _: bool = create_path(pbs_buildcfg::PROXMOX_BACKUP_RUN_DIR_M!(), None, Some(opts))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Return User info for the 'backup' user (``getpwnam_r(3)``)
|
||||
pub fn backup_user() -> Result<nix::unistd::User, Error> {
|
||||
pbs_tools::sys::query_user(pbs_buildcfg::BACKUP_USER_NAME)?
|
||||
.ok_or_else(|| format_err!("Unable to lookup '{}' user.", pbs_buildcfg::BACKUP_USER_NAME))
|
||||
}
|
||||
|
Reference in New Issue
Block a user