use std::path::PathBuf;
pub fn cache_dir() -> PathBuf {
dirs_cache_dir().join("videre").join("thumbnails")
}
pub fn thumb_path(hash: &str, size: u32) -> PathBuf {
cache_dir().join(format!("{hash}_{size}.jpg"))
}
pub fn thumb_exists(hash: &str, size: u32) -> bool {
thumb_path(hash, size).exists()
}
pub fn face_thumb_path(hash: &str, face_id: i64, size: u32) -> PathBuf {
cache_dir().join(format!("{hash}_face{face_id}_{size}.jpg"))
}
pub fn face_thumb_exists(hash: &str, face_id: i64, size: u32) -> bool {
face_thumb_path(hash, face_id, size).exists()
}
pub fn original_path(hash: &str) -> PathBuf {
cache_dir().join(format!("{hash}_original.jpg"))
}
pub fn original_exists(hash: &str) -> bool {
original_path(hash).exists()
}
const HASH_HEX_LEN: usize = 64;
pub fn hash_from_cache_filename(filename: &str) -> Option<&str> {
if !filename.ends_with(".jpg") {
return None;
}
let bytes = filename.as_bytes();
if bytes.len() <= HASH_HEX_LEN || bytes[HASH_HEX_LEN] != b'_' {
return None;
}
let hash = &filename[..HASH_HEX_LEN];
if hash.bytes().all(|b| b.is_ascii_hexdigit()) {
Some(hash)
} else {
None
}
}
pub fn original_tmp_path(hash: &str) -> PathBuf {
cache_dir().join(format!("{hash}_original.tmp{}", std::process::id()))
}
pub fn thumb_tmp_path(hash: &str, size: u32) -> PathBuf {
cache_dir().join(format!("{hash}_{size}.tmp{}", std::process::id()))
}
fn dirs_cache_dir() -> PathBuf {
std::env::var_os("HOME")
.map(|home| PathBuf::from(home).join(".cache"))
.unwrap_or_else(|| PathBuf::from(".cache"))
}
pub fn migrate_legacy_dupe_cache() {
let old = dirs_cache_dir().join("dupe").join("thumbnails");
let new = cache_dir();
migrate_dir(&old, &new);
}
fn migrate_dir(old: &std::path::Path, new: &std::path::Path) {
if old.is_dir() && !new.exists() {
if let Some(parent) = new.parent() {
let _ = std::fs::create_dir_all(parent);
}
let _ = std::fs::rename(old, new);
if let Some(old_parent) = old.parent() {
let _ = std::fs::remove_dir(old_parent); }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn thumb_path_is_keyed_by_hash_and_size() {
let p1 = thumb_path("abc123", 240);
let p2 = thumb_path("abc123", 1200);
let p3 = thumb_path("def456", 240);
assert_ne!(p1, p2, "different sizes must produce different paths");
assert_ne!(p1, p3, "different hashes must produce different paths");
assert!(p1.to_string_lossy().contains("abc123_240.jpg"));
}
#[test]
fn thumb_exists_false_for_missing_file() {
assert!(!thumb_exists("nonexistent-hash-xyz", 240));
}
#[test]
fn cache_dir_is_under_videre() {
assert!(cache_dir().to_string_lossy().contains("videre"));
assert!(!cache_dir().to_string_lossy().contains("/dupe/"));
}
#[test]
fn face_thumb_path_is_keyed_by_hash_face_id_and_size() {
let p1 = face_thumb_path("abc123", 1, 140);
let p2 = face_thumb_path("abc123", 2, 140);
let p3 = face_thumb_path("def456", 1, 140);
assert_ne!(p1, p2, "different face ids must produce different paths");
assert_ne!(p1, p3, "different hashes must produce different paths");
assert!(p1.to_string_lossy().contains("abc123_face1_140.jpg"));
}
#[test]
fn face_thumb_exists_false_for_missing_file() {
assert!(!face_thumb_exists("nonexistent-hash-xyz", 99, 140));
}
#[test]
fn original_path_is_keyed_by_hash() {
let p1 = original_path("abc123");
let p2 = original_path("def456");
assert_ne!(p1, p2);
assert!(p1.to_string_lossy().contains("abc123_original.jpg"));
}
#[test]
fn original_exists_false_for_missing_file() {
assert!(!original_exists("nonexistent-hash-xyz"));
}
#[test]
fn original_tmp_path_differs_from_final_path_and_is_keyed_by_hash() {
let tmp = original_tmp_path("abc123");
let final_path = original_path("abc123");
assert_ne!(tmp, final_path);
assert!(tmp.to_string_lossy().contains("abc123_original.tmp"));
}
fn test_hash(seed: &str) -> String {
seed.repeat((HASH_HEX_LEN / seed.len()) + 1)[..HASH_HEX_LEN].to_string()
}
#[test]
fn hash_from_cache_filename_parses_thumb_path() {
let h1 = test_hash("0123456789abcdef");
assert_eq!(hash_from_cache_filename(&format!("{h1}_240.jpg")), Some(h1.as_str()));
assert_eq!(hash_from_cache_filename(&format!("{h1}_1200.jpg")), Some(h1.as_str()));
}
#[test]
fn hash_from_cache_filename_parses_face_thumb_path() {
let h1 = test_hash("0123456789abcdef");
assert_eq!(hash_from_cache_filename(&format!("{h1}_face3_140.jpg")), Some(h1.as_str()));
}
#[test]
fn hash_from_cache_filename_parses_original_path() {
let h1 = test_hash("0123456789abcdef");
assert_eq!(hash_from_cache_filename(&format!("{h1}_original.jpg")), Some(h1.as_str()));
}
#[test]
fn hash_from_cache_filename_distinguishes_different_hashes() {
let h2 = test_hash("fedcba9876543210");
assert_eq!(hash_from_cache_filename(&format!("{h2}_240.jpg")), Some(h2.as_str()));
}
#[test]
fn hash_from_cache_filename_rejects_tmp_files() {
let h1 = test_hash("0123456789abcdef");
assert_eq!(hash_from_cache_filename(&format!("{h1}_original.tmp1234")), None);
assert_eq!(hash_from_cache_filename(&format!("{h1}_240.tmp5678")), None);
}
#[test]
fn hash_from_cache_filename_rejects_too_short_or_malformed_names() {
assert_eq!(hash_from_cache_filename("short_240.jpg"), None);
assert_eq!(hash_from_cache_filename(".DS_Store"), None);
let non_hex_64 = "g".repeat(HASH_HEX_LEN);
assert_eq!(hash_from_cache_filename(&format!("{non_hex_64}_240.jpg")), None);
}
#[test]
fn migrate_dir_moves_old_into_place() {
let tmp = std::env::temp_dir().join(format!("thumb_migrate_{}", std::process::id()));
let old = tmp.join("old_cache");
let new = tmp.join("new_cache");
std::fs::create_dir_all(&old).unwrap();
std::fs::write(old.join("h_240.jpg"), b"x").unwrap();
migrate_dir(&old, &new);
assert!(new.join("h_240.jpg").exists(), "cached file must survive migration");
assert!(!old.exists(), "old dir must be gone after migration");
let _ = std::fs::remove_dir_all(&tmp);
}
}