From 916a54caa2aeb0691ad1066ee4168795bb75596e Mon Sep 17 00:00:00 2001 From: Benjamin Slingo Date: Sat, 23 Aug 2025 11:29:44 -0400 Subject: [PATCH] Add missing event thumbnail upload endpoint - Fixed 502 error when uploading thumbnails via admin panel - Added /api/upload/events/:id/thumbnail route and handler - Thumbnails saved with thumb_ prefix for organization - Includes proper file cleanup when replacing existing thumbnails - Resolves CORS issues by ensuring endpoint exists for proper middleware handling --- src/upload.rs | 80 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/src/upload.rs b/src/upload.rs index 2f3adab..08fd9b0 100644 --- a/src/upload.rs +++ b/src/upload.rs @@ -17,6 +17,7 @@ pub fn routes() -> Router { .route("/bulletins/:id/pdf", post(upload_bulletin_pdf)) .route("/bulletins/:id/cover", post(upload_bulletin_cover)) .route("/events/:id/image", post(upload_event_image)) + .route("/events/:id/thumbnail", post(upload_event_thumbnail)) .route("/pending_events/:id/image", post(upload_pending_event_image)) .nest_service("/files", tower_http::services::ServeDir::new("uploads")) .layer(DefaultBodyLimit::max(50 * 1024 * 1024)) @@ -245,3 +246,82 @@ async fn upload_pending_event_image( Err(ApiError::ValidationError("No file found in request".to_string())) } + +async fn upload_event_thumbnail( + Path(id): Path, + State(state): State, + mut multipart: Multipart, +) -> Result> { + while let Some(field) = multipart.next_field().await.map_err(|_| ApiError::ValidationError("Invalid multipart".to_string()))? { + if field.name() == Some("file") { + let filename = field.file_name() + .ok_or_else(|| ApiError::ValidationError("No filename provided".to_string()))? + .to_string(); + + let ext = filename.split('.').last().unwrap_or("jpg").to_lowercase(); + if !["jpg", "jpeg", "png", "webp", "gif"].contains(&ext.as_str()) { + return Err(ApiError::ValidationError("Only image files allowed".to_string())); + } + + // Get current event to check for existing thumbnail + let current_event = sqlx::query!( + "SELECT thumbnail FROM events WHERE id = $1", + id + ) + .fetch_optional(&state.pool) + .await + .map_err(|_| ApiError::ValidationError("Failed to fetch event".to_string()))? + .ok_or_else(|| ApiError::NotFound("Event not found".to_string()))?; + + let file_id = Uuid::new_v4(); + let file_path = format!("uploads/events/thumb_{}.{}", file_id, ext); + let full_path = PathBuf::from(&file_path); + + if let Some(parent) = full_path.parent() { + tokio::fs::create_dir_all(parent).await.map_err(|_| ApiError::ValidationError("Failed to create directory".to_string()))?; + } + + let data = field.bytes().await.map_err(|_| ApiError::ValidationError("Failed to read file".to_string()))?; + let mut file = File::create(&full_path).await.map_err(|_| ApiError::ValidationError("Failed to create file".to_string()))?; + file.write_all(&data).await.map_err(|_| ApiError::ValidationError("Failed to write file".to_string()))?; + + // Update event record with full URL + let url_builder = UrlBuilder::new(); + let thumbnail_url = url_builder.build_upload_url(&format!("events/thumb_{}.{}", file_id, ext)); + + sqlx::query!( + "UPDATE events SET thumbnail = $1, updated_at = NOW() WHERE id = $2", + thumbnail_url, + id + ) + .execute(&state.pool) + .await + .map_err(|_| ApiError::ValidationError("Failed to update event record".to_string()))?; + + // Delete old thumbnail file if it exists + if let Some(old_thumbnail_url) = current_event.thumbnail { + // Extract file path from URL (remove base URL if present) + if let Some(relative_path) = old_thumbnail_url.strip_prefix("https://").and_then(|s| s.split_once('/')).map(|(_, path)| path) + .or_else(|| old_thumbnail_url.strip_prefix("http://").and_then(|s| s.split_once('/')).map(|(_, path)| path)) + .or_else(|| Some(old_thumbnail_url.as_str())) { + + let old_file_path = PathBuf::from(relative_path); + if old_file_path.exists() { + if let Err(_) = tokio::fs::remove_file(&old_file_path).await { + tracing::warn!("Failed to delete old event thumbnail: {:?}", old_file_path); + } + } + } + } + + return Ok(Json(json!({ + "success": true, + "file_path": file_path, + "thumbnail_url": thumbnail_url, + "message": "Event thumbnail uploaded successfully" + }))); + } + } + + Err(ApiError::ValidationError("No file found in request".to_string())) +}