1use std::path::{Path, PathBuf};
7use thiserror::Error;
8
9pub const LARGE_FILE_SIZE: usize = 1024 * 1024;
11
12#[derive(Debug, Clone)]
14pub struct FileOperationContext {
15 pub operation: FileOperation,
17 pub file_path: PathBuf,
19 pub purpose: String,
21 pub caller: String,
23 pub related_paths: Vec<PathBuf>,
25}
26
27#[derive(Debug, Clone, PartialEq)]
29pub enum FileOperation {
30 Read,
32 Write,
34 Exists,
36 Metadata,
38 Canonicalize,
40 CreateDir,
42 Validate,
44}
45
46impl std::fmt::Display for FileOperation {
47 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48 match self {
49 FileOperation::Read => write!(f, "reading"),
50 FileOperation::Write => write!(f, "writing"),
51 FileOperation::Exists => write!(f, "checking if file exists"),
52 FileOperation::Metadata => write!(f, "getting file metadata"),
53 FileOperation::Canonicalize => write!(f, "resolving path"),
54 FileOperation::CreateDir => write!(f, "creating directory"),
55 FileOperation::Validate => write!(f, "validating file path"),
56 }
57 }
58}
59
60impl FileOperationContext {
61 pub fn new(
63 operation: FileOperation,
64 file_path: impl Into<PathBuf>,
65 purpose: impl Into<String>,
66 caller: impl Into<String>,
67 ) -> Self {
68 Self {
69 operation,
70 file_path: file_path.into(),
71 purpose: purpose.into(),
72 caller: caller.into(),
73 related_paths: Vec::new(),
74 }
75 }
76
77 pub fn with_related_path(mut self, path: impl Into<PathBuf>) -> Self {
79 self.related_paths.push(path.into());
80 self
81 }
82
83 pub fn with_related_paths<I>(mut self, paths: I) -> Self
85 where
86 I: IntoIterator,
87 I::Item: Into<PathBuf>,
88 {
89 for path in paths {
90 self.related_paths.push(path.into());
91 }
92 self
93 }
94}
95
96#[derive(Error, Debug)]
98#[error("File operation failed: {operation} on {file_path}")]
99pub struct FileOperationError {
100 pub operation: FileOperation,
102 pub file_path: PathBuf,
104 pub purpose: String,
106 pub caller: String,
108 #[source]
110 pub source: std::io::Error,
111 pub related_paths: Vec<PathBuf>,
113}
114
115impl FileOperationError {
116 pub fn new(context: FileOperationContext, source: std::io::Error) -> Self {
118 Self {
119 operation: context.operation,
120 file_path: context.file_path,
121 purpose: context.purpose,
122 caller: context.caller,
123 source,
124 related_paths: context.related_paths,
125 }
126 }
127
128 pub fn user_message(&self) -> String {
130 let operation_name = match self.operation {
131 FileOperation::Read => "reading",
132 FileOperation::Write => "writing",
133 FileOperation::Exists => "checking if file exists",
134 FileOperation::Metadata => "getting file metadata",
135 FileOperation::Canonicalize => "resolving path",
136 FileOperation::CreateDir => "creating directory",
137 FileOperation::Validate => "validating file path",
138 };
139
140 let mut message = format!(
141 "Failed {} file '{}' for {} ({})",
142 operation_name,
143 self.file_path.display(),
144 self.purpose,
145 self.caller
146 );
147
148 match self.source.kind() {
150 std::io::ErrorKind::NotFound => {
151 message.push_str("\n\nThe file does not exist at the specified path.");
152
153 if self.file_path.extension().and_then(|s| s.to_str()) == Some("md") {
155 message.push_str("\n\nFor markdown files, check:");
156 message.push_str("\n- The file exists in the expected location");
157 message.push_str("\n- The filename is spelled correctly (case-sensitive)");
158 message.push_str(&format!(
159 "\n- The file should be relative to: {}",
160 self.related_paths
161 .first()
162 .map(|p| p.display().to_string())
163 .unwrap_or_else(|| "project root".to_string())
164 ));
165 }
166
167 if self.purpose.contains("template") || self.purpose.contains("render") {
168 message.push_str("\n\nFor template errors, ensure:");
169 message.push_str("\n- All referenced files exist");
170 message.push_str("\n- File paths in templates are correct");
171 message.push_str("\n- Dependencies are properly declared in frontmatter");
172 }
173 }
174 std::io::ErrorKind::PermissionDenied => {
175 message.push_str(&format!(
176 "\n\nPermission denied. Check file/directory permissions for: {}",
177 self.file_path.display()
178 ));
179 }
180 std::io::ErrorKind::InvalidData => {
181 message.push_str("\n\nThe file contains invalid data or encoding.");
182 if self.purpose.contains("UTF-8") || self.purpose.contains("read") {
183 message.push_str("\nEnsure the file contains valid UTF-8 text.");
184 }
185 }
186 _ => {
187 message.push_str(&format!("\n\nError details: {}", self.source));
188 }
189 }
190
191 if !self.related_paths.is_empty() {
193 message.push_str("\n\nRelated paths:");
194 for path in &self.related_paths {
195 message.push_str(&format!("\n - {}", path.display()));
196 }
197 }
198
199 message
200 }
201}
202
203pub trait FileResultExt<T> {
205 fn with_file_context(
207 self,
208 operation: FileOperation,
209 file_path: impl Into<PathBuf>,
210 purpose: impl Into<String>,
211 caller: impl Into<String>,
212 ) -> Result<T, FileOperationError>;
213}
214
215impl<T> FileResultExt<T> for Result<T, std::io::Error> {
216 fn with_file_context(
217 self,
218 operation: FileOperation,
219 file_path: impl Into<PathBuf>,
220 purpose: impl Into<String>,
221 caller: impl Into<String>,
222 ) -> Result<T, FileOperationError> {
223 self.map_err(|io_error| {
224 let context = FileOperationContext::new(operation, file_path, purpose, caller);
225 FileOperationError::new(context, io_error)
226 })
227 }
228}
229
230pub struct FileOps;
232
233impl FileOps {
234 pub async fn read_with_context(
236 path: &Path,
237 purpose: &str,
238 caller: &str,
239 ) -> Result<String, FileOperationError> {
240 tokio::fs::read_to_string(path).await.with_file_context(
241 FileOperation::Read,
242 path,
243 purpose,
244 caller,
245 )
246 }
247
248 pub async fn exists_with_context(
250 path: &Path,
251 purpose: &str,
252 caller: &str,
253 ) -> Result<bool, FileOperationError> {
254 tokio::fs::metadata(path)
255 .await
256 .map(|_| true)
257 .or_else(|e| {
258 if e.kind() == std::io::ErrorKind::NotFound {
259 Ok(false)
260 } else {
261 Err(e)
262 }
263 })
264 .with_file_context(FileOperation::Exists, path, purpose, caller)
265 }
266
267 pub async fn metadata_with_context(
269 path: &Path,
270 purpose: &str,
271 caller: &str,
272 ) -> Result<std::fs::Metadata, FileOperationError> {
273 tokio::fs::metadata(path).await.with_file_context(
274 FileOperation::Metadata,
275 path,
276 purpose,
277 caller,
278 )
279 }
280}
281
282#[cfg(test)]
283mod tests {
284 use super::*;
285 use std::io::{Error, ErrorKind};
286
287 #[test]
288 fn test_file_operation_context_creation() {
289 let context = FileOperationContext::new(
290 FileOperation::Read,
291 "/path/to/file.md",
292 "template rendering",
293 "content_filter",
294 );
295
296 assert_eq!(context.operation, FileOperation::Read);
297 assert_eq!(context.file_path, PathBuf::from("/path/to/file.md"));
298 assert_eq!(context.purpose, "template rendering");
299 assert_eq!(context.caller, "content_filter");
300 }
301
302 #[test]
303 fn test_file_operation_error_user_message() {
304 let io_error = Error::new(ErrorKind::NotFound, "file not found");
305 let context = FileOperationContext::new(
306 FileOperation::Read,
307 "docs/styleguide.md",
308 "template rendering",
309 "content_filter",
310 )
311 .with_related_path("/project/root");
312
313 let file_error = FileOperationError::new(context, io_error);
314 let message = file_error.user_message();
315
316 assert!(message.contains("Failed reading file"));
317 assert!(message.contains("docs/styleguide.md"));
318 assert!(message.contains("template rendering"));
319 assert!(message.contains("content_filter"));
320 assert!(message.contains("does not exist"));
321 assert!(message.contains("Related paths"));
322 }
323
324 #[test]
325 fn test_file_result_ext() {
326 let io_error = Error::new(ErrorKind::PermissionDenied, "access denied");
327 let result: Result<String, std::io::Error> = Err(io_error);
328
329 let enhanced_result = result.with_file_context(
330 FileOperation::Write,
331 "/tmp/test.txt",
332 "saving configuration",
333 "config_module",
334 );
335
336 assert!(enhanced_result.is_err());
337 let error = enhanced_result.unwrap_err();
338 assert_eq!(error.operation, FileOperation::Write);
339 assert_eq!(error.purpose, "saving configuration");
340 assert_eq!(error.caller, "config_module");
341 }
342
343 #[tokio::test]
344 async fn test_exists_with_context_success() {
345 let temp_dir = tempfile::tempdir().unwrap();
346 let test_file = temp_dir.path().join("test.txt");
347 std::fs::write(&test_file, "test content").unwrap();
348
349 let result =
350 FileOps::exists_with_context(&test_file, "checking if file exists", "test_module")
351 .await;
352
353 assert!(result.is_ok());
354 assert!(result.unwrap());
355 }
356
357 #[tokio::test]
358 async fn test_exists_with_context_not_found() {
359 let temp_dir = tempfile::tempdir().unwrap();
360 let nonexistent_file = temp_dir.path().join("nonexistent.txt");
361
362 let result = FileOps::exists_with_context(
363 &nonexistent_file,
364 "checking if file exists",
365 "test_module",
366 )
367 .await;
368
369 assert!(result.is_ok());
371 assert!(!result.unwrap());
372 }
373
374 #[tokio::test]
375 async fn test_metadata_with_context_success() {
376 let temp_dir = tempfile::tempdir().unwrap();
377 let test_file = temp_dir.path().join("test.txt");
378 std::fs::write(&test_file, "test content").unwrap();
379
380 let result =
381 FileOps::metadata_with_context(&test_file, "getting file metadata", "test_module")
382 .await;
383
384 assert!(result.is_ok());
385 let metadata = result.unwrap();
386 assert!(metadata.is_file());
387 }
388
389 #[tokio::test]
390 async fn test_metadata_with_context_not_found() {
391 let temp_dir = tempfile::tempdir().unwrap();
392 let nonexistent_file = temp_dir.path().join("nonexistent.txt");
393
394 let result = FileOps::metadata_with_context(
395 &nonexistent_file,
396 "getting file metadata",
397 "test_module",
398 )
399 .await;
400
401 assert!(result.is_err());
402 let error = result.unwrap_err();
403 assert_eq!(error.operation, FileOperation::Metadata);
404 assert_eq!(error.purpose, "getting file metadata");
405 assert_eq!(error.caller, "test_module");
406 }
407
408 #[tokio::test]
409 async fn test_with_related_paths() {
410 let temp_dir = tempfile::tempdir().unwrap();
411 let main_file = temp_dir.path().join("main.md");
412
413 std::fs::write(&main_file, "# Main file").unwrap();
414
415 let result =
416 FileOps::read_with_context(&main_file, "reading main file", "test_module").await;
417
418 assert!(result.is_ok());
419 let content = result.unwrap();
420 assert_eq!(content, "# Main file");
421 }
422
423 #[test]
424 fn test_permission_denied_error() {
425 let io_error =
427 std::io::Error::new(std::io::ErrorKind::PermissionDenied, "Permission denied");
428 let context = FileOperationContext::new(
429 FileOperation::Read,
430 "/root/secret.txt",
431 "reading secret file",
432 "security_module",
433 );
434
435 let file_error = FileOperationError::new(context, io_error);
436
437 assert!(matches!(file_error.source.kind(), std::io::ErrorKind::PermissionDenied));
438 assert_eq!(file_error.operation, FileOperation::Read);
439 assert_eq!(file_error.file_path, PathBuf::from("/root/secret.txt"));
440 assert_eq!(file_error.purpose, "reading secret file");
441 assert_eq!(file_error.caller, "security_module");
442 }
443
444 #[tokio::test]
445 async fn test_invalid_utf8_handling() {
446 let temp_dir = tempfile::tempdir().unwrap();
447 let test_file = temp_dir.path().join("invalid_utf8.txt");
448
449 let invalid_bytes = &[0xFF, 0xFE, 0xFD];
451 std::fs::write(&test_file, invalid_bytes).unwrap();
452
453 let result =
454 FileOps::read_with_context(&test_file, "reading file as string", "test_module").await;
455
456 assert!(result.is_err());
457 let error = result.unwrap_err();
458 assert_eq!(error.operation, FileOperation::Read);
459 assert_eq!(error.purpose, "reading file as string");
460 assert!(matches!(error.source.kind(), std::io::ErrorKind::InvalidData));
462 }
463
464 #[tokio::test]
465 async fn test_read_with_context_large_file() {
466 let temp_dir = tempfile::tempdir().unwrap();
467 let test_file = temp_dir.path().join("large.txt");
468
469 let large_content = "x".repeat(LARGE_FILE_SIZE);
471 std::fs::write(&test_file, &large_content).unwrap();
472
473 let result =
474 FileOps::read_with_context(&test_file, "reading large file", "test_module").await;
475
476 assert!(result.is_ok());
477 let read_content = result.unwrap();
478 assert_eq!(read_content.len(), large_content.len());
479 }
480
481 }