122 lines
4.8 KiB
Rust
122 lines
4.8 KiB
Rust
use std::fs::{create_dir_all, copy};
|
|
use std::{fs::File, path::PathBuf};
|
|
use std::path::Path;
|
|
|
|
use clap::Parser;
|
|
use exif::{Tag, In, Value, DateTime};
|
|
|
|
#[derive(Parser, Debug)]
|
|
#[command(author, version, about, long_about = None)]
|
|
struct CliArgs {
|
|
/// Pictures library directory
|
|
backup_root: String,
|
|
/// Picture to backup
|
|
file_path: String,
|
|
/// Do not create any directory or copy any file. Only prints out the operations it would perform
|
|
#[arg(short, long)]
|
|
dry_run: bool
|
|
}
|
|
|
|
fn main() {
|
|
let cli = CliArgs::parse();
|
|
|
|
if !cli.dry_run {
|
|
validate_args(&cli);
|
|
}
|
|
|
|
let filename = Path::new(&cli.file_path);
|
|
let file = File::open(&filename).expect(&format!("ERROR: cannot open the {} file", &cli.file_path));
|
|
let datetime = get_picture_datetime(&cli.file_path, &file);
|
|
let picture_dir = find_backup_dir(&cli.backup_root, &datetime);
|
|
|
|
if !picture_dir.exists() {
|
|
if !cli.dry_run {
|
|
create_dir_all(&picture_dir)
|
|
.expect(&format!("ERROR: cannot create directory at {}", &picture_dir.display()));
|
|
} else {
|
|
eprint!("Would mkdir {}", &picture_dir.display());
|
|
}
|
|
} else if !picture_dir.is_dir() {
|
|
panic!("ERROR: {} already exists and is not a directory", &picture_dir.display())
|
|
}
|
|
|
|
let target_filename = picture_dir
|
|
.join(filename.file_name()
|
|
.expect(&format!("Error: Incorrect file name {}", filename.display())));
|
|
if !target_filename.is_file() {
|
|
if !cli.dry_run {
|
|
copy(&filename, &target_filename)
|
|
.expect(&format!("ERROR: cannot copy {} to {}", &filename.display(), &target_filename.display()));
|
|
} else {
|
|
eprint!("Would copy {} to {}", filename.display(), target_filename.display());
|
|
}
|
|
} else {
|
|
if same_files(&filename, &target_filename) {
|
|
eprint!("File already archived: {}", &filename.display())
|
|
} else {
|
|
panic!("ERROR: {} already exists in {}, but the two files are different",
|
|
&filename.display(),
|
|
&target_filename.display())
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Retrieves when the picture has been shot from the EXIF metadata.
|
|
fn get_picture_datetime(file_path: &str, file: &File) -> DateTime {
|
|
let mut bufreader = std::io::BufReader::new(file);
|
|
let exifreader = exif::Reader::new();
|
|
let exif = exifreader.read_from_container(&mut bufreader)
|
|
.expect(&format!("ERROR: cannot read EXIF metadata for picture {}", file_path));
|
|
let datetime_field = exif.get_field(Tag::DateTimeOriginal, In::PRIMARY)
|
|
.expect(&format!("ERROR: missing datetime tag for picture {}", file_path));
|
|
match datetime_field.value {
|
|
Value::Ascii(ref vec) if !vec.is_empty() =>
|
|
if let Ok(datetime) = DateTime::from_ascii(&vec[0]) {
|
|
datetime
|
|
} else {
|
|
panic!("ERROR: incorrect datetime format for file {}", file_path)
|
|
}
|
|
_ =>
|
|
panic!("ERROR: cannot parse ASCII from datetime EXIF field for file {}", file_path)
|
|
}
|
|
}
|
|
|
|
/// Directory in which we want to save the picture.
|
|
fn find_backup_dir(backup_root: &str, datetime: &DateTime) -> PathBuf {
|
|
let backup_root = Path::new(backup_root);
|
|
backup_root
|
|
.join(datetime.year.to_string())
|
|
.join(datetime.month.to_string())
|
|
.join(datetime.day.to_string())
|
|
}
|
|
|
|
/// Sanity function making sure the user did not give us complete
|
|
/// garbage data.
|
|
fn validate_args(args: &CliArgs) {
|
|
if !Path::new(&args.file_path).is_file() {
|
|
panic!("ERROR: {} is not a file", &args.file_path);
|
|
};
|
|
if Path::new(&args.backup_root).is_file() {
|
|
panic!("ERROR: {} is a file, not a valid backup dir", &args.file_path);
|
|
};
|
|
}
|
|
|
|
/// Compare two files and check if they're the same. We're not really
|
|
/// comparing the whole file, it'd be too expensive. We assume that if
|
|
/// two pictures have the same EXIF data, the same size and the same
|
|
/// creation date, they're the same.
|
|
fn same_files(source: &Path, target: &Path) -> bool {
|
|
let source_file = File::open(&source)
|
|
.expect(&format!("Error: cannot open file {}", &source.display()))
|
|
.metadata()
|
|
.expect(&format!("Error: cannot get metadata of file {}", &source.display()));
|
|
let target_file = File::open(&target)
|
|
.expect(&format!("Error: cannot open file {}", &target.display()))
|
|
.metadata()
|
|
.expect(&format!("Error: cannot get metadata of file {}", &target.display()));
|
|
let source_created = source_file.created().expect(&format!("ERROR: cannot find created datetime for {}", &source.display()));
|
|
let target_created = target_file.created().expect(&format!("ERROR: cannot find created datetime for {}", &target.display()));
|
|
|
|
source_file.len() == target_file.len() && source_created == target_created
|
|
}
|