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

1use crate::response::ApiResponse;
2use axum::{
3    extract::{Path, State},
4    http::{HeaderMap, HeaderValue, StatusCode, header},
5    response::{IntoResponse, Json, Response},
6};
7use db::models::assignment_interpreter::{
8    Column as InterpreterColumn, Entity as InterpreterEntity,
9};
10use sea_orm::{ColumnTrait, EntityTrait, QueryFilter};
11use serde::Serialize;
12use std::{env, path::PathBuf};
13use tokio::{fs::File as FsFile, io::AsyncReadExt};
14use util::state::AppState;
15
16/// GET /api/modules/{module_id}/assignments/{assignment_id}/interpreter
17///
18/// Download the interpreter file for an assignment. Only one interpreter may exist per assignment.
19///
20/// ### Path Parameters
21/// - `module_id` (i64): The ID of the module containing the assignment
22/// - `assignment_id` (i64): The ID of the assignment containing the interpreter
23///
24/// ### Responses
25/// - `200 OK`: Returns the interpreter as a binary attachment
26/// - `404 Not Found`: If no interpreter exists for the assignment, or if the file is missing on disk
27/// - `500 Internal Server Error`: If DB or file read fails
28///
29pub async fn download_interpreter(
30    State(app_state): State<AppState>,
31    Path((_module_id, assignment_id)): Path<(i64, i64)>,
32) -> Response {
33    let db = app_state.db();
34
35    // There should be at most one interpreter per assignment
36    let interpreter = match InterpreterEntity::find()
37        .filter(InterpreterColumn::AssignmentId.eq(assignment_id))
38        .one(db)
39        .await
40    {
41        Ok(Some(interpreter)) => interpreter,
42        Ok(None) => {
43            return (
44                StatusCode::NOT_FOUND,
45                Json(ApiResponse::<()>::error("Interpreter not found")),
46            )
47                .into_response();
48        }
49        Err(err) => {
50            eprintln!("DB error fetching interpreter: {:?}", err);
51            return (
52                StatusCode::INTERNAL_SERVER_ERROR,
53                Json(ApiResponse::<()>::error("Database error")),
54            )
55                .into_response();
56        }
57    };
58
59    let storage_root =
60        env::var("ASSIGNMENT_STORAGE_ROOT").unwrap_or_else(|_| "data/interpreters".to_string());
61    let fs_path = PathBuf::from(storage_root).join(&interpreter.path);
62
63    if tokio::fs::metadata(&fs_path).await.is_err() {
64        return (
65            StatusCode::NOT_FOUND,
66            Json(ApiResponse::<()>::error("Interpreter file missing on disk")),
67        )
68            .into_response();
69    }
70
71    let mut file_handle = match FsFile::open(&fs_path).await {
72        Ok(f) => f,
73        Err(err) => {
74            eprintln!("File open error: {:?}", err);
75            return (
76                StatusCode::INTERNAL_SERVER_ERROR,
77                Json(ApiResponse::<()>::error("Could not open interpreter file")),
78            )
79                .into_response();
80        }
81    };
82
83    let mut buffer = Vec::new();
84    if let Err(err) = file_handle.read_to_end(&mut buffer).await {
85        eprintln!("File read error: {:?}", err);
86        return (
87            StatusCode::INTERNAL_SERVER_ERROR,
88            Json(ApiResponse::<()>::error("Failed to read interpreter file")),
89        )
90            .into_response();
91    }
92
93    let mut headers = HeaderMap::new();
94    headers.insert(
95        header::CONTENT_DISPOSITION,
96        HeaderValue::from_str(&format!(
97            "attachment; filename=\"{}\"",
98            interpreter.filename
99        ))
100        .unwrap_or_else(|_| HeaderValue::from_static("attachment")),
101    );
102    headers.insert(
103        header::CONTENT_TYPE,
104        HeaderValue::from_static("application/octet-stream"),
105    );
106
107    (StatusCode::OK, headers, buffer).into_response()
108}
109
110
111#[derive(Debug, Serialize)]
112pub struct InterpreterInfo {
113    pub id: i64,
114    pub assignment_id: i64,
115    pub filename: String,
116    pub path: String,
117    pub command: String,
118    pub created_at: String,
119    pub updated_at: String,
120}
121
122/// GET /api/modules/{module_id}/assignments/{assignment_id}/interpreter/info
123/// 
124/// Returns metadata about the current interpreter (if any).
125/// - 200 OK with metadata
126/// - 404 Not Found if no interpreter present
127/// - 500 on DB or other errors
128pub async fn get_interpreter_info(
129    State(app_state): State<AppState>,
130    Path((_module_id, assignment_id)): Path<(i64, i64)>,
131) -> Response {
132    let db = app_state.db();
133
134    let interpreter = match InterpreterEntity::find()
135        .filter(InterpreterColumn::AssignmentId.eq(assignment_id))
136        .one(db)
137        .await
138    {
139        Ok(Some(m)) => m,
140        Ok(None) => {
141            return (
142                StatusCode::NOT_FOUND,
143                Json(ApiResponse::<()>::error("Interpreter not found")),
144            )
145                .into_response();
146        }
147        Err(err) => {
148            eprintln!("DB error fetching interpreter info: {:?}", err);
149            return (
150                StatusCode::INTERNAL_SERVER_ERROR,
151                Json(ApiResponse::<()>::error("Database error")),
152            )
153                .into_response();
154        }
155    };
156
157    let payload = InterpreterInfo {
158        id: interpreter.id,
159        assignment_id: interpreter.assignment_id,
160        filename: interpreter.filename,
161        path: interpreter.path,
162        command: interpreter.command,
163        created_at: interpreter.created_at.to_rfc3339(),
164        updated_at: interpreter.updated_at.to_rfc3339(),
165    };
166
167    (
168        StatusCode::OK,
169        Json(ApiResponse::<InterpreterInfo>::success(payload, "OK")),
170    )
171        .into_response()
172}