bekape/src/main.rs

127 lines
4.0 KiB
Rust

use regex::Regex;
use std::fs;
use std::time::SystemTime;
use std::vec::Vec;
use structopt::StructOpt;
/// A simple backup program
#[derive(StructOpt, Debug)]
#[structopt(name = "Bekape")]
struct Opt {
/// The folder to backup
#[structopt(name = "source")]
source: String,
/// The folder to store the backup
#[structopt(name = "destination")]
destination: String,
}
fn browse_recursively(dir: &str) -> (Vec<String>, Vec<String>) {
let mut content = Vec::new();
let mut errors = Vec::new();
let ls = fs::read_dir(dir);
if let Ok(ls) = ls {
for entry in ls {
if let Ok(entry) = entry {
let full_path = entry.path().to_string_lossy().to_string();
if let Ok(file_type) = entry.file_type() {
if file_type.is_dir() {
let mut result = browse_recursively(&full_path);
content.append(&mut result.0);
errors.append(&mut result.1);
} else {
content.push(full_path);
}
} else {
errors.push(full_path);
}
}
}
} else {
errors.push(dir.to_string());
}
(content, errors)
}
fn get_file_metadata(file: &str) -> (SystemTime, u64) {
if let Ok(metadata) = fs::metadata(file) {
let time = metadata.modified().unwrap_or(std::time::UNIX_EPOCH);
let size = metadata.len();
return (time, size);
};
(std::time::UNIX_EPOCH, 0)
}
fn backup_element(path: &str, src_root: &str, bkp_root: &str, prev_bkp_root: &Option<String>) {
let src_file = format!("{}{}", src_root, path);
let bkp_file = format!("{}{}", bkp_root, path);
let p = bkp_file.rfind('/').unwrap(); // TODO: Is unwrap okay here?
let bkp_folder = bkp_file[0..p].to_string();
let _ = fs::create_dir_all(&bkp_folder);
let mut backup_done = false;
if let Some(prev_bkp_root) = prev_bkp_root {
let prev_bkp_file = format!("{}{}", prev_bkp_root, path);
if std::path::Path::new(&prev_bkp_file).exists() {
let prev_bkp_metadata = get_file_metadata(&prev_bkp_file);
let src_metadata = get_file_metadata(&src_file);
backup_done = prev_bkp_metadata.0 > src_metadata.0
&& prev_bkp_metadata.1 == src_metadata.1
&& fs::hard_link(&prev_bkp_file, &bkp_file).is_ok();
}
}
if !backup_done && fs::copy(&src_file, &bkp_file).is_err() {
eprintln!("Could not copy: {} -> {}", src_file, bkp_file);
}
}
fn main() {
let opt = Opt::from_args();
let dt = chrono::offset::Utc::now().format("%Y-%m-%d_%H-%M-%S");
let backup_folder = format!("{}/backup_{}_wip", opt.destination, dt);
if fs::create_dir(&backup_folder).is_err() {
eprintln!("Could not create backup directory: {}", &backup_folder);
std::process::exit(1);
}
// find previous backup
let backup_regex = Regex::new(r"backup_\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}$").unwrap();
let ls = fs::read_dir(&opt.destination);
let prev_backup = if let Ok(ls) = ls {
let mut ls = ls
.filter_map(|s| s.ok())
.map(|s| s.path().to_string_lossy().trim().to_owned())
.filter(|s| backup_regex.is_match(&s))
.collect::<Vec<_>>();
if ls.is_empty() {
None
} else {
ls.sort();
ls.last().cloned()
}
} else {
None
};
let result = browse_recursively(&opt.source);
let content = result.0.iter().map(|s| {
let l = opt.source.len();
s[l..].to_string()
});
for err in result.1 {
eprintln!("Could not read {}.", err);
}
for elem in content {
backup_element(&elem, &opt.source, &backup_folder, &prev_backup);
}
let new_name = backup_folder.replace("_wip", "");
if fs::rename(&backup_folder, &new_name).is_err() {
eprintln!("Could not rename folder.");
}
}