From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by lists.proxmox.com (Postfix) with ESMTPS id 909246A663 for ; Tue, 16 Feb 2021 18:08:25 +0100 (CET) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 873F41939E for ; Tue, 16 Feb 2021 18:07:55 +0100 (CET) Received: from proxmox-new.maurer-it.com (proxmox-new.maurer-it.com [212.186.127.180]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by firstgate.proxmox.com (Proxmox) with ESMTPS id B683018E9D for ; Tue, 16 Feb 2021 18:07:34 +0100 (CET) Received: from proxmox-new.maurer-it.com (localhost.localdomain [127.0.0.1]) by proxmox-new.maurer-it.com (Proxmox) with ESMTP id 826E0461C0 for ; Tue, 16 Feb 2021 18:07:34 +0100 (CET) From: Stefan Reiter To: pbs-devel@lists.proxmox.com Date: Tue, 16 Feb 2021 18:06:59 +0100 Message-Id: <20210216170710.31767-12-s.reiter@proxmox.com> X-Mailer: git-send-email 2.20.1 In-Reply-To: <20210216170710.31767-1-s.reiter@proxmox.com> References: <20210216170710.31767-1-s.reiter@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-SPAM-LEVEL: Spam detection results: 0 AWL -0.427 Adjusted score from AWL reputation of From: address KAM_ASCII_DIVIDERS 0.8 Spam that uses ascii formatting tricks KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment RCVD_IN_DNSWL_MED -2.3 Sender listed at https://www.dnswl.org/, medium trust SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record URIBL_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to URIBL was blocked. See http://wiki.apache.org/spamassassin/DnsBlocklists#dnsbl-block for more information. [control.in, api2.rs, proxmox-file-restore.rs, elf-strip-unused-dependencies.sh, case.edu] Subject: [pbs-devel] [PATCH proxmox-backup 11/22] file-restore: add binary and basic commands X-BeenThere: pbs-devel@lists.proxmox.com X-Mailman-Version: 2.1.29 Precedence: list List-Id: Proxmox Backup Server development discussion List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , X-List-Received-Date: Tue, 16 Feb 2021 17:08:25 -0000 From: Dominik Csapak For now it only supports 'list' and 'extract' commands for 'pxar.didx' files. This should be the foundation for a general file-restore interface that is shared with block-level snapshots. This is packaged as a seperate .deb file, since for block level restore it will need to depend on pve-qemu-kvm, which we want to seperate from proxmox-backup-client. [original code for proxmox-file-restore.rs] Signed-off-by: Dominik Csapak [code cleanups/clippy, use helpers::list_dir_content/ArchiveEntry, no /block subdir for .fidx files, seperate binary and package] Signed-off-by: Stefan Reiter --- Cargo.toml | 2 +- Makefile | 9 +- debian/control | 11 + debian/control.in | 10 + debian/proxmox-file-restore.bash-completion | 1 + debian/proxmox-file-restore.bc | 8 + debian/proxmox-file-restore.install | 3 + debian/proxmox-file-restore.triggers | 1 + debian/rules | 7 +- docs/Makefile | 10 +- docs/command-line-tools.rst | 5 + docs/proxmox-file-restore/description.rst | 4 + docs/proxmox-file-restore/man1.rst | 28 ++ src/api2.rs | 2 +- src/bin/proxmox-file-restore.rs | 342 ++++++++++++++++++++ zsh-completions/_proxmox-file-restore | 13 + 16 files changed, 449 insertions(+), 7 deletions(-) create mode 100644 debian/proxmox-file-restore.bash-completion create mode 100644 debian/proxmox-file-restore.bc create mode 100644 debian/proxmox-file-restore.install create mode 100644 debian/proxmox-file-restore.triggers create mode 100644 docs/proxmox-file-restore/description.rst create mode 100644 docs/proxmox-file-restore/man1.rst create mode 100644 src/bin/proxmox-file-restore.rs create mode 100644 zsh-completions/_proxmox-file-restore diff --git a/Cargo.toml b/Cargo.toml index a436e1ad..28ca8e64 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -60,7 +60,7 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" siphasher = "0.3" syslog = "4.0" -tokio = { version = "1.0", features = [ "fs", "io-util", "macros", "net", "parking_lot", "process", "rt", "rt-multi-thread", "signal", "time" ] } +tokio = { version = "1.0", features = [ "fs", "io-util", "io-std", "macros", "net", "parking_lot", "process", "rt", "rt-multi-thread", "signal", "time" ] } tokio-openssl = "0.6.1" tokio-stream = "0.1.0" tokio-util = { version = "0.6", features = [ "codec" ] } diff --git a/Makefile b/Makefile index b2ef9d32..3b865083 100644 --- a/Makefile +++ b/Makefile @@ -9,6 +9,7 @@ SUBDIRS := etc www docs # Binaries usable by users USR_BIN := \ proxmox-backup-client \ + proxmox-file-restore \ pxar \ pmtx \ pmt @@ -46,9 +47,12 @@ SERVER_DEB=${PACKAGE}-server_${DEB_VERSION}_${ARCH}.deb SERVER_DBG_DEB=${PACKAGE}-server-dbgsym_${DEB_VERSION}_${ARCH}.deb CLIENT_DEB=${PACKAGE}-client_${DEB_VERSION}_${ARCH}.deb CLIENT_DBG_DEB=${PACKAGE}-client-dbgsym_${DEB_VERSION}_${ARCH}.deb +RESTORE_DEB=proxmox-file-restore_${DEB_VERSION}_${ARCH}.deb +RESTORE_DBG_DEB=proxmox-file-restore-dbgsym_${DEB_VERSION}_${ARCH}.deb DOC_DEB=${PACKAGE}-docs_${DEB_VERSION}_all.deb -DEBS=${SERVER_DEB} ${SERVER_DBG_DEB} ${CLIENT_DEB} ${CLIENT_DBG_DEB} +DEBS=${SERVER_DEB} ${SERVER_DBG_DEB} ${CLIENT_DEB} ${CLIENT_DBG_DEB} \ + ${RESTORE_DEB} ${RESTORE_DBG_DEB} DSC = rust-${PACKAGE}_${DEB_VERSION}.dsc @@ -151,8 +155,9 @@ install: $(COMPILED_BINS) $(MAKE) -C docs install .PHONY: upload -upload: ${SERVER_DEB} ${CLIENT_DEB} ${DOC_DEB} +upload: ${SERVER_DEB} ${CLIENT_DEB} ${RESTORE_DEB} ${DOC_DEB} # check if working directory is clean git diff --exit-code --stat && git diff --exit-code --stat --staged tar cf - ${SERVER_DEB} ${SERVER_DBG_DEB} ${DOC_DEB} | ssh -X repoman@repo.proxmox.com upload --product pbs --dist buster tar cf - ${CLIENT_DEB} ${CLIENT_DBG_DEB} | ssh -X repoman@repo.proxmox.com upload --product "pbs,pve,pmg" --dist buster + tar cf - ${RESTORE_DEB} ${RESTORE_DBG_DEB} | ssh -X repoman@repo.proxmox.com upload --product "pbs,pve,pmg" --dist buster diff --git a/debian/control b/debian/control index c0bc61bc..57d47a85 100644 --- a/debian/control +++ b/debian/control @@ -52,6 +52,7 @@ Build-Depends: debhelper (>= 11), librust-syslog-4+default-dev, librust-tokio-1+default-dev, librust-tokio-1+fs-dev, + librust-tokio-1+io-std-dev, librust-tokio-1+io-util-dev, librust-tokio-1+macros-dev, librust-tokio-1+net-dev, @@ -145,3 +146,13 @@ Depends: libjs-extjs, Architecture: all Description: Proxmox Backup Documentation This package contains the Proxmox Backup Documentation files. + +Package: proxmox-file-restore +Architecture: any +Depends: ${misc:Depends}, + ${shlibs:Depends}, +Recommends: pve-qemu-kvm (>= 5.0.0-9), +Description: PBS single file restore for pxar and block device backups + This package contains the Proxmox Backup single file restore client for + restoring individual files and folders from both host/container and VM/block + device backups. It includes a block device restore driver using QEMU. diff --git a/debian/control.in b/debian/control.in index b4b4d22e..f9fb8fe4 100644 --- a/debian/control.in +++ b/debian/control.in @@ -42,3 +42,13 @@ Depends: libjs-extjs, Architecture: all Description: Proxmox Backup Documentation This package contains the Proxmox Backup Documentation files. + +Package: proxmox-file-restore +Architecture: any +Depends: ${misc:Depends}, + ${shlibs:Depends}, +Recommends: pve-qemu-kvm (>= 5.0.0-9), +Description: PBS single file restore for pxar and block device backups + This package contains the Proxmox Backup single file restore client for + restoring individual files and folders from both host/container and VM/block + device backups. It includes a block device restore driver using QEMU. diff --git a/debian/proxmox-file-restore.bash-completion b/debian/proxmox-file-restore.bash-completion new file mode 100644 index 00000000..7160209c --- /dev/null +++ b/debian/proxmox-file-restore.bash-completion @@ -0,0 +1 @@ +debian/proxmox-file-restore.bc proxmox-file-restore diff --git a/debian/proxmox-file-restore.bc b/debian/proxmox-file-restore.bc new file mode 100644 index 00000000..646ebdd2 --- /dev/null +++ b/debian/proxmox-file-restore.bc @@ -0,0 +1,8 @@ +# proxmox-file-restore bash completion + +# see http://tiswww.case.edu/php/chet/bash/FAQ +# and __ltrim_colon_completions() in /usr/share/bash-completion/bash_completion +# this modifies global var, but I found no better way +COMP_WORDBREAKS=${COMP_WORDBREAKS//:} + +complete -C 'proxmox-file-restore bashcomplete' proxmox-file-restore diff --git a/debian/proxmox-file-restore.install b/debian/proxmox-file-restore.install new file mode 100644 index 00000000..2082e46b --- /dev/null +++ b/debian/proxmox-file-restore.install @@ -0,0 +1,3 @@ +usr/bin/proxmox-file-restore +usr/share/man/man1/proxmox-file-restore.1 +usr/share/zsh/vendor-completions/_proxmox-file-restore diff --git a/debian/proxmox-file-restore.triggers b/debian/proxmox-file-restore.triggers new file mode 100644 index 00000000..998cda4b --- /dev/null +++ b/debian/proxmox-file-restore.triggers @@ -0,0 +1 @@ +interest-noawait pbs-file-restore-initramfs diff --git a/debian/rules b/debian/rules index 22671c0a..ce2db72e 100755 --- a/debian/rules +++ b/debian/rules @@ -52,8 +52,11 @@ override_dh_dwz: override_dh_strip: dh_strip - for exe in $$(find debian/proxmox-backup-client/usr \ - debian/proxmox-backup-server/usr -executable -type f); do \ + for exe in $$(find \ + debian/proxmox-backup-client/usr \ + debian/proxmox-backup-server/usr \ + debian/proxmox-file-restore/usr \ + -executable -type f); do \ debian/scripts/elf-strip-unused-dependencies.sh "$$exe" || true; \ done diff --git a/docs/Makefile b/docs/Makefile index 4dc0019b..f6af8916 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -5,6 +5,7 @@ GENERATED_SYNOPSIS := \ proxmox-backup-client/synopsis.rst \ proxmox-backup-client/catalog-shell-synopsis.rst \ proxmox-backup-manager/synopsis.rst \ + proxmox-file-restore/synopsis.rst \ pxar/synopsis.rst \ pmtx/synopsis.rst \ pmt/synopsis.rst \ @@ -27,7 +28,8 @@ MAN1_PAGES := \ proxmox-tape.1 \ proxmox-backup-proxy.1 \ proxmox-backup-client.1 \ - proxmox-backup-manager.1 + proxmox-backup-manager.1 \ + proxmox-file-restore.1 MAN5_PAGES := \ media-pool.cfg.5 \ @@ -185,6 +187,12 @@ proxmox-backup-manager.1: proxmox-backup-manager/man1.rst proxmox-backup-manage proxmox-backup-proxy.1: proxmox-backup-proxy/man1.rst proxmox-backup-proxy/description.rst rst2man $< >$@ +proxmox-file-restore/synopsis.rst: ${COMPILEDIR}/proxmox-file-restore + ${COMPILEDIR}/proxmox-file-restore printdoc > proxmox-file-restore/synopsis.rst + +proxmox-file-restore.1: proxmox-file-restore/man1.rst proxmox-file-restore/description.rst proxmox-file-restore/synopsis.rst + rst2man $< >$@ + .PHONY: onlinehelpinfo onlinehelpinfo: @echo "Generating OnlineHelpInfo.js..." diff --git a/docs/command-line-tools.rst b/docs/command-line-tools.rst index 9b0a1290..bf3a92cc 100644 --- a/docs/command-line-tools.rst +++ b/docs/command-line-tools.rst @@ -6,6 +6,11 @@ Command Line Tools .. include:: proxmox-backup-client/description.rst +``proxmox-file-restore`` +~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. include:: proxmox-file-restore/description.rst + ``proxmox-backup-manager`` ~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/proxmox-file-restore/description.rst b/docs/proxmox-file-restore/description.rst new file mode 100644 index 00000000..34872663 --- /dev/null +++ b/docs/proxmox-file-restore/description.rst @@ -0,0 +1,4 @@ +This is just a test. + +.. NOTE:: No further info. + diff --git a/docs/proxmox-file-restore/man1.rst b/docs/proxmox-file-restore/man1.rst new file mode 100644 index 00000000..fe3625b1 --- /dev/null +++ b/docs/proxmox-file-restore/man1.rst @@ -0,0 +1,28 @@ +========================== +proxmox-file-restore +========================== + +.. include:: ../epilog.rst + +----------------------------------------------------------------------- +Command line tool for restoring files and directories from PBS archives +----------------------------------------------------------------------- + +:Author: |AUTHOR| +:Version: Version |VERSION| +:Manual section: 1 + + +Synopsis +========== + +.. include:: synopsis.rst + + +Description +============ + +.. include:: description.rst + + +.. include:: ../pbs-copyright.rst diff --git a/src/api2.rs b/src/api2.rs index b7230f75..132e2c2a 100644 --- a/src/api2.rs +++ b/src/api2.rs @@ -12,7 +12,7 @@ pub mod version; pub mod ping; pub mod pull; pub mod tape; -mod helpers; +pub mod helpers; use proxmox::api::router::SubdirMap; use proxmox::api::Router; diff --git a/src/bin/proxmox-file-restore.rs b/src/bin/proxmox-file-restore.rs new file mode 100644 index 00000000..f2d2ce3a --- /dev/null +++ b/src/bin/proxmox-file-restore.rs @@ -0,0 +1,342 @@ +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::Value; + +use proxmox::api::{ + api, + cli::{run_cli_command, CliCommand, CliCommandMap, CliEnvironment}, +}; +use pxar::accessor::aio::Accessor; + +use proxmox_backup::api2::{helpers, types::ArchiveEntry}; +use proxmox_backup::backup::{ + decrypt_key, BackupDir, BufferedDynamicReader, CatalogReader, CryptConfig, CryptMode, + DirEntryAttribute, IndexFile, LocalDynamicReadAt, CATALOG_NAME, +}; +use proxmox_backup::client::{BackupReader, RemoteChunkReader}; +use proxmox_backup::pxar::{create_zip, extract_sub_dir}; +use proxmox_backup::tools; + +// use "pub" so rust doesn't complain about "unused" functions in the module +pub mod proxmox_client_tools; +use proxmox_client_tools::{ + complete_group_or_snapshot, complete_repository, connect, extract_repository_from_value, key, + key::{crypto_parameters, format_key_source}, + KEYFD_SCHEMA, KEYFILE_SCHEMA, REPO_URL_SCHEMA, +}; + +enum ExtractPath { + ListArchives, + Pxar(String, Vec), +} + +fn parse_path(path: String, base64: bool) -> Result { + let mut bytes = if base64 { + base64::decode(path)? + } else { + path.into_bytes() + }; + + if bytes == b"/" { + return Ok(ExtractPath::ListArchives); + } + + while bytes.len() > 0 && 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 { + bail!("'{}' is not supported for file-restore", file); + } +} + +#[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, + }, + } + } +)] +/// List a directory from a backup snapshot. +async fn list(param: Value) -> Result, Error> { + let repo = extract_repository_from_value(¶m)?; + let base64 = param["base64"].as_bool().unwrap_or(false); + let path = parse_path( + tools::required_string_param(¶m, "path")?.to_string(), + base64, + )?; + let snapshot: BackupDir = tools::required_string_param(¶m, "snapshot")?.parse()?; + + let crypto = crypto_parameters(¶m)?; + let crypt_config = match crypto.enc_key { + None => None, + Some(ref key) => { + let (key, _, _) = + decrypt_key(&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))?; + + match path { + ExtractPath::ListArchives => { + let mut entries = vec![]; + for file in manifest.files() { + match file.filename.rsplitn(2, '.').next().unwrap() { + "didx" => {} + "fidx" => {} + _ => continue, // ignore all non fidx/didx + } + let path = format!("/{}", file.filename); + let attr = DirEntryAttribute::Directory { start: 0 }; + entries.push(ArchiveEntry::new(path.as_bytes(), &attr)); + } + + 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); + + helpers::list_dir_content(&mut catalog_reader, &fullpath) + } + } +} + +#[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, + } + } + } +)] +/// Restore files from a backup snapshot. +async fn extract(param: Value) -> Result { + let repo = extract_repository_from_value(¶m)?; + let verbose = param["verbose"].as_bool().unwrap_or(false); + let base64 = param["base64"].as_bool().unwrap_or(false); + let orig_path = tools::required_string_param(¶m, "path")?.to_string(); + let path = parse_path(orig_path.clone(), base64)?; + + let target = match param["target"].as_str() { + Some(target) if target == "-" => None, + Some(target) => Some(PathBuf::from(target)), + None => Some(std::env::current_dir()?), + }; + + let snapshot: BackupDir = tools::required_string_param(¶m, "snapshot")?.parse()?; + + let crypto = crypto_parameters(¶m)?; + let crypt_config = match crypto.enc_key { + None => None, + Some(ref key) => { + let (key, _, _) = + decrypt_key(&key.key, &key::get_encryption_key_password).map_err(|err| { + eprintln!("{}", format_key_source(&key.source, "encryption")); + err + })?; + Some(Arc::new(CryptConfig::new(key)?)) + } + }; + + 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) + .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?; + + 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(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?; + } + } + } + } + _ => { + bail!("cannot extract '{}'", orig_path); + } + } + + Ok(Value::Null) +} + +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", tools::complete_file_name); + + let cmd_def = CliCommandMap::new() + .insert("list", list_cmd_def) + .insert("extract", restore_cmd_def); + + let rpcenv = CliEnvironment::new(); + run_cli_command( + cmd_def, + rpcenv, + Some(|future| proxmox_backup::tools::runtime::main(future)), + ); +} diff --git a/zsh-completions/_proxmox-file-restore b/zsh-completions/_proxmox-file-restore new file mode 100644 index 00000000..e2e48c7a --- /dev/null +++ b/zsh-completions/_proxmox-file-restore @@ -0,0 +1,13 @@ +#compdef _proxmox-backup-client() proxmox-backup-client + +function _proxmox-backup-client() { + local cwords line point cmd curr prev + cworkds=${#words[@]} + line=$words + point=${#line} + cmd=${words[1]} + curr=${words[cwords]} + prev=${words[cwords-1]} + compadd -- $(COMP_CWORD="$cwords" COMP_LINE="$line" COMP_POINT="$point" \ + proxmox-file-restore bashcomplete "$cmd" "$curr" "$prev") +} -- 2.20.1