gravityfile_core/
error.rs1use std::fmt;
4use std::path::PathBuf;
5
6use serde::{Deserialize, Serialize};
7use thiserror::Error;
8
9#[derive(Debug, Error, Serialize)]
11pub enum ScanError {
12 #[error("Permission denied: {path}: {}", serialize_io_error_display(source))]
14 PermissionDenied {
15 path: PathBuf,
16 #[source]
17 #[serde(serialize_with = "serialize_io_error")]
18 source: std::io::Error,
19 },
20
21 #[error("Path not found: {path}: {}", serialize_io_error_display(source))]
23 NotFound {
24 path: PathBuf,
25 #[source]
26 #[serde(serialize_with = "serialize_io_error")]
27 source: std::io::Error,
28 },
29
30 #[error("I/O error at {path}: {source}")]
32 Io {
33 path: PathBuf,
34 #[source]
35 #[serde(serialize_with = "serialize_io_error")]
36 source: std::io::Error,
37 },
38
39 #[error("Operation interrupted")]
41 Interrupted,
42
43 #[error("Too many errors ({count}), aborting")]
45 TooManyErrors { count: usize },
46
47 #[error("Invalid configuration: {message}")]
49 InvalidConfig { message: String },
50
51 #[error("Root path is not a directory: {path}")]
53 NotADirectory { path: PathBuf },
54
55 #[error("{message}")]
57 Other { message: String },
58}
59
60fn serialize_io_error<S: serde::Serializer>(
61 error: &std::io::Error,
62 s: S,
63) -> Result<S::Ok, S::Error> {
64 s.serialize_str(&error.to_string())
65}
66
67fn serialize_io_error_display(error: &std::io::Error) -> String {
68 error.to_string()
69}
70
71impl ScanError {
72 pub fn io(path: impl Into<PathBuf>, source: std::io::Error) -> Self {
74 let path = path.into();
75 match source.kind() {
76 std::io::ErrorKind::PermissionDenied => Self::PermissionDenied { path, source },
77 std::io::ErrorKind::NotFound => Self::NotFound { path, source },
78 _ => Self::Io { path, source },
79 }
80 }
81}
82
83#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
85pub enum WarningKind {
86 PermissionDenied,
88 BrokenSymlink,
90 ReadError,
92 MetadataError,
94 CrossFilesystem,
96}
97
98#[derive(Debug, Clone, Serialize, Deserialize)]
100pub struct ScanWarning {
101 pub path: PathBuf,
103 pub message: String,
105 pub kind: WarningKind,
107}
108
109impl fmt::Display for ScanWarning {
110 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
111 write!(f, "{}", self.message)
112 }
113}
114
115impl ScanWarning {
116 pub fn new(path: impl Into<PathBuf>, kind: WarningKind, message: impl Into<String>) -> Self {
118 Self {
119 path: path.into(),
120 message: message.into(),
121 kind,
122 }
123 }
124
125 pub fn permission_denied(path: impl Into<PathBuf>) -> Self {
127 let path = path.into();
128 Self {
129 message: format!("Permission denied: {}", path.display()),
130 path,
131 kind: WarningKind::PermissionDenied,
132 }
133 }
134
135 pub fn broken_symlink(path: impl Into<PathBuf>, target: &str) -> Self {
137 let path = path.into();
138 Self {
139 message: format!("Broken symlink: {} -> {target}", path.display()),
140 path,
141 kind: WarningKind::BrokenSymlink,
142 }
143 }
144
145 pub fn read_error(path: impl Into<PathBuf>, error: &std::io::Error) -> Self {
147 let path = path.into();
148 Self {
149 message: format!("Read error: {error}"),
150 path,
151 kind: WarningKind::ReadError,
152 }
153 }
154}
155
156#[cfg(test)]
157mod tests {
158 use super::*;
159
160 #[test]
161 fn test_scan_error_io() {
162 let err = ScanError::io(
163 "/test/path",
164 std::io::Error::new(std::io::ErrorKind::PermissionDenied, "denied"),
165 );
166 assert!(matches!(err, ScanError::PermissionDenied { .. }));
167 }
168
169 #[test]
170 fn test_scan_error_preserves_source() {
171 let err = ScanError::io(
172 "/test/path",
173 std::io::Error::new(std::io::ErrorKind::NotFound, "not found"),
174 );
175 match err {
176 ScanError::NotFound { source, .. } => {
177 assert_eq!(source.kind(), std::io::ErrorKind::NotFound);
178 }
179 _ => panic!("Expected NotFound variant"),
180 }
181 }
182
183 #[test]
184 fn test_scan_warning_creation() {
185 let warning = ScanWarning::permission_denied("/test/path");
186 assert_eq!(warning.kind, WarningKind::PermissionDenied);
187 assert!(warning.message.contains("Permission denied"));
188 assert_eq!(format!("{warning}"), warning.message);
189 }
190}