sqlpage/
file_cache.rs

1use crate::webserver::routing::FileStore;
2use crate::webserver::ErrorWithStatus;
3use crate::AppState;
4use actix_web::http::StatusCode;
5use anyhow::Context;
6use async_trait::async_trait;
7use chrono::{DateTime, TimeZone, Utc};
8use std::collections::HashMap;
9use std::path::{Path, PathBuf};
10use std::sync::atomic::{
11    AtomicU64,
12    Ordering::{Acquire, Release},
13};
14use std::sync::Arc;
15use std::time::SystemTime;
16use tokio::sync::RwLock;
17
18/// The maximum time in milliseconds that a file can be cached before its freshness is checked
19/// (in production mode)
20const MAX_STALE_CACHE_MS: u64 = 150;
21
22#[derive(Default)]
23struct Cached<T> {
24    last_checked_at: AtomicU64,
25    content: Arc<T>,
26}
27
28impl<T> Cached<T> {
29    fn new(content: T) -> Self {
30        let s = Self {
31            last_checked_at: AtomicU64::new(0),
32            content: Arc::new(content),
33        };
34        s.update_check_time();
35        s
36    }
37    fn last_check_time(&self) -> DateTime<Utc> {
38        self.last_checked_at
39            .load(Acquire)
40            .saturating_mul(MAX_STALE_CACHE_MS)
41            .try_into()
42            .ok()
43            .and_then(|millis| Utc.timestamp_millis_opt(millis).single())
44            .expect("file timestamp out of bound")
45    }
46    fn update_check_time(&self) {
47        self.last_checked_at.store(Self::elapsed(), Release);
48    }
49    fn elapsed() -> u64 {
50        let timestamp_millis = (SystemTime::now().duration_since(SystemTime::UNIX_EPOCH))
51            .expect("invalid duration")
52            .as_millis();
53        let elapsed_intervals = timestamp_millis / u128::from(MAX_STALE_CACHE_MS);
54        u64::try_from(elapsed_intervals).expect("invalid date")
55    }
56    fn needs_check(&self) -> bool {
57        self.last_checked_at
58            .load(Acquire)
59            .saturating_add(MAX_STALE_CACHE_MS)
60            < Self::elapsed()
61    }
62    /// Creates a new cached entry with the same content but a new check time set to now
63    fn make_fresh(&self) -> Self {
64        Self {
65            last_checked_at: AtomicU64::from(Self::elapsed()),
66            content: Arc::clone(&self.content),
67        }
68    }
69}
70
71pub struct FileCache<T: AsyncFromStrWithState> {
72    cache: Arc<RwLock<HashMap<PathBuf, Cached<T>>>>,
73    /// Files that are loaded at the beginning of the program,
74    /// and used as fallback when there is no match for the request in the file system
75    static_files: HashMap<PathBuf, Cached<T>>,
76}
77
78impl<T: AsyncFromStrWithState> FileStore for FileCache<T> {
79    async fn contains(&self, path: &Path) -> anyhow::Result<bool> {
80        Ok(self.cache.read().await.contains_key(path) || self.static_files.contains_key(path))
81    }
82}
83
84impl<T: AsyncFromStrWithState> Default for FileCache<T> {
85    fn default() -> Self {
86        Self::new()
87    }
88}
89
90impl<T: AsyncFromStrWithState> FileCache<T> {
91    #[must_use]
92    pub fn new() -> Self {
93        Self {
94            cache: Arc::default(),
95            static_files: HashMap::new(),
96        }
97    }
98
99    /// Adds a static file to the cache so that it will never be looked up from the disk
100    pub fn add_static(&mut self, path: PathBuf, contents: T) {
101        log::trace!("Adding static file {} to the cache.", path.display());
102        self.static_files.insert(path, Cached::new(contents));
103    }
104
105    /// Gets a file from the cache, or loads it from the file system if it's not there
106    /// This is a privileged operation; it should not be used for user-provided paths
107    pub async fn get(&self, app_state: &AppState, path: &Path) -> anyhow::Result<Arc<T>> {
108        self.get_with_privilege(app_state, path, true).await
109    }
110
111    /// Gets a file from the cache, or loads it from the file system if it's not there
112    /// The privileged parameter is used to determine whether the access should be denied
113    /// if the file is in the sqlpage/ config directory
114    pub async fn get_with_privilege(
115        &self,
116        app_state: &AppState,
117        path: &Path,
118        privileged: bool,
119    ) -> anyhow::Result<Arc<T>> {
120        log::trace!("Attempting to get from cache {}", path.display());
121        if let Some(cached) = self.cache.read().await.get(path) {
122            if app_state.config.environment.is_prod() && !cached.needs_check() {
123                log::trace!(
124                    "Cache answer without filesystem lookup for {}",
125                    path.display()
126                );
127                return Ok(Arc::clone(&cached.content));
128            }
129            match app_state
130                .file_system
131                .modified_since(app_state, path, cached.last_check_time(), privileged)
132                .await
133            {
134                Ok(false) => {
135                    log::trace!(
136                        "Cache answer with filesystem metadata read for {}",
137                        path.display()
138                    );
139                    cached.update_check_time();
140                    return Ok(Arc::clone(&cached.content));
141                }
142                Ok(true) => log::trace!("{} was changed, updating cache...", path.display()),
143                Err(e) => log::trace!(
144                    "Cannot read metadata of {}, re-loading it: {:#}",
145                    path.display(),
146                    e
147                ),
148            }
149        }
150        // Read lock is released
151        log::trace!("Loading and parsing {}", path.display());
152        let file_contents = app_state
153            .file_system
154            .read_to_string(app_state, path, privileged)
155            .await;
156
157        let parsed = match file_contents {
158            Ok(contents) => {
159                let value = T::from_str_with_state(app_state, &contents, path).await?;
160                Ok(Cached::new(value))
161            }
162            // If a file is not found, we try to load it from the static files
163            Err(e)
164                if e.downcast_ref()
165                    == Some(&ErrorWithStatus {
166                        status: StatusCode::NOT_FOUND,
167                    }) =>
168            {
169                if let Some(static_file) = self.static_files.get(path) {
170                    log::trace!(
171                        "File {} not found, loading it from static files instead.",
172                        path.display()
173                    );
174                    let cached: Cached<T> = static_file.make_fresh();
175                    Ok(cached)
176                } else {
177                    Err(e).with_context(|| format!("Couldn't load {} into cache", path.display()))
178                }
179            }
180            Err(e) => {
181                Err(e).with_context(|| format!("Couldn't load {} into cache", path.display()))
182            }
183        };
184
185        match parsed {
186            Ok(value) => {
187                let new_val = Arc::clone(&value.content);
188                log::trace!("Writing to cache {}", path.display());
189                self.cache.write().await.insert(PathBuf::from(path), value);
190                log::trace!("Done writing to cache {}", path.display());
191                log::trace!("{} loaded in cache", path.display());
192                Ok(new_val)
193            }
194            Err(e) => {
195                log::trace!(
196                    "Evicting {} from the cache because the following error occurred: {}",
197                    path.display(),
198                    e
199                );
200                log::trace!("Removing from cache {}", path.display());
201                self.cache.write().await.remove(path);
202                log::trace!("Done removing from cache {}", path.display());
203                Err(e)
204            }
205        }
206    }
207}
208
209#[async_trait(? Send)]
210pub trait AsyncFromStrWithState: Sized {
211    /// Parses the string into an object.
212    async fn from_str_with_state(
213        app_state: &AppState,
214        source: &str,
215        source_path: &Path,
216    ) -> anyhow::Result<Self>;
217}