mod application;
mod constants;
mod error;
mod sysexits;
use crate::application::{Application, config_file, init_config};
use anyhow::{Result, bail};
use clap::{Parser, Subcommand, ValueEnum};
use error::HbackupError;
use hbackup::job::{BackupModel, CompressFormat, Job, Level, display_jobs, run_job, run_jobs};
use std::io::{self, ErrorKind, Write};
use std::path::{Path, PathBuf};
use std::process;
#[tokio::main]
async fn main() -> Result<()> {
let subcommand = Opt::parse().subcommand.unwrap_or_else(|| {
eprintln!("bk requires at least one command to execute. See 'bk --help' for usage.");
process::exit(sysexits::EX_KEYWORD);
});
init_config();
match subcommand {
Command::Add {
source,
target,
compression,
level,
ignore,
model,
} => {
add(source, target, compression, level, ignore, model)?;
}
Command::Run {
source,
target,
compression,
id,
level,
ignore,
model,
} => {
match (id, source, target) {
(Some(ids), _, _) => {
run_by_id(ids);
}
(_, Some(source), Some(target)) => {
let source = canonicalize(source)?;
let target = canonicalize(target)?;
if compression.is_some() && model == Some(BackupModel::Mirror) {
bail!(HbackupError::InvalidCompressionForMirror);
}
let job = Job::temp_job(source, target, compression, level, ignore, model);
run_job(&job)?;
}
_ => run()?,
}
}
Command::List { id, gte, lte } => {
let jobs = if let Some(ids) = id {
Application::list_by_ids(ids)
} else if let Some(gte) = gte {
Application::list_by_gte(gte)
} else if let Some(lte) = lte {
Application::list_by_lte(lte)
} else {
Application::get_jobs()
};
println!("{}", display_jobs(jobs));
}
Command::Delete { id, all, yes } => {
delete(id, all, yes)?;
}
Command::Edit {
id,
source,
target,
compression,
level,
ignore,
clear,
model,
swap,
} => {
let edit_params = EditParams {
id,
source,
target,
compression,
level,
ignore,
clear,
model,
swap,
};
edit(edit_params)?;
}
Command::Config => {
println!(" {}", config_file().display());
}
}
Ok(())
}
#[derive(Parser)]
#[command(version, about, long_about = None)]
struct Opt {
#[command(subcommand)]
pub subcommand: Option<Command>,
}
#[derive(Subcommand, Debug)]
enum Command {
Add {
source: PathBuf,
target: PathBuf,
#[arg(short, long)]
compression: Option<CompressFormat>,
#[arg(short, long, requires = "compression")]
level: Option<Level>,
#[arg(short = 'g', long, value_delimiter = ',')]
ignore: Option<Vec<String>>,
#[arg(short, long, required = false)]
model: Option<BackupModel>,
},
Run {
#[arg(required = false, requires = "target")]
source: Option<PathBuf>,
#[arg(required = false, requires = "source")]
target: Option<PathBuf>,
#[arg(short, long, required = false)]
compression: Option<CompressFormat>,
#[arg(short, long, required = false, requires = "compression")]
level: Option<Level>,
#[arg(short, long, required = false, value_delimiter = ',', conflicts_with_all = ["source", "target", "compression"])]
id: Option<Vec<u32>>,
#[arg(short = 'g', long, value_delimiter = ',')]
ignore: Option<Vec<String>>,
#[arg(short, long, required = false)]
model: Option<BackupModel>,
},
List {
#[arg(short, long, required = false, value_delimiter = ',', conflicts_with_all = ["gte", "lte"])]
id: Option<Vec<u32>>,
#[arg(short = 'g', long, required = false, conflicts_with_all = ["id", "lte"])]
gte: Option<u32>,
#[arg(short = 'l', long, required = false, conflicts_with_all = ["id", "gte"])]
lte: Option<u32>,
},
Delete {
#[arg(value_delimiter = ',', conflicts_with = "all")]
id: Option<Vec<u32>>,
#[arg(short, long, conflicts_with = "id")]
all: bool,
#[arg(short = 'y', long, conflicts_with = "id")]
yes: bool,
},
Edit {
id: u32,
#[arg(short, long, required_unless_present_any = ["target", "compression", "level", "ignore", "model", "clear", "swap"])]
source: Option<PathBuf>,
#[arg(short, long, required_unless_present_any = ["source", "compression", "level", "ignore", "model", "clear", "swap"])]
target: Option<PathBuf>,
#[arg(short, long, required_unless_present_any = ["source", "target", "level", "ignore", "model", "clear", "swap"])]
compression: Option<CompressFormat>,
#[arg(short, long, required_unless_present_any = ["source", "target", "compression", "ignore", "model", "clear", "swap"])]
level: Option<Level>,
#[arg(short = 'g', long, value_delimiter = ',', required_unless_present_any = ["source", "target", "compression", "level", "model", "clear", "swap"])]
ignore: Option<Vec<String>>,
#[arg(short, long, required_unless_present_any = ["source", "target", "compression", "level", "ignore", "clear", "swap"])]
model: Option<BackupModel>,
#[arg(long, value_delimiter = ',', required_unless_present_any = ["source", "target", "compression", "level", "ignore", "model", "swap"])]
clear: Option<Vec<ClearField>>,
#[arg(long, conflicts_with_all = ["source", "target"], required_unless_present_any = ["source", "target", "compression", "level", "ignore", "model", "clear"])]
swap: bool,
},
Config,
}
#[derive(Debug, Clone, ValueEnum)]
enum ClearField {
Compression,
Level,
Ignore,
Model,
}
struct EditParams {
pub id: u32,
pub source: Option<PathBuf>,
pub target: Option<PathBuf>,
pub compression: Option<CompressFormat>,
pub level: Option<Level>,
pub ignore: Option<Vec<String>>,
pub clear: Option<Vec<ClearField>>,
pub model: Option<BackupModel>,
pub swap: bool,
}
fn add(
source: PathBuf,
target: PathBuf,
comp: Option<CompressFormat>,
level: Option<Level>,
ignore: Option<Vec<String>>,
model: Option<BackupModel>,
) -> Result<()> {
let source = canonicalize(source)?;
let target = canonicalize(target)?;
if comp.is_some() && model == Some(BackupModel::Mirror) {
return Err(HbackupError::InvalidCompressionForMirror.into());
}
let mut app = Application::load_config();
app.add_job(source, target, comp, level, ignore, model)?;
app.write()?;
Ok(())
}
fn run() -> Result<()> {
let jobs = Application::get_jobs();
if jobs.is_empty() {
println!("No jobs are backed up!");
} else if jobs.len() == 1 {
run_job(&jobs[0])?;
} else {
run_jobs(jobs)?;
}
Ok(())
}
fn run_by_id(ids: Vec<u32>) {
let jobs = Application::get_jobs();
if jobs.is_empty() {
println!("No jobs are backed up!");
return;
}
let mut vec = vec![];
for id in ids {
match jobs.iter().find(|j| j.id == id) {
Some(job) => {
vec.push(job.clone());
}
None => {
eprintln!("Job with id {id} not found.");
}
}
}
if vec.is_empty() {
process::exit(1);
} else if vec.len() == 1 {
if let Err(e) = run_job(&vec[0]) {
eprintln!("Failed to run job with id {}: {e}\n", vec[0].id);
process::exit(sysexits::EX_IOERR);
}
} else if let Err(e) = run_jobs(vec) {
eprintln!("Failed to run jobs: {e}\n");
process::exit(sysexits::EX_IOERR);
}
}
fn delete(id: Option<Vec<u32>>, all: bool, yes: bool) -> Result<()> {
if all {
let mut app = Application::load_config();
if app.jobs.is_empty() {
println!("No jobs to delete");
return Ok(());
}
if yes {
app.reset_jobs();
app.write()?;
println!("All jobs deleted successfully.");
return Ok(());
}
loop {
print!("Are you sure you want to delete all jobs? (y/n): ");
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
if input.trim().to_lowercase() == "n" {
return Ok(());
} else if input.trim().to_lowercase() == "y" {
app.reset_jobs();
app.write()?;
println!("All jobs deleted successfully.");
return Ok(());
} else {
println!("\nInvalid input. Please enter 'y' or 'n'.");
}
}
} else if let Some(ids) = id {
let mut app = Application::load_config();
let mut msg = String::new();
ids.into_iter().for_each(|id| match app.remove_job(id) {
Some(_) => msg.push_str(&format!("Job with id {id} deleted successfully.\n")),
None => msg.push_str(&format!(
"Job deletion failed. Job with id {id} cannot be found.\n"
)),
});
app.write()?;
msg.remove(msg.len() - 1);
println!("{}", msg);
} else {
bail!("Either --all or --id must be specified.");
}
Ok(())
}
fn edit(params: EditParams) -> Result<()> {
let EditParams {
id,
source,
target,
compression,
level,
ignore,
model,
clear,
swap,
} = params;
let source = source.map(canonicalize);
let target = target.map(canonicalize);
if compression.is_some() && model == Some(BackupModel::Mirror) {
bail!(HbackupError::InvalidCompressionForMirror);
}
let mut app = Application::load_config();
if app.jobs.is_empty() {
println!("Job with id {id} not found.");
return Ok(());
}
if let Some(job) = app.jobs.iter_mut().find(|j| j.id == id) {
if let Some(path) = source {
job.source = path?;
}
if let Some(path) = target {
job.target = path?;
}
if let Some(clear_fields) = &clear {
for field in clear_fields {
match field {
ClearField::Compression => {
job.compression = None;
job.level = None; }
ClearField::Level => {
job.level = None;
}
ClearField::Ignore => {
job.ignore = None;
}
ClearField::Model => {
job.model = None;
}
}
}
}
if let Some(comp) = compression {
job.compression = Some(comp);
}
if let Some(lvl) = level {
if job.compression.is_none() {
bail!(
"The compression format is not set, and the compression level cannot be updated."
);
}
job.level = Some(lvl);
}
if let Some(ign) = ignore {
job.ignore = Some(ign);
}
if let Some(model) = model {
job.model = Some(model)
}
if job.compression.is_some() && job.model == Some(BackupModel::Mirror) {
bail!(HbackupError::InvalidCompressionForMirror);
}
if swap {
if !job.target.exists() {
bail!(
"Cannot swap source and target paths for job id {id} because target path does not exist.\ntarget path: {:?}",
job.target
);
} else if !(job.target.is_file() && job.source.is_file()) {
bail!(
"Cannot swap source and target paths for job id {id} because both source and target paths must be files.\nsource path: {:?}\ntarget path: {:?}",
job.source,
job.target
);
}
std::mem::swap(&mut job.source, &mut job.target);
}
app.write()?;
println!("Job with id {id} edited successfully.");
} else {
println!("Job with id {id} not found.");
}
Ok(())
}
fn canonicalize(path: impl AsRef<Path>) -> Result<PathBuf> {
let path = path.as_ref();
match path.canonicalize() {
Ok(p) => Ok(p),
Err(e) => match e.kind() {
ErrorKind::NotFound => Err(HbackupError::PathNotFound(path.to_path_buf()).into()),
ErrorKind::PermissionDenied => {
Err(HbackupError::PermissionDenied(path.to_path_buf()).into())
}
_ => Err(e.into()),
},
}
}