rocket_dyn_templates/
context.rs

1use std::path::{Path, PathBuf};
2use std::collections::HashMap;
3use std::error::Error;
4
5use crate::engine::Engines;
6use crate::template::TemplateInfo;
7
8use rocket::http::ContentType;
9use normpath::PathExt;
10
11pub(crate) type Callback =
12    Box<dyn Fn(&mut Engines) -> Result<(), Box<dyn Error>> + Send + Sync + 'static>;
13
14pub(crate) struct Context {
15    /// The root of the template directory.
16    pub root: PathBuf,
17    /// Mapping from template name to its information.
18    pub templates: HashMap<String, TemplateInfo>,
19    /// Loaded template engines
20    pub engines: Engines,
21}
22
23pub(crate) use self::manager::ContextManager;
24
25impl Context {
26    /// Load all of the templates at `root`, initialize them using the relevant
27    /// template engine, and store all of the initialized state in a `Context`
28    /// structure, which is returned if all goes well.
29    pub fn initialize(root: &Path, callback: &Callback) -> Option<Context> {
30        fn is_file_with_ext(entry: &walkdir::DirEntry, ext: &str) -> bool {
31            let is_file = entry.file_type().is_file();
32            let has_ext = entry.path().extension().map_or(false, |e| e == ext);
33            is_file && has_ext
34        }
35
36        let root = match root.normalize() {
37            Ok(root) => root.into_path_buf(),
38            Err(e) => {
39                error!("Invalid template directory '{}': {}.", root.display(), e);
40                return None;
41            }
42        };
43
44        let mut templates: HashMap<String, TemplateInfo> = HashMap::new();
45        for &ext in Engines::ENABLED_EXTENSIONS {
46            for entry in walkdir::WalkDir::new(&root).follow_links(true) {
47                let entry = match entry {
48                    Ok(entry) if is_file_with_ext(&entry, ext) => entry,
49                    Ok(_) | Err(_) => continue,
50                };
51
52                let (template, data_type_str) = split_path(&root, entry.path());
53                if let Some(info) = templates.get(&*template) {
54                    warn!(
55                        %template,
56                        first_path = %entry.path().display(),
57                        second_path = info.path.as_ref().map(|p| display(p.display())),
58                        data_type = %info.data_type,
59                        "Template name '{template}' can refer to multiple templates.\n\
60                         First path will be used. Second path is ignored."
61                    );
62
63                    continue;
64                }
65
66                let data_type = data_type_str.as_ref()
67                    .and_then(|ext| ContentType::from_extension(ext))
68                    .unwrap_or(ContentType::Text);
69
70                templates.insert(template, TemplateInfo {
71                    path: Some(entry.into_path()),
72                    engine_ext: ext,
73                    data_type,
74                });
75            }
76        }
77
78        let mut engines = Engines::init(&templates)?;
79        if let Err(reason) = callback(&mut engines) {
80            error!(%reason, "template customization callback failed");
81            return None;
82        }
83
84        for (name, engine_ext) in engines.templates() {
85            if !templates.contains_key(name) {
86                let data_type = Path::new(name).extension()
87                    .and_then(|osstr| osstr.to_str())
88                    .and_then(ContentType::from_extension)
89                    .unwrap_or(ContentType::Text);
90
91                let info = TemplateInfo { path: None, engine_ext, data_type };
92                templates.insert(name.to_string(), info);
93            }
94        }
95
96        Some(Context { root, templates, engines })
97    }
98}
99
100#[cfg(not(debug_assertions))]
101mod manager {
102    use std::ops::Deref;
103    use super::Context;
104
105    /// Wraps a Context. With `cfg(debug_assertions)` active, this structure
106    /// additionally provides a method to reload the context at runtime.
107    pub(crate) struct ContextManager(Context);
108
109    impl ContextManager {
110        pub fn new(ctxt: Context) -> ContextManager {
111            ContextManager(ctxt)
112        }
113
114        pub fn context<'a>(&'a self) -> impl Deref<Target=Context> + 'a {
115            &self.0
116        }
117
118        pub fn is_reloading(&self) -> bool {
119            false
120        }
121    }
122}
123
124#[cfg(debug_assertions)]
125mod manager {
126    use std::ops::{Deref, DerefMut};
127    use std::sync::{RwLock, Mutex};
128    use std::sync::mpsc::{channel, Receiver};
129
130    use notify::{recommended_watcher, Error, Event, RecommendedWatcher, RecursiveMode, Watcher};
131
132    use super::{Callback, Context};
133
134    /// Wraps a Context. With `cfg(debug_assertions)` active, this structure
135    /// additionally provides a method to reload the context at runtime.
136    pub(crate) struct ContextManager {
137        /// The current template context, inside an RwLock so it can be updated.
138        context: RwLock<Context>,
139        /// A filesystem watcher and the receive queue for its events.
140        watcher: Option<(RecommendedWatcher, Mutex<Receiver<Result<Event, Error>>>)>,
141    }
142
143    impl ContextManager {
144        pub fn new(ctxt: Context) -> ContextManager {
145            let (tx, rx) = channel();
146            let watcher = recommended_watcher(tx).and_then(|mut watcher| {
147                watcher.watch(&ctxt.root.canonicalize()?, RecursiveMode::Recursive)?;
148                Ok(watcher)
149            });
150
151            let watcher = match watcher {
152                Ok(watcher) => Some((watcher, Mutex::new(rx))),
153                Err(e) => {
154                    warn!("live template reloading initialization failed: {e}\n\
155                        live template reloading is unavailable");
156                    None
157                }
158            };
159
160            ContextManager { watcher, context: RwLock::new(ctxt), }
161        }
162
163        pub fn context(&self) -> impl Deref<Target=Context> + '_ {
164            self.context.read().unwrap()
165        }
166
167        pub fn is_reloading(&self) -> bool {
168            self.watcher.is_some()
169        }
170
171        fn context_mut(&self) -> impl DerefMut<Target=Context> + '_ {
172            self.context.write().unwrap()
173        }
174
175        /// Checks whether any template files have changed on disk. If there
176        /// have been changes since the last reload, all templates are
177        /// reinitialized from disk and the user's customization callback is run
178        /// again.
179        pub fn reload_if_needed(&self, callback: &Callback) {
180            let templates_changes = self.watcher.as_ref()
181                .map(|(_, rx)| rx.lock().expect("fsevents lock").try_iter().count() > 0);
182
183            if let Some(true) = templates_changes {
184                debug!("template change detected: reloading templates");
185                let root = self.context().root.clone();
186                if let Some(new_ctxt) = Context::initialize(&root, callback) {
187                    *self.context_mut() = new_ctxt;
188                } else {
189                    warn!("error while reloading template\n\
190                        existing templates will remain active.")
191                };
192            }
193        }
194    }
195}
196
197/// Removes the file path's extension or does nothing if there is none.
198fn remove_extension(path: &Path) -> PathBuf {
199    let stem = match path.file_stem() {
200        Some(stem) => stem,
201        None => return path.to_path_buf()
202    };
203
204    match path.parent() {
205        Some(parent) => parent.join(stem),
206        None => PathBuf::from(stem)
207    }
208}
209
210/// Splits a path into a name that may be used to identify the template, and the
211/// template's data type, if any.
212fn split_path(root: &Path, path: &Path) -> (String, Option<String>) {
213    let rel_path = path.strip_prefix(root).unwrap().to_path_buf();
214    let path_no_ext = remove_extension(&rel_path);
215    let data_type = path_no_ext.extension();
216    let mut name = remove_extension(&path_no_ext).to_string_lossy().into_owned();
217
218    // Ensure template name consistency on Windows systems
219    if cfg!(windows) {
220        name = name.replace('\\', "/");
221    }
222
223    (name, data_type.map(|d| d.to_string_lossy().into_owned()))
224}
225
226#[cfg(test)]
227mod tests {
228    use super::*;
229
230    #[test]
231    fn template_path_index_html() {
232        for root in &["/", "/a/b/c/", "/a/b/c/d/", "/a/"] {
233            for filename in &["index.html.hbs", "index.html.tera"] {
234                let path = Path::new(root).join(filename);
235                let (name, data_type) = split_path(Path::new(root), &path);
236
237                assert_eq!(name, "index");
238                assert_eq!(data_type, Some("html".into()));
239            }
240        }
241    }
242
243    #[test]
244    fn template_path_subdir_index_html() {
245        for root in &["/", "/a/b/c/", "/a/b/c/d/", "/a/"] {
246            for sub in &["a/", "a/b/", "a/b/c/", "a/b/c/d/"] {
247                for filename in &["index.html.hbs", "index.html.tera"] {
248                    let path = Path::new(root).join(sub).join(filename);
249                    let (name, data_type) = split_path(Path::new(root), &path);
250
251                    let expected_name = format!("{}index", sub);
252                    assert_eq!(name, expected_name.as_str());
253                    assert_eq!(data_type, Some("html".into()));
254                }
255            }
256        }
257    }
258
259    #[test]
260    fn template_path_doc_examples() {
261        fn name_for(path: &str) -> String {
262            split_path(Path::new("templates/"), &Path::new("templates/").join(path)).0
263        }
264
265        assert_eq!(name_for("index.html.hbs"), "index");
266        assert_eq!(name_for("index.tera"), "index");
267        assert_eq!(name_for("index.hbs"), "index");
268        assert_eq!(name_for("dir/index.hbs"), "dir/index");
269        assert_eq!(name_for("dir/index.html.tera"), "dir/index");
270        assert_eq!(name_for("index.template.html.hbs"), "index.template");
271        assert_eq!(name_for("subdir/index.template.html.hbs"), "subdir/index.template");
272    }
273}