use std::fs;
use std::path::{Path, PathBuf};
use std::time::{SystemTime, Duration};
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
use std::io::{self, Write, Read, BufRead};
use std::fs::OpenOptions;
use std::collections::BTreeMap;
#[derive(Debug, Clone, Default)]
pub struct CacheMeta {
pub headers: BTreeMap<String, String>,
pub range: Option<(u64, u64)>, }
pub struct Cache {
dir: PathBuf,
ttl: Duration,
}
impl Cache {
pub fn new(dir: PathBuf, ttl_secs: u64) -> Self {
fs::create_dir_all(&dir).ok();
Self { dir, ttl: Duration::from_secs(ttl_secs) }
}
pub fn get(&self, key: &str, range: Option<(u64, u64)>) -> Option<(Vec<u8>, CacheMeta)> {
let path = self.key_path(key, range);
let meta_path = self.meta_path(key, range);
let meta = fs::metadata(&path).ok()?;
let modified = meta.modified().ok()?;
if SystemTime::now().duration_since(modified).ok()? > self.ttl {
let _ = fs::remove_file(&path);
let _ = fs::remove_file(&meta_path);
return None;
}
let data = fs::read(&path).ok()?;
let meta = Self::read_meta(&meta_path).unwrap_or_default();
Some((data, meta))
}
pub fn set(&self, key: &str, range: Option<(u64, u64)>, value: &[u8], meta: &CacheMeta) {
let path = self.key_path(key, range);
let meta_path = self.meta_path(key, range);
let _ = fs::write(&path, value);
let _ = Self::write_meta(&meta_path, meta);
}
pub fn stream_to_cache(&self, key: &str, range: Option<(u64, u64)>, mut reader: impl Read, meta: &CacheMeta) -> io::Result<()> {
let path = self.key_path(key, range);
let meta_path = self.meta_path(key, range);
let mut file = OpenOptions::new().create(true).write(true).truncate(true).open(&path)?;
io::copy(&mut reader, &mut file)?;
Self::write_meta(&meta_path, meta)?;
Ok(())
}
pub fn remove(&self, key: &str, range: Option<(u64, u64)>) {
let path = self.key_path(key, range);
let meta_path = self.meta_path(key, range);
let _ = fs::remove_file(&path);
let _ = fs::remove_file(&meta_path);
}
pub fn key_path(&self, key: &str, range: Option<(u64, u64)>) -> PathBuf {
let mut hasher = DefaultHasher::new();
key.hash(&mut hasher);
if let Some((start, end)) = range {
(start, end).hash(&mut hasher);
}
let hash = hasher.finish();
self.dir.join(format!("{}.cache", hash))
}
pub fn meta_path(&self, key: &str, range: Option<(u64, u64)>) -> PathBuf {
let mut hasher = DefaultHasher::new();
key.hash(&mut hasher);
if let Some((start, end)) = range {
(start, end).hash(&mut hasher);
}
let hash = hasher.finish();
self.dir.join(format!("{}.meta", hash))
}
fn write_meta(path: &Path, meta: &CacheMeta) -> io::Result<()> {
let mut file = OpenOptions::new().create(true).write(true).truncate(true).open(path)?;
for (k, v) in &meta.headers {
writeln!(file, "header:{}:{}", k, v)?;
}
if let Some((start, end)) = meta.range {
writeln!(file, "range:{}-{}", start, end)?;
}
Ok(())
}
fn read_meta(path: &Path) -> io::Result<CacheMeta> {
let mut meta = CacheMeta::default();
let file = OpenOptions::new().read(true).open(path)?;
for line in io::BufReader::new(file).lines() {
let line = line?;
if let Some(rest) = line.strip_prefix("header:") {
if let Some((k, v)) = rest.split_once(":") {
meta.headers.insert(k.to_string(), v.to_string());
}
} else if let Some(rest) = line.strip_prefix("range:") {
if let Some((s, e)) = rest.split_once('-') {
if let (Ok(start), Ok(end)) = (s.parse(), e.parse()) {
meta.range = Some((start, end));
}
}
}
}
Ok(meta)
}
}