neutralts/bif/
parse_bif_cache.rs

1#![doc = include_str!("../../doc/bif-cache.md")]
2
3use crate::{bif::constants::*, bif::Bif, bif::BifError, constants::*, utils::*};
4use md5::Digest;
5use sha2::Sha256;
6use std::fs;
7use std::fs::File;
8use std::io::Write;
9use std::path::Path;
10use std::time::{SystemTime, UNIX_EPOCH};
11
12impl<'a> Bif<'a> {
13    /*
14        {:cache; /expires/id/only_custom_id/ >> ... :} {:* expires in seconds *:}
15        {:cache; /expires/id/ >> ... :}
16        {:cache; /expires/ >> ... :}
17        {:!cache; ... :}
18    */
19    pub(crate) fn parse_bif_cache(&mut self) -> Result<(), BifError> {
20        if self.mod_filter || self.mod_scope {
21            return Err(self.bif_error(BIF_ERROR_MODIFIER_NOT_ALLOWED));
22        }
23
24        self.extract_params_code(false);
25
26        if self.params.contains("{:flg;") {
27            return Err(self.bif_error(BIF_ERROR_FLAGS_NOT_ALLOWED));
28        }
29
30        if self.mod_negate {
31            if self.inherit.in_cache {
32                self.out = self.raw.to_string();
33            } else {
34                // If it is not in a cache block, it is now resolved.
35                self.out = new_child_parse!(self, &self.code, self.mod_scope);
36            }
37            return Ok(());
38        }
39
40        let restore_in_cache = self.inherit.in_cache;
41        let context = &self.shared.schema["data"]["CONTEXT"];
42        let has_post = !is_empty_key(context, "POST");
43        let has_get = !is_empty_key(context, "GET");
44        let has_cookies = !is_empty_key(context, "COOKIES");
45
46        if self.shared.cache_disable
47            || (has_post && !self.shared.cache_on_post)
48            || (has_get && !self.shared.cache_on_get)
49            || (has_cookies && !self.shared.cache_on_cookies)
50        {
51            if self.code.contains(BIF_OPEN) {
52                self.code = new_child_parse!(self, &self.code, self.mod_scope);
53            }
54            self.out = self.code.clone();
55            return Ok(());
56        }
57
58        self.inherit.in_cache = true;
59        let args = self.extract_args();
60        self.inherit.in_cache = restore_in_cache;
61
62        // require expires
63        let expires = args
64            .get(1)
65            .cloned()
66            .ok_or_else(|| self.bif_error("arguments 'expires' not found"))?;
67
68        // optional id
69        let mut id = args.get(2).cloned().unwrap_or("".to_string());
70
71        // optional only_custom_id
72        let only_custom_id: bool = match args.get(3) {
73            Some(value) => !matches!(value.as_str(), "false" | "0" | ""),
74            None => false,
75        };
76
77        if !only_custom_id {
78            id.push_str(&self.shared.lang);
79            id.push_str(&expires);
80            if has_post {
81                id.push_str(
82                    &serde_json::to_string(&self.shared.schema["data"]["CONTEXT"]["POST"]).unwrap(),
83                );
84            }
85            if has_get {
86                id.push_str(
87                    &serde_json::to_string(&self.shared.schema["data"]["CONTEXT"]["GET"]).unwrap(),
88                );
89            }
90            if has_cookies {
91                id.push_str(
92                    &serde_json::to_string(&self.shared.schema["data"]["CONTEXT"]["COOKIES"])
93                        .unwrap(),
94                );
95            }
96            id.push_str(&self.get_data("CONTEXT->HOST"));
97            id.push_str(&self.get_data("CONTEXT->ROUTE"));
98            id.push_str(&self.code);
99        }
100
101        let mut hasher = Sha256::new();
102        hasher.update(id.clone());
103        let cache_id = format!("{:x}", hasher.finalize());
104        let cache_dir = self.get_cache_dir(&cache_id);
105        let file = format!("{}/{}-{}", cache_dir, &cache_id, expires);
106        let file_path = Path::new(&file);
107
108        if file_path.exists()
109            && !self.cache_file_expires(file_path, expires.parse::<u64>().unwrap_or(0))
110        {
111            if let Ok(content) = fs::read_to_string(file_path) {
112                self.out = content;
113            } else {
114                // The output is created even if there is an error
115                if self.code.contains(BIF_OPEN) {
116                    self.inherit.in_cache = true;
117                    self.out = new_child_parse!(self, &self.code, self.mod_scope);
118                    self.inherit.in_cache = restore_in_cache;
119                }
120                return Err(
121                    self.bif_error(&format!("Failed to read cache {}", file_path.display()))
122                );
123            }
124        } else {
125            if self.code.contains(BIF_OPEN) {
126                self.inherit.in_cache = true;
127                self.code = new_child_parse!(self, &self.code, self.mod_scope);
128                self.inherit.in_cache = restore_in_cache;
129            }
130
131            // The output is created even if there is an error
132            self.out = self.code.clone();
133
134            // Create cache dir
135            self.set_cache_dir(&cache_dir)?;
136
137            // Write in cache
138            match File::create(&file_path) {
139                Ok(mut file) => {
140                    if let Err(e) = file.write_all(&self.code.as_bytes()) {
141                        return Err(self.bif_error(&format!(
142                            "Failed to write to cache {}: {}",
143                            file_path.display(),
144                            e.to_string()
145                        )));
146                    }
147                }
148                Err(e) => {
149                    return Err(self.bif_error(&format!(
150                        "Failed to create file {}: {}",
151                        file_path.display(),
152                        e.to_string()
153                    )))
154                }
155            }
156        }
157
158        Ok(())
159    }
160
161    pub(crate) fn cache_file_expires(&self, file_path: &Path, expires: u64) -> bool {
162        let now: u64 = SystemTime::now()
163            .duration_since(UNIX_EPOCH)
164            .unwrap()
165            .as_secs();
166
167        let metadata = match fs::metadata(file_path) {
168            Ok(meta) => meta,
169            Err(_) => return true,
170        };
171
172        let modified_time = match metadata.modified() {
173            Ok(time) => time,
174            Err(_) => return true,
175        };
176
177        let duration_since_epoch = match modified_time.duration_since(UNIX_EPOCH) {
178            Ok(duration) => duration,
179            Err(_) => return true,
180        };
181
182        let file_modified_time = duration_since_epoch.as_secs();
183        let expiration_time = file_modified_time + expires;
184
185        if now > expiration_time {
186            return true;
187        }
188
189        false
190    }
191
192    pub(crate) fn set_cache_dir(&self, cache_dir: &str) -> Result<(), BifError> {
193        let cache_dir_levels = Path::new(&cache_dir);
194
195        match fs::create_dir_all(cache_dir_levels) {
196            Ok(_) => Ok(()),
197            Err(e) => {
198                return Err(self.bif_error(&format!(
199                    "Failed to create cache directory {}: {}",
200                    cache_dir,
201                    e.to_string()
202                )))
203            }
204        }
205    }
206
207    pub(crate) fn get_cache_dir(&self, file: &str) -> String {
208        let mut cache_dir = self.shared.cache_dir.clone();
209
210        if !self.shared.cache_prefix.is_empty() {
211            cache_dir.push_str("/");
212            cache_dir.push_str(&self.shared.cache_prefix);
213        }
214
215        cache_dir.push_str("/");
216        cache_dir.push_str(&file[0..3]);
217
218        cache_dir.to_string()
219    }
220}
221
222#[cfg(test)]
223#[path = "parse_bif_cache_tests.rs"]
224mod tests;