read_url/cache/
cache.rs

1use super::super::errors::*;
2
3use {
4    kutil::std::error::*,
5    rand::{distr::*, *},
6    std::{collections::*, env::*, fs::*, path::*, sync::*},
7    tracing::info,
8};
9
10const RANDOM_NAME_LENGTH: usize = 32;
11
12/// Common reference type for [PathBuf].
13pub type PathBufRef = Arc<Mutex<PathBuf>>;
14
15type PathBufRefMap = LazyLock<Mutex<HashMap<String, PathBufRef>>>;
16
17//
18// UrlCache
19//
20
21/// Cache for a [UrlContext](super::super::context::UrlContext).
22#[derive(Debug)]
23pub struct UrlCache {
24    /// Base directory.
25    pub base_directory: PathBuf,
26
27    /// Files owned by this cache.
28    pub files: PathBufRefMap,
29
30    /// Directories owned by this cache.
31    pub directories: PathBufRefMap,
32}
33
34// type MutexError<'own> = PoisonError<MutexGuard<'own, Vec<String>>>;
35
36impl UrlCache {
37    /// Constructor.
38    pub fn new(base_directory: Option<PathBuf>) -> Self {
39        let base_directory = base_directory.unwrap_or_else(|| Self::default_base_directory());
40
41        Self {
42            base_directory,
43            files: LazyLock::new(|| HashMap::default().into()),
44            directories: LazyLock::new(|| HashMap::default().into()),
45        }
46    }
47
48    /// Default base directory.
49    pub fn default_base_directory() -> PathBuf {
50        temp_dir().join("read-url")
51    }
52
53    /// Resets the cache, deleting all owned files and directories.
54    pub fn reset(&self) -> Result<(), UrlError> {
55        let mut errors = Vec::default();
56
57        let mut files = self.files.lock()?;
58        for path in files.values() {
59            let path = path.lock()?;
60            info!("deleting file: {}", path.display());
61            let path = path.as_path();
62            if let Err(error) = remove_file(path) {
63                errors.push(error.with_path(path));
64            }
65        }
66        files.clear();
67
68        let mut directories = self.directories.lock()?;
69        for path in directories.values() {
70            let path = path.lock()?;
71            info!("deleting directory: {}", path.display());
72            let path = path.as_path();
73            if let Err(error) = remove_dir_all(path) {
74                errors.push(error.with_path(path));
75            }
76        }
77        directories.clear();
78
79        if errors.is_empty() { Ok(()) } else { Err(UrlError::IoMany(errors)) }
80    }
81
82    /// Get a cache file.
83    ///
84    /// If it already exists returns the path and true. Otherwise generates a path and returns it and false.
85    pub fn file(&self, key: &str, prefix: &str) -> Result<(PathBufRef, bool), UrlError> {
86        let key = key.to_string();
87
88        let mut files = self.files.lock()?;
89        match files.get(&key) {
90            Some(path) => {
91                info!("existing file: {}", path.clone().lock()?.display());
92                Ok((path.clone(), true))
93            }
94
95            None => {
96                let path = self.new_path(prefix)?;
97                info!("new file: {}", path.display());
98                let path = Arc::new(Mutex::new(path));
99                files.insert(key, path.clone());
100                Ok((path, false))
101            }
102        }
103    }
104
105    /// Get a cache directory.
106    ///
107    /// If it already exists returns the path and true. Otherwise creates it ([create_dir_all]) in a
108    /// generated path and returns it and false.
109    pub fn directory(&self, key: &str, prefix: &str) -> Result<(PathBufRef, bool), UrlError> {
110        let key = key.to_string();
111
112        let mut directories = self.directories.lock()?;
113        match directories.get(&key) {
114            Some(path) => {
115                info!("existing directory: {}", path.clone().lock()?.display());
116                Ok((path.clone(), true))
117            }
118
119            None => {
120                let path = self.new_path(prefix)?;
121                info!("new directory: {}", path.display());
122                let path = Arc::new(Mutex::new(path));
123                directories.insert(key, path.clone());
124                Ok((path, false))
125            }
126        }
127    }
128
129    pub(crate) fn new_path(&self, prefix: &str) -> Result<PathBuf, UrlError> {
130        create_dir_all(&self.base_directory).with_path(&self.base_directory)?;
131
132        // We'll avoid case distinction because Windows doesn't
133        let distribution = Uniform::new_inclusive('a', 'z').expect("Uniform::new_inclusive");
134        let path: String = rng().sample_iter(distribution).take(RANDOM_NAME_LENGTH).collect();
135        let path = prefix.to_string() + &path;
136        Ok(self.base_directory.join(path))
137    }
138}
139
140impl Drop for UrlCache {
141    fn drop(&mut self) {
142        _ = self.reset();
143    }
144}