db/models/assignment_interpreter.rs
1// models/assignment_interpreter.rs
2
3use chrono::{DateTime, Utc};
4use sea_orm::ActiveValue::Set;
5use sea_orm::entity::prelude::*;
6use std::env;
7use std::fs;
8use std::path::PathBuf;
9
10/// Represents an interpreter file associated with an assignment,
11/// including the command used to run it.
12#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
13#[sea_orm(table_name = "interpreters")]
14pub struct Model {
15 #[sea_orm(primary_key)]
16 pub id: i64,
17
18 /// Foreign key reference to an assignment.
19 pub assignment_id: i64,
20
21 /// Original file name.
22 pub filename: String,
23
24 /// Relative path to the stored file from the storage root.
25 pub path: String,
26
27 /// Command to run this interpreter.
28 pub command: String,
29
30 pub created_at: DateTime<Utc>,
31 pub updated_at: DateTime<Utc>,
32}
33
34#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
35pub enum Relation {
36 #[sea_orm(
37 belongs_to = "super::assignment::Entity",
38 from = "Column::AssignmentId",
39 to = "super::assignment::Column::Id"
40 )]
41 Assignment,
42}
43
44impl ActiveModelBehavior for ActiveModel {}
45
46impl Model {
47 /// Returns the base directory for interpreter file storage from the environment.
48 pub fn storage_root() -> PathBuf {
49 env::var("ASSIGNMENT_STORAGE_ROOT")
50 .map(PathBuf::from)
51 .unwrap_or_else(|_| PathBuf::from("data/interpreters"))
52 }
53
54 /// Computes the full directory path based on module ID and assignment ID.
55 pub fn full_directory_path(module_id: i64, assignment_id: i64) -> PathBuf {
56 Self::storage_root()
57 .join(format!("module_{}", module_id))
58 .join(format!("assignment_{}", assignment_id))
59 .join("interpreter")
60 }
61
62 pub async fn save_file(
63 db: &DatabaseConnection,
64 assignment_id: i64,
65 module_id: i64,
66 filename: &str,
67 command: &str,
68 bytes: &[u8],
69 ) -> Result<Self, sea_orm::DbErr> {
70 use crate::models::assignment_interpreter::{Column, Entity as AssignmentInterpreter};
71 use sea_orm::{ColumnTrait, EntityTrait, QueryFilter};
72
73 let existing = AssignmentInterpreter::find()
74 .filter(Column::AssignmentId.eq(assignment_id))
75 .all(db)
76 .await?;
77
78 for record in existing {
79 let existing_path = Self::storage_root().join(&record.path);
80 let _ = fs::remove_file(existing_path);
81 record.delete(db).await?;
82 }
83
84 let now = Utc::now();
85
86 let partial = ActiveModel {
87 assignment_id: Set(assignment_id),
88 filename: Set(filename.to_string()),
89 path: Set("".to_string()),
90 command: Set(command.to_string()),
91 created_at: Set(now),
92 updated_at: Set(now),
93 ..Default::default()
94 };
95
96 let inserted: Model = partial.insert(db).await?;
97
98 let ext = PathBuf::from(filename)
99 .extension()
100 .map(|e| e.to_string_lossy().to_string());
101
102 let stored_filename = match ext {
103 Some(ext) => format!("{}.{}", inserted.id, ext),
104 None => inserted.id.to_string(),
105 };
106
107 let dir_path = Self::full_directory_path(module_id, assignment_id);
108 fs::create_dir_all(&dir_path)
109 .map_err(|e| sea_orm::DbErr::Custom(format!("Failed to create directory: {}", e)))?;
110
111 let file_path = dir_path.join(&stored_filename);
112 let relative_path = file_path
113 .strip_prefix(Self::storage_root())
114 .unwrap()
115 .to_string_lossy()
116 .to_string();
117
118 fs::write(&file_path, bytes)
119 .map_err(|e| sea_orm::DbErr::Custom(format!("Failed to write file: {}", e)))?;
120
121 let mut model: ActiveModel = inserted.into();
122 model.path = Set(relative_path);
123 model.updated_at = Set(Utc::now());
124
125 model.update(db).await
126 }
127
128 /// Load interpreter file content from disk.
129 pub fn load_file(&self) -> Result<Vec<u8>, std::io::Error> {
130 let full_path = Self::storage_root().join(&self.path);
131 fs::read(full_path)
132 }
133
134 /// Delete the interpreter file from disk (but not DB record).
135 pub fn delete_file_only(&self) -> Result<(), std::io::Error> {
136 let full_path = Self::storage_root().join(&self.path);
137 fs::remove_file(full_path)
138 }
139}
140
141// TODO : FIX THIS TEST FAILLING ON GITHUB
142
143// #[cfg(test)]
144// mod tests {
145// use super::*;
146// use crate::test_utils::setup_test_db;
147// use chrono::Utc;
148// use sea_orm::Set;
149// use tempfile::TempDir;
150
151// fn fake_bytes() -> Vec<u8> {
152// vec![0x50, 0x4B, 0x03, 0x04] // ZIP file signature
153// }
154
155// fn override_storage_dir(temp: &TempDir) {
156// unsafe {
157// std::env::set_var("ASSIGNMENT_STORAGE_ROOT", temp.path());
158// }
159// }
160
161// #[tokio::test]
162// async fn test_save_and_load_file() {
163// let temp_dir = TempDir::new().unwrap();
164// override_storage_dir(&temp_dir);
165// let db = setup_test_db().await;
166
167// // Insert dummy module so assignment FK passes
168// let _module = crate::models::module::ActiveModel {
169// code: Set("COS301".to_string()),
170// year: Set(2025),
171// description: Set(Some("Capstone".to_string())),
172// created_at: Set(Utc::now()),
173// updated_at: Set(Utc::now()),
174// ..Default::default()
175// }
176// .insert(&db)
177// .await
178// .expect("Insert module failed");
179
180// // Insert dummy assignment using enum value for assignment_type
181// let _assignment = crate::models::assignment::Model::create(
182// &db,
183// 1,
184// "Test Assignment",
185// Some("Desc"),
186// crate::models::assignment::AssignmentType::Practical,
187// Utc::now(),
188// Utc::now(),
189// )
190// .await
191// .expect("Insert assignment failed");
192
193// let content = fake_bytes();
194// let filename = "interpreter.sh";
195// let command = "sh interpreter.sh";
196
197// let saved = Model::save_file(&db, 1, 1, filename, command, &content)
198// .await
199// .expect("Failed to save interpreter");
200
201// assert_eq!(saved.assignment_id, 1);
202// assert_eq!(saved.filename, filename);
203// assert_eq!(saved.command, command);
204
205// // Confirm file on disk
206// let full_path = Model::storage_root().join(&saved.path);
207// assert!(full_path.exists());
208
209// // Load contents
210// let bytes = saved.load_file().unwrap();
211// assert_eq!(bytes, content);
212
213// // Delete file only
214// saved.delete_file_only().unwrap();
215// assert!(!full_path.exists());
216// }
217//}