librespot_core/
cache.rs

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
30/// Some kind of data structure that holds some paths, the size of these files and a timestamp.
31/// It keeps track of the file sizes and is able to pop the path with the oldest timestamp if
32/// a given limit is exceeded.
33struct 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    /// Creates a new instance with the given size limit.
42    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    /// Adds an entry to this data structure.
52    ///
53    /// If this file is already contained, it will be updated accordingly.
54    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            // It's important that decreasing happens after
59            // increasing the size, to prevent an overflow.
60            self.in_use -= old_size;
61        }
62    }
63
64    /// Returns true if the limit is exceeded.
65    fn exceeds_limit(&self) -> bool {
66        self.in_use > self.size_limit
67    }
68
69    /// Returns the least recently accessed file if the size of the cache exceeds
70    /// the limit.
71    ///
72    /// The entry is removed from the data structure, but the caller is responsible
73    /// to delete the file in the file system.
74    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    /// Updates the timestamp of an existing element. Returns `true` if the item did exist.
93    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    /// Removes an element with the specified path. Returns `true` if the item did exist.
100    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    /// Returns access time and file size of a given path.
121    fn get_metadata(file: &Path) -> io::Result<(SystemTime, u64)> {
122        let metadata = file.metadata()?;
123
124        // The first of the following timestamps which is available will be chosen as access time:
125        // 1. Access time
126        // 2. Modification time
127        // 3. Creation time
128        // 4. Current time
129        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    /// Recursively search a directory for files and add them to the `limiter` struct.
141    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/// A cache for volume, credentials and audio files.
261#[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        // This closure is just convencience to enable the question mark operator
317        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 the file did not exist, the file was probably not written
328                // before. Otherwise, log the error.
329                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        // b (500) -> a (500)  => sum: 1000 <= 1000
461        assert!(!limiter.exceeds_limit());
462        assert_eq!(limiter.pop(), None);
463
464        limiter.add(Path::new("c"), 1000, ordered_time(3));
465
466        // b (500) -> a (500) -> c (1000)  => sum: 2000 > 1000
467        assert!(limiter.exceeds_limit());
468        assert_eq!(limiter.pop().as_deref(), Some(Path::new("b")));
469        // a (500) -> c (1000)  => sum: 1500 > 1000
470        assert_eq!(limiter.pop().as_deref(), Some(Path::new("a")));
471        // c (1000)   => sum: 1000 <= 1000
472        assert_eq!(limiter.pop().as_deref(), None);
473
474        limiter.add(Path::new("d"), 5, ordered_time(2));
475        // d (5) -> c (1000) => sum: 1005 > 1000
476        assert_eq!(limiter.pop().as_deref(), Some(Path::new("d")));
477        // c (1000)   => sum: 1000 <= 1000
478        assert_eq!(limiter.pop().as_deref(), None);
479
480        // Test updating
481
482        limiter.add(Path::new("e"), 500, ordered_time(3));
483        //  c (1000) -> e (500)  => sum: 1500 > 1000
484        assert!(limiter.update(Path::new("c"), ordered_time(4)));
485        // e (500) -> c (1000)  => sum: 1500 > 1000
486        assert_eq!(limiter.pop().as_deref(), Some(Path::new("e")));
487        // c (1000)  => sum: 1000 <= 1000
488
489        // Test removing
490        limiter.add(Path::new("f"), 500, ordered_time(2));
491        assert!(limiter.remove(Path::new("c")));
492        assert!(!limiter.exceeds_limit());
493    }
494}