torrust_tracker_deployer_lib/domain/template/
embedded.rs1use rust_embed::RustEmbed;
28use std::fs;
29use std::path::{Path, PathBuf};
30use thiserror::Error;
31
32#[derive(Debug, Error)]
34pub enum TemplateManagerError {
35 #[error("Failed to create templates directory: {path}")]
36 DirectoryCreation {
37 path: String,
38 #[source]
39 source: std::io::Error,
40 },
41
42 #[error("Template file not found in embedded resources: {relative_path}")]
43 TemplateNotFound { relative_path: String },
44
45 #[error("Invalid UTF-8 in embedded template: {relative_path}")]
46 InvalidUtf8 {
47 relative_path: String,
48 #[source]
49 source: std::str::Utf8Error,
50 },
51
52 #[error("Failed to create parent directory for template: {path}")]
53 ParentDirectoryCreation {
54 path: String,
55 #[source]
56 source: std::io::Error,
57 },
58
59 #[error("Failed to write template file: {path}")]
60 TemplateWrite {
61 path: String,
62 #[source]
63 source: std::io::Error,
64 },
65
66 #[error("Template file already exists: {path}")]
67 TemplateAlreadyExists { path: String },
68}
69
70#[derive(RustEmbed)]
72#[folder = "templates/"]
73pub struct EmbeddedTemplates;
74
75pub struct TemplateManager {
77 templates_dir: PathBuf,
78}
79
80impl TemplateManager {
81 pub fn new<P: Into<PathBuf>>(templates_dir: P) -> Self {
83 Self {
84 templates_dir: templates_dir.into(),
85 }
86 }
87
88 pub fn ensure_templates_dir(&self) -> Result<(), TemplateManagerError> {
94 if !self.templates_dir.exists() {
95 fs::create_dir_all(&self.templates_dir).map_err(|source| {
96 TemplateManagerError::DirectoryCreation {
97 path: self.templates_dir.display().to_string(),
98 source,
99 }
100 })?;
101 }
102 Ok(())
103 }
104
105 pub fn reset_templates_dir(&self) -> Result<(), TemplateManagerError> {
116 self.clean_templates_dir()?;
117 self.ensure_templates_dir()?;
118 Ok(())
119 }
120
121 pub fn get_template_path(&self, relative_path: &str) -> Result<PathBuf, TemplateManagerError> {
130 let template_path = self.templates_dir.join(relative_path);
131
132 if template_path.exists() {
134 return Ok(template_path);
135 }
136
137 self.create_template_from_embedded(relative_path)?;
139
140 Ok(template_path)
141 }
142
143 fn create_template_from_embedded(
145 &self,
146 relative_path: &str,
147 ) -> Result<(), TemplateManagerError> {
148 let template_path = self.templates_dir.join(relative_path);
149
150 if template_path.exists() {
152 return Err(TemplateManagerError::TemplateAlreadyExists {
153 path: template_path.display().to_string(),
154 });
155 }
156
157 let embedded_file = EmbeddedTemplates::get(relative_path).ok_or_else(|| {
159 TemplateManagerError::TemplateNotFound {
160 relative_path: relative_path.to_string(),
161 }
162 })?;
163
164 let content = std::str::from_utf8(&embedded_file.data).map_err(|source| {
165 TemplateManagerError::InvalidUtf8 {
166 relative_path: relative_path.to_string(),
167 source,
168 }
169 })?;
170
171 if let Some(parent) = template_path.parent() {
173 fs::create_dir_all(parent).map_err(|source| {
174 TemplateManagerError::ParentDirectoryCreation {
175 path: template_path.display().to_string(),
176 source,
177 }
178 })?;
179 }
180
181 fs::write(&template_path, content).map_err(|source| {
183 TemplateManagerError::TemplateWrite {
184 path: template_path.display().to_string(),
185 source,
186 }
187 })?;
188
189 tracing::debug!("Created template from embedded resources: {relative_path}");
190
191 Ok(())
192 }
193
194 #[must_use]
196 pub fn templates_dir(&self) -> &Path {
197 &self.templates_dir
198 }
199
200 pub fn clean_templates_dir(&self) -> Result<(), TemplateManagerError> {
209 if self.templates_dir.exists() {
210 fs::remove_dir_all(&self.templates_dir).map_err(|source| {
211 TemplateManagerError::DirectoryCreation {
212 path: self.templates_dir.display().to_string(),
213 source,
214 }
215 })?;
216 tracing::debug!(
217 "Cleaned templates directory: {}",
218 self.templates_dir.display()
219 );
220 }
221 Ok(())
222 }
223}
224
225#[cfg(test)]
226mod tests {
227 use super::*;
228 #[cfg(unix)]
229 use std::os::unix::fs::PermissionsExt;
230 use tempfile::TempDir;
231
232 #[test]
233 fn it_should_create_templates_directory() {
234 let temp_dir = TempDir::new().unwrap();
235 let templates_path = temp_dir.path().join("test_templates");
236
237 let manager = TemplateManager::new(&templates_path);
238
239 assert!(!templates_path.exists());
241
242 manager.ensure_templates_dir().unwrap();
244 assert!(templates_path.exists());
245 assert!(templates_path.is_dir());
246 }
247
248 #[test]
249 fn it_should_create_template_from_embedded_resources() {
250 let temp_dir = TempDir::new().unwrap();
251 let templates_path = temp_dir.path().join("test_templates");
252
253 let manager = TemplateManager::new(&templates_path);
254 manager.ensure_templates_dir().unwrap();
255
256 let template_path = manager.get_template_path("ansible/ansible.cfg").unwrap();
258
259 assert!(template_path.exists());
261 assert!(template_path.is_file());
262
263 let content = fs::read_to_string(&template_path).unwrap();
265 assert!(!content.is_empty());
266 }
267
268 #[test]
269 fn it_should_fail_when_template_not_found_in_embedded_resources() {
270 let temp_dir = TempDir::new().unwrap();
271 let templates_path = temp_dir.path().join("test_templates");
272
273 let manager = TemplateManager::new(&templates_path);
274 manager.ensure_templates_dir().unwrap();
275
276 let result = manager.get_template_path("non-existent/template.txt");
278
279 assert!(result.is_err());
281 let error = result.unwrap_err();
282 match error {
283 TemplateManagerError::TemplateNotFound { relative_path } => {
284 assert_eq!(relative_path, "non-existent/template.txt");
285 }
286 _ => panic!("Expected TemplateNotFound error, got: {error:?}"),
287 }
288 }
289
290 #[cfg(unix)]
291 #[test]
292 fn it_should_fail_when_directory_creation_is_denied() {
293 let temp_dir = TempDir::new().unwrap();
294 let read_only_dir = temp_dir.path().join("read_only");
295 fs::create_dir(&read_only_dir).unwrap();
296
297 let mut perms = fs::metadata(&read_only_dir).unwrap().permissions();
299 perms.set_mode(0o444); fs::set_permissions(&read_only_dir, perms).unwrap();
301
302 let templates_path = read_only_dir.join("should_fail");
303 let manager = TemplateManager::new(&templates_path);
304
305 let result = manager.ensure_templates_dir();
307
308 assert!(result.is_err());
309 let error = result.unwrap_err();
310 match error {
311 TemplateManagerError::DirectoryCreation { path, source } => {
312 assert!(path.contains("should_fail"));
313 assert_eq!(source.kind(), std::io::ErrorKind::PermissionDenied);
314 }
315 _ => panic!("Expected DirectoryCreation error, got: {error:?}"),
316 }
317
318 let mut perms = fs::metadata(&read_only_dir).unwrap().permissions();
320 perms.set_mode(0o755);
321 fs::set_permissions(&read_only_dir, perms).unwrap();
322 }
323
324 #[cfg(unix)]
325 #[test]
326 fn it_should_fail_when_parent_directory_creation_is_denied() {
327 let temp_dir = TempDir::new().unwrap();
328 let read_only_dir = temp_dir.path().join("read_only");
329 fs::create_dir(&read_only_dir).unwrap();
330
331 let mut perms = fs::metadata(&read_only_dir).unwrap().permissions();
333 perms.set_mode(0o444); fs::set_permissions(&read_only_dir, perms).unwrap();
335
336 let templates_path = temp_dir.path().join("templates");
337 let manager = TemplateManager::new(&templates_path);
338 manager.ensure_templates_dir().unwrap();
339
340 let failing_manager = TemplateManager::new(&read_only_dir);
343
344 let result = failing_manager.get_template_path("some/nested/template.txt");
345
346 assert!(result.is_err());
347 let error = result.unwrap_err();
348 match error {
349 TemplateManagerError::TemplateNotFound { .. } => {
350 }
353 _ => panic!("Unexpected error: {error:?}"),
354 }
355
356 let result = failing_manager.get_template_path("ansible/ansible.cfg");
358
359 assert!(result.is_err());
360 let error = result.unwrap_err();
361 match error {
362 TemplateManagerError::ParentDirectoryCreation { path, source } => {
363 assert!(path.contains("ansible.cfg"));
364 assert_eq!(source.kind(), std::io::ErrorKind::PermissionDenied);
365 }
366 _ => panic!("Expected ParentDirectoryCreation error, got: {error:?}"),
367 }
368
369 let mut perms = fs::metadata(&read_only_dir).unwrap().permissions();
371 perms.set_mode(0o755);
372 fs::set_permissions(&read_only_dir, perms).unwrap();
373 }
374
375 #[cfg(unix)]
376 #[test]
377 fn it_should_fail_when_template_write_is_denied() {
378 let temp_dir = TempDir::new().unwrap();
379 let templates_path = temp_dir.path().join("templates");
380 let manager = TemplateManager::new(&templates_path);
381 manager.ensure_templates_dir().unwrap();
382
383 let ansible_dir = templates_path.join("ansible");
385 fs::create_dir(&ansible_dir).unwrap();
386
387 let mut perms = fs::metadata(&ansible_dir).unwrap().permissions();
389 perms.set_mode(0o444); fs::set_permissions(&ansible_dir, perms).unwrap();
391
392 let result = manager.get_template_path("ansible/ansible.cfg");
394
395 assert!(result.is_err());
396 let error = result.unwrap_err();
397 match error {
398 TemplateManagerError::TemplateWrite { path, source } => {
399 assert!(path.contains("ansible.cfg"));
400 assert_eq!(source.kind(), std::io::ErrorKind::PermissionDenied);
401 }
402 _ => panic!("Expected TemplateWrite error, got: {error:?}"),
403 }
404
405 let mut perms = fs::metadata(&ansible_dir).unwrap().permissions();
407 perms.set_mode(0o755);
408 fs::set_permissions(&ansible_dir, perms).unwrap();
409 }
410
411 #[test]
412 fn it_should_return_existing_template_path_without_recreating() {
413 let temp_dir = TempDir::new().unwrap();
414 let templates_path = temp_dir.path().join("test_templates");
415
416 let manager = TemplateManager::new(&templates_path);
417 manager.ensure_templates_dir().unwrap();
418
419 let template_path1 = manager.get_template_path("ansible/ansible.cfg").unwrap();
421 assert!(template_path1.exists());
422
423 let original_content = fs::read_to_string(&template_path1).unwrap();
425 fs::write(&template_path1, "modified content").unwrap();
426
427 let template_path2 = manager.get_template_path("ansible/ansible.cfg").unwrap();
429 assert_eq!(template_path1, template_path2);
430
431 let current_content = fs::read_to_string(&template_path2).unwrap();
433 assert_eq!(current_content, "modified content");
434 assert_ne!(current_content, original_content);
435 }
436
437 #[test]
438 fn it_should_provide_correct_templates_dir_path() {
439 let test_path = PathBuf::from("/test/path");
440 let manager = TemplateManager::new(&test_path);
441
442 assert_eq!(manager.templates_dir(), Path::new("/test/path"));
443 }
444
445 #[test]
446 fn it_should_handle_nested_template_paths() {
447 let temp_dir = TempDir::new().unwrap();
448 let templates_path = temp_dir.path().join("test_templates");
449
450 let manager = TemplateManager::new(&templates_path);
451 manager.ensure_templates_dir().unwrap();
452
453 let template_path = manager.get_template_path("tofu/lxd/main.tf").unwrap();
455
456 assert!(template_path.exists());
457 assert!(template_path.is_file());
458
459 assert!(template_path.parent().unwrap().exists());
461 assert!(template_path.parent().unwrap().parent().unwrap().exists());
462 }
463
464 #[test]
465 fn it_should_fail_when_trying_to_create_existing_template() {
466 let temp_dir = TempDir::new().unwrap();
467 let templates_path = temp_dir.path().join("test_templates");
468
469 let manager = TemplateManager::new(&templates_path);
470 manager.ensure_templates_dir().unwrap();
471
472 let first_path = manager.get_template_path("ansible/ansible.cfg").unwrap();
474
475 let second_path = manager.get_template_path("ansible/ansible.cfg").unwrap();
477
478 assert_eq!(first_path, second_path);
480 assert!(second_path.exists());
481 }
482 #[test]
483 fn it_should_clean_templates_directory() {
484 let temp_dir = TempDir::new().unwrap();
485 let templates_path = temp_dir.path().join("test_templates");
486
487 let manager = TemplateManager::new(&templates_path);
488 manager.ensure_templates_dir().unwrap();
489
490 let template_path1 = manager.get_template_path("ansible/ansible.cfg").unwrap();
492 let template_path2 = manager.get_template_path("tofu/lxd/main.tf").unwrap();
493
494 assert!(template_path1.exists());
496 assert!(template_path2.exists());
497 assert!(templates_path.exists());
498
499 manager.clean_templates_dir().unwrap();
501
502 assert!(!templates_path.exists());
504 assert!(!template_path1.exists());
505 assert!(!template_path2.exists());
506 }
507
508 #[test]
509 fn it_should_handle_clean_on_nonexistent_directory() {
510 let temp_dir = TempDir::new().unwrap();
511 let templates_path = temp_dir.path().join("nonexistent_templates");
512
513 let manager = TemplateManager::new(&templates_path);
514
515 let result = manager.clean_templates_dir();
517 assert!(result.is_ok());
518 }
519
520 #[test]
521 fn it_should_reset_templates_directory() {
522 let temp_dir = TempDir::new().unwrap();
523 let templates_path = temp_dir.path().join("test_templates");
524
525 let manager = TemplateManager::new(&templates_path);
526
527 assert!(!templates_path.exists());
529
530 manager.ensure_templates_dir().unwrap();
532 let template_path = manager.get_template_path("ansible/ansible.cfg").unwrap();
533 assert!(template_path.exists());
534 assert!(templates_path.exists());
535
536 manager.reset_templates_dir().unwrap();
538
539 assert!(templates_path.exists());
541 assert!(templates_path.is_dir());
542 assert!(!template_path.exists());
544 }
545
546 #[test]
547 fn it_should_reset_templates_directory_on_nonexistent_directory() {
548 let temp_dir = TempDir::new().unwrap();
549 let templates_path = temp_dir.path().join("nonexistent_templates");
550
551 let manager = TemplateManager::new(&templates_path);
552
553 assert!(!templates_path.exists());
555
556 let result = manager.reset_templates_dir();
558 assert!(result.is_ok());
559
560 assert!(templates_path.exists());
562 assert!(templates_path.is_dir());
563 }
564}