gglib_core/download/
errors.rs

1//! Download error types.
2//!
3//! These errors are designed to be serializable and not depend on external
4//! error types like `std::io::Error`. For I/O errors, we capture the kind
5//! and message as strings.
6
7use serde::{Deserialize, Serialize};
8use thiserror::Error;
9
10/// Error type for download operations.
11///
12/// Designed to be serializable across FFI boundaries (Tauri, CLI, etc.)
13/// without depending on non-serializable types like `std::io::Error`.
14#[derive(Clone, Debug, Error, Serialize, Deserialize, PartialEq, Eq)]
15pub enum DownloadError {
16    /// I/O error during file operations.
17    #[error("I/O error ({kind}): {message}")]
18    Io {
19        /// The kind of I/O error (e.g., "not found", "permission denied").
20        kind: String,
21        /// Detailed error message.
22        message: String,
23    },
24
25    /// Network/HTTP error during download.
26    #[error("Network error: {message}")]
27    Network {
28        /// Detailed error message.
29        message: String,
30        /// HTTP status code if available.
31        #[serde(skip_serializing_if = "Option::is_none")]
32        status_code: Option<u16>,
33    },
34
35    /// Model or file not found on the remote server.
36    #[error("Not found: {message}")]
37    NotFound {
38        /// What was not found (model ID, file, etc.).
39        message: String,
40    },
41
42    /// Invalid quantization specified.
43    #[error("Invalid quantization: {value}")]
44    InvalidQuantization {
45        /// The invalid quantization string.
46        value: String,
47    },
48
49    /// Failed to resolve quantization to a file.
50    #[error("Resolution failed: {message}")]
51    ResolutionFailed {
52        /// Detailed error message.
53        message: String,
54    },
55
56    /// Queue is full, cannot add more downloads.
57    #[error("Queue full: maximum {max_size} downloads allowed")]
58    QueueFull {
59        /// Maximum queue capacity.
60        max_size: u32,
61    },
62
63    /// Download is already queued.
64    #[error("Already queued: {id}")]
65    AlreadyQueued {
66        /// The download ID that's already in the queue.
67        id: String,
68    },
69
70    /// Download not found in queue.
71    #[error("Not in queue: {id}")]
72    NotInQueue {
73        /// The download ID that wasn't found.
74        id: String,
75    },
76
77    /// Download was cancelled by user.
78    #[error("Download cancelled")]
79    Cancelled,
80
81    /// Download was interrupted and can be resumed.
82    #[error("Download interrupted at {bytes_downloaded} bytes")]
83    Interrupted {
84        /// Bytes downloaded before interruption.
85        bytes_downloaded: u64,
86    },
87
88    /// Integrity check failed (checksum mismatch).
89    #[error("Integrity check failed: expected {expected}, got {actual}")]
90    IntegrityFailed {
91        /// Expected checksum.
92        expected: String,
93        /// Actual checksum computed.
94        actual: String,
95    },
96
97    /// General/uncategorized error.
98    #[error("{message}")]
99    Other {
100        /// Error message.
101        message: String,
102    },
103}
104
105impl DownloadError {
106    /// Create an I/O error from kind and message strings.
107    pub fn io(kind: impl Into<String>, message: impl Into<String>) -> Self {
108        Self::Io {
109            kind: kind.into(),
110            message: message.into(),
111        }
112    }
113
114    /// Create an I/O error from a `std::io::Error`.
115    ///
116    /// This captures the error kind name and message for serialization.
117    #[must_use]
118    pub fn from_io_error(err: &std::io::Error) -> Self {
119        let kind = err.kind();
120        Self::Io {
121            kind: format!("{kind:?}"),
122            message: err.to_string(),
123        }
124    }
125
126    /// Create a network error.
127    pub fn network(message: impl Into<String>) -> Self {
128        Self::Network {
129            message: message.into(),
130            status_code: None,
131        }
132    }
133
134    /// Create a network error with HTTP status code.
135    pub fn network_with_status(message: impl Into<String>, status_code: u16) -> Self {
136        Self::Network {
137            message: message.into(),
138            status_code: Some(status_code),
139        }
140    }
141
142    /// Create a not found error.
143    pub fn not_found(message: impl Into<String>) -> Self {
144        Self::NotFound {
145            message: message.into(),
146        }
147    }
148
149    /// Create an invalid quantization error.
150    pub fn invalid_quantization(value: impl Into<String>) -> Self {
151        Self::InvalidQuantization {
152            value: value.into(),
153        }
154    }
155
156    /// Create a resolution failed error.
157    pub fn resolution_failed(message: impl Into<String>) -> Self {
158        Self::ResolutionFailed {
159            message: message.into(),
160        }
161    }
162
163    /// Create a queue full error.
164    #[must_use]
165    pub const fn queue_full(max_size: u32) -> Self {
166        Self::QueueFull { max_size }
167    }
168
169    /// Create an already queued error.
170    pub fn already_queued(id: impl Into<String>) -> Self {
171        Self::AlreadyQueued { id: id.into() }
172    }
173
174    /// Create a not in queue error.
175    pub fn not_in_queue(id: impl Into<String>) -> Self {
176        Self::NotInQueue { id: id.into() }
177    }
178
179    /// Create an integrity check failed error.
180    pub fn integrity_failed(expected: impl Into<String>, actual: impl Into<String>) -> Self {
181        Self::IntegrityFailed {
182            expected: expected.into(),
183            actual: actual.into(),
184        }
185    }
186
187    /// Create a generic error.
188    pub fn other(message: impl Into<String>) -> Self {
189        Self::Other {
190            message: message.into(),
191        }
192    }
193
194    /// Check if this error is recoverable (can retry).
195    #[must_use]
196    pub const fn is_recoverable(&self) -> bool {
197        matches!(
198            self,
199            Self::Network { .. } | Self::Interrupted { .. } | Self::Io { .. }
200        )
201    }
202
203    /// Check if this is a cancellation.
204    #[must_use]
205    pub const fn is_cancelled(&self) -> bool {
206        matches!(self, Self::Cancelled)
207    }
208
209    /// Convert to a user-friendly message.
210    #[must_use]
211    pub fn user_message(&self) -> String {
212        match self {
213            Self::Io { message, .. } => format!("File operation failed: {message}"),
214            Self::Network {
215                message,
216                status_code: Some(code),
217            } => {
218                format!("Network error (HTTP {code}): {message}")
219            }
220            Self::Network { message, .. } => format!("Network error: {message}"),
221            Self::NotFound { message } => format!("Not found: {message}"),
222            Self::InvalidQuantization { value } => {
223                format!("Invalid quantization '{value}'. Use values like `Q4_K_M`, `Q5_K_S`, etc.")
224            }
225            Self::ResolutionFailed { message } => format!("Could not resolve file: {message}"),
226            Self::QueueFull { max_size } => {
227                format!(
228                    "Download queue is full (max {max_size} items). Wait for a download to complete."
229                )
230            }
231            Self::AlreadyQueued { id } => {
232                format!("Download '{id}' is already in the queue.")
233            }
234            Self::NotInQueue { id } => {
235                format!("Download '{id}' is not in the queue.")
236            }
237            Self::Cancelled => "Download was cancelled.".to_string(),
238            Self::Interrupted { bytes_downloaded } => {
239                format!("Download interrupted after {bytes_downloaded} bytes. You can resume it.")
240            }
241            Self::IntegrityFailed { .. } => {
242                "File integrity check failed. The download may be corrupted.".to_string()
243            }
244            Self::Other { message } => message.clone(),
245        }
246    }
247}
248
249/// Convenience result type for download operations.
250pub type DownloadResult<T> = Result<T, DownloadError>;
251
252#[cfg(test)]
253mod tests {
254    use super::*;
255
256    #[test]
257    fn test_io_error_from_std() {
258        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
259        let err = DownloadError::from_io_error(&io_err);
260
261        match err {
262            DownloadError::Io { kind, message } => {
263                assert_eq!(kind, "NotFound");
264                assert!(message.contains("file not found"));
265            }
266            _ => panic!("Expected Io variant"),
267        }
268    }
269
270    #[test]
271    fn test_error_serialization() {
272        let err = DownloadError::network_with_status("timeout", 408);
273        let json = serde_json::to_string(&err).unwrap();
274        assert!(json.contains("408"));
275        assert!(json.contains("timeout"));
276
277        let parsed: DownloadError = serde_json::from_str(&json).unwrap();
278        assert_eq!(parsed, err);
279    }
280
281    #[test]
282    fn test_is_recoverable() {
283        assert!(DownloadError::network("timeout").is_recoverable());
284        assert!(
285            DownloadError::Interrupted {
286                bytes_downloaded: 100
287            }
288            .is_recoverable()
289        );
290        assert!(!DownloadError::Cancelled.is_recoverable());
291        assert!(!DownloadError::invalid_quantization("bad").is_recoverable());
292    }
293
294    #[test]
295    fn test_user_messages() {
296        let err = DownloadError::queue_full(5);
297        assert!(err.user_message().contains('5'));
298        assert!(err.user_message().contains("full"));
299    }
300}