127 lines
4.0 KiB
Rust
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.");
|
|
}
|
|
}
|