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

1use crate::response::ApiResponse;
2use axum::{
3    Json,
4    extract::{Multipart, Path, State},
5    http::StatusCode,
6    response::IntoResponse,
7};
8use db::models::assignment_interpreter::Model as InterpreterModel;
9use serde::Serialize;
10use util::state::AppState;
11
12#[derive(Debug, Serialize)]
13pub struct UploadedInterpreterMetadata {
14    pub id: i64,
15    pub assignment_id: i64,
16    pub filename: String,
17    pub path: String,
18    pub command: String,
19    pub created_at: String,
20    pub updated_at: String,
21}
22
23/// POST /api/modules/{module_id}/assignments/{assignment_id}/interpreter
24///
25/// Upload an interpreter file for an assignment. Only one interpreter may exist per assignment.
26/// Existing interpreters with the same filename will be overwritten.
27///
28/// ### Path Parameters
29/// - `module_id` (i64): The ID of the module containing the assignment
30/// - `assignment_id` (i64): The ID of the assignment to upload the interpreter for
31///
32/// ### Request Body (Multipart Form Data)
33/// - `command` (string, required): The command to execute the interpreter (e.g., "python3 main.py")
34/// - `file` (file, required): The interpreter file to upload
35///
36/// ### Responses
37/// - `201 Created` → success with metadata
38/// - `400 Bad Request` → missing command/file, empty file, multiple files, etc.
39/// - `500 Internal Server Error` → database or file write errors
40///
41pub async fn upload_interpreter(
42    State(app_state): State<AppState>,
43    Path((module_id, assignment_id)): Path<(i64, i64)>,
44    mut multipart: Multipart,
45) -> impl IntoResponse {
46    let db = app_state.db();
47
48    let mut command: Option<String> = None;
49    let mut file_name: Option<String> = None;
50    let mut file_bytes: Option<Vec<u8>> = None;
51    let mut file_count = 0;
52
53    while let Some(field) = multipart.next_field().await.unwrap_or(None) {
54        let name = field.name().unwrap_or("");
55
56        match name {
57            "command" => {
58                if let Ok(cmd) = field.text().await {
59                    command = Some(cmd);
60                }
61            }
62            "file" => {
63                if file_count > 0 {
64                    return (
65                        StatusCode::BAD_REQUEST,
66                        Json(ApiResponse::<UploadedInterpreterMetadata>::error(
67                            "Only one file may be uploaded per request",
68                        )),
69                    )
70                        .into_response();
71                }
72                file_name = field.file_name().map(|s| s.to_string());
73                file_bytes = Some(field.bytes().await.unwrap_or_default().to_vec());
74                file_count += 1;
75            }
76            _ => continue,
77        }
78    }
79
80    let command = match command {
81        Some(c) if !c.trim().is_empty() => c,
82        _ => {
83            return (
84                StatusCode::BAD_REQUEST,
85                Json(ApiResponse::<UploadedInterpreterMetadata>::error(
86                    "Missing required field: command",
87                )),
88            )
89                .into_response();
90        }
91    };
92
93    let file_name = match file_name {
94        Some(name) => name,
95        None => {
96            return (
97                StatusCode::BAD_REQUEST,
98                Json(ApiResponse::<UploadedInterpreterMetadata>::error(
99                    "Missing file upload",
100                )),
101            )
102                .into_response();
103        }
104    };
105
106    let file_bytes = match file_bytes {
107        Some(bytes) if !bytes.is_empty() => bytes,
108        _ => {
109            return (
110                StatusCode::BAD_REQUEST,
111                Json(ApiResponse::<UploadedInterpreterMetadata>::error(
112                    "Empty file provided",
113                )),
114            )
115                .into_response();
116        }
117    };
118
119    match InterpreterModel::save_file(
120        db,
121        assignment_id,
122        module_id,
123        &file_name,
124        &command,
125        &file_bytes,
126    )
127    .await
128    {
129        Ok(saved) => {
130            let metadata = UploadedInterpreterMetadata {
131                id: saved.id,
132                assignment_id: saved.assignment_id,
133                filename: saved.filename,
134                path: saved.path,
135                command: saved.command,
136                created_at: saved.created_at.to_rfc3339(),
137                updated_at: saved.updated_at.to_rfc3339(),
138            };
139            (
140                StatusCode::CREATED,
141                Json(ApiResponse::success(
142                    metadata,
143                    "Interpreter uploaded successfully",
144                )),
145            )
146                .into_response()
147        }
148        Err(e) => {
149            eprintln!("Interpreter save error: {:?}", e);
150            return (
151                StatusCode::INTERNAL_SERVER_ERROR,
152                Json(ApiResponse::<UploadedInterpreterMetadata>::error(
153                    "Failed to save interpreter",
154                )),
155            )
156                .into_response();
157        }
158    }
159}