picobak/src/main.rs

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
}