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