db/models/
assignment_overwrite_file.rs

1use chrono::{DateTime, Utc};
2use sea_orm::entity::prelude::*;
3use sea_orm::{ActiveValue::Set, DatabaseConnection, EntityTrait};
4use std::env;
5use std::fs;
6use std::path::PathBuf;
7
8/// Represents a file used to overwrite specific parts of an assignment during evaluation.
9/// Includes metadata such as its related assignment, task, filename, and storage path.
10#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
11#[sea_orm(table_name = "assignment_overwrite_files")]
12pub struct Model {
13    #[sea_orm(primary_key)]
14    pub id: i64,
15    pub assignment_id: i64,
16    pub task_id: i64,
17    pub filename: String,
18    pub path: String,
19    pub created_at: DateTime<Utc>,
20    pub updated_at: DateTime<Utc>,
21}
22
23#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
24pub enum Relation {
25    #[sea_orm(
26        belongs_to = "super::assignment::Entity",
27        from = "Column::AssignmentId",
28        to = "super::assignment::Column::Id"
29    )]
30    Assignment,
31
32    #[sea_orm(
33        belongs_to = "super::assignment_task::Entity",
34        from = "Column::TaskId",
35        to = "super::assignment_task::Column::TaskNumber"
36    )]
37    AssignmentTask,
38}
39
40impl ActiveModelBehavior for ActiveModel {}
41
42impl Model {
43    pub fn storage_root() -> PathBuf {
44        env::var("ASSIGNMENT_STORAGE_ROOT")
45            .map(PathBuf::from)
46            .unwrap_or_else(|_| PathBuf::from("data/assignment_files"))
47    }
48
49    pub fn full_directory_path(module_id: i64, assignment_id: i64, task_number: i64) -> PathBuf {
50        Self::storage_root()
51            .join(format!("module_{module_id}"))
52            .join(format!("assignment_{assignment_id}"))
53            .join("overwrite_files")
54            .join(format!("task_{task_number}"))
55    }
56
57    pub fn full_path(&self) -> PathBuf {
58        Self::storage_root().join(&self.path)
59    }
60
61    pub async fn save_file(
62        db: &DatabaseConnection,
63        assignment_id: i64,
64        task_id: i64,
65        filename: &str,
66        bytes: &[u8],
67    ) -> Result<Self, DbErr> {
68        let now = Utc::now();
69
70        let partial = ActiveModel {
71            assignment_id: Set(assignment_id),
72            task_id: Set(task_id),
73            filename: Set(filename.to_string()),
74            path: Set("".to_string()),
75            created_at: Set(now),
76            updated_at: Set(now),
77            ..Default::default()
78        };
79
80        let inserted: Model = partial.insert(db).await?;
81
82        let ext = PathBuf::from(filename)
83            .extension()
84            .map(|e| e.to_string_lossy().to_string());
85
86        let stored_filename = match ext {
87            Some(ext) => format!("{}.{}", inserted.id, ext),
88            None => inserted.id.to_string(),
89        };
90
91        let assignment = super::assignment::Entity::find_by_id(assignment_id)
92            .one(db)
93            .await
94            .map_err(|e| DbErr::Custom(format!("DB error finding assignment: {}", e)))?
95            .ok_or_else(|| DbErr::Custom("Assignment not found".to_string()))?;
96
97        let module_id = assignment.module_id;
98
99        let task = super::assignment_task::Entity::find_by_id(task_id)
100            .one(db)
101            .await
102            .map_err(|e| DbErr::Custom(format!("DB error finding task: {}", e)))?
103            .ok_or_else(|| DbErr::Custom("Task not found".to_string()))?;
104
105        let task_number = task.task_number;
106
107        let dir_path = Self::full_directory_path(module_id, assignment_id, task_number);
108        fs::create_dir_all(&dir_path)
109            .map_err(|e| 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| 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    /// Loads the file contents from disk based on the path stored in the model.
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    /// Deletes the file from disk (but not the 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}