Rust · 5490 bytes Raw Blame History
1 //! Disk cache for remote images
2
3 use anyhow::{Context, Result};
4 use serde::{Deserialize, Serialize};
5 use std::collections::HashMap;
6 use std::path::PathBuf;
7 use std::time::SystemTime;
8
9 /// Disk cache for remote images
10 pub struct DiskCache {
11 cache_dir: PathBuf,
12 max_size: u64,
13 index: CacheIndex,
14 }
15
16 /// Cache index stored on disk
17 #[derive(Debug, Default, Serialize, Deserialize)]
18 struct CacheIndex {
19 entries: HashMap<String, CacheEntry>,
20 total_size: u64,
21 }
22
23 /// Individual cache entry
24 #[derive(Debug, Clone, Serialize, Deserialize)]
25 struct CacheEntry {
26 /// Hash of the original URI
27 uri_hash: String,
28 /// Original URI
29 original_uri: String,
30 /// Path to cached file (relative to cache dir)
31 file_path: String,
32 /// File size in bytes
33 size: u64,
34 /// When the file was fetched
35 fetched_at: SystemTime,
36 /// Last access time
37 last_accessed: SystemTime,
38 /// HTTP ETag for conditional requests
39 etag: Option<String>,
40 }
41
42 impl DiskCache {
43 /// Create a new disk cache
44 pub fn new(cache_dir: PathBuf, max_size_mb: u64) -> Result<Self> {
45 std::fs::create_dir_all(&cache_dir)
46 .with_context(|| format!("Failed to create cache directory: {}", cache_dir.display()))?;
47
48 let index_path = cache_dir.join("index.json");
49 let index = if index_path.exists() {
50 let data = std::fs::read_to_string(&index_path)?;
51 serde_json::from_str(&data).unwrap_or_default()
52 } else {
53 CacheIndex::default()
54 };
55
56 Ok(Self {
57 cache_dir,
58 max_size: max_size_mb * 1024 * 1024,
59 index,
60 })
61 }
62
63 /// Get the default cache directory
64 pub fn default_dir() -> Option<PathBuf> {
65 dirs::cache_dir().map(|d| d.join("garbg"))
66 }
67
68 /// Check if a URI is cached
69 pub fn is_cached(&self, uri: &str) -> bool {
70 let hash = Self::hash_uri(uri);
71 if let Some(entry) = self.index.entries.get(&hash) {
72 let path = self.cache_dir.join(&entry.file_path);
73 path.exists()
74 } else {
75 false
76 }
77 }
78
79 /// Get cached file path for a URI
80 pub fn get(&mut self, uri: &str) -> Option<PathBuf> {
81 let hash = Self::hash_uri(uri);
82 if let Some(entry) = self.index.entries.get_mut(&hash) {
83 let path = self.cache_dir.join(&entry.file_path);
84 if path.exists() {
85 entry.last_accessed = SystemTime::now();
86 return Some(path);
87 }
88 }
89 None
90 }
91
92 /// Store data in the cache
93 pub fn store(&mut self, uri: &str, data: &[u8], etag: Option<String>) -> Result<PathBuf> {
94 let hash = Self::hash_uri(uri);
95
96 // Create subdirectory based on first 2 chars of hash
97 let subdir = &hash[..2];
98 let dir = self.cache_dir.join(subdir);
99 std::fs::create_dir_all(&dir)?;
100
101 // Write file
102 let file_path = format!("{}/{}", subdir, hash);
103 let full_path = self.cache_dir.join(&file_path);
104 std::fs::write(&full_path, data)?;
105
106 // Update index
107 let entry = CacheEntry {
108 uri_hash: hash.clone(),
109 original_uri: uri.to_string(),
110 file_path,
111 size: data.len() as u64,
112 fetched_at: SystemTime::now(),
113 last_accessed: SystemTime::now(),
114 etag,
115 };
116
117 // Remove old entry if exists
118 if let Some(old) = self.index.entries.remove(&hash) {
119 self.index.total_size -= old.size;
120 }
121
122 self.index.total_size += entry.size;
123 self.index.entries.insert(hash, entry);
124
125 // Evict if over size limit
126 self.evict_if_needed()?;
127
128 // Save index
129 self.save_index()?;
130
131 Ok(full_path)
132 }
133
134 /// Evict old entries if cache is over size limit
135 fn evict_if_needed(&mut self) -> Result<()> {
136 if self.index.total_size <= self.max_size {
137 return Ok(());
138 }
139
140 // Sort by last accessed time (oldest first)
141 let mut entries: Vec<_> = self.index.entries.values().cloned().collect();
142 entries.sort_by_key(|e| e.last_accessed);
143
144 // Remove oldest until under limit
145 for entry in entries {
146 if self.index.total_size <= self.max_size {
147 break;
148 }
149
150 let path = self.cache_dir.join(&entry.file_path);
151 if path.exists() {
152 std::fs::remove_file(&path)?;
153 }
154
155 self.index.total_size -= entry.size;
156 self.index.entries.remove(&entry.uri_hash);
157 }
158
159 Ok(())
160 }
161
162 /// Save the cache index to disk
163 fn save_index(&self) -> Result<()> {
164 let index_path = self.cache_dir.join("index.json");
165 let data = serde_json::to_string_pretty(&self.index)?;
166 std::fs::write(index_path, data)?;
167 Ok(())
168 }
169
170 /// Clear the entire cache
171 pub fn clear(&mut self) -> Result<()> {
172 for entry in self.index.entries.values() {
173 let path = self.cache_dir.join(&entry.file_path);
174 if path.exists() {
175 let _ = std::fs::remove_file(&path);
176 }
177 }
178 self.index.entries.clear();
179 self.index.total_size = 0;
180 self.save_index()?;
181 Ok(())
182 }
183
184 /// Hash a URI for cache key
185 fn hash_uri(uri: &str) -> String {
186 let hash = blake3::hash(uri.as_bytes());
187 hash.to_hex().to_string()
188 }
189 }
190