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//}