1use std::{
2 cmp::Reverse,
3 collections::HashMap,
4 fs::{self, File},
5 io::{self, Read, Write},
6 path::{Path, PathBuf},
7 sync::{Arc, Mutex},
8 time::SystemTime,
9};
10
11use priority_queue::PriorityQueue;
12use thiserror::Error;
13
14use crate::{Error, FileId, authentication::Credentials, error::ErrorKind};
15
16const CACHE_LIMITER_POISON_MSG: &str = "cache limiter mutex should not be poisoned";
17
18#[derive(Debug, Error)]
19pub enum CacheError {
20 #[error("audio cache location is not configured")]
21 Path,
22}
23
24impl From<CacheError> for Error {
25 fn from(err: CacheError) -> Self {
26 Error::failed_precondition(err)
27 }
28}
29
30struct SizeLimiter {
34 queue: PriorityQueue<PathBuf, Reverse<SystemTime>>,
35 sizes: HashMap<PathBuf, u64>,
36 size_limit: u64,
37 in_use: u64,
38}
39
40impl SizeLimiter {
41 fn new(limit: u64) -> Self {
43 Self {
44 queue: PriorityQueue::new(),
45 sizes: HashMap::new(),
46 size_limit: limit,
47 in_use: 0,
48 }
49 }
50
51 fn add(&mut self, file: &Path, size: u64, accessed: SystemTime) {
55 self.in_use += size;
56 self.queue.push(file.to_owned(), Reverse(accessed));
57 if let Some(old_size) = self.sizes.insert(file.to_owned(), size) {
58 self.in_use -= old_size;
61 }
62 }
63
64 fn exceeds_limit(&self) -> bool {
66 self.in_use > self.size_limit
67 }
68
69 fn pop(&mut self) -> Option<PathBuf> {
75 if self.exceeds_limit() {
76 if let Some((next, _)) = self.queue.pop() {
77 if let Some(size) = self.sizes.remove(&next) {
78 self.in_use -= size;
79 } else {
80 error!("`queue` and `sizes` should have the same keys.");
81 }
82 Some(next)
83 } else {
84 error!("in_use was > 0, so the queue should have contained an item.");
85 None
86 }
87 } else {
88 None
89 }
90 }
91
92 fn update(&mut self, file: &Path, access_time: SystemTime) -> bool {
94 self.queue
95 .change_priority(file, Reverse(access_time))
96 .is_some()
97 }
98
99 fn remove(&mut self, file: &Path) -> bool {
101 if self.queue.remove(file).is_none() {
102 return false;
103 }
104
105 if let Some(size) = self.sizes.remove(file) {
106 self.in_use -= size;
107 } else {
108 error!("`queue` and `sizes` should have the same keys.");
109 }
110
111 true
112 }
113}
114
115struct FsSizeLimiter {
116 limiter: Mutex<SizeLimiter>,
117}
118
119impl FsSizeLimiter {
120 fn get_metadata(file: &Path) -> io::Result<(SystemTime, u64)> {
122 let metadata = file.metadata()?;
123
124 let access_time = metadata
130 .accessed()
131 .or_else(|_| metadata.modified())
132 .or_else(|_| metadata.created())
133 .unwrap_or_else(|_| SystemTime::now());
134
135 let size = metadata.len();
136
137 Ok((access_time, size))
138 }
139
140 fn init_dir(limiter: &mut SizeLimiter, path: &Path) {
142 let list_dir = match fs::read_dir(path) {
143 Ok(list_dir) => list_dir,
144 Err(e) => {
145 warn!("Could not read directory {path:?} in cache dir: {e}");
146 return;
147 }
148 };
149
150 for entry in list_dir {
151 let entry = match entry {
152 Ok(entry) => entry,
153 Err(e) => {
154 warn!("Could not directory {path:?} in cache dir: {e}");
155 return;
156 }
157 };
158
159 match entry.file_type() {
160 Ok(file_type) if file_type.is_dir() || file_type.is_symlink() => {
161 Self::init_dir(limiter, &entry.path())
162 }
163 Ok(file_type) if file_type.is_file() => {
164 let path = entry.path();
165 match Self::get_metadata(&path) {
166 Ok((access_time, size)) => {
167 limiter.add(&path, size, access_time);
168 }
169 Err(e) => {
170 warn!("Could not read file {path:?} in cache dir: {e}")
171 }
172 }
173 }
174 Ok(ft) => {
175 warn!(
176 "File {:?} in cache dir has unsupported type {:?}",
177 entry.path(),
178 ft
179 )
180 }
181 Err(e) => {
182 warn!(
183 "Could not get type of file {:?} in cache dir: {}",
184 entry.path(),
185 e
186 )
187 }
188 };
189 }
190 }
191
192 fn add(&self, file: &Path, size: u64) {
193 self.limiter
194 .lock()
195 .expect(CACHE_LIMITER_POISON_MSG)
196 .add(file, size, SystemTime::now())
197 }
198
199 fn touch(&self, file: &Path) -> bool {
200 self.limiter
201 .lock()
202 .expect(CACHE_LIMITER_POISON_MSG)
203 .update(file, SystemTime::now())
204 }
205
206 fn remove(&self, file: &Path) -> bool {
207 self.limiter
208 .lock()
209 .expect(CACHE_LIMITER_POISON_MSG)
210 .remove(file)
211 }
212
213 fn prune_internal<F: FnMut() -> Option<PathBuf>>(mut pop: F) -> Result<(), Error> {
214 let mut first = true;
215 let mut count = 0;
216 let mut last_error = None;
217
218 while let Some(file) = pop() {
219 if first {
220 debug!("Cache dir exceeds limit, removing least recently used files.");
221 first = false;
222 }
223
224 let res = fs::remove_file(&file);
225 if let Err(e) = res {
226 warn!("Could not remove file {file:?} from cache dir: {e}");
227 last_error = Some(e);
228 } else {
229 count += 1;
230 }
231 }
232
233 if count > 0 {
234 info!("Removed {count} cache files.");
235 }
236
237 if let Some(err) = last_error {
238 Err(err.into())
239 } else {
240 Ok(())
241 }
242 }
243
244 fn prune(&self) -> Result<(), Error> {
245 Self::prune_internal(|| self.limiter.lock().expect(CACHE_LIMITER_POISON_MSG).pop())
246 }
247
248 fn new(path: &Path, limit: u64) -> Result<Self, Error> {
249 let mut limiter = SizeLimiter::new(limit);
250
251 Self::init_dir(&mut limiter, path);
252 Self::prune_internal(|| limiter.pop())?;
253
254 Ok(Self {
255 limiter: Mutex::new(limiter),
256 })
257 }
258}
259
260#[derive(Clone)]
262pub struct Cache {
263 credentials_location: Option<PathBuf>,
264 volume_location: Option<PathBuf>,
265 audio_location: Option<PathBuf>,
266 size_limiter: Option<Arc<FsSizeLimiter>>,
267}
268
269impl Cache {
270 pub fn new<P: AsRef<Path>>(
271 credentials_path: Option<P>,
272 volume_path: Option<P>,
273 audio_path: Option<P>,
274 size_limit: Option<u64>,
275 ) -> Result<Self, Error> {
276 let mut size_limiter = None;
277
278 if let Some(location) = &credentials_path {
279 fs::create_dir_all(location)?;
280 }
281
282 let credentials_location = credentials_path
283 .as_ref()
284 .map(|p| p.as_ref().join("credentials.json"));
285
286 if let Some(location) = &volume_path {
287 fs::create_dir_all(location)?;
288 }
289
290 let volume_location = volume_path.as_ref().map(|p| p.as_ref().join("volume"));
291
292 if let Some(location) = &audio_path {
293 fs::create_dir_all(location)?;
294
295 if let Some(limit) = size_limit {
296 let limiter = FsSizeLimiter::new(location.as_ref(), limit)?;
297 size_limiter = Some(Arc::new(limiter));
298 }
299 }
300
301 let audio_location = audio_path.map(|p| p.as_ref().to_owned());
302
303 let cache = Cache {
304 credentials_location,
305 volume_location,
306 audio_location,
307 size_limiter,
308 };
309
310 Ok(cache)
311 }
312
313 pub fn credentials(&self) -> Option<Credentials> {
314 let location = self.credentials_location.as_ref()?;
315
316 let read = || -> Result<Credentials, Error> {
318 let mut file = File::open(location)?;
319 let mut contents = String::new();
320 file.read_to_string(&mut contents)?;
321 Ok(serde_json::from_str(&contents)?)
322 };
323
324 match read() {
325 Ok(c) => Some(c),
326 Err(e) => {
327 if e.kind != ErrorKind::NotFound {
330 warn!("Error reading credentials from cache: {e}");
331 }
332 None
333 }
334 }
335 }
336
337 pub fn save_credentials(&self, cred: &Credentials) {
338 if let Some(location) = &self.credentials_location {
339 let result = File::create(location).and_then(|mut file| {
340 let data = serde_json::to_string(cred)?;
341 write!(file, "{data}")
342 });
343
344 if let Err(e) = result {
345 warn!("Cannot save credentials to cache: {e}")
346 }
347 }
348 }
349
350 pub fn volume(&self) -> Option<u16> {
351 let location = self.volume_location.as_ref()?;
352
353 let read = || -> Result<u16, Error> {
354 let mut file = File::open(location)?;
355 let mut contents = String::new();
356 file.read_to_string(&mut contents)?;
357 Ok(contents.parse()?)
358 };
359
360 match read() {
361 Ok(v) => Some(v),
362 Err(e) => {
363 if e.kind != ErrorKind::NotFound {
364 warn!("Error reading volume from cache: {e}");
365 }
366 None
367 }
368 }
369 }
370
371 pub fn save_volume(&self, volume: u16) {
372 if let Some(ref location) = self.volume_location {
373 let result = File::create(location).and_then(|mut file| write!(file, "{volume}"));
374 if let Err(e) = result {
375 warn!("Cannot save volume to cache: {e}");
376 }
377 }
378 }
379
380 pub fn file_path(&self, file: FileId) -> Option<PathBuf> {
381 match file.to_base16() {
382 Ok(name) => self.audio_location.as_ref().map(|location| {
383 let mut path = location.join(&name[0..2]);
384 path.push(&name[2..]);
385 path
386 }),
387 Err(e) => {
388 warn!("Invalid FileId: {e}");
389 None
390 }
391 }
392 }
393
394 pub fn file(&self, file: FileId) -> Option<File> {
395 let path = self.file_path(file)?;
396 match File::open(&path) {
397 Ok(file) => {
398 if let Some(limiter) = self.size_limiter.as_deref() {
399 if !limiter.touch(&path) {
400 error!("limiter could not touch {path:?}");
401 }
402 }
403 Some(file)
404 }
405 Err(e) => {
406 if e.kind() != io::ErrorKind::NotFound {
407 warn!("Error reading file from cache: {e}")
408 }
409 None
410 }
411 }
412 }
413
414 pub fn save_file<F: Read>(&self, file: FileId, contents: &mut F) -> Result<PathBuf, Error> {
415 if let Some(path) = self.file_path(file) {
416 if let Some(parent) = path.parent() {
417 if let Ok(size) = fs::create_dir_all(parent)
418 .and_then(|_| File::create(&path))
419 .and_then(|mut file| io::copy(contents, &mut file))
420 {
421 if let Some(limiter) = self.size_limiter.as_deref() {
422 limiter.add(&path, size);
423 limiter.prune()?;
424 }
425 return Ok(path);
426 }
427 }
428 }
429 Err(CacheError::Path.into())
430 }
431
432 pub fn remove_file(&self, file: FileId) -> Result<(), Error> {
433 let path = self.file_path(file).ok_or(CacheError::Path)?;
434
435 fs::remove_file(&path)?;
436 if let Some(limiter) = self.size_limiter.as_deref() {
437 limiter.remove(&path);
438 }
439
440 Ok(())
441 }
442}
443
444#[cfg(test)]
445mod test {
446 use super::*;
447 use std::time::Duration;
448
449 fn ordered_time(v: u64) -> SystemTime {
450 SystemTime::UNIX_EPOCH + Duration::from_secs(v)
451 }
452
453 #[test]
454 fn test_size_limiter() {
455 let mut limiter = SizeLimiter::new(1000);
456
457 limiter.add(Path::new("a"), 500, ordered_time(2));
458 limiter.add(Path::new("b"), 500, ordered_time(1));
459
460 assert!(!limiter.exceeds_limit());
462 assert_eq!(limiter.pop(), None);
463
464 limiter.add(Path::new("c"), 1000, ordered_time(3));
465
466 assert!(limiter.exceeds_limit());
468 assert_eq!(limiter.pop().as_deref(), Some(Path::new("b")));
469 assert_eq!(limiter.pop().as_deref(), Some(Path::new("a")));
471 assert_eq!(limiter.pop().as_deref(), None);
473
474 limiter.add(Path::new("d"), 5, ordered_time(2));
475 assert_eq!(limiter.pop().as_deref(), Some(Path::new("d")));
477 assert_eq!(limiter.pop().as_deref(), None);
479
480 limiter.add(Path::new("e"), 500, ordered_time(3));
483 assert!(limiter.update(Path::new("c"), ordered_time(4)));
485 assert_eq!(limiter.pop().as_deref(), Some(Path::new("e")));
487 limiter.add(Path::new("f"), 500, ordered_time(2));
491 assert!(limiter.remove(Path::new("c")));
492 assert!(!limiter.exceeds_limit());
493 }
494}