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

1use std::{env, path::PathBuf};
2use axum::{
3    extract::{State, Path},
4    http::{header, HeaderMap, HeaderValue, StatusCode},
5    response::{IntoResponse, Json, Response},
6};
7use tokio::{fs::File as FsFile, io::AsyncReadExt};
8use sea_orm::{
9    ColumnTrait,
10    EntityTrait,
11    QueryFilter,
12};
13use util::state::AppState;
14use crate::response::ApiResponse;
15use db::models::assignment_file::{
16    Column as FileColumn,
17    Entity as FileEntity,
18};
19use crate::routes::modules::assignments::common::File;
20
21/// GET /api/modules/{module_id}/assignments/{assignment_id}/files/{file_id}
22///
23/// Download a specific file from an assignment. Accessible to users assigned to the module.
24///
25/// ### Path Parameters
26/// - `module_id` (i64): The ID of the module containing the assignment
27/// - `assignment_id` (i64): The ID of the assignment containing the file
28/// - `file_id` (i64): The ID of the file to download
29///
30/// ### Responses
31///
32/// - `200 OK`: Returns the file as a binary attachment with appropriate headers
33/// - `404 Not Found`
34/// ```json
35/// {
36///   "success": false,
37///   "message": "File not found" // or "File missing on disk"
38/// }
39/// ```
40///
41/// - `500 Internal Server Error`
42/// ```json
43/// {
44///   "success": false,
45///   "message": "Database error" // or "Could not open file" or "Failed to read file"
46/// }
47/// ```
48///
49pub async fn download_file(
50    State(app_state): State<AppState>,
51    Path((_module_id, assignment_id, file_id)): Path<(i64, i64, i64)>,
52) -> Response {
53    let db = app_state.db();
54
55    let file = FileEntity::find()
56        .filter(FileColumn::Id.eq(file_id as i32))
57        .filter(FileColumn::AssignmentId.eq(assignment_id as i32))
58        .one(db)
59        .await.unwrap().unwrap();
60
61    let storage_root = env::var("ASSIGNMENT_STORAGE_ROOT").unwrap_or_else(|_| "data/assignment_files".to_string());
62    let fs_path = PathBuf::from(storage_root).join(&file.path);
63
64    if tokio::fs::metadata(&fs_path).await.is_err() {
65        return (
66            StatusCode::NOT_FOUND,
67            Json(ApiResponse::<()>::error("File missing on disk")),
68        )
69            .into_response();
70    }
71
72    let mut file_handle = match FsFile::open(&fs_path).await {
73        Ok(f) => f,
74        Err(err) => {
75            eprintln!("File open error: {:?}", err);
76            return (
77                StatusCode::INTERNAL_SERVER_ERROR,
78                Json(ApiResponse::<()>::error("Could not open file")),
79            )
80                .into_response();
81        }
82    };
83
84    let mut buffer = Vec::new();
85    if let Err(err) = file_handle.read_to_end(&mut buffer).await {
86        eprintln!("File read error: {:?}", err);
87        return (
88            StatusCode::INTERNAL_SERVER_ERROR,
89            Json(ApiResponse::<()>::error("Failed to read file")),
90        )
91            .into_response();
92    }
93
94    let mut headers = HeaderMap::new();
95    headers.insert(
96        header::CONTENT_DISPOSITION,
97        HeaderValue::from_str(&format!("attachment; filename=\"{}\"", file.filename))
98            .unwrap_or_else(|_| HeaderValue::from_static("attachment")),
99    );
100    headers.insert(
101        header::CONTENT_TYPE,
102        HeaderValue::from_static("application/octet-stream"),
103    );
104
105    (StatusCode::OK, headers, buffer).into_response()
106}
107
108/// GET /api/modules/{module_id}/assignments/{assignment_id}/files
109///
110/// List all files associated with an assignment. Accessible to users assigned to the module.
111///
112/// ### Path Parameters
113/// - `module_id` (i64): The ID of the module containing the assignment
114/// - `assignment_id` (i64): The ID of the assignment to list files for
115///
116/// ### Responses
117///
118/// - `200 OK`
119/// ```json
120/// {
121///   "success": true,
122///   "message": "Assignment files retrieved successfully",
123///   "data": [
124///     {
125///       "id": "123",
126///       "filename": "assignment.pdf",
127///       "path": "module_456/assignment_789/assignment.pdf",
128///       "created_at": "2024-01-01T00:00:00Z",
129///       "updated_at": "2024-01-01T00:00:00Z"
130///     }
131///   ]
132/// }
133/// ```
134///
135/// - `404 Not Found`
136/// ```json
137/// {
138///   "success": false,
139///   "message": "Assignment not found"
140/// }
141/// ```
142///
143/// - `500 Internal Server Error`
144/// ```json
145/// {
146///   "success": false,
147///   "message": "Database error" // or "Failed to retrieve files"
148/// }
149/// ```
150///
151pub async fn list_files(
152    State(app_state): State<AppState>,
153    Path((_, assignment_id)): Path<(i64, i64)>
154) -> Response {
155    let db = app_state.db();
156
157    match FileEntity::find()
158        .filter(FileColumn::AssignmentId.eq(assignment_id as i32))
159        .all(db)
160        .await
161    {
162        Ok(files) => {
163            let file_list: Vec<File> = files
164                .into_iter()
165                .map(|f| File {
166                    id: f.id.to_string(),
167                    filename: f.filename,
168                    path: f.path,
169                    file_type: f.file_type.to_string(),
170                    created_at: f.created_at.to_rfc3339(),
171                    updated_at: f.updated_at.to_rfc3339(),
172                })
173                .collect();
174
175            (
176                StatusCode::OK,
177                Json(ApiResponse::success(
178                    file_list,
179                    "Assignment files retrieved successfully",
180                )),
181            )
182                .into_response()
183        }
184        Err(err) => {
185            eprintln!("DB error fetching files: {:?}", err);
186            (
187                StatusCode::INTERNAL_SERVER_ERROR,
188                Json(ApiResponse::<Vec<File>>::error("Failed to retrieve files")),
189            )
190                .into_response()
191        }
192    }
193}