api/routes/modules/assignments/files/
post.rs

1use 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
36/// POST /api/modules/{module_id}/assignments/{assignment_id}/files
37///
38/// Upload a single file to an assignment. Only accessible by lecturers assigned to the module.
39///
40/// ### Path Parameters
41/// - `module_id` (i64): The ID of the module containing the assignment
42/// - `assignment_id` (i64): The ID of the assignment to upload the file to
43///
44/// ### Request Body (Multipart Form Data)
45/// - `file_type` (string, required): The type of file. Must be one of: `spec`, `main`, `memo`, etc.
46/// - `file` (file, required): The file to upload. Only one file per request is allowed.
47///
48/// ### Responses
49///
50/// - `201 Created`
51/// ```json
52/// {
53///   "success": true,
54///   "message": "File uploaded successfully",
55///   "data": {
56///     "id": 123,
57///     "assignment_id": 456,
58///     "filename": "assignment.pdf",
59///     "path": "module_456/assignment_789/assignment.pdf",
60///     "created_at": "2024-01-01T00:00:00Z",
61///     "updated_at": "2024-01-01T00:00:00Z"
62///   }
63/// }
64/// ```
65///
66/// - `400 Bad Request`
67/// ```json
68/// {
69///   "success": false,
70///   "message": "Invalid file_type" // or "Missing required field: file_type" or "Missing file upload" or "Empty file provided" or "Only one file may be uploaded per request"
71/// }
72/// ```
73///
74/// - `404 Not Found`
75/// ```json
76/// {
77///   "success": false,
78///   "message": "Assignment not found"
79/// }
80/// ```
81///
82/// - `500 Internal Server Error`
83/// ```json
84/// {
85///   "success": false,
86///   "message": "Database error" // or "Failed to save file"
87/// }
88/// ```
89///
90pub 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}