Fallback to mtime when no EXIF available

We add some logic to read the file mtime and fallback using that when
there's not enough informations in the EXIF container.
This commit is contained in:
Félix Baylac Jacqué 2023-08-24 16:01:53 +02:00
parent c713e3d5a1
commit cec77e8766
3 changed files with 276 additions and 44 deletions

206
Cargo.lock generated
View File

@ -2,6 +2,21 @@
# It is not intended for manual editing.
version = 3
[[package]]
name = "android-tzdata"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
[[package]]
name = "android_system_properties"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
dependencies = [
"libc",
]
[[package]]
name = "anstream"
version = "0.3.2"
@ -51,12 +66,24 @@ dependencies = [
"windows-sys",
]
[[package]]
name = "autocfg"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "bitflags"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635"
[[package]]
name = "bumpalo"
version = "3.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1"
[[package]]
name = "cc"
version = "1.0.83"
@ -66,6 +93,27 @@ dependencies = [
"libc",
]
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "chrono"
version = "0.4.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec837a71355b28f6556dbd569b37b3f363091c0bd4b2e735674521b4c5fd9bc5"
dependencies = [
"android-tzdata",
"iana-time-zone",
"js-sys",
"num-traits",
"time",
"wasm-bindgen",
"winapi",
]
[[package]]
name = "clap"
version = "4.3.24"
@ -113,6 +161,12 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
[[package]]
name = "core-foundation-sys"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa"
[[package]]
name = "errno"
version = "0.3.2"
@ -146,6 +200,29 @@ version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b"
[[package]]
name = "iana-time-zone"
version = "0.1.57"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2fad5b825842d2b38bd206f3e81d6957625fd7f0a361e345c30e01a0ae2dd613"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"wasm-bindgen",
"windows",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
dependencies = [
"cc",
]
[[package]]
name = "is-terminal"
version = "0.4.9"
@ -157,6 +234,15 @@ dependencies = [
"windows-sys",
]
[[package]]
name = "js-sys"
version = "0.3.64"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a"
dependencies = [
"wasm-bindgen",
]
[[package]]
name = "kamadak-exif"
version = "0.5.5"
@ -178,12 +264,27 @@ version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57bcfdad1b858c2db7c38303a6d2ad4dfaf5eb53dfeb0910128b2c26d6158503"
[[package]]
name = "log"
version = "0.4.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f"
[[package]]
name = "mutate_once"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16cf681a23b4d0a43fc35024c176437f9dcd818db34e0f42ab456a0ee5ad497b"
[[package]]
name = "num-traits"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2"
dependencies = [
"autocfg",
]
[[package]]
name = "once_cell"
version = "1.18.0"
@ -191,9 +292,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
[[package]]
name = "pictures-backup"
name = "picobak"
version = "0.1.0"
dependencies = [
"chrono",
"clap",
"kamadak-exif",
]
@ -246,6 +348,17 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "time"
version = "0.1.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a"
dependencies = [
"libc",
"wasi",
"winapi",
]
[[package]]
name = "unicode-ident"
version = "1.0.11"
@ -258,6 +371,97 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
[[package]]
name = "wasi"
version = "0.10.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"
[[package]]
name = "wasm-bindgen"
version = "0.2.87"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342"
dependencies = [
"cfg-if",
"wasm-bindgen-macro",
]
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.87"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd"
dependencies = [
"bumpalo",
"log",
"once_cell",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.87"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.87"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b"
dependencies = [
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.87"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1"
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-sys"
version = "0.48.0"

View File

@ -1,5 +1,5 @@
[package]
name = "pictures-backup"
name = "picobak"
version = "0.1.0"
edition = "2021"
authors = [ "Ninjatrappeur <ninjatrappeur@alternativebit.fr>" ]
@ -7,5 +7,10 @@ license = "GPL-3.0"
repository = "https://git.alternativebit.fr/NinjaTrappeur/picobak"
[dependencies]
chrono = { version = "0.4.26", features = ["clock"] }
clap = { version = "4.3.24", features = ["derive"] }
kamadak-exif = "0.5.5"
[profile.release]
strip = true
lto = true

View File

@ -3,7 +3,8 @@ use std::{fs::File, path::PathBuf};
use std::path::Path;
use clap::Parser;
use exif::{Tag, In, Value, DateTime};
use exif::{Tag, In, Value};
use chrono::{Utc, DateTime, Datelike, NaiveDateTime};
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
@ -25,16 +26,16 @@ fn main() {
}
let filename = Path::new(&cli.file_path);
let file = File::open(&filename).expect(&format!("ERROR: cannot open the {} file", &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)
.expect(&format!("ERROR: cannot create directory at {}", &picture_dir.display()));
.unwrap_or_else(|_| panic!("ERROR: cannot create directory at {}", &picture_dir.display()));
} else {
eprint!("Would mkdir {}", &picture_dir.display());
eprintln!("Would mkdir {}", &picture_dir.display());
}
} else if !picture_dir.is_dir() {
panic!("ERROR: {} already exists and is not a directory", &picture_dir.display())
@ -42,52 +43,74 @@ fn main() {
let target_filename = picture_dir
.join(filename.file_name()
.expect(&format!("Error: Incorrect file name {}", filename.display())));
.unwrap_or_else(|| panic!("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()));
copy(filename, &target_filename)
.unwrap_or_else(|_| panic!("ERROR: cannot copy {} to {}", &filename.display(), &target_filename.display()));
} else {
eprint!("Would copy {} to {}", filename.display(), target_filename.display());
eprintln!("Would copy {} to {}", filename.display(), target_filename.display());
}
} else if same_files(filename, &target_filename) {
eprintln!("File already archived: {}", &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())
}
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 {
/// 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 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)
let exif = exifreader.read_from_container(&mut bufreader);
match exif {
Ok(exif) => {
match exif.get_field(Tag::DateTimeOriginal, In::PRIMARY) {
Some(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();
if let Ok(naive_datetime) = NaiveDateTime::parse_from_str(&str_date, "%Y:%m:%d %H:%M:%S") {
DateTime::from_utc(naive_datetime, Utc)
} else {
panic!("ERROR: incorrect datetime format for file {}", file_path)
}
}
_ =>
panic!("ERROR: cannot parse ASCII from datetime EXIF field for file {}", file_path)
}
},
// There's no EXIF datetime field. Let's use the file creation time.
None => get_file_modified_time(file_path, file)
}
_ =>
panic!("ERROR: cannot parse ASCII from datetime EXIF field for file {}", file_path)
},
Err(_e) => get_file_modified_time(file_path, file)
}
}
/// Directory in which we want to save the picture.
fn find_backup_dir(backup_root: &str, datetime: &DateTime) -> PathBuf {
/// 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(datetime.year.to_string())
.join(datetime.month.to_string())
.join(datetime.day.to_string())
.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
@ -106,16 +129,16 @@ fn validate_args(args: &CliArgs) {
/// 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()))
let source_file = File::open(source)
.unwrap_or_else(|_| panic!("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()))
.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()
.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()));
.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_created == target_created
source_file.len() == target_file.len() && source_modified == target_modified
}