db/models/
assignment_submission_output.rs1use chrono::{DateTime, Utc};
2use sea_orm::entity::prelude::*;
3use sea_orm::{ActiveValue::Set, DatabaseConnection, EntityTrait};
4use std::fs;
5use std::io::ErrorKind;
6use std::path::PathBuf;
7use std::{env, io};
8
9use crate::models::assignment_submission;
10
11#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
16#[sea_orm(table_name = "assignment_submission_outputs")]
17pub struct Model {
18 #[sea_orm(primary_key)]
19 pub id: i64,
20 pub task_id: i64,
21 pub submission_id: i64,
22 pub path: String,
23 pub created_at: DateTime<Utc>,
24 pub updated_at: DateTime<Utc>,
25}
26
27#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
28pub enum Relation {
29 #[sea_orm(
30 belongs_to = "super::assignment_task::Entity",
31 from = "Column::TaskId",
32 to = "super::assignment_task::Column::Id"
33 )]
34 AssignmentTask,
35
36 #[sea_orm(
37 belongs_to = "super::user::Entity",
38 from = "Column::SubmissionId",
39 to = "super::user::Column::Id"
40 )]
41 AssignmentSubmission,
42}
43
44impl ActiveModelBehavior for ActiveModel {}
45
46impl Model {
47 pub fn storage_root() -> PathBuf {
48 let relative_root = env::var("ASSIGNMENT_STORAGE_ROOT")
49 .unwrap_or_else(|_| "data/assignment_files".to_string());
50
51 let mut dir = std::env::current_dir().expect("Failed to get current dir");
52
53 while let Some(parent) = dir.parent() {
54 if dir.ends_with("backend") {
55 return dir.join(relative_root);
56 }
57 dir = parent.to_path_buf();
58 }
59
60 PathBuf::from(relative_root)
61 }
62 pub fn full_directory_path(
63 module_id: i64,
64 assignment_id: i64,
65 user_id: i64,
66 attempt_number: i64,
67 ) -> PathBuf {
68 Self::storage_root()
69 .join(format!("module_{module_id}"))
70 .join(format!("assignment_{assignment_id}"))
71 .join("assignment_submissions")
72 .join(format!("user_{user_id}"))
73 .join(format!("attempt_{attempt_number}"))
74 .join("submission_output")
75 }
76
77 pub fn full_path(&self) -> PathBuf {
78 Self::storage_root().join(&self.path)
79 }
80
81 pub async fn delete_for_submission(
82 db: &DatabaseConnection,
83 submission_id: i64,
84 ) -> Result<(), DbErr> {
85 use sea_orm::QueryFilter;
86
87 let outputs = Entity::find()
89 .filter(Column::SubmissionId.eq(submission_id))
90 .all(db)
91 .await?;
92
93 for output in outputs {
94 let path = output.full_path();
96 if path.exists() {
97 if let Err(e) = fs::remove_file(&path) {
98 eprintln!("Failed to delete file {:?}: {}", path, e);
99 }
100 }
101
102 let am: ActiveModel = output.into();
104 am.delete(db).await?;
105 }
106
107 Ok(())
108 }
109
110 pub async fn save_file(
111 db: &DatabaseConnection,
112 task_id: i64,
113 submission_id: i64,
114 filename: &str,
115 bytes: &[u8],
116 ) -> Result<Self, DbErr> {
117 let now = Utc::now();
118
119 let partial = ActiveModel {
120 task_id: Set(task_id),
121 submission_id: Set(submission_id),
122 path: Set("".to_string()),
123 created_at: Set(now),
124 updated_at: Set(now),
125 ..Default::default()
126 };
127
128 let inserted: Model = partial.insert(db).await?;
129
130 let ext = PathBuf::from(filename)
131 .extension()
132 .map(|e| e.to_string_lossy().to_string());
133
134 let stored_filename = match ext {
135 Some(ext) => format!("{}.{}", inserted.id, ext),
136 None => inserted.id.to_string(),
137 };
138
139 let submission = super::assignment_submission::Entity::find_by_id(submission_id)
141 .one(db)
142 .await
143 .map_err(|e| DbErr::Custom(format!("DB error finding submission: {}", e)))?
144 .ok_or_else(|| DbErr::Custom("Submission not found".to_string()))?;
145
146 let assignment = super::assignment::Entity::find_by_id(submission.assignment_id)
147 .one(db)
148 .await
149 .map_err(|e| DbErr::Custom(format!("DB error finding assignment: {}", e)))?
150 .ok_or_else(|| DbErr::Custom("Assignment not found".to_string()))?;
151
152 let dir_path = Self::full_directory_path(
153 assignment.module_id,
154 assignment.id,
155 submission.user_id,
156 submission.attempt,
157 );
158 fs::create_dir_all(&dir_path)
159 .map_err(|e| DbErr::Custom(format!("Failed to create directory: {e}")))?;
160
161 let file_path = dir_path.join(&stored_filename);
162 let relative_path = file_path
163 .strip_prefix(Self::storage_root())
164 .unwrap()
165 .to_string_lossy()
166 .to_string();
167
168 fs::write(&file_path, bytes)
169 .map_err(|e| DbErr::Custom(format!("Failed to write file: {e}")))?;
170
171 let mut model: ActiveModel = inserted.into();
172 model.path = Set(relative_path);
173 model.updated_at = Set(Utc::now());
174
175 model.update(db).await
176 }
177
178 pub async fn get_output(
181 db: &DatabaseConnection,
182 module_id: i64,
183 assignment_id: i64,
184 submission_id: i64,
185 ) -> io::Result<Vec<(i64, String)>> {
186 let submission = assignment_submission::Entity::find_by_id(submission_id)
187 .one(db)
188 .await
189 .map_err(|e| io::Error::new(ErrorKind::Other, format!("DB error: {}", e)))?
190 .ok_or_else(|| io::Error::new(ErrorKind::NotFound, "Submission not found"))?;
191
192 let base_dir_path = Self::storage_root()
193 .join(format!("module_{module_id}"))
194 .join(format!("assignment_{assignment_id}"))
195 .join("assignment_submissions")
196 .join(format!("user_{}", submission.user_id))
197 .join(format!("attempt_{}", submission.attempt))
198 .join("submission_output");
199
200 if !base_dir_path.exists() {
201 return Err(io::Error::new(
202 ErrorKind::NotFound,
203 format!(
204 "Submission output directory {:?} does not exist",
205 base_dir_path
206 ),
207 ));
208 }
209
210 let mut results = Vec::new();
211 for entry in fs::read_dir(&base_dir_path)? {
212 let entry = entry?;
213 if entry.file_type()?.is_file() {
214 let file_path = entry.path();
215 if let Some(file_name) = file_path.file_stem().and_then(|n| n.to_str()) {
216 if let Ok(output_id) = file_name.parse::<i64>() {
217 let output = Entity::find_by_id(output_id)
219 .one(db)
220 .await
221 .map_err(|e| {
222 io::Error::new(ErrorKind::Other, format!("DB error: {}", e))
223 })?
224 .ok_or_else(|| {
225 io::Error::new(ErrorKind::NotFound, "Output not found")
226 })?;
227
228 let content = fs::read_to_string(&file_path)?;
229 results.push((output.task_id, content));
230 }
231 }
232 }
233 }
234
235 Ok(results)
236 }
237}