Viewing:
use std::collections::HashSet;
use std::env;
use std::fs::File;
use age;
use anyhow::{Result, bail};
use clap::ArgMatches;
use dirs;
use ignore;
use rpassword;
use tar::{Builder,Archive};
mod config;
use secrecy::Secret;
fn main() -> Result<()> {
let matches = config::get_matches();
if let Some(matches) = matches.subcommand_matches("backup") {
backup(&matches)?;
} else if let Some(matches) = matches.subcommand_matches("restore") {
restore(&matches)?;
} else {
println!("Expected backup or restore command. Nothing to do. Exiting.")
}
Ok(())
}
fn get_pw(prompt: &str) -> String {
let mut pw = String::from("a");
let mut confirm = String::from("b");
while pw != confirm {
pw = rpassword::read_password_from_tty(Some(prompt)).unwrap();
confirm = rpassword::read_password_from_tty(Some("Confirm: ")).unwrap();
}
return pw;
}
fn backup(matches: &ArgMatches) -> Result<()> {
let mut seen_files: HashSet<String> = HashSet::new();
let mut seen_patterns: HashSet<String> = HashSet::new();
let user_home = match dirs::home_dir() {
Some(dir) => match dir.as_path().to_str() {
Some(dir) => format!("{}", dir),
None => bail!("home dir exists but is not a dir"),
},
None => match env::var("HOME") {
Ok(val) => format!("{}", val),
Err(_) => {
bail!("no home var and no dirs::home_dir")
},
},
};
let backup_dir = ensure_backup_dir(matches, &user_home);
println!("user home is {}", &user_home);
for raw_pattern in matches.values_of("patterns").expect("needed patterns to do backup") {
let pattern = raw_pattern.trim_end_matches(|c| c == '/');
println!("backing up {}", pattern);
let archive_name = get_archive_name(pattern, &user_home);
if !seen_patterns.insert(archive_name.clone()) {
continue;
}
let archive_path = format!("{}/{}.tar.age", backup_dir, archive_name);
let mut file = File::create(archive_path.clone()).expect(&format!("Needed to create {}", archive_path));
let passphrase = get_pw(&format!("Password for {}: ", archive_path));
let encryptor = age::Encryptor::with_user_passphrase(Secret::new(passphrase.to_owned()));
let writer = encryptor.wrap_output(&mut file).unwrap();
let mut archive = Builder::new(writer);
for result in ignore::WalkBuilder::new(pattern).hidden(false).build() {
match result {
Ok(entry) => {
let file_type = entry.file_type();
if file_type.is_none() {
continue;
}
let is_dir = file_type.and_then(|x: std::fs::FileType| Some(x.is_dir())).unwrap_or(false);
if let Some(file_type) = file_type {
if file_type.is_symlink() {
eprintln!("skipping symlink {:?}", entry.path().to_str());
continue;
}
}
if let Some(file_path) = entry.path().to_str() {
if seen_files.contains(file_path) {
continue
} else {
seen_files.insert(file_path.to_string());
}
let mut place_in_archive = if file_path.starts_with(user_home.as_str()) {
file_path.replacen(user_home.as_str(), "", 1)
} else {
file_path.to_string()
};
if place_in_archive.starts_with("/") {
place_in_archive = place_in_archive.replacen("/", "", 1);
}
if is_dir {
archive.append_dir(place_in_archive, file_path)
.expect(format!("could not add dir {}", pattern).as_str());
} else {
archive.append_path_with_name(file_path, place_in_archive)
.expect(format!("could not add path {}", pattern).as_str());
}
} else {
eprintln!("bad entry {:?}", entry)
}
},
Err(x) => {
eprintln!("failed backing up file {}", x)
},
}
}
let some_ref = archive.into_inner()?;
some_ref.finish()?;
}
Ok(())
}
fn get_archive_name(pattern: &str, user_home: &str) -> String {
let archive_name = if pattern.starts_with(user_home) {
pattern.replacen(user_home, "", 1)
} else {
pattern.to_string()
};
return archive_name.trim_start_matches(|c| c == '/')
.replace("/", "_")
.replace(".", "_")
.replace(" ", "_")
.replace("*", "_");
}
fn ensure_backup_dir(matches: &ArgMatches, user_home: &String) -> String {
if let Some(dir) = matches.value_of("backdir") {
dir.to_string()
} else {
user_home.to_string()
}
}
fn restore(matches: &ArgMatches) -> Result<()> {
let dest = matches.value_of("dest").expect("dest is required"); // dest required
for backup in matches.values_of("backups").expect("needed backups to do restore") {
let backup_handle = match File::open(backup) {
Ok(x) => x,
Err(x) => {
println!("failed {} backup: {}", backup, x);
continue
}
};
let d = match age::Decryptor::new(backup_handle) {
Ok(age::Decryptor::Passphrase(d)) => d,
_ => {
println!("failed {} backup: couldn't decrypt as password", backup);
continue
}
};
if let Err(x) = std::fs::create_dir_all(dest) {
println!("failed {} backup: {}", backup, x);
continue
};
let passphrase = get_pw(&format!("Password for {}: ", backup));
let result = match d.decrypt(&Secret::new(passphrase), None) {
Ok(x) => x,
Err(x) => {
println!("failed {} backup: {}", backup, x);
continue
},
};
if let Err(x) = Archive::new(result).unpack(dest) {
println!("Unable to unpack archive for {}: {}", backup, x);
};
}
Ok(())
}