tsk/assets/
filesystem.rs

1use anyhow::{Context, Result};
2use async_trait::async_trait;
3use std::path::PathBuf;
4
5use crate::assets::AssetManager;
6
7/// A filesystem-based implementation of AssetManager that reads assets from a directory
8pub struct FileSystemAssetManager {
9    base_path: PathBuf,
10}
11
12impl FileSystemAssetManager {
13    /// Creates a new FileSystemAssetManager with the given base path
14    pub fn new(base_path: PathBuf) -> Self {
15        Self { base_path }
16    }
17}
18
19#[async_trait]
20impl AssetManager for FileSystemAssetManager {
21    fn get_template(&self, template_type: &str) -> Result<String> {
22        // Try both direct path and templates subdirectory
23        let direct_path = self.base_path.join(format!("{template_type}.md"));
24        let templates_path = self
25            .base_path
26            .join("templates")
27            .join(format!("{template_type}.md"));
28
29        let template_path = if direct_path.exists() {
30            direct_path
31        } else if templates_path.exists() {
32            templates_path
33        } else {
34            return Err(anyhow::anyhow!("Template '{}' not found", template_type));
35        };
36
37        std::fs::read_to_string(&template_path)
38            .with_context(|| format!("Failed to read template file: {}", template_path.display()))
39    }
40
41    fn get_dockerfile(&self, dockerfile_name: &str) -> Result<Vec<u8>> {
42        let dockerfile_path = self
43            .base_path
44            .join("dockerfiles")
45            .join(dockerfile_name)
46            .join("Dockerfile");
47
48        if !dockerfile_path.exists() {
49            return Err(anyhow::anyhow!(
50                "Dockerfile '{}' not found",
51                dockerfile_name
52            ));
53        }
54
55        std::fs::read(&dockerfile_path)
56            .with_context(|| format!("Failed to read dockerfile: {}", dockerfile_path.display()))
57    }
58
59    fn get_dockerfile_file(&self, dockerfile_name: &str, file_path: &str) -> Result<Vec<u8>> {
60        let file_full_path = self
61            .base_path
62            .join("dockerfiles")
63            .join(dockerfile_name)
64            .join(file_path);
65
66        if !file_full_path.exists() {
67            return Err(anyhow::anyhow!(
68                "Dockerfile file '{}/{}' not found",
69                dockerfile_name,
70                file_path
71            ));
72        }
73
74        std::fs::read(&file_full_path).with_context(|| {
75            format!(
76                "Failed to read dockerfile file: {}",
77                file_full_path.display()
78            )
79        })
80    }
81
82    fn list_templates(&self) -> Vec<String> {
83        let mut templates = Vec::new();
84
85        // Check direct path
86        if let Ok(entries) = std::fs::read_dir(&self.base_path) {
87            for entry in entries.flatten() {
88                if let Ok(file_type) = entry.file_type() {
89                    if file_type.is_file() {
90                        if let Some(file_name) = entry.file_name().to_str() {
91                            if file_name.ends_with(".md") {
92                                let template_name = file_name.trim_end_matches(".md");
93                                templates.push(template_name.to_string());
94                            }
95                        }
96                    }
97                }
98            }
99        }
100
101        // Check templates subdirectory
102        let templates_dir = self.base_path.join("templates");
103        if let Ok(entries) = std::fs::read_dir(&templates_dir) {
104            for entry in entries.flatten() {
105                if let Ok(file_type) = entry.file_type() {
106                    if file_type.is_file() {
107                        if let Some(file_name) = entry.file_name().to_str() {
108                            if file_name.ends_with(".md") {
109                                let template_name = file_name.trim_end_matches(".md");
110                                if !templates.contains(&template_name.to_string()) {
111                                    templates.push(template_name.to_string());
112                                }
113                            }
114                        }
115                    }
116                }
117            }
118        }
119
120        templates.sort();
121        templates
122    }
123
124    fn list_dockerfiles(&self) -> Vec<String> {
125        let mut dockerfiles = Vec::new();
126        let dockerfiles_dir = self.base_path.join("dockerfiles");
127
128        if let Ok(entries) = std::fs::read_dir(&dockerfiles_dir) {
129            for entry in entries.flatten() {
130                if let Ok(file_type) = entry.file_type() {
131                    if file_type.is_dir() {
132                        if let Some(dir_name) = entry.file_name().to_str() {
133                            // Check if this directory contains subdirectories with Dockerfiles
134                            let layer_dir = dockerfiles_dir.join(dir_name);
135                            if let Ok(layer_entries) = std::fs::read_dir(&layer_dir) {
136                                for layer_entry in layer_entries.flatten() {
137                                    if let Ok(layer_file_type) = layer_entry.file_type() {
138                                        if layer_file_type.is_dir() {
139                                            if let Some(layer_name) =
140                                                layer_entry.file_name().to_str()
141                                            {
142                                                let dockerfile_path =
143                                                    layer_dir.join(layer_name).join("Dockerfile");
144                                                if dockerfile_path.exists() {
145                                                    dockerfiles
146                                                        .push(format!("{dir_name}/{layer_name}"));
147                                                }
148                                            }
149                                        }
150                                    }
151                                }
152                            }
153                        }
154                    }
155                }
156            }
157        }
158
159        dockerfiles.sort();
160        dockerfiles
161    }
162}
163
164#[cfg(test)]
165mod tests {
166    use super::*;
167    use std::fs;
168    use tempfile::TempDir;
169
170    #[test]
171    fn test_get_template_success() {
172        let temp_dir = TempDir::new().unwrap();
173        let templates_dir = temp_dir.path().join("templates");
174        fs::create_dir(&templates_dir).unwrap();
175
176        let template_content = "# Feature Template\n\n{{DESCRIPTION}}";
177        fs::write(templates_dir.join("feat.md"), template_content).unwrap();
178
179        let manager = FileSystemAssetManager::new(templates_dir);
180        let result = manager.get_template("feat").unwrap();
181
182        assert_eq!(result, template_content);
183    }
184
185    #[test]
186    fn test_get_template_not_found() {
187        let temp_dir = TempDir::new().unwrap();
188        let templates_dir = temp_dir.path().join("templates");
189        fs::create_dir(&templates_dir).unwrap();
190
191        let manager = FileSystemAssetManager::new(templates_dir);
192        let result = manager.get_template("nonexistent");
193
194        assert!(result.is_err());
195        assert!(result.unwrap_err().to_string().contains("not found"));
196    }
197
198    #[test]
199    fn test_list_templates() {
200        let temp_dir = TempDir::new().unwrap();
201        let templates_dir = temp_dir.path().join("templates");
202        fs::create_dir(&templates_dir).unwrap();
203
204        fs::write(templates_dir.join("feat.md"), "content").unwrap();
205        fs::write(templates_dir.join("fix.md"), "content").unwrap();
206        fs::write(templates_dir.join("doc.md"), "content").unwrap();
207        fs::write(templates_dir.join("not-a-template.txt"), "content").unwrap();
208
209        let manager = FileSystemAssetManager::new(templates_dir);
210        let templates = manager.list_templates();
211
212        assert_eq!(templates, vec!["doc", "feat", "fix"]);
213    }
214
215    #[test]
216    fn test_list_templates_empty_directory() {
217        let temp_dir = TempDir::new().unwrap();
218        let templates_dir = temp_dir.path().join("templates");
219        fs::create_dir(&templates_dir).unwrap();
220
221        let manager = FileSystemAssetManager::new(templates_dir);
222        let templates = manager.list_templates();
223
224        assert!(templates.is_empty());
225    }
226
227    #[test]
228    fn test_list_templates_nonexistent_directory() {
229        let temp_dir = TempDir::new().unwrap();
230        let templates_dir = temp_dir.path().join("nonexistent");
231
232        let manager = FileSystemAssetManager::new(templates_dir);
233        let templates = manager.list_templates();
234
235        assert!(templates.is_empty());
236    }
237
238    #[test]
239    fn test_dockerfile_methods_return_error() {
240        let temp_dir = TempDir::new().unwrap();
241        let manager = FileSystemAssetManager::new(temp_dir.path().to_path_buf());
242
243        assert!(manager.get_dockerfile("test").is_err());
244        assert!(manager.get_dockerfile_file("test", "file").is_err());
245        assert!(manager.list_dockerfiles().is_empty());
246    }
247}