Skip to main content

torrust_tracker_deployer_lib/domain/template/
file_ops.rs

1//! File operations for template processing
2//!
3//! This module provides file operations including copying and writing files
4//! with automatic directory creation for template processing workflows.
5
6use std::path::Path;
7use thiserror::Error;
8
9/// Errors that can occur during file operations
10#[derive(Error, Debug)]
11pub enum FileOperationError {
12    /// Failed to create the output directory
13    #[error("Failed to create directory: {path}")]
14    DirectoryCreation { path: String },
15
16    /// Failed to write the file to the output path
17    #[error("Failed to write file to: {path}")]
18    FileWrite { path: String },
19
20    /// Failed to copy the file from source to destination
21    #[error("Failed to copy file from {source_path} to {dest_path}")]
22    FileCopy {
23        source_path: String,
24        dest_path: String,
25    },
26}
27
28/// Copy a file, creating parent directories if necessary
29///
30/// This function copies files without template processing and creates
31/// any necessary parent directories in the destination path.
32///
33/// # Errors
34/// Returns `FileOperationError::DirectoryCreation` if the destination directory cannot be created,
35/// or `FileOperationError::FileCopy` if the file cannot be copied
36pub fn copy_file_with_dir_creation(
37    source: &Path,
38    destination: &Path,
39) -> Result<(), FileOperationError> {
40    // Ensure destination directory exists
41    if let Some(parent) = destination.parent() {
42        std::fs::create_dir_all(parent).map_err(|_| FileOperationError::DirectoryCreation {
43            path: parent.display().to_string(),
44        })?;
45    }
46
47    std::fs::copy(source, destination).map_err(|_| FileOperationError::FileCopy {
48        source_path: source.display().to_string(),
49        dest_path: destination.display().to_string(),
50    })?;
51
52    Ok(())
53}
54
55/// Write content to a file, creating parent directories if necessary
56///
57/// # Errors
58/// Returns `FileOperationError::DirectoryCreation` if the parent directory cannot be created,
59/// or `FileOperationError::FileWrite` if the file cannot be written
60pub fn write_file_with_dir_creation(
61    output_path: &Path,
62    content: &str,
63) -> Result<(), FileOperationError> {
64    // Create output directory if it doesn't exist
65    if let Some(parent) = output_path.parent() {
66        std::fs::create_dir_all(parent).map_err(|_| FileOperationError::DirectoryCreation {
67            path: parent.display().to_string(),
68        })?;
69    }
70
71    // Write the content to the file
72    std::fs::write(output_path, content).map_err(|_| FileOperationError::FileWrite {
73        path: output_path.display().to_string(),
74    })?;
75
76    Ok(())
77}
78
79#[cfg(test)]
80mod tests {
81    use super::*;
82    use std::fs;
83    use tempfile::TempDir;
84
85    mod copy_file_with_dir_creation {
86        use super::*;
87
88        #[test]
89        fn it_should_copy_file_to_existing_directory() {
90            let temp_dir = TempDir::new().unwrap();
91            let source_file = temp_dir.path().join("source.txt");
92            let dest_file = temp_dir.path().join("dest.txt");
93
94            fs::write(&source_file, "test content").unwrap();
95
96            let result = copy_file_with_dir_creation(&source_file, &dest_file);
97
98            assert!(result.is_ok());
99            assert!(dest_file.exists());
100            let content = fs::read_to_string(&dest_file).unwrap();
101            assert_eq!(content, "test content");
102        }
103
104        #[test]
105        fn it_should_copy_file_and_create_parent_directories() {
106            let temp_dir = TempDir::new().unwrap();
107            let source_file = temp_dir.path().join("source.txt");
108            let dest_file = temp_dir.path().join("deep/nested/path/dest.txt");
109
110            fs::write(&source_file, "nested test").unwrap();
111
112            let result = copy_file_with_dir_creation(&source_file, &dest_file);
113
114            assert!(result.is_ok());
115            assert!(dest_file.exists());
116            let content = fs::read_to_string(&dest_file).unwrap();
117            assert_eq!(content, "nested test");
118        }
119
120        #[test]
121        fn it_should_fail_when_source_file_does_not_exist() {
122            let temp_dir = TempDir::new().unwrap();
123            let source_file = temp_dir.path().join("nonexistent.txt");
124            let dest_file = temp_dir.path().join("dest.txt");
125
126            let result = copy_file_with_dir_creation(&source_file, &dest_file);
127
128            assert!(result.is_err());
129            match result.unwrap_err() {
130                FileOperationError::FileCopy {
131                    source_path,
132                    dest_path,
133                } => {
134                    assert!(source_path.contains("nonexistent.txt"));
135                    assert!(dest_path.contains("dest.txt"));
136                }
137                FileOperationError::DirectoryCreation { .. } => {
138                    panic!("Expected FileCopy error, got DirectoryCreation")
139                }
140                FileOperationError::FileWrite { .. } => {
141                    panic!("Expected FileCopy error, got FileWrite")
142                }
143            }
144        }
145
146        #[test]
147        fn it_should_overwrite_existing_destination_file() {
148            let temp_dir = TempDir::new().unwrap();
149            let source_file = temp_dir.path().join("source.txt");
150            let dest_file = temp_dir.path().join("dest.txt");
151
152            fs::write(&source_file, "new content").unwrap();
153            fs::write(&dest_file, "old content").unwrap();
154
155            let result = copy_file_with_dir_creation(&source_file, &dest_file);
156
157            assert!(result.is_ok());
158            let content = fs::read_to_string(&dest_file).unwrap();
159            assert_eq!(content, "new content");
160        }
161
162        #[test]
163        fn it_should_copy_binary_file_correctly() {
164            let temp_dir = TempDir::new().unwrap();
165            let source_file = temp_dir.path().join("binary.bin");
166            let dest_file = temp_dir.path().join("copied.bin");
167
168            let binary_data = vec![0x00, 0x01, 0xFF, 0x7F, 0x80];
169            fs::write(&source_file, &binary_data).unwrap();
170
171            let result = copy_file_with_dir_creation(&source_file, &dest_file);
172
173            assert!(result.is_ok());
174            let copied_data = fs::read(&dest_file).unwrap();
175            assert_eq!(copied_data, binary_data);
176        }
177    }
178
179    mod write_file_with_dir_creation {
180        use super::*;
181
182        #[test]
183        fn it_should_write_content_to_existing_directory() {
184            let temp_dir = TempDir::new().unwrap();
185            let file_path = temp_dir.path().join("test.txt");
186            let content = "hello world";
187
188            let result = write_file_with_dir_creation(&file_path, content);
189
190            assert!(result.is_ok());
191            assert!(file_path.exists());
192            let read_content = fs::read_to_string(&file_path).unwrap();
193            assert_eq!(read_content, content);
194        }
195
196        #[test]
197        fn it_should_write_file_and_create_parent_directories() {
198            let temp_dir = TempDir::new().unwrap();
199            let file_path = temp_dir.path().join("deep/nested/structure/file.txt");
200            let content = "nested content";
201
202            let result = write_file_with_dir_creation(&file_path, content);
203
204            assert!(result.is_ok());
205            assert!(file_path.exists());
206            let read_content = fs::read_to_string(&file_path).unwrap();
207            assert_eq!(read_content, content);
208        }
209
210        #[test]
211        fn it_should_overwrite_existing_file() {
212            let temp_dir = TempDir::new().unwrap();
213            let file_path = temp_dir.path().join("existing.txt");
214
215            fs::write(&file_path, "original content").unwrap();
216
217            let new_content = "updated content";
218            let result = write_file_with_dir_creation(&file_path, new_content);
219
220            assert!(result.is_ok());
221            let read_content = fs::read_to_string(&file_path).unwrap();
222            assert_eq!(read_content, new_content);
223        }
224
225        #[test]
226        fn it_should_handle_empty_content() {
227            let temp_dir = TempDir::new().unwrap();
228            let file_path = temp_dir.path().join("empty.txt");
229
230            let result = write_file_with_dir_creation(&file_path, "");
231
232            assert!(result.is_ok());
233            assert!(file_path.exists());
234            let read_content = fs::read_to_string(&file_path).unwrap();
235            assert_eq!(read_content, "");
236        }
237
238        #[test]
239        fn it_should_handle_unicode_content() {
240            let temp_dir = TempDir::new().unwrap();
241            let file_path = temp_dir.path().join("unicode.txt");
242            let content = "Hello 世界! 🚀 Émojis and spëcial chars";
243
244            let result = write_file_with_dir_creation(&file_path, content);
245
246            assert!(result.is_ok());
247            let read_content = fs::read_to_string(&file_path).unwrap();
248            assert_eq!(read_content, content);
249        }
250
251        #[test]
252        fn it_should_handle_multiline_content() {
253            let temp_dir = TempDir::new().unwrap();
254            let file_path = temp_dir.path().join("multiline.txt");
255            let content = "Line 1\nLine 2\nLine 3\n\nLine 5";
256
257            let result = write_file_with_dir_creation(&file_path, content);
258
259            assert!(result.is_ok());
260            let read_content = fs::read_to_string(&file_path).unwrap();
261            assert_eq!(read_content, content);
262        }
263    }
264
265    mod error_handling {
266        use super::*;
267
268        #[test]
269        fn it_should_display_directory_creation_error_correctly() {
270            let error = FileOperationError::DirectoryCreation {
271                path: "/some/path".to_string(),
272            };
273            let error_string = format!("{error}");
274            assert!(error_string.contains("Failed to create directory"));
275            assert!(error_string.contains("/some/path"));
276        }
277
278        #[test]
279        fn it_should_display_file_write_error_correctly() {
280            let error = FileOperationError::FileWrite {
281                path: "/output/file.txt".to_string(),
282            };
283            let error_string = format!("{error}");
284            assert!(error_string.contains("Failed to write file to"));
285            assert!(error_string.contains("/output/file.txt"));
286        }
287
288        #[test]
289        fn it_should_display_file_copy_error_correctly() {
290            let error = FileOperationError::FileCopy {
291                source_path: "/source/file.txt".to_string(),
292                dest_path: "/dest/file.txt".to_string(),
293            };
294            let error_string = format!("{error}");
295            assert!(error_string.contains("Failed to copy file from"));
296            assert!(error_string.contains("/source/file.txt"));
297            assert!(error_string.contains("/dest/file.txt"));
298        }
299
300        #[test]
301        fn it_should_support_debug_formatting_for_errors() {
302            let write_error = FileOperationError::FileWrite {
303                path: "/test/path".to_string(),
304            };
305            let copy_error = FileOperationError::DirectoryCreation {
306                path: "/test/dir".to_string(),
307            };
308            let file_copy_error = FileOperationError::FileCopy {
309                source_path: "/src/file".to_string(),
310                dest_path: "/dst/file".to_string(),
311            };
312
313            let write_debug = format!("{write_error:?}");
314            let copy_debug = format!("{copy_error:?}");
315            let file_copy_debug = format!("{file_copy_error:?}");
316
317            assert!(write_debug.contains("FileWrite"));
318            assert!(copy_debug.contains("DirectoryCreation"));
319            assert!(file_copy_debug.contains("FileCopy"));
320        }
321    }
322}