use std::path::PathBuf;
use anyhow::anyhow;
use anyhow::Result;
use clap::Args as Arguments;
use clap::Parser;
use clap::Subcommand;
use time::Duration;
fn parse_duration(s: &str) -> Result<Duration> {
let durations = [("d", 1), ("w", 7), ("m", 30), ("y", 365)];
for (suffix, multiplier) in &durations {
if let Some(base) = s.strip_suffix(suffix) {
if let Ok(count) = base.parse::<u64>() {
return Ok(Duration::days(i64::try_from(count)? * multiplier))
}
}
}
Err(anyhow!("invalid duration provided: {s}"))
}
fn parse_command(s: &str) -> Result<(String, Vec<String>)> {
let mut iter = s.split(' ');
let command = iter
.next()
.ok_or_else(|| anyhow!("provided command is empty"))?
.to_string();
let args = iter.map(str::to_string).collect();
Ok((command, args))
}
#[derive(Debug, Arguments)]
pub struct Tag {
#[clap(long = "tag", default_value_t)]
pub tag: String,
}
#[derive(Debug, Arguments)]
pub struct RemoteCommand {
#[clap(long, value_parser = parse_command)]
pub remote_command: Option<(String, Vec<String>)>,
}
#[derive(Debug, Parser)]
#[clap(version = env!("VERSION"))]
pub struct Args {
#[command(subcommand)]
pub command: Command,
#[clap(long = "trace", global = true)]
pub trace: bool,
}
#[derive(Debug, Subcommand)]
pub enum Command {
Backup(Backup),
Purge(Purge),
Restore(Restore),
Snapshot(Snapshot),
}
#[derive(Debug, Arguments)]
pub struct Backup {
pub subvolumes: Vec<PathBuf>,
#[clap(short, long)]
pub source: Option<PathBuf>,
#[clap(short, long)]
pub destination: PathBuf,
#[command(flatten)]
pub tag: Tag,
#[command(flatten)]
pub remote_command: RemoteCommand,
}
#[derive(Debug, Arguments)]
pub struct Restore {
pub subvolumes: Vec<PathBuf>,
#[clap(short, long)]
pub source: PathBuf,
#[clap(short, long)]
pub destination: Option<PathBuf>,
#[command(flatten)]
pub remote_command: RemoteCommand,
#[clap(long)]
pub snapshots_only: bool,
}
#[derive(Debug, Arguments)]
pub struct Purge {
pub subvolumes: Vec<PathBuf>,
#[clap(short, long)]
pub source: Option<PathBuf>,
#[clap(short, long)]
pub destination: Option<PathBuf>,
#[command(flatten)]
pub tag: Tag,
#[command(flatten)]
pub remote_command: RemoteCommand,
#[clap(long, value_parser = parse_duration)]
pub keep_for: Duration,
}
#[derive(Debug, Arguments)]
pub struct Snapshot {
pub subvolumes: Vec<PathBuf>,
#[clap(short, long)]
pub repository: Option<PathBuf>,
#[command(flatten)]
pub tag: Tag,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn duration_parsing() {
assert_eq!(parse_duration("1d").unwrap(), Duration::days(1));
assert_eq!(parse_duration("23w").unwrap(), Duration::weeks(23));
assert_eq!(parse_duration("2m").unwrap(), Duration::days(60));
assert_eq!(parse_duration("3y").unwrap(), Duration::days(3 * 365));
assert!(parse_duration("xxx")
.unwrap_err()
.to_string()
.contains("invalid duration provided"));
}
#[test]
fn command_parsing() {
assert_eq!(
parse_command("ssh").unwrap(),
("ssh".to_string(), Vec::new())
);
assert_eq!(
parse_command("ssh server").unwrap(),
("ssh".to_string(), vec!["server".to_string()])
);
}
}