main
use ::session::AuthedUser;
use api::http::{Json, Path, Query, Response, State};
use api::rest::{AppResponseData, NoData};
use imaging::layout::ImageVariant;
use serde::Deserialize;
use utoipa::{IntoParams, ToSchema};
use uuid::Uuid;
use crate::errors::ApiError;
use crate::services::attachments::{
AttachmentDto, CreateAttachmentCmd, FinalizeAttachmentCmd, UploadIntentDto,
};
use crate::services::service::ChatService;
#[utoipa::path(
post,
path = "/chat/attachments",
tag = "chat",
description = "为一次附件上传预留记录并返回预签名 PUT 地址。",
request_body = CreateAttachmentCmd,
responses(
(status = 200, description = "upload intent", body = AppResponseData<UploadIntentDto>),
(status = 400, description = "bad request", body = AppResponseData<NoData>),
(status = 429, description = "too many pending attachments", body = AppResponseData<NoData>),
(status = 401, description = "authentication required", body = AppResponseData<NoData>)
)
)]
pub async fn create_attachment(
State(service): State<ChatService>,
AuthedUser(uid): AuthedUser,
Json(cmd): Json<CreateAttachmentCmd>,
) -> Result<AppResponseData<UploadIntentDto>, ApiError> {
let intent = service.create_attachment(uid, cmd).await?;
Ok(AppResponseData::ok(intent))
}
#[utoipa::path(
post,
path = "/chat/attachments/{attachment_id}/finalize",
tag = "chat",
description = "确认对象已上传,校验大小与类型,并生成压缩预览图(失败时回退原图)。",
params(("attachment_id" = Uuid, Path, description = "attachment id")),
request_body = FinalizeAttachmentCmd,
responses(
(status = 200, description = "attachment finalized", body = AppResponseData<AttachmentDto>),
(status = 400, description = "bad request", body = AppResponseData<NoData>),
(status = 404, description = "not found", body = AppResponseData<NoData>),
(status = 401, description = "authentication required", body = AppResponseData<NoData>)
)
)]
pub async fn finalize_attachment(
State(service): State<ChatService>,
AuthedUser(uid): AuthedUser,
Path(attachment_id): Path<Uuid>,
Json(cmd): Json<FinalizeAttachmentCmd>,
) -> Result<AppResponseData<AttachmentDto>, ApiError> {
let dto = service.finalize_attachment(uid, attachment_id, cmd).await?;
Ok(AppResponseData::ok(dto))
}
#[utoipa::path(
get,
path = "/chat/attachments/{attachment_id}",
tag = "chat",
description = "读取附件元数据。",
params(("attachment_id" = Uuid, Path, description = "attachment id")),
responses(
(status = 200, description = "attachment metadata", body = AppResponseData<AttachmentDto>),
(status = 404, description = "not found", body = AppResponseData<NoData>),
(status = 401, description = "authentication required", body = AppResponseData<NoData>)
)
)]
pub async fn get_attachment(
State(service): State<ChatService>,
AuthedUser(uid): AuthedUser,
Path(attachment_id): Path<Uuid>,
) -> Result<AppResponseData<AttachmentDto>, ApiError> {
let dto = service.get_attachment_meta(uid, attachment_id).await?;
Ok(AppResponseData::ok(dto))
}
/// `?variant=` selector. Omitting it keeps the historical meaning of this route
/// (the original bytes, which is what a download link must hand back); the
/// transcript grid asks for `preview` explicitly.
#[derive(Debug, Clone, Default, Deserialize, IntoParams, ToSchema)]
pub struct AttachmentVariantQuery {
#[serde(default)]
pub variant: Option<ImageVariant>,
}
#[utoipa::path(
get,
path = "/chat/attachments/{attachment_id}/file",
tag = "chat",
description = "以安全响应头流式下载聊天附件。默认返回上传原图(下载语义不变),`?variant=preview` 返回压缩预览图;没有可用预览图时同样返回原图。",
params(("attachment_id" = Uuid, Path, description = "attachment id"), AttachmentVariantQuery),
responses(
(status = 200, description = "file stream"),
(status = 404, description = "not found", body = AppResponseData<NoData>),
(status = 401, description = "authentication required", body = AppResponseData<NoData>)
)
)]
pub async fn download_attachment(
State(service): State<ChatService>,
AuthedUser(uid): AuthedUser,
Path(attachment_id): Path<Uuid>,
Query(query): Query<AttachmentVariantQuery>,
) -> Result<Response, ApiError> {
let (row, body, content_type) = service
.download_attachment(
uid,
attachment_id,
query.variant.unwrap_or(ImageVariant::Original),
)
.await?;
let mut response = actix_web::HttpResponse::Ok().streaming(body);
let headers = response.headers_mut();
for (name, value) in ChatService::attachment_content_headers(&row, content_type) {
if let Ok(value) = actix_web::http::header::HeaderValue::from_str(&value) {
headers.insert(name, value);
}
}
// Attachments are immutable once finalized, so a preview may be cached
// privately; the original stays uncached to keep download semantics exactly
// as they were.
if query.variant == Some(ImageVariant::Preview) {
headers.insert(
actix_web::http::header::CACHE_CONTROL,
"private, max-age=300".parse().expect("static header value"),
);
}
Ok(response)
}