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 pub root: PathBuf,
17 pub templates: HashMap<String, TemplateInfo>,
19 pub engines: Engines,
21}
22
23pub(crate) use self::manager::ContextManager;
24
25impl Context {
26 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 (name, data_type_str) = split_path(&root, entry.path());
53 if let Some(info) = templates.get(&*name) {
54 warn_!("Template name '{}' does not have a unique source.", name);
55 match info.path {
56 Some(ref path) => info_!("Existing path: {:?}", path),
57 None => info_!("Existing Content-Type: {}", info.data_type),
58 }
59
60 info_!("Additional path: {:?}", entry.path());
61 warn_!("Keeping existing template '{}'.", name);
62 continue;
63 }
64
65 let data_type = data_type_str.as_ref()
66 .and_then(|ext| ContentType::from_extension(ext))
67 .unwrap_or(ContentType::Text);
68
69 templates.insert(name, TemplateInfo {
70 path: Some(entry.into_path()),
71 engine_ext: ext,
72 data_type,
73 });
74 }
75 }
76
77 let mut engines = Engines::init(&templates)?;
78 if let Err(e) = callback(&mut engines) {
79 error_!("Template customization callback failed.");
80 error_!("{}", e);
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(|ext| ContentType::from_extension(ext))
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 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 pub(crate) struct ContextManager {
137 context: RwLock<Context>,
139 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!("Failed to enable live template reloading: {}", e);
155 debug_!("Reload error: {:?}", e);
156 warn_!("Live template reloading is unavailable.");
157 None
158 }
159 };
160
161 ContextManager { watcher, context: RwLock::new(ctxt), }
162 }
163
164 pub fn context(&self) -> impl Deref<Target=Context> + '_ {
165 self.context.read().unwrap()
166 }
167
168 pub fn is_reloading(&self) -> bool {
169 self.watcher.is_some()
170 }
171
172 fn context_mut(&self) -> impl DerefMut<Target=Context> + '_ {
173 self.context.write().unwrap()
174 }
175
176 pub fn reload_if_needed(&self, callback: &Callback) {
181 let templates_changes = self.watcher.as_ref()
182 .map(|(_, rx)| rx.lock().expect("fsevents lock").try_iter().count() > 0);
183
184 if let Some(true) = templates_changes {
185 info_!("Change detected: reloading templates.");
186 let root = self.context().root.clone();
187 if let Some(new_ctxt) = Context::initialize(&root, &callback) {
188 *self.context_mut() = new_ctxt;
189 } else {
190 warn_!("An error occurred while reloading templates.");
191 warn_!("Existing templates will remain active.");
192 };
193 }
194 }
195 }
196}
197
198fn remove_extension(path: &Path) -> PathBuf {
200 let stem = match path.file_stem() {
201 Some(stem) => stem,
202 None => return path.to_path_buf()
203 };
204
205 match path.parent() {
206 Some(parent) => parent.join(stem),
207 None => PathBuf::from(stem)
208 }
209}
210
211fn split_path(root: &Path, path: &Path) -> (String, Option<String>) {
214 let rel_path = path.strip_prefix(root).unwrap().to_path_buf();
215 let path_no_ext = remove_extension(&rel_path);
216 let data_type = path_no_ext.extension();
217 let mut name = remove_extension(&path_no_ext).to_string_lossy().into_owned();
218
219 if cfg!(windows) {
221 name = name.replace("\\", "/");
222 }
223
224 (name, data_type.map(|d| d.to_string_lossy().into_owned()))
225}
226
227#[cfg(test)]
228mod tests {
229 use super::*;
230
231 #[test]
232 fn template_path_index_html() {
233 for root in &["/", "/a/b/c/", "/a/b/c/d/", "/a/"] {
234 for filename in &["index.html.hbs", "index.html.tera"] {
235 let path = Path::new(root).join(filename);
236 let (name, data_type) = split_path(Path::new(root), &path);
237
238 assert_eq!(name, "index");
239 assert_eq!(data_type, Some("html".into()));
240 }
241 }
242 }
243
244 #[test]
245 fn template_path_subdir_index_html() {
246 for root in &["/", "/a/b/c/", "/a/b/c/d/", "/a/"] {
247 for sub in &["a/", "a/b/", "a/b/c/", "a/b/c/d/"] {
248 for filename in &["index.html.hbs", "index.html.tera"] {
249 let path = Path::new(root).join(sub).join(filename);
250 let (name, data_type) = split_path(Path::new(root), &path);
251
252 let expected_name = format!("{}index", sub);
253 assert_eq!(name, expected_name.as_str());
254 assert_eq!(data_type, Some("html".into()));
255 }
256 }
257 }
258 }
259
260 #[test]
261 fn template_path_doc_examples() {
262 fn name_for(path: &str) -> String {
263 split_path(Path::new("templates/"), &Path::new("templates/").join(path)).0
264 }
265
266 assert_eq!(name_for("index.html.hbs"), "index");
267 assert_eq!(name_for("index.tera"), "index");
268 assert_eq!(name_for("index.hbs"), "index");
269 assert_eq!(name_for("dir/index.hbs"), "dir/index");
270 assert_eq!(name_for("dir/index.html.tera"), "dir/index");
271 assert_eq!(name_for("index.template.html.hbs"), "index.template");
272 assert_eq!(name_for("subdir/index.template.html.hbs"), "subdir/index.template");
273 }
274}