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
18const 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 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 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 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 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 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 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 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 async fn from_str_with_state(
213 app_state: &AppState,
214 source: &str,
215 source_path: &Path,
216 ) -> anyhow::Result<Self>;
217}