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