Skip to main content

grip/
cache.rs

1// Copyright 2026 Umberto Gotti <umberto.gotti@umbertogotti.dev>
2// Licensed under the MIT License
3// SPDX-License-Identifier: MIT
4
5use std::cell::{Cell, RefCell};
6use std::collections::HashMap;
7use std::fs;
8use std::path::{Path, PathBuf};
9use std::time::SystemTime;
10
11use anyhow::Result;
12use serde::{Deserialize, Serialize};
13
14use crate::item_counts::ItemCounts;
15use crate::traits::cache_store::CacheStore;
16
17#[derive(Debug)]
18pub struct Cache {
19    cache_dir: PathBuf,
20    store: RefCell<HashMap<PathBuf, CachedEntry>>,
21    dirty: Cell<bool>,
22}
23
24#[derive(Debug, Clone, Serialize, Deserialize)]
25struct CachedEntry {
26    mtime_secs: u128,
27    len: u64,
28    counts: ItemCounts,
29}
30
31impl Cache {
32    #[must_use]
33    pub fn new(root: &Path) -> Self {
34        let cache_dir = root.join(".grip_cache");
35        let store = if cache_dir.exists() {
36            Self::load(&cache_dir).unwrap_or_default()
37        } else {
38            HashMap::new()
39        };
40        Self {
41            cache_dir,
42            store: RefCell::new(store),
43            dirty: Cell::new(false),
44        }
45    }
46
47    fn mtime_secs(path: &Path) -> Option<u128> {
48        let metadata = fs::metadata(path).ok()?;
49        let mtime = metadata.modified().ok()?;
50        Some(mtime.duration_since(SystemTime::UNIX_EPOCH).ok()?.as_secs() as u128)
51    }
52
53    fn load(cache_dir: &Path) -> Result<HashMap<PathBuf, CachedEntry>> {
54        let path = cache_dir.join("cache.json");
55        let json = fs::read_to_string(&path)?;
56        Ok(serde_json::from_str(&json)?)
57    }
58}
59
60impl CacheStore for Cache {
61    fn get(&self, path: &Path) -> Option<ItemCounts> {
62        let mtime_secs = Self::mtime_secs(path)?;
63        let len = fs::metadata(path).ok()?.len();
64        let store = self.store.borrow();
65        let entry = store.get(path)?;
66        if entry.mtime_secs == mtime_secs && entry.len == len {
67            Some(entry.counts.clone())
68        } else {
69            None
70        }
71    }
72
73    fn set(&self, path: &Path, source: &str, counts: &ItemCounts) {
74        let Some(mtime_secs) = Self::mtime_secs(path) else {
75            return;
76        };
77        self.store.borrow_mut().insert(
78            path.to_path_buf(),
79            CachedEntry {
80                mtime_secs,
81                len: source.len() as u64,
82                counts: counts.clone(),
83            },
84        );
85        self.dirty.set(true);
86    }
87
88    fn flush(&self) {
89        if !self.dirty.get() {
90            return;
91        }
92        let _ = fs::create_dir_all(&self.cache_dir);
93        if let Ok(json) = serde_json::to_string(&*self.store.borrow()) {
94            let _ = fs::write(self.cache_dir.join("cache.json"), json);
95        }
96    }
97}