2 use std
::os
::unix
::ffi
::OsStrExt
;
3 use std
::path
::PathBuf
;
6 use anyhow
::{bail, format_err, Error}
;
7 use serde_json
::{json, Value}
;
9 use proxmox_sys
::fs
::{create_path, CreateOptions}
;
10 use proxmox_router
::cli
::{
11 complete_file_name
, default_table_format_options
,
12 format_and_print_result_full
, get_output_format
,
14 CliCommand
, CliCommandMap
, CliEnvironment
, ColumnConfig
, OUTPUT_FORMAT
,
16 use proxmox_schema
::api
;
17 use pxar
::accessor
::aio
::Accessor
;
18 use pxar
::decoder
::aio
::Decoder
;
20 use pbs_tools
::crypt_config
::CryptConfig
;
21 use pbs_api_types
::CryptMode
;
22 use pbs_datastore
::CATALOG_NAME
;
23 use pbs_datastore
::backup_info
::BackupDir
;
24 use pbs_datastore
::catalog
::{ArchiveEntry, CatalogReader, DirEntryAttribute}
;
25 use pbs_datastore
::dynamic_index
::{BufferedDynamicReader, LocalDynamicReadAt}
;
26 use pbs_datastore
::index
::IndexFile
;
27 use pbs_config
::key_config
::decrypt_key
;
28 use pbs_client
::{BackupReader, RemoteChunkReader}
;
29 use pbs_client
::pxar
::{create_zip, extract_sub_dir, extract_sub_dir_seq}
;
30 use pbs_client
::tools
::{
31 complete_group_or_snapshot
, complete_repository
, connect
, extract_repository_from_value
,
33 crypto_parameters_keep_fd
, format_key_source
, get_encryption_key_password
, KEYFD_SCHEMA
,
40 pub use block_driver
::*;
45 mod block_driver_qemu
;
49 Pxar(String
, Vec
<u8>),
53 fn parse_path(path
: String
, base64
: bool
) -> Result
<ExtractPath
, Error
> {
54 let mut bytes
= if base64
{
56 .map_err(|err
| format_err
!("Failed base64-decoding path '{}' - {}", path
, err
))?
62 return Ok(ExtractPath
::ListArchives
);
65 while !bytes
.is_empty() && bytes
[0] == b'
/'
{
70 let slash_pos
= bytes
.iter().position(|c
| *c
== b'
/'
).unwrap_or(bytes
.len());
71 let path
= bytes
.split_off(slash_pos
);
72 let file
= String
::from_utf8(bytes
)?
;
76 if file
.ends_with(".pxar.didx") {
77 Ok(ExtractPath
::Pxar(file
, path
))
78 } else if file
.ends_with(".img.fidx") {
79 Ok(ExtractPath
::VM(file
, path
))
81 bail
!("'{}' is not supported for file-restore", file
);
85 fn keyfile_path(param
: &Value
) -> Option
<String
> {
86 if let Some(Value
::String(keyfile
)) = param
.get("keyfile") {
87 return Some(keyfile
.to_owned());
90 if let Some(Value
::Number(keyfd
)) = param
.get("keyfd") {
91 return Some(format
!("/dev/fd/{}", keyfd
));
101 schema
: REPO_URL_SCHEMA
,
106 description
: "Group/Snapshot path.",
109 description
: "Path to restore. Directories will be restored as .zip files.",
114 description
: "If set, 'path' will be interpreted as base64 encoded.",
119 schema
: KEYFILE_SCHEMA
,
123 schema
: KEYFD_SCHEMA
,
131 type: BlockDriverType
,
135 schema
: OUTPUT_FORMAT
,
141 description
: "A list of elements under the given path",
148 /// List a directory from a backup snapshot.
154 ) -> Result
<(), Error
> {
155 let repo
= extract_repository_from_value(¶m
)?
;
156 let snapshot
: BackupDir
= snapshot
.parse()?
;
157 let path
= parse_path(path
, base64
)?
;
159 let keyfile
= keyfile_path(¶m
);
160 let crypto
= crypto_parameters_keep_fd(¶m
)?
;
161 let crypt_config
= match crypto
.enc_key
{
165 decrypt_key(&key
.key
, &get_encryption_key_password
).map_err(|err
| {
166 eprintln
!("{}", format_key_source(&key
.source
, "encryption"));
169 Some(Arc
::new(CryptConfig
::new(key
)?
))
173 let client
= connect(&repo
)?
;
174 let client
= BackupReader
::start(
176 crypt_config
.clone(),
178 snapshot
.group().backup_type(),
179 snapshot
.group().backup_id(),
180 snapshot
.backup_time(),
185 let (manifest
, _
) = client
.download_manifest().await?
;
186 manifest
.check_fingerprint(crypt_config
.as_ref().map(Arc
::as_ref
))?
;
188 let result
= match path
{
189 ExtractPath
::ListArchives
=> {
190 let mut entries
= vec
![];
191 for file
in manifest
.files() {
192 if !file
.filename
.ends_with(".pxar.didx") && !file
.filename
.ends_with(".img.fidx") {
195 let path
= format
!("/{}", file
.filename
);
196 let attr
= if file
.filename
.ends_with(".pxar.didx") {
197 // a pxar file is a file archive, so it's root is also a directory root
198 Some(&DirEntryAttribute
::Directory { start: 0 }
)
202 entries
.push(ArchiveEntry
::new_with_size(path
.as_bytes(), attr
, Some(file
.size
)));
207 ExtractPath
::Pxar(file
, mut path
) => {
209 .download_dynamic_index(&manifest
, CATALOG_NAME
)
211 let most_used
= index
.find_most_used_chunks(8);
212 let file_info
= manifest
.lookup_file_info(CATALOG_NAME
)?
;
213 let chunk_reader
= RemoteChunkReader
::new(
216 file_info
.chunk_crypt_mode(),
219 let reader
= BufferedDynamicReader
::new(index
, chunk_reader
);
220 let mut catalog_reader
= CatalogReader
::new(reader
);
222 let mut fullpath
= file
.into_bytes();
223 fullpath
.append(&mut path
);
225 catalog_reader
.list_dir_contents(&fullpath
)
227 ExtractPath
::VM(file
, path
) => {
228 let details
= SnapRestoreDetails
{
234 let driver
: Option
<BlockDriverType
> = match param
.get("driver") {
235 Some(drv
) => Some(serde_json
::from_value(drv
.clone())?
),
238 data_list(driver
, details
, file
, path
).await
242 let options
= default_table_format_options()
243 .sortby("type", false)
244 .sortby("text", false)
245 .column(ColumnConfig
::new("type"))
246 .column(ColumnConfig
::new("text").header("name"))
247 .column(ColumnConfig
::new("mtime").header("last modified"))
248 .column(ColumnConfig
::new("size"));
250 let output_format
= get_output_format(¶m
);
251 format_and_print_result_full(
253 &API_METHOD_LIST
.returns
,
265 schema
: REPO_URL_SCHEMA
,
270 description
: "Group/Snapshot path.",
273 description
: "Path to restore. Directories will be restored as .zip files if extracted to stdout.",
278 description
: "If set, 'path' will be interpreted as base64 encoded.",
285 description
: "Target directory path. Use '-' to write to standard output.",
288 schema
: KEYFILE_SCHEMA
,
292 schema
: KEYFD_SCHEMA
,
301 description
: "Print verbose information",
306 type: BlockDriverType
,
312 /// Restore files from a backup snapshot.
317 target
: Option
<String
>,
320 ) -> Result
<(), Error
> {
321 let repo
= extract_repository_from_value(¶m
)?
;
322 let snapshot
: BackupDir
= snapshot
.parse()?
;
323 let orig_path
= path
;
324 let path
= parse_path(orig_path
.clone(), base64
)?
;
326 let target
= match target
{
327 Some(target
) if target
== "-" => None
,
328 Some(target
) => Some(PathBuf
::from(target
)),
329 None
=> Some(std
::env
::current_dir()?
),
332 let keyfile
= keyfile_path(¶m
);
333 let crypto
= crypto_parameters_keep_fd(¶m
)?
;
334 let crypt_config
= match crypto
.enc_key
{
338 decrypt_key(&key
.key
, &get_encryption_key_password
).map_err(|err
| {
339 eprintln
!("{}", format_key_source(&key
.source
, "encryption"));
342 Some(Arc
::new(CryptConfig
::new(key
)?
))
346 let client
= connect(&repo
)?
;
347 let client
= BackupReader
::start(
349 crypt_config
.clone(),
351 snapshot
.group().backup_type(),
352 snapshot
.group().backup_id(),
353 snapshot
.backup_time(),
357 let (manifest
, _
) = client
.download_manifest().await?
;
360 ExtractPath
::Pxar(archive_name
, path
) => {
361 let file_info
= manifest
.lookup_file_info(&archive_name
)?
;
363 .download_dynamic_index(&manifest
, &archive_name
)
365 let most_used
= index
.find_most_used_chunks(8);
366 let chunk_reader
= RemoteChunkReader
::new(
369 file_info
.chunk_crypt_mode(),
372 let reader
= BufferedDynamicReader
::new(index
, chunk_reader
);
374 let archive_size
= reader
.archive_size();
375 let reader
= LocalDynamicReadAt
::new(reader
);
376 let decoder
= Accessor
::new(reader
, archive_size
).await?
;
377 extract_to_target(decoder
, &path
, target
, verbose
).await?
;
379 ExtractPath
::VM(file
, path
) => {
380 let details
= SnapRestoreDetails
{
386 let driver
: Option
<BlockDriverType
> = match param
.get("driver") {
387 Some(drv
) => Some(serde_json
::from_value(drv
.clone())?
),
391 if let Some(mut target
) = target
{
392 let reader
= data_extract(driver
, details
, file
, path
.clone(), true).await?
;
393 let decoder
= Decoder
::from_tokio(reader
).await?
;
394 extract_sub_dir_seq(&target
, decoder
, verbose
).await?
;
396 // we extracted a .pxarexclude-cli file auto-generated by the VM when encoding the
397 // archive, this file is of no use for the user, so try to remove it
398 target
.push(".pxarexclude-cli");
399 std
::fs
::remove_file(target
).map_err(|e
| {
400 format_err
!("unable to remove temporary .pxarexclude-cli file - {}", e
)
403 let mut reader
= data_extract(driver
, details
, file
, path
.clone(), false).await?
;
404 tokio
::io
::copy(&mut reader
, &mut tokio
::io
::stdout()).await?
;
408 bail
!("cannot extract '{}'", orig_path
);
415 async
fn extract_to_target
<T
>(
416 decoder
: Accessor
<T
>,
418 target
: Option
<PathBuf
>,
420 ) -> Result
<(), Error
>
422 T
: pxar
::accessor
::ReadAt
+ Clone
+ Send
+ Sync
+ Unpin
+ '
static,
424 let path
= if path
.is_empty() { b"/" }
else { path }
;
426 let root
= decoder
.open_root().await?
;
428 .lookup(OsStr
::from_bytes(path
))
430 .ok_or_else(|| format_err
!("error opening '{:?}'", path
))?
;
432 if let Some(target
) = target
{
433 extract_sub_dir(target
, decoder
, OsStr
::from_bytes(path
), verbose
).await?
;
436 pxar
::EntryKind
::File { .. }
=> {
437 tokio
::io
::copy(&mut file
.contents().await?
, &mut tokio
::io
::stdout()).await?
;
443 OsStr
::from_bytes(path
),
455 let list_cmd_def
= CliCommand
::new(&API_METHOD_LIST
)
456 .arg_param(&["snapshot", "path"])
457 .completion_cb("repository", complete_repository
)
458 .completion_cb("snapshot", complete_group_or_snapshot
);
460 let restore_cmd_def
= CliCommand
::new(&API_METHOD_EXTRACT
)
461 .arg_param(&["snapshot", "path", "target"])
462 .completion_cb("repository", complete_repository
)
463 .completion_cb("snapshot", complete_group_or_snapshot
)
464 .completion_cb("target", complete_file_name
);
466 let status_cmd_def
= CliCommand
::new(&API_METHOD_STATUS
);
467 let stop_cmd_def
= CliCommand
::new(&API_METHOD_STOP
)
468 .arg_param(&["name"])
469 .completion_cb("name", complete_block_driver_ids
);
471 let cmd_def
= CliCommandMap
::new()
472 .insert("list", list_cmd_def
)
473 .insert("extract", restore_cmd_def
)
474 .insert("status", status_cmd_def
)
475 .insert("stop", stop_cmd_def
);
477 let rpcenv
= CliEnvironment
::new();
481 Some(|future
| proxmox_async
::runtime
::main(future
)),
485 /// Returns a runtime dir owned by the current user.
486 /// Note that XDG_RUNTIME_DIR is not always available, especially for non-login users like
487 /// "www-data", so we use a custom one in /run/proxmox-backup/<uid> instead.
488 pub fn get_user_run_dir() -> Result
<std
::path
::PathBuf
, Error
> {
489 let uid
= nix
::unistd
::Uid
::current();
490 let mut path
: std
::path
::PathBuf
= pbs_buildcfg
::PROXMOX_BACKUP_RUN_DIR
.into();
491 path
.push(uid
.to_string());
493 std
::fs
::create_dir_all(&path
)?
;
497 /// FIXME: proxmox-file-restore should not depend on this!
498 fn create_run_dir() -> Result
<(), Error
> {
499 let backup_user
= backup_user()?
;
500 let opts
= CreateOptions
::new()
501 .owner(backup_user
.uid
)
502 .group(backup_user
.gid
);
503 let _
: bool
= create_path(pbs_buildcfg
::PROXMOX_BACKUP_RUN_DIR_M
!(), None
, Some(opts
))?
;
507 /// Return User info for the 'backup' user (``getpwnam_r(3)``)
508 pub fn backup_user() -> Result
<nix
::unistd
::User
, Error
> {
509 nix
::unistd
::User
::from_name(pbs_buildcfg
::BACKUP_USER_NAME
)?
510 .ok_or_else(|| format_err
!("Unable to lookup '{}' user.", pbs_buildcfg
::BACKUP_USER_NAME
))