use crate::types::PreprocessorOutput;
use crate::cache::GraphCacheKey;
use anyhow::{anyhow, Result};
use sha2::{Digest, Sha256};
use std::fs;
use std::path::Path;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum CachePoint {
C0,
C1,
C2,
C3,
}
impl CachePoint {
pub fn all() -> &'static [CachePoint] {
&[CachePoint::C0, CachePoint::C1, CachePoint::C2, CachePoint::C3]
}
pub fn cascade(&self) -> Vec<CachePoint> {
CachePoint::all().iter().copied().filter(|p| p >= self).collect()
}
pub fn dir_name(&self) -> &'static str {
match self {
CachePoint::C0 => "c0-pdf",
CachePoint::C1 => "c1-xhtml",
CachePoint::C2 => "c2-preprocessor",
CachePoint::C3 => "c3-graph",
}
}
pub fn from_str_with_all(s: &str) -> Result<Option<Self>> {
match s.to_lowercase().as_str() {
"c0" => Ok(Some(CachePoint::C0)),
"c1" => Ok(Some(CachePoint::C1)),
"c2" => Ok(Some(CachePoint::C2)),
"c3" => Ok(Some(CachePoint::C3)),
"all" => Ok(None), _ => Err(anyhow!("Invalid cache point: '{}'. Use c0, c1, c2, c3, or all", s)),
}
}
}
impl std::fmt::Display for CachePoint {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", match self {
CachePoint::C0 => "C0 (PDF)",
CachePoint::C1 => "C1 (XHTML)",
CachePoint::C2 => "C2 (Preprocessor)",
CachePoint::C3 => "C3 (Graph)",
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FreshFrom {
None,
C0,
C1,
C2,
C3,
}
impl FreshFrom {
pub fn should_use_cache(&self, point: CachePoint) -> bool {
match self {
FreshFrom::None => true,
FreshFrom::C0 => false,
FreshFrom::C1 => point < CachePoint::C1,
FreshFrom::C2 => point < CachePoint::C2,
FreshFrom::C3 => point < CachePoint::C3,
}
}
pub fn parse(s: &str) -> Result<Self> {
match s.to_lowercase().as_str() {
"c0" => Ok(FreshFrom::C0),
"c1" => Ok(FreshFrom::C1),
"c2" => Ok(FreshFrom::C2),
"c3" => Ok(FreshFrom::C3),
_ => Err(anyhow!("Invalid fresh-from value: '{}'. Use c0, c1, c2, or c3", s)),
}
}
}
#[derive(Debug, Clone)]
pub struct CacheDefaults {
pub c0_pdf: bool,
pub c1_xhtml: bool,
pub c2_preprocessor: bool,
pub c3_graph: bool,
}
impl Default for CacheDefaults {
fn default() -> Self {
Self {
c0_pdf: false,
c1_xhtml: true,
c2_preprocessor: true,
c3_graph: false,
}
}
}
impl CacheDefaults {
pub fn should_write(&self, point: CachePoint) -> bool {
match point {
CachePoint::C0 => self.c0_pdf,
CachePoint::C1 => self.c1_xhtml,
CachePoint::C2 => self.c2_preprocessor,
CachePoint::C3 => self.c3_graph,
}
}
}
pub struct CacheClearResult {
pub deleted: Vec<(CachePoint, usize)>,
}
pub trait DocumentStorage {
fn get_pdf(&self, hash: &str) -> Result<Option<Vec<u8>>>;
fn store_pdf(&self, hash: &str, data: &[u8]) -> Result<()>;
fn get_xhtml(&self, pdf_hash: &str) -> Result<Option<String>>;
fn store_xhtml(&self, pdf_hash: &str, xhtml: &str) -> Result<()>;
fn get_preprocessor_output(&self, pdf_hash: &str) -> Result<Option<PreprocessorOutput>>;
fn store_preprocessor_output(&self, pdf_hash: &str, output: &PreprocessorOutput) -> Result<()>;
fn get_graph_output(&self, cache_key: &GraphCacheKey) -> Result<Option<crate::cache::GraphCacheValue>>;
fn store_graph_output(&self, cache_key: &GraphCacheKey, cache_value: &crate::cache::GraphCacheValue) -> Result<()>;
fn clear_cache(&self, from_point: Option<CachePoint>) -> Result<CacheClearResult>;
}
pub struct FileStorage {
cache_dir: String,
}
impl FileStorage {
pub fn new(cache_dir: &str) -> Result<Self> {
fs::create_dir_all(cache_dir)?;
for point in CachePoint::all() {
fs::create_dir_all(format!("{}/{}", cache_dir, point.dir_name()))?;
}
fs::create_dir_all(format!("{cache_dir}/debug"))?;
Ok(Self {
cache_dir: cache_dir.to_string(),
})
}
pub fn cache_dir(&self) -> &str {
&self.cache_dir
}
fn pdf_path(&self, hash: &str) -> String {
format!("{}/c0-pdf/{}.pdf", self.cache_dir, hash)
}
fn xhtml_path(&self, hash: &str) -> String {
format!("{}/c1-xhtml/{}.xhtml", self.cache_dir, hash)
}
fn preprocessor_path(&self, hash: &str) -> String {
format!("{}/c2-preprocessor/{}.json", self.cache_dir, hash)
}
fn graph_path(&self, cache_key: &GraphCacheKey) -> String {
format!("{}/c3-graph/{}.json", self.cache_dir, cache_key.to_cache_hash())
}
fn clear_dir(&self, point: CachePoint) -> Result<usize> {
let dir = format!("{}/{}", self.cache_dir, point.dir_name());
let path = Path::new(&dir);
if !path.exists() {
return Ok(0);
}
let mut count = 0;
for entry in fs::read_dir(path)? {
let entry = entry?;
if entry.file_type()?.is_file() {
fs::remove_file(entry.path())?;
count += 1;
}
}
Ok(count)
}
}
impl DocumentStorage for FileStorage {
fn get_pdf(&self, hash: &str) -> Result<Option<Vec<u8>>> {
let path = self.pdf_path(hash);
if Path::new(&path).exists() {
Ok(Some(fs::read(path)?))
} else {
Ok(None)
}
}
fn store_pdf(&self, hash: &str, data: &[u8]) -> Result<()> {
let path = self.pdf_path(hash);
fs::write(path, data)?;
Ok(())
}
fn get_xhtml(&self, pdf_hash: &str) -> Result<Option<String>> {
let path = self.xhtml_path(pdf_hash);
if Path::new(&path).exists() {
Ok(Some(fs::read_to_string(path)?))
} else {
Ok(None)
}
}
fn store_xhtml(&self, pdf_hash: &str, xhtml: &str) -> Result<()> {
let path = self.xhtml_path(pdf_hash);
fs::write(path, xhtml)?;
Ok(())
}
fn get_preprocessor_output(&self, pdf_hash: &str) -> Result<Option<PreprocessorOutput>> {
let path = self.preprocessor_path(pdf_hash);
if Path::new(&path).exists() {
let json_str = fs::read_to_string(path)?;
let output: PreprocessorOutput = serde_json::from_str(&json_str)
.map_err(|e| anyhow!("Failed to deserialize cached PreprocessorOutput: {}", e))?;
Ok(Some(output))
} else {
Ok(None)
}
}
fn store_preprocessor_output(&self, pdf_hash: &str, output: &PreprocessorOutput) -> Result<()> {
let path = self.preprocessor_path(pdf_hash);
let json_str = serde_json::to_string_pretty(output)
.map_err(|e| anyhow!("Failed to serialize PreprocessorOutput: {}", e))?;
fs::write(path, json_str)?;
Ok(())
}
fn get_graph_output(&self, cache_key: &GraphCacheKey) -> Result<Option<crate::cache::GraphCacheValue>> {
let path = self.graph_path(cache_key);
if Path::new(&path).exists() {
let json_str = fs::read_to_string(path)?;
let cache_value: crate::cache::GraphCacheValue = serde_json::from_str(&json_str)
.map_err(|e| anyhow!("Failed to deserialize cached GraphCacheValue: {}", e))?;
Ok(Some(cache_value))
} else {
Ok(None)
}
}
fn store_graph_output(&self, cache_key: &GraphCacheKey, cache_value: &crate::cache::GraphCacheValue) -> Result<()> {
let path = self.graph_path(cache_key);
let json_str = serde_json::to_string_pretty(cache_value)
.map_err(|e| anyhow!("Failed to serialize GraphCacheValue: {}", e))?;
fs::write(path, json_str)?;
Ok(())
}
fn clear_cache(&self, from_point: Option<CachePoint>) -> Result<CacheClearResult> {
let points_to_clear: Vec<CachePoint> = match from_point {
Some(point) => point.cascade(),
None => CachePoint::all().to_vec(), };
let mut deleted = Vec::new();
for point in points_to_clear {
let count = self.clear_dir(point)?;
if count > 0 {
deleted.push((point, count));
}
}
if from_point.is_none() {
let debug_dir = format!("{}/debug", self.cache_dir);
if Path::new(&debug_dir).exists() {
let mut count = 0;
for entry in fs::read_dir(&debug_dir)? {
let entry = entry?;
if entry.file_type()?.is_file() {
fs::remove_file(entry.path())?;
count += 1;
}
}
if count > 0 {
println!(" Deleted: {} files from debug/", count);
}
}
}
Ok(CacheClearResult { deleted })
}
}
pub fn calculate_pdf_hash(pdf_bytes: &[u8]) -> String {
let chunk_size = 1024; let mut hasher = Sha256::new();
hasher.update(pdf_bytes.len().to_le_bytes());
let start_end = std::cmp::min(chunk_size, pdf_bytes.len());
hasher.update(&pdf_bytes[0..start_end]);
if pdf_bytes.len() > chunk_size {
let end_start = pdf_bytes.len() - chunk_size;
hasher.update(&pdf_bytes[end_start..]);
}
format!("{:x}", hasher.finalize())
}
pub fn calculate_config_hash<T: serde::Serialize>(config: &T) -> Result<String> {
let config_json = serde_json::to_string(config)
.map_err(|e| anyhow!("Failed to serialize config for hashing: {}", e))?;
let mut hasher = Sha256::new();
hasher.update(config_json.as_bytes());
Ok(format!("{:x}", hasher.finalize()))
}
pub fn calculate_xhtml_hash(xhtml: &str) -> String {
let mut hasher = Sha256::new();
hasher.update(xhtml.as_bytes());
format!("{:x}", hasher.finalize())
}
pub struct NoOpStorage;
impl Default for NoOpStorage {
fn default() -> Self {
Self::new()
}
}
impl NoOpStorage {
pub fn new() -> Self {
Self
}
}
impl DocumentStorage for NoOpStorage {
fn get_pdf(&self, _hash: &str) -> Result<Option<Vec<u8>>> { Ok(None) }
fn store_pdf(&self, _hash: &str, _data: &[u8]) -> Result<()> { Ok(()) }
fn get_xhtml(&self, _pdf_hash: &str) -> Result<Option<String>> { Ok(None) }
fn store_xhtml(&self, _pdf_hash: &str, _xhtml: &str) -> Result<()> { Ok(()) }
fn get_preprocessor_output(&self, _pdf_hash: &str) -> Result<Option<PreprocessorOutput>> { Ok(None) }
fn store_preprocessor_output(&self, _pdf_hash: &str, _output: &PreprocessorOutput) -> Result<()> { Ok(()) }
fn get_graph_output(&self, _cache_key: &GraphCacheKey) -> Result<Option<crate::cache::GraphCacheValue>> { Ok(None) }
fn store_graph_output(&self, _cache_key: &GraphCacheKey, _cache_value: &crate::cache::GraphCacheValue) -> Result<()> { Ok(()) }
fn clear_cache(&self, _from_point: Option<CachePoint>) -> Result<CacheClearResult> {
Ok(CacheClearResult { deleted: vec![] })
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_pdf_hash_consistency() {
let pdf_data = b"test pdf content with some data";
let hash1 = calculate_pdf_hash(pdf_data);
let hash2 = calculate_pdf_hash(pdf_data);
assert_eq!(hash1, hash2);
}
#[test]
fn test_pdf_hash_uniqueness() {
let pdf1 = b"test pdf content 1";
let pdf2 = b"test pdf content 2";
let hash1 = calculate_pdf_hash(pdf1);
let hash2 = calculate_pdf_hash(pdf2);
assert_ne!(hash1, hash2);
}
#[test]
fn test_file_storage_roundtrip() {
let temp_dir = std::env::temp_dir().join("blazegraph_test_cache_cr11");
let _ = std::fs::remove_dir_all(&temp_dir); let storage = FileStorage::new(temp_dir.to_str().unwrap()).unwrap();
let test_data = b"test pdf data";
let hash = "test_hash";
storage.store_pdf(hash, test_data).unwrap();
let retrieved = storage.get_pdf(hash).unwrap();
assert_eq!(retrieved, Some(test_data.to_vec()));
let xhtml = "<html><body>test</body></html>";
storage.store_xhtml(hash, xhtml).unwrap();
let retrieved_xhtml = storage.get_xhtml(hash).unwrap();
assert_eq!(retrieved_xhtml, Some(xhtml.to_string()));
let xhtml_path = format!("{}/c1-xhtml/{}.xhtml", temp_dir.display(), hash);
assert!(Path::new(&xhtml_path).exists());
std::fs::remove_dir_all(temp_dir).ok();
}
#[test]
fn test_cache_point_ordering() {
assert!(CachePoint::C0 < CachePoint::C1);
assert!(CachePoint::C1 < CachePoint::C2);
assert!(CachePoint::C2 < CachePoint::C3);
}
#[test]
fn test_cache_point_cascade() {
assert_eq!(CachePoint::C0.cascade(), vec![CachePoint::C0, CachePoint::C1, CachePoint::C2, CachePoint::C3]);
assert_eq!(CachePoint::C1.cascade(), vec![CachePoint::C1, CachePoint::C2, CachePoint::C3]);
assert_eq!(CachePoint::C2.cascade(), vec![CachePoint::C2, CachePoint::C3]);
assert_eq!(CachePoint::C3.cascade(), vec![CachePoint::C3]);
}
#[test]
fn test_fresh_from_cache_bypass() {
assert!(FreshFrom::None.should_use_cache(CachePoint::C0));
assert!(FreshFrom::None.should_use_cache(CachePoint::C3));
assert!(!FreshFrom::C0.should_use_cache(CachePoint::C0));
assert!(!FreshFrom::C0.should_use_cache(CachePoint::C3));
assert!(FreshFrom::C2.should_use_cache(CachePoint::C0));
assert!(FreshFrom::C2.should_use_cache(CachePoint::C1));
assert!(!FreshFrom::C2.should_use_cache(CachePoint::C2));
assert!(!FreshFrom::C2.should_use_cache(CachePoint::C3));
}
#[test]
fn test_clear_cache_cascade() {
let temp_dir = std::env::temp_dir().join("blazegraph_test_clear_cr11");
let _ = std::fs::remove_dir_all(&temp_dir);
let storage = FileStorage::new(temp_dir.to_str().unwrap()).unwrap();
storage.store_xhtml("hash1", "<html>test</html>").unwrap();
storage.store_xhtml("hash2", "<html>test2</html>").unwrap();
let result = storage.clear_cache(Some(CachePoint::C1)).unwrap();
assert!(result.deleted.iter().any(|(p, c)| *p == CachePoint::C1 && *c == 2));
assert!(storage.get_xhtml("hash1").unwrap().is_none());
assert!(storage.get_xhtml("hash2").unwrap().is_none());
std::fs::remove_dir_all(temp_dir).ok();
}
#[test]
fn test_cache_defaults() {
let defaults = CacheDefaults::default();
assert!(!defaults.should_write(CachePoint::C0));
assert!(defaults.should_write(CachePoint::C1));
assert!(defaults.should_write(CachePoint::C2));
assert!(!defaults.should_write(CachePoint::C3));
}
}