diff --git a/Cargo.toml b/Cargo.toml index fa574c9..fb2c641 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,7 @@ edition = "2021" [dependencies] anyhow = "1.0" bincode = "1.3" -clap = { version = "4.0.32", features = ["cargo", "env"] } +pico-args = "0.4" md5 = "0.7.0" scopeguard = "1.1.0" serde = "1.0" diff --git a/src/main.rs b/src/main.rs index e2aed8e..26b0ae9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,91 +1,106 @@ -use anyhow::{Context, Error}; -use clap::{command, Arg, ArgAction}; +use std::ffi::OsString; + +use anyhow::{bail, Context, Error}; use proxmox_sys::linux::tty; mod vma; mod vma2pbs; use vma2pbs::{backup_vma_to_pbs, BackupVmaToPbsArgs}; -fn main() -> Result<(), Error> { - let matches = command!() - .arg( - Arg::new("repository") - .long("repository") - .value_name("auth_id@host:port:datastore") - .help("Repository URL") - .required(true), - ) - .arg( - Arg::new("vmid") - .long("vmid") - .value_name("VMID") - .help("Backup ID") - .required(true), - ) - .arg( - Arg::new("fingerprint") - .long("fingerprint") - .value_name("FINGERPRINT") - .help("Proxmox Backup Server Fingerprint") - .env("PBS_FINGERPRINT"), - ) - .arg( - Arg::new("keyfile") - .long("keyfile") - .value_name("KEYFILE") - .help("Key file"), - ) - .arg( - Arg::new("master_keyfile") - .long("master_keyfile") - .value_name("MASTER_KEYFILE") - .help("Master key file"), - ) - .arg( - Arg::new("compress") - .long("compress") - .short('c') - .help("Compress the Backup") - .action(ArgAction::SetTrue), - ) - .arg( - Arg::new("encrypt") - .long("encrypt") - .short('e') - .help("Encrypt the Backup") - .action(ArgAction::SetTrue), - ) - .arg( - Arg::new("password-file") - .long("password-file") - .value_name("PASSWORD_FILE") - .help("Password file"), - ) - .arg( - Arg::new("key-password-file") - .long("key-password-file") - .value_name("KEY_PASSWORD_FILE") - .help("Key password file"), - ) - .arg(Arg::new("vma_file")) - .get_matches(); +const CMD_HELP: &str = "\ +Usage: vma-to-pbs [OPTIONS] --repository --vmid [vma_file] - let pbs_repository = matches.get_one::("repository").unwrap().to_string(); - let vmid = matches.get_one::("vmid").unwrap().to_string(); +Arguments: + [vma_file] - let fingerprint = matches - .get_one::("fingerprint") - .context("Fingerprint not set. Use $PBS_FINGERPRINT or --fingerprint")? - .to_string(); +Options: + --repository + Repository URL + --vmid + Backup ID + --fingerprint + Proxmox Backup Server Fingerprint [env: PBS_FINGERPRINT=] + --keyfile + Key file + --master_keyfile + Master key file + -c, --compress + Compress the Backup + -e, --encrypt + Encrypt the Backup + --password_file + Password file + --key_password_file + Key password file + -h, --help + Print help + -V, --version + Print version +"; - let keyfile = matches.get_one::("keyfile"); - let master_keyfile = matches.get_one::("master_keyfile"); - let compress = matches.get_flag("compress"); - let encrypt = matches.get_flag("encrypt"); +fn parse_args() -> Result { + let mut args: Vec<_> = std::env::args_os().collect(); + args.remove(0); // remove the executable path. - let vma_file_path = matches.get_one::("vma_file"); + let mut first_later_args_index = 0; + let options = ["-h", "--help", "-c", "--compress", "-e", "--encrypt"]; - let password_file = matches.get_one::("password-file"); + for (i, arg) in args.iter().enumerate() { + if let Some(arg) = arg.to_str() { + if arg.starts_with('-') { + if arg == "--" { + args.remove(i); + first_later_args_index = i; + break; + } + + first_later_args_index = i + 1; + + if !options.contains(&arg) { + first_later_args_index += 1; + } + } + } + } + + let forwarded_args = if first_later_args_index > args.len() { + Vec::new() + } else { + args.split_off(first_later_args_index) + }; + + let mut args = pico_args::Arguments::from_vec(args); + + if args.contains(["-h", "--help"]) { + print!("{CMD_HELP}"); + std::process::exit(0); + } + + let pbs_repository = args.value_from_str("--repository")?; + let vmid = args.value_from_str("--vmid")?; + let fingerprint = args.opt_value_from_str("--fingerprint")?; + let keyfile = args.opt_value_from_str("--keyfile")?; + let master_keyfile = args.opt_value_from_str("--master_keyfile")?; + let compress = args.contains(["-c", "--compress"]); + let encrypt = args.contains(["-e", "--encrypt"]); + let password_file: Option = args.opt_value_from_str("--password-file")?; + let key_password_file: Option = args.opt_value_from_str("--key-password-file")?; + + if !args.finish().is_empty() { + bail!("unexpected extra arguments, use '-h' for usage"); + } + + let fingerprint = match fingerprint { + Some(v) => v, + None => std::env::var("PBS_FINGERPRINT") + .context("Fingerprint not set. Use $PBS_FINGERPRINT or --fingerprint")?, + }; + + if forwarded_args.len() > 1 { + bail!("too many arguments"); + } + + let vma_file_path = forwarded_args.first(); let pbs_password = match password_file { Some(password_file) => { @@ -101,46 +116,65 @@ fn main() -> Result<(), Error> { password } - None => String::from_utf8(tty::read_password("Password: ")?)?, + None => { + if vma_file_path.is_none() { + bail!( + "Please use --password-file to provide the password \ + when passing the VMA file to stdin" + ); + } + + String::from_utf8(tty::read_password("Password: ")?)? + } }; let key_password = match keyfile { - Some(_) => { - let key_password_file = matches.get_one::("key_password_file"); + Some(_) => Some(match key_password_file { + Some(key_password_file) => { + let mut key_password = std::fs::read_to_string(key_password_file) + .context("Could not read key password file")?; - Some(match key_password_file { - Some(key_password_file) => { - let mut key_password = std::fs::read_to_string(key_password_file) - .context("Could not read key password file")?; - - if key_password.ends_with('\n') || key_password.ends_with('\r') { + if key_password.ends_with('\n') || key_password.ends_with('\r') { + key_password.pop(); + if key_password.ends_with('\r') { key_password.pop(); - if key_password.ends_with('\r') { - key_password.pop(); - } } - - key_password } - None => String::from_utf8(tty::read_password("Key Password: ")?)?, - }) - } + + key_password + } + None => { + if vma_file_path.is_none() { + bail!( + "Please use --key-password-file to provide the password \ + when passing the VMA file to stdin" + ); + } + + String::from_utf8(tty::read_password("Key Password: ")?)? + } + }), None => None, }; - let args = BackupVmaToPbsArgs { + let options = BackupVmaToPbsArgs { vma_file_path: vma_file_path.cloned(), pbs_repository, backup_id: vmid, pbs_password, - keyfile: keyfile.cloned(), + keyfile, key_password, - master_keyfile: master_keyfile.cloned(), + master_keyfile, fingerprint, compress, encrypt, }; + Ok(options) +} + +fn main() -> Result<(), Error> { + let args = parse_args()?; backup_vma_to_pbs(args)?; Ok(()) diff --git a/src/vma2pbs.rs b/src/vma2pbs.rs index 8422502..ed573d8 100644 --- a/src/vma2pbs.rs +++ b/src/vma2pbs.rs @@ -1,7 +1,7 @@ use std::cell::RefCell; use std::collections::hash_map::Entry; use std::collections::HashMap; -use std::ffi::{c_char, CStr, CString}; +use std::ffi::{c_char, CStr, CString, OsString}; use std::fs::File; use std::io::{stdin, BufRead, BufReader, Read}; use std::ptr; @@ -22,7 +22,7 @@ use crate::vma::VmaReader; const VMA_CLUSTER_SIZE: usize = 65536; pub struct BackupVmaToPbsArgs { - pub vma_file_path: Option, + pub vma_file_path: Option, pub pbs_repository: String, pub backup_id: String, pub pbs_password: String,