1use axum::{
4 http::StatusCode,
5 response::{IntoResponse, Response},
6 Json,
7};
8use serde_json::json;
9
10#[derive(Debug)]
13pub enum AppError {
14 NotFound,
16 Gone,
17 Forbidden,
18 Conflict,
19
20 FileEmpty,
23 FilenameEmpty,
25 FilenameTooLong,
27 InvalidFilename,
29 GzipDecodeFailed,
31 InvalidTtlValue,
33 TtlTooLow,
35 TtlTooHigh,
37 MissingFileField,
39 PublicUrlNotConfigured,
41 InvalidIdFormat,
43
44 TusMissingLength,
47 TusMissingOffset,
49 TusOffsetMismatch,
51 TusSessionNotFound,
53 TusStagingError,
55
56 PayloadTooLarge,
58 RateLimited,
59
60 JuicehostUnreachable(String),
63 JuicehostRejected(String),
65 InsufficientStorage(String),
67 JuicehostDeleteFailed(String),
69 JuicehostRenameFailed(String),
71
72 FilesystemError(std::io::Error),
74 DatabaseError(rusqlite::Error),
75 DbPoolError(String),
77 TaskPanicked(String),
79 BadRequest(String),
80 InvalidMultipart(String),
81 Internal(String),
82}
83
84impl IntoResponse for AppError {
85 fn into_response(self) -> Response {
86 let (status, error_code, message) = match self {
87 AppError::NotFound => (
89 StatusCode::NOT_FOUND,
90 "FILE_NOT_FOUND",
91 "File not found",
92 ),
93 AppError::Gone => (
94 StatusCode::GONE,
95 "FILE_EXPIRED",
96 "File has expired",
97 ),
98 AppError::Forbidden => (
99 StatusCode::FORBIDDEN,
100 "FORBIDDEN",
101 "Invalid delete token",
102 ),
103 AppError::Conflict => (
104 StatusCode::CONFLICT,
105 "CONFLICT",
106 "Could not allocate a new ID, please retry",
107 ),
108
109 AppError::FileEmpty => (
111 StatusCode::BAD_REQUEST,
112 "FILE_EMPTY",
113 "Uploaded file is empty",
114 ),
115 AppError::FilenameEmpty => (
116 StatusCode::BAD_REQUEST,
117 "FILENAME_EMPTY",
118 "Filename is empty after sanitisation",
119 ),
120 AppError::FilenameTooLong => (
121 StatusCode::BAD_REQUEST,
122 "FILENAME_TOO_LONG",
123 "Filename exceeds the maximum length of 255 characters",
124 ),
125 AppError::InvalidFilename => (
126 StatusCode::BAD_REQUEST,
127 "INVALID_FILENAME",
128 "Filename contains invalid characters",
129 ),
130 AppError::GzipDecodeFailed => (
131 StatusCode::BAD_REQUEST,
132 "GZIP_DECODE_FAILED",
133 "Gzip decompression failed, the data may be corrupt or not actually gzip-compressed",
134 ),
135 AppError::InvalidTtlValue => (
136 StatusCode::BAD_REQUEST,
137 "INVALID_TTL",
138 "Retention time must be a valid number",
139 ),
140 AppError::TtlTooLow => (
141 StatusCode::BAD_REQUEST,
142 "TTL_TOO_LOW",
143 "Retention time is below the minimum allowed",
144 ),
145 AppError::TtlTooHigh => (
146 StatusCode::BAD_REQUEST,
147 "TTL_TOO_HIGH",
148 "Retention time exceeds the maximum allowed",
149 ),
150 AppError::MissingFileField => (
151 StatusCode::BAD_REQUEST,
152 "MISSING_FILE_FIELD",
153 "No file field was found in the upload request",
154 ),
155 AppError::PublicUrlNotConfigured => (
156 StatusCode::INTERNAL_SERVER_ERROR,
157 "PUBLIC_URL_NOT_CONFIGURED",
158 "Server is not properly configured, public URL is missing",
159 ),
160 AppError::InvalidIdFormat => (
161 StatusCode::BAD_REQUEST,
162 "INVALID_ID_FORMAT",
163 "File ID contains invalid characters",
164 ),
165
166 AppError::TusMissingLength => (
168 StatusCode::BAD_REQUEST,
169 "TUS_MISSING_LENGTH",
170 "Missing or invalid Upload-Length header",
171 ),
172 AppError::TusMissingOffset => (
173 StatusCode::BAD_REQUEST,
174 "TUS_MISSING_OFFSET",
175 "Missing or invalid Upload-Offset header",
176 ),
177 AppError::TusOffsetMismatch => (
178 StatusCode::CONFLICT,
179 "TUS_OFFSET_MISMATCH",
180 "Upload offset does not match server state",
181 ),
182 AppError::TusSessionNotFound => (
183 StatusCode::NOT_FOUND,
184 "TUS_SESSION_NOT_FOUND",
185 "TUS upload session not found — it may have expired or been cancelled",
186 ),
187 AppError::TusStagingError => (
188 StatusCode::INTERNAL_SERVER_ERROR,
189 "TUS_STAGING_ERROR",
190 "Failed to initialise TUS upload on server",
191 ),
192
193 AppError::PayloadTooLarge => (
195 StatusCode::PAYLOAD_TOO_LARGE,
196 "FILE_TOO_LARGE",
197 "File exceeds the maximum allowed size",
198 ),
199 AppError::RateLimited => (
200 StatusCode::TOO_MANY_REQUESTS,
201 "RATE_LIMITED",
202 "Too many uploads — please wait before trying again",
203 ),
204
205 AppError::JuicehostUnreachable(ref e) => {
207 tracing::error!("juicehost unreachable: {}", e);
208 (
209 StatusCode::BAD_GATEWAY,
210 "STORAGE_UNREACHABLE",
211 "File storage backend is unreachable — please try again later",
212 )
213 }
214 AppError::JuicehostRejected(ref e) => {
215 tracing::error!("juicehost rejected upload: {}", e);
216 let clean = e
218 .strip_prefix('[')
219 .and_then(|s| s.find("] "))
220 .map(|i| &e[i + 2..])
221 .and_then(|s| s.rsplit_once(" (status="))
222 .map(|(msg, _)| msg)
223 .unwrap_or(e.as_str());
224 (
225 StatusCode::BAD_GATEWAY,
226 "STORAGE_REJECTED",
227 clean,
228 )
229 }
230 AppError::InsufficientStorage(ref e) => {
231 tracing::warn!("juicehost out of storage: {}", e);
232 let clean = e
233 .strip_prefix('[')
234 .and_then(|s| s.find("] "))
235 .map(|i| &e[i + 2..])
236 .and_then(|s| s.rsplit_once(" (status="))
237 .map(|(msg, _)| msg)
238 .unwrap_or("This instance is out of storage! Try again later.");
239 (
240 StatusCode::SERVICE_UNAVAILABLE,
241 "INSUFFICIENT_STORAGE",
242 clean,
243 )
244 }
245 AppError::JuicehostDeleteFailed(ref e) => {
246 tracing::error!("juicehost delete failed: {}", e);
247 (
248 StatusCode::BAD_GATEWAY,
249 "STORAGE_DELETE_FAILED",
250 "Failed to remove file from storage backend",
251 )
252 }
253 AppError::JuicehostRenameFailed(ref e) => {
254 tracing::error!("juicehost rename failed: {}", e);
255 (
256 StatusCode::BAD_GATEWAY,
257 "STORAGE_RENAME_FAILED",
258 "Failed to rename file on storage backend",
259 )
260 }
261
262 AppError::FilesystemError(ref e) => {
264 tracing::error!("filesystem error: {}", e);
265 (
266 StatusCode::INTERNAL_SERVER_ERROR,
267 "DISK_ERROR",
268 "A server disk error occurred",
269 )
270 }
271 AppError::DatabaseError(ref e) => {
272 tracing::error!("database error: {}", e);
273 (
274 StatusCode::INTERNAL_SERVER_ERROR,
275 "DATABASE_ERROR",
276 "A database error occurred",
277 )
278 }
279 AppError::DbPoolError(ref e) => {
280 tracing::error!("database pool error: {}", e);
281 (
282 StatusCode::SERVICE_UNAVAILABLE,
283 "DB_POOL_ERROR",
284 "The server is temporarily unable to process uploads — please try again",
285 )
286 }
287 AppError::TaskPanicked(ref e) => {
288 tracing::error!("background task panicked: {}", e);
289 (
290 StatusCode::INTERNAL_SERVER_ERROR,
291 "TASK_PANICKED",
292 "An internal processing task failed unexpectedly",
293 )
294 }
295 AppError::BadRequest(ref msg) => (StatusCode::BAD_REQUEST, "BAD_REQUEST", msg.as_str()),
296 AppError::InvalidMultipart(ref msg) => {
297 (StatusCode::BAD_REQUEST, "INVALID_MULTIPART", msg.as_str())
298 }
299 AppError::Internal(ref msg) => {
300 tracing::error!("internal error: {}", msg);
301 (
302 StatusCode::INTERNAL_SERVER_ERROR,
303 "INTERNAL_ERROR",
304 msg.as_str(),
305 )
306 }
307 };
308
309 let body = Json(json!({
310 "error": error_code,
311 "message": message,
312 }));
313
314 (status, body).into_response()
315 }
316}
317
318impl From<std::io::Error> for AppError {
319 fn from(err: std::io::Error) -> Self {
320 AppError::FilesystemError(err)
321 }
322}
323
324impl From<rusqlite::Error> for AppError {
325 fn from(err: rusqlite::Error) -> Self {
326 AppError::DatabaseError(err)
327 }
328}