use anyhow::Result;
use blake3::Hasher;
use std::fs;
use std::path::{Path, PathBuf};
use crate::prompt::{Exclude, Preset, prompt_version};
pub struct CacheKeyInput<'a> {
pub source: &'a Path,
pub page: usize,
pub total_pages: usize,
pub dpi: u32,
pub model: &'a str,
pub preset: Preset,
pub excludes: &'a [Exclude],
pub instruction: Option<&'a str>,
pub prev_tail: Option<&'a str>,
pub input_mode: &'a str,
}
pub fn cache_path(override_path: Option<&Path>) -> PathBuf {
override_path
.map(|p| p.to_path_buf())
.unwrap_or_else(|| PathBuf::from(".lmocr/cache"))
}
pub fn load_cached(path: &Path) -> Option<String> {
fs::read_to_string(path).ok()
}
pub fn save_cached(path: &Path, content: &str) -> Result<()> {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
fs::write(path, content)?;
Ok(())
}
pub fn cache_key(input: &CacheKeyInput<'_>) -> Result<String> {
let metadata = fs::metadata(input.source)?;
let modified = metadata
.modified()
.ok()
.and_then(|m| m.duration_since(std::time::UNIX_EPOCH).ok())
.map(|d| d.as_secs())
.unwrap_or(0);
let size = metadata.len();
let mut hasher = Hasher::new();
hasher.update(input.source.to_string_lossy().as_bytes());
hasher.update(&size.to_le_bytes());
hasher.update(&modified.to_le_bytes());
hasher.update(&input.page.to_le_bytes());
hasher.update(&input.total_pages.to_le_bytes());
hasher.update(&input.dpi.to_le_bytes());
hasher.update(input.model.as_bytes());
hasher.update(format!("{:?}", input.preset).as_bytes());
hasher.update(format!("{:?}", input.excludes).as_bytes());
hasher.update(prompt_version().as_bytes());
hasher.update(input.input_mode.as_bytes());
if let Some(extra) = input.instruction {
hasher.update(extra.as_bytes());
}
if let Some(tail) = input.prev_tail {
hasher.update(tail.as_bytes());
}
Ok(hasher.finalize().to_hex().to_string())
}
#[cfg(test)]
mod tests {
use super::{CacheKeyInput, cache_key};
use crate::prompt::{Exclude, Preset};
use std::fs;
use tempfile::TempDir;
#[test]
fn cache_key_differs_by_input_mode() {
let dir = TempDir::new().unwrap();
let source = dir.path().join("scan.png");
fs::write(&source, b"fake image").unwrap();
let excludes: &[Exclude] = &[];
let single = cache_key(&CacheKeyInput {
source: &source,
page: 1,
total_pages: 1,
dpi: 0,
model: "google/gemini-3-flash-preview",
preset: Preset::Markdown,
excludes,
instruction: None,
prev_tail: None,
input_mode: "single_image",
})
.unwrap();
let folder = cache_key(&CacheKeyInput {
source: &source,
page: 1,
total_pages: 1,
dpi: 0,
model: "google/gemini-3-flash-preview",
preset: Preset::Markdown,
excludes,
instruction: None,
prev_tail: None,
input_mode: "image_folder_page",
})
.unwrap();
assert_ne!(single, folder);
}
}