use axum::extract::{Multipart, State};
use axum::response::{IntoResponse, Response};
use serde::{Deserialize, Serialize};
use std::io::Cursor;
use std::path::PathBuf;
use uuid::Uuid;
use common::prelude::{Link, MountError};
use crate::ServiceState;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UpdateResponse {
pub mount_path: String,
pub link: Link,
pub mime_type: String,
}
pub async fn handler(
State(state): State<ServiceState>,
mut multipart: Multipart,
) -> Result<impl IntoResponse, UpdateError> {
let mut bucket_id: Option<Uuid> = None;
let mut mount_path: Option<String> = None;
let mut file_data: Option<Vec<u8>> = None;
while let Some(field) = multipart
.next_field()
.await
.map_err(|e| UpdateError::MultipartError(e.to_string()))?
{
let field_name = field.name().unwrap_or("").to_string();
match field_name.as_str() {
"bucket_id" => {
let text = field
.text()
.await
.map_err(|e| UpdateError::MultipartError(e.to_string()))?;
bucket_id = Some(
Uuid::parse_str(&text)
.map_err(|_| UpdateError::InvalidRequest("Invalid bucket_id".into()))?,
);
}
"mount_path" => {
mount_path = Some(
field
.text()
.await
.map_err(|e| UpdateError::MultipartError(e.to_string()))?,
);
}
"file" => {
file_data = Some(
field
.bytes()
.await
.map_err(|e| UpdateError::MultipartError(e.to_string()))?
.to_vec(),
);
}
_ => {}
}
}
let bucket_id =
bucket_id.ok_or_else(|| UpdateError::InvalidRequest("bucket_id is required".into()))?;
let mount_path =
mount_path.ok_or_else(|| UpdateError::InvalidRequest("mount_path is required".into()))?;
let file_data =
file_data.ok_or_else(|| UpdateError::InvalidRequest("file is required".into()))?;
let mount_path_buf = PathBuf::from(&mount_path);
if !mount_path_buf.is_absolute() {
return Err(UpdateError::InvalidPath(
"Mount path must be absolute".into(),
));
}
tracing::info!(
"UPDATE API: Updating file in bucket {} at {}",
bucket_id,
mount_path
);
let mime_type = mime_guess::from_path(&mount_path_buf)
.first_or_octet_stream()
.to_string();
let mut mount = state.peer().mount(bucket_id).await?;
let file_exists = mount.get(&mount_path_buf).await.is_ok();
if file_exists {
tracing::info!("UPDATE API: Removing existing file at {}", mount_path);
mount.rm(&mount_path_buf).await.map_err(|e| {
tracing::error!("UPDATE API: Failed to remove existing file: {}", e);
UpdateError::Mount(e)
})?;
} else {
tracing::info!("UPDATE API: File doesn't exist, will create new");
}
let reader = Cursor::new(file_data);
mount.add(&mount_path_buf, reader).await?;
tracing::info!("UPDATE API: Added new content to {}", mount_path);
let new_bucket_link = state.peer().save_mount(&mount, None).await?;
tracing::info!(
"UPDATE API: Updated {} in bucket {}, new link: {}",
mount_path,
bucket_id,
new_bucket_link.hash()
);
Ok((
http::StatusCode::OK,
axum::Json(UpdateResponse {
mount_path,
link: new_bucket_link,
mime_type,
}),
)
.into_response())
}
#[derive(Debug, thiserror::Error)]
pub enum UpdateError {
#[error("Invalid path: {0}")]
InvalidPath(String),
#[error("Invalid request: {0}")]
InvalidRequest(String),
#[error("Multipart error: {0}")]
MultipartError(String),
#[error("Mount error: {0}")]
Mount(#[from] MountError),
}
impl IntoResponse for UpdateError {
fn into_response(self) -> Response {
match self {
UpdateError::InvalidPath(msg)
| UpdateError::InvalidRequest(msg)
| UpdateError::MultipartError(msg) => (
http::StatusCode::BAD_REQUEST,
format!("Bad request: {}", msg),
)
.into_response(),
UpdateError::Mount(_) => (
http::StatusCode::INTERNAL_SERVER_ERROR,
"Unexpected error".to_string(),
)
.into_response(),
}
}
}