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;
use linked_hash_map::LinkedHashMap;
use std::sync::Mutex;
use linked_hash_map::Entry;
#[derive(Debug, Clone, Default)]
pub struct CacheMeta {
pub headers: BTreeMap<String, String>,
pub range: Option<(u64, u64)>, }
pub struct Cache {
dir: PathBuf,
ttl: Duration,
mem: Mutex<MemCache>,
}
const MEM_CACHE_CAPACITY: usize = 128;
type MemKey = (String, Option<(u64, u64)>);
type MemValue = (Vec<u8>, CacheMeta, SystemTime);
struct MemCache {
map: LinkedHashMap<MemKey, MemValue>,
hits: usize,
misses: usize,
ttl: Duration,
}
impl MemCache {
fn new() -> Self {
Self { map: LinkedHashMap::with_capacity(MEM_CACHE_CAPACITY), hits: 0, misses: 0, ttl: Duration::from_secs(3600) }
}
fn get(&mut self, key: &MemKey, ttl: Duration) -> Option<(Vec<u8>, CacheMeta)> {
if let Some((data, meta, inserted)) = self.map.get(key) {
if inserted.elapsed().unwrap_or(ttl + Duration::from_secs(1)) > ttl {
self.map.remove(key);
self.misses += 1;
return None;
}
self.hits += 1;
Some((data.clone(), meta.clone()))
} else {
self.misses += 1;
None
}
}
fn stats(&self) -> (usize, usize) {
(self.hits, self.misses)
}
fn put(&mut self, key: MemKey, val: (Vec<u8>, CacheMeta)) {
match self.map.entry(key) {
Entry::Occupied(mut occ) => {
let v = occ.get_mut();
v.0 = val.0;
v.1 = val.1;
v.2 = SystemTime::now();
}
Entry::Vacant(vac) => {
vac.insert((val.0, val.1, SystemTime::now()));
}
}
if self.map.len() > MEM_CACHE_CAPACITY {
let mut removed = false;
while self.map.len() > MEM_CACHE_CAPACITY {
let oldest = self.map.front().map(|(k, _)| k.clone());
if let Some(k) = oldest {
self.map.remove(&k);
removed = true;
}
}
if removed {
self.cleanup_expired();
}
}
}
fn cleanup_expired(&mut self) {
let now = SystemTime::now();
let ttl = self.ttl;
let expired: Vec<_> = self.map.iter()
.filter_map(|(k, (_, _, inserted))| {
if now.duration_since(*inserted).unwrap_or(ttl + Duration::from_secs(1)) > ttl {
Some(k.clone())
} else {
None
}
})
.collect();
for k in expired {
self.map.remove(&k);
}
}
}
impl Cache {
pub fn mem_stats(&self) -> (usize, usize) {
self.mem.lock().map(|m| m.stats()).unwrap_or((0, 0))
}
pub fn new(dir: PathBuf, ttl_secs: u64) -> Self {
fs::create_dir_all(&dir).ok();
let ttl = Duration::from_secs(ttl_secs);
Self {
dir,
ttl,
mem: Mutex::new({
let mut m = MemCache::new();
m.ttl = ttl;
m
}),
}
}
pub fn get(&self, key: &str, range: Option<(u64, u64)>) -> Option<(Vec<u8>, CacheMeta)> {
let mem_key = (key.to_string(), range);
{
let mut mem = self.mem.lock().ok()?;
if let Some(val) = mem.get(&mem_key, self.ttl) {
return Some(val);
}
}
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();
if let Ok(mut mem) = self.mem.lock() {
mem.put(mem_key, (data.clone(), meta.clone()));
}
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);
if let Ok(mut file) = fs::File::create(&path) {
let _ = file.write_all(value);
}
let _ = Self::write_meta(&meta_path, meta);
let mem_key = (key.to_string(), range);
let mem_val = (value.to_vec(), meta.clone());
if let Ok(mut mem) = self.mem.lock() {
mem.put(mem_key, mem_val);
}
}
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)
}
}