use actix_multipart::Multipart; use actix_web::{HttpResponse, web}; use serde::Deserialize; use utoipa::IntoParams; use crate::api::response::{ApiErrorResponse, ApiResponse}; use crate::api::user::upload_avatar::parse_avatar_field; use crate::error::AppError; use crate::service::AppService; use crate::session::Session; #[derive(Debug, Deserialize, IntoParams)] pub struct PathParams { pub workspace_name: String, pub repo_name: String, pub release_id: uuid::Uuid, } #[derive(Debug, Deserialize, IntoParams)] pub struct AssetPathParams { pub workspace_name: String, pub repo_name: String, pub release_id: uuid::Uuid, pub asset_id: uuid::Uuid, } #[derive(Debug, Deserialize, IntoParams)] pub struct ListQueryParams { pub limit: Option, pub offset: Option, } #[utoipa::path( post, path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/releases/{release_id}/assets", tag = "Repos", operation_id = "repoUploadReleaseAsset", params(PathParams), request_body(content_type = "multipart/form-data"), responses( (status = 201, description = "Asset uploaded", body = ApiResponse), (status = 401, description = "Unauthorized", body = ApiErrorResponse), (status = 404, description = "Release not found", body = ApiErrorResponse), ), security(("session_cookie" = [])) )] pub async fn upload_asset( service: web::Data, session: Session, path: web::Path, payload: Multipart, ) -> Result { let (data, content_type, file_name) = parse_avatar_field(payload).await?; let filename = file_name.unwrap_or_else(|| "asset.bin".to_string()); let result = service .repo .repo_upload_release_asset( &session, &path.workspace_name, &path.repo_name, path.release_id, &filename, data, content_type .as_deref() .unwrap_or("application/octet-stream"), ) .await?; Ok(HttpResponse::Created().json(ApiResponse::new(result))) } #[utoipa::path( get, path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/releases/{release_id}/assets", tag = "Repos", operation_id = "repoListReleaseAssets", params(PathParams, ListQueryParams), responses( (status = 200, description = "List of release assets", body = ApiResponse>), (status = 401, description = "Unauthorized", body = ApiErrorResponse), ), security(("session_cookie" = [])) )] pub async fn list_assets( service: web::Data, session: Session, path: web::Path, query: web::Query, ) -> Result { let result = service .repo .repo_list_release_assets( &session, &path.workspace_name, &path.repo_name, path.release_id, query.limit.unwrap_or(50), query.offset.unwrap_or(0), ) .await?; Ok(HttpResponse::Ok().json(ApiResponse::new(result))) } #[utoipa::path( delete, path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/releases/{release_id}/assets/{asset_id}", tag = "Repos", operation_id = "repoDeleteReleaseAsset", params(AssetPathParams), responses( (status = 200, description = "Asset deleted", body = ApiResponse), (status = 401, description = "Unauthorized", body = ApiErrorResponse), (status = 404, description = "Not found", body = ApiErrorResponse), ), security(("session_cookie" = [])) )] pub async fn delete_asset( service: web::Data, session: Session, path: web::Path, ) -> Result { service .repo .repo_delete_release_asset( &session, &path.workspace_name, &path.repo_name, path.release_id, path.asset_id, ) .await?; Ok(HttpResponse::Ok().json(ApiResponse::new("asset deleted".to_string()))) } #[utoipa::path( get, path = "/api/v1/workspaces/{workspace_name}/repos/{repo_name}/releases/{release_id}/assets/{asset_id}/download", tag = "Repos", operation_id = "repoDownloadReleaseAsset", params(AssetPathParams), responses( (status = 302, description = "Redirect to download URL"), (status = 404, description = "Not found", body = ApiErrorResponse), ), security(("session_cookie" = [])) )] pub async fn download_asset( service: web::Data, session: Session, path: web::Path, ) -> Result { let url = service .repo .repo_get_release_asset_download_url( &session, &path.workspace_name, &path.repo_name, path.release_id, path.asset_id, ) .await?; Ok(HttpResponse::Found() .insert_header(("Location", url)) .finish()) }