Initial Commit
This commit is contained in:
@@ -0,0 +1,185 @@
|
||||
use axum::{
|
||||
extract::{Multipart, Path, State},
|
||||
Json,
|
||||
};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use super::{AppError, AppState};
|
||||
|
||||
const GALLERY_DIR: &str = "/data/gallery";
|
||||
|
||||
pub async fn list_all_gallery(
|
||||
State(state): State<AppState>,
|
||||
) -> Result<Json<serde_json::Value>, AppError> {
|
||||
let rows = sqlx::query(
|
||||
r#"SELECT g.id, g.catalog_id, g.filename, g.caption, g.created_at,
|
||||
c.name AS target_name, c.common_name AS target_common_name
|
||||
FROM gallery g
|
||||
LEFT JOIN catalog c ON c.id = g.catalog_id
|
||||
ORDER BY g.created_at DESC"#,
|
||||
)
|
||||
.fetch_all(&state.pool)
|
||||
.await?;
|
||||
|
||||
let items: Vec<serde_json::Value> = rows.iter().map(|r| {
|
||||
use sqlx::Row;
|
||||
let id: i32 = r.try_get("id").unwrap_or_default();
|
||||
let catalog_id: String = r.try_get("catalog_id").unwrap_or_default();
|
||||
let filename: String = r.try_get("filename").unwrap_or_default();
|
||||
serde_json::json!({
|
||||
"id": id,
|
||||
"catalog_id": &catalog_id,
|
||||
"filename": &filename,
|
||||
"url": format!("/api/gallery/{}/{}", catalog_id, filename),
|
||||
"caption": r.try_get::<Option<String>, _>("caption").unwrap_or_default(),
|
||||
"created_at": r.try_get::<i64, _>("created_at").unwrap_or_default(),
|
||||
"target_name": r.try_get::<Option<String>, _>("target_name").unwrap_or_default(),
|
||||
"target_common_name": r.try_get::<Option<String>, _>("target_common_name").unwrap_or_default(),
|
||||
})
|
||||
}).collect();
|
||||
|
||||
Ok(Json(serde_json::json!({ "items": items })))
|
||||
}
|
||||
|
||||
pub async fn list_gallery(
|
||||
State(state): State<AppState>,
|
||||
Path(catalog_id): Path<String>,
|
||||
) -> Result<Json<serde_json::Value>, AppError> {
|
||||
let rows = sqlx::query(
|
||||
"SELECT * FROM gallery WHERE catalog_id = ? ORDER BY created_at DESC",
|
||||
)
|
||||
.bind(&catalog_id)
|
||||
.fetch_all(&state.pool)
|
||||
.await?;
|
||||
|
||||
let items: Vec<serde_json::Value> = rows.iter().map(|r| {
|
||||
use sqlx::Row;
|
||||
let id: i32 = r.try_get("id").unwrap_or_default();
|
||||
let filename: String = r.try_get("filename").unwrap_or_default();
|
||||
serde_json::json!({
|
||||
"id": id,
|
||||
"catalog_id": catalog_id,
|
||||
"filename": filename,
|
||||
"url": format!("/api/gallery/{}/{}", catalog_id, filename),
|
||||
"caption": r.try_get::<Option<String>, _>("caption").unwrap_or_default(),
|
||||
"created_at": r.try_get::<i64, _>("created_at").unwrap_or_default(),
|
||||
})
|
||||
}).collect();
|
||||
|
||||
Ok(Json(serde_json::json!({ "items": items })))
|
||||
}
|
||||
|
||||
pub async fn upload_image(
|
||||
State(state): State<AppState>,
|
||||
Path(catalog_id): Path<String>,
|
||||
mut multipart: Multipart,
|
||||
) -> Result<Json<serde_json::Value>, AppError> {
|
||||
let mut image_bytes: Option<Vec<u8>> = None;
|
||||
let mut orig_filename = String::from("image.jpg");
|
||||
let mut caption: Option<String> = None;
|
||||
let mut log_id: Option<i32> = None;
|
||||
|
||||
while let Some(field) = multipart.next_field().await.map_err(|e| AppError::Internal(e.to_string()))? {
|
||||
let name = field.name().unwrap_or("").to_string();
|
||||
match name.as_str() {
|
||||
"file" => {
|
||||
orig_filename = field.file_name().unwrap_or("image.jpg").to_string();
|
||||
let bytes = field.bytes().await.map_err(|e| AppError::Internal(e.to_string()))?;
|
||||
if bytes.len() > 50 * 1024 * 1024 {
|
||||
return Err(AppError::BadRequest("File exceeds 50MB limit".to_string()));
|
||||
}
|
||||
image_bytes = Some(bytes.to_vec());
|
||||
}
|
||||
"caption" => {
|
||||
caption = Some(field.text().await.map_err(|e| AppError::Internal(e.to_string()))?);
|
||||
}
|
||||
"log_id" => {
|
||||
log_id = field.text().await.ok().and_then(|s| s.parse().ok());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
let bytes = image_bytes.ok_or_else(|| AppError::BadRequest("No file uploaded".to_string()))?;
|
||||
|
||||
// Convert TIFF to JPEG if needed, else store as-is
|
||||
let ext = std::path::Path::new(&orig_filename)
|
||||
.extension()
|
||||
.and_then(|e| e.to_str())
|
||||
.unwrap_or("jpg")
|
||||
.to_lowercase();
|
||||
|
||||
let (final_bytes, final_ext) = if ext == "tiff" || ext == "tif" {
|
||||
match convert_tiff_to_jpeg(&bytes) {
|
||||
Ok(jpeg) => (jpeg, "jpg".to_string()),
|
||||
Err(_) => (bytes, ext),
|
||||
}
|
||||
} else {
|
||||
(bytes, ext)
|
||||
};
|
||||
|
||||
// Generate unique filename
|
||||
let uid = uuid::Uuid::new_v4();
|
||||
let filename = format!("{}.{}", uid, final_ext);
|
||||
|
||||
// Ensure directory exists
|
||||
let dir = PathBuf::from(GALLERY_DIR).join(&catalog_id);
|
||||
tokio::fs::create_dir_all(&dir)
|
||||
.await
|
||||
.map_err(|e| AppError::Internal(format!("Failed to create gallery dir: {}", e)))?;
|
||||
|
||||
let file_path = dir.join(&filename);
|
||||
tokio::fs::write(&file_path, &final_bytes)
|
||||
.await
|
||||
.map_err(|e| AppError::Internal(format!("Failed to write image: {}", e)))?;
|
||||
|
||||
let id: i64 = sqlx::query_scalar(
|
||||
"INSERT INTO gallery (catalog_id, log_id, filename, caption) VALUES (?, ?, ?, ?) RETURNING id",
|
||||
)
|
||||
.bind(&catalog_id)
|
||||
.bind(log_id)
|
||||
.bind(&filename)
|
||||
.bind(&caption)
|
||||
.fetch_one(&state.pool)
|
||||
.await?;
|
||||
|
||||
Ok(Json(serde_json::json!({
|
||||
"id": id,
|
||||
"catalog_id": catalog_id,
|
||||
"filename": filename,
|
||||
"url": format!("/api/gallery/{}/{}", catalog_id, filename),
|
||||
})))
|
||||
}
|
||||
|
||||
pub async fn delete_image(
|
||||
State(state): State<AppState>,
|
||||
Path(id): Path<i32>,
|
||||
) -> Result<Json<serde_json::Value>, AppError> {
|
||||
let row = sqlx::query("SELECT catalog_id, filename FROM gallery WHERE id = ?")
|
||||
.bind(id)
|
||||
.fetch_optional(&state.pool)
|
||||
.await?
|
||||
.ok_or_else(|| AppError::NotFound(format!("Gallery image {} not found", id)))?;
|
||||
|
||||
use sqlx::Row;
|
||||
let catalog_id: String = row.try_get("catalog_id").unwrap_or_default();
|
||||
let filename: String = row.try_get("filename").unwrap_or_default();
|
||||
|
||||
let file_path = PathBuf::from(GALLERY_DIR).join(&catalog_id).join(&filename);
|
||||
let _ = tokio::fs::remove_file(&file_path).await;
|
||||
|
||||
sqlx::query("DELETE FROM gallery WHERE id = ?")
|
||||
.bind(id)
|
||||
.execute(&state.pool)
|
||||
.await?;
|
||||
|
||||
Ok(Json(serde_json::json!({ "id": id, "status": "deleted" })))
|
||||
}
|
||||
|
||||
fn convert_tiff_to_jpeg(bytes: &[u8]) -> anyhow::Result<Vec<u8>> {
|
||||
let img = image::load_from_memory(bytes)?;
|
||||
let mut output = Vec::new();
|
||||
let mut cursor = std::io::Cursor::new(&mut output);
|
||||
img.write_to(&mut cursor, image::ImageOutputFormat::Jpeg(90))?;
|
||||
Ok(output)
|
||||
}
|
||||
Reference in New Issue
Block a user