picobak/src/main.rs
Félix Baylac Jacqué 2cf564f1c4 Refactoring: simplify exif/mtime logic
Refactor the exif parsing/mtime parsing logic to have two clear
branches.
2023-08-24 18:19:28 +02:00

143 lines
5.9 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};
use chrono::{Utc, DateTime, Datelike, NaiveDateTime};
#[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).unwrap_or_else(|_| panic!("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)
.unwrap_or_else(|_| panic!("ERROR: cannot create directory at {}", &picture_dir.display()));
} else {
eprintln!("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()
.unwrap_or_else(|| panic!("Error: Incorrect file name {}", filename.display())));
if !target_filename.is_file() {
if !cli.dry_run {
copy(filename, &target_filename)
.unwrap_or_else(|_| panic!("ERROR: cannot copy {} to {}", &filename.display(), &target_filename.display()));
} else {
eprintln!("Would copy {} to {}", filename.display(), target_filename.display());
}
} else if same_files(filename, &target_filename) {
eprintln!("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.
/// If no datetime EXIF data is attached to the file, use the file
/// last modification date.
fn get_picture_datetime(file_path: &str, file: &File) -> DateTime<Utc> {
let exif_datetime = get_picture_exif_datetime(file);
exif_datetime.unwrap_or_else(|| get_file_modified_time(file_path, file))
}
/// Retrieves the picture EXIF datetime.
fn get_picture_exif_datetime(file: &File) -> Option<DateTime<Utc>> {
let mut bufreader = std::io::BufReader::new(file);
let exifreader = exif::Reader::new();
let exif = exifreader.read_from_container(&mut bufreader);
exif.map(
|exif| {
exif.get_field(Tag::DateTimeOriginal, In::PRIMARY).and_then(
|datetime_field| {
match datetime_field.value {
Value::Ascii(ref vec) if !vec.is_empty() => {
// Meh… I know…
let str_date = String::from_utf8(vec[0].to_vec()).unwrap();
NaiveDateTime::parse_from_str(&str_date, "%Y:%m:%d %H:%M:%S")
.map(|naive_datetime| DateTime::from_utc(naive_datetime, Utc))
.ok()
}
_ => None
}
})
}).ok().flatten()
}
/// If we cannot load the EXIF creation datetime, we end up using the
/// last modified time of the file.
fn get_file_modified_time(file_path: &str, file: &File) -> DateTime<Utc> {
eprintln!("No EXIF information available for {}, falling back to file mtime.", file_path);
let systemtime = file.metadata()
.unwrap_or_else(|_| panic!("Cannot retrieve UNIX file metadata for {}", file_path))
.modified()
.unwrap_or_else(|_| panic!("Cannot retrieve modified time for {}", file_path));
systemtime.into()
}
/// Return directory in which we want to save the picture.
fn find_backup_dir(backup_root: &str, datetime: &DateTime<Utc>) -> PathBuf {
let backup_root = Path::new(backup_root);
backup_root
.join(format!("{:04}", datetime.year()))
.join(format!("{:02}", datetime.month()))
.join(format!("{:02}", datetime.day()))
}
/// 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)
.unwrap_or_else(|_| panic!("Error: cannot open file {}", &source.display()))
.metadata()
.unwrap_or_else(|_| panic!("Error: cannot get metadata of file {}", &source.display()));
let target_file = File::open(target)
.unwrap_or_else(|_| panic!("Error: cannot open file {}", &target.display()))
.metadata()
.unwrap_or_else(|_| panic!("Error: cannot get metadata of file {}", &target.display()));
let source_modified = source_file.modified().unwrap_or_else(|_| panic!("ERROR: cannot find created datetime for {}", &source.display()));
let target_modified = target_file.modified().unwrap_or_else(|_| panic!("ERROR: cannot find created datetime for {}", &target.display()));
source_file.len() == target_file.len() && source_modified == target_modified
}