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
This commit is contained in:
Benjamin Slingo 2025-08-23 11:29:44 -04:00
parent 17aeb7d55e
commit 916a54caa2

View file

@ -17,6 +17,7 @@ pub fn routes() -> Router<crate::AppState> {
.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<Uuid>,
State(state): State<crate::AppState>,
mut multipart: Multipart,
) -> Result<Json<Value>> {
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()))
}