api/routes/modules/assignments/files/
post.rs1use axum::{
2 extract::{State, Multipart, Path},
3 http::StatusCode,
4 response::IntoResponse,
5 Json,
6};
7use serde::Serialize;
8use db::models::assignment_file::{
9 FileType,
10 Model as FileModel,
11};
12use util::state::AppState;
13use crate::response::ApiResponse;
14
15#[derive(Debug, Serialize)]
16pub struct UploadedFileMetadata {
17 pub id: i64,
18 pub assignment_id: i64,
19 pub filename: String,
20 pub path: String,
21 pub created_at: String,
22 pub updated_at: String,
23}
24
25#[derive(Debug, Serialize)]
26pub struct AssignmentSubmissionMetadata {
27 pub id: i64,
28 pub assignment_id: i64,
29 pub user_id: i64,
30 pub filename: String,
31 pub path: String,
32 pub created_at: String,
33 pub updated_at: String,
34}
35
36pub async fn upload_files(
91 State(app_state): State<AppState>,
92 Path((module_id, assignment_id)): Path<(i64, i64)>,
93 mut multipart: Multipart,
94) -> impl IntoResponse {
95 let db = app_state.db();
96
97 let mut file_type: Option<FileType> = None;
98 let mut file_name: Option<String> = None;
99 let mut file_bytes: Option<Vec<u8>> = None;
100 let mut file_count = 0;
101
102 while let Some(field) = multipart.next_field().await.unwrap_or(None) {
103 let name = field.name().unwrap_or("");
104
105 match name {
106 "file_type" => {
107 if let Ok(ftype_str) = field.text().await {
108 match ftype_str.parse::<FileType>() {
109 Ok(ftype) => file_type = Some(ftype),
110 Err(_) => {
111 return (
112 StatusCode::BAD_REQUEST,
113 Json(ApiResponse::<UploadedFileMetadata>::error(
114 "Invalid file_type",
115 )),
116 )
117 .into_response();
118 }
119 }
120 }
121 }
122 "file" => {
123 if file_count > 0 {
124 return (
125 StatusCode::BAD_REQUEST,
126 Json(ApiResponse::<UploadedFileMetadata>::error(
127 "Only one file may be uploaded per request",
128 )),
129 )
130 .into_response();
131 }
132 file_name = field.file_name().map(|s| s.to_string());
133 file_bytes = Some(field.bytes().await.unwrap_or_default().to_vec());
134 file_count += 1;
135 }
136 _ => continue,
137 }
138 }
139
140 let file_type = match file_type {
141 Some(ft) => ft,
142 None => {
143 return (
144 StatusCode::BAD_REQUEST,
145 Json(ApiResponse::<UploadedFileMetadata>::error(
146 "Missing required field: file_type",
147 )),
148 )
149 .into_response();
150 }
151 };
152
153 let file_name = match file_name {
154 Some(name) => name,
155 None => {
156 return (
157 StatusCode::BAD_REQUEST,
158 Json(ApiResponse::<UploadedFileMetadata>::error(
159 "Missing file upload",
160 )),
161 )
162 .into_response();
163 }
164 };
165
166 let file_bytes = match file_bytes {
167 Some(bytes) if !bytes.is_empty() => bytes,
168 _ => {
169 return (
170 StatusCode::BAD_REQUEST,
171 Json(ApiResponse::<UploadedFileMetadata>::error(
172 "Empty file provided",
173 )),
174 )
175 .into_response();
176 }
177 };
178
179 match FileModel::save_file(
180 db,
181 assignment_id,
182 module_id,
183 file_type.clone(),
184 &file_name,
185 &file_bytes,
186 )
187 .await
188 {
189 Ok(saved) => {
190 let metadata = UploadedFileMetadata {
191 id: saved.id,
192 assignment_id: saved.assignment_id,
193 filename: saved.filename,
194 path: saved.path,
195 created_at: saved.created_at.to_rfc3339(),
196 updated_at: saved.updated_at.to_rfc3339(),
197 };
198 (
199 StatusCode::CREATED,
200 Json(ApiResponse::success(metadata, "File uploaded successfully")),
201 )
202 .into_response()
203 }
204 Err(e) => {
205 eprintln!("File save error: {:?}", e);
206 (
207 StatusCode::INTERNAL_SERVER_ERROR,
208 Json(ApiResponse::<UploadedFileMetadata>::error(
209 "Failed to save file",
210 )),
211 )
212 .into_response()
213 }
214 }
215}