db/models/
assignment_memo_output.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 the output generated by the interpreter for an assignment memo.
9///
10/// Each output is linked to a specific assignment and task.
11/// Timestamps are included to track when the file was created and last updated.
12#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
13#[sea_orm(table_name = "assignment_memo_outputs")]
14pub struct Model {
15    /// Primary key of the output.
16    #[sea_orm(primary_key)]
17    pub id: i64,
18    /// ID of the related assignment.
19    pub assignment_id: i64,
20    /// ID of the related task.
21    pub task_id: i64,
22    /// Relative file path from the storage root.
23    pub path: String,
24    /// Timestamp when the output was created.
25    pub created_at: DateTime<Utc>,
26    /// Timestamp when the output was last updated.
27    pub updated_at: DateTime<Utc>,
28}
29
30#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
31pub enum Relation {
32    /// Link to the related assignment.
33    #[sea_orm(
34        belongs_to = "super::assignment::Entity",
35        from = "Column::AssignmentId",
36        to = "super::assignment::Column::Id"
37    )]
38    Assignment,
39
40    /// Link to the related task.
41    #[sea_orm(
42        belongs_to = "super::assignment_task::Entity",
43        from = "Column::TaskId",
44        to = "super::assignment_task::Column::Id"
45    )]
46    AssignmentTask,
47}
48
49impl ActiveModelBehavior for ActiveModel {}
50
51impl Model {
52    /// Returns the root directory used for storing assignment submissions on disk.
53    ///
54    /// # Returns
55    /// - `PathBuf` pointing to the base directory.
56    ///
57    /// Uses the `ASSIGNMENT_STORAGE_ROOT` environment variable if set,
58    /// otherwise defaults to `data/assignment_files`.
59    pub fn storage_root() -> PathBuf {
60        let relative_root = env::var("ASSIGNMENT_STORAGE_ROOT")
61            .unwrap_or_else(|_| "data/assignment_files".to_string());
62        
63        let project_root = env::current_dir().expect("Failed to get current dir");
64        
65        project_root.join(relative_root)
66    }
67
68    /// Constructs the full directory path for a memo output based on
69    /// its assignment and task identifiers.
70    pub fn full_directory_path(module_id: i64, assignment_id: i64) -> PathBuf {
71        Self::storage_root()
72            .join(format!("module_{module_id}"))
73            .join(format!("assignment_{assignment_id}"))
74            .join(format!("memo_output"))
75    }
76
77    /// Computes the absolute path to the stored output file on disk.
78    pub fn full_path(&self) -> PathBuf {
79        Self::storage_root().join(&self.path)
80    }
81
82    /// Saves a memo output file to disk and creates or updates its metadata in the database.
83    pub async fn save_file(
84        db: &DatabaseConnection,
85        assignment_id: i64,
86        task_id: i64,
87        filename: &str,
88        bytes: &[u8],
89    ) -> Result<Self, DbErr> {
90        let now = Utc::now();
91
92        let partial = ActiveModel {
93            assignment_id: Set(assignment_id),
94            task_id: Set(task_id),
95            path: Set("".to_string()),
96            created_at: Set(now),
97            updated_at: Set(now),
98            ..Default::default()
99        };
100
101        let inserted: Model = partial.insert(db).await?;
102
103        let ext = PathBuf::from(filename)
104            .extension()
105            .map(|e| e.to_string_lossy().to_string());
106
107        let stored_filename = match ext {
108            Some(ext) => format!("{}.{}", inserted.id, ext),
109            None => inserted.id.to_string(),
110        };
111
112        //Get assignment
113        let assignment = super::assignment::Entity::find_by_id(assignment_id)
114            .one(db)
115            .await
116            .map_err(|e| DbErr::Custom(format!("DB error finding assignment: {}", e)))?
117            .ok_or_else(|| DbErr::Custom("Assignment not found".to_string()))?;
118
119        let module_id = assignment.module_id;
120
121        let dir_path = Self::full_directory_path(module_id, assignment_id);
122        fs::create_dir_all(&dir_path)
123            .map_err(|e| DbErr::Custom(format!("Failed to create directory: {e}")))?;
124
125        let file_path = dir_path.join(&stored_filename);
126        let relative_path = file_path
127            .strip_prefix(Self::storage_root())
128            .unwrap()
129            .to_string_lossy()
130            .to_string();
131
132        fs::write(&file_path, bytes)
133            .map_err(|e| DbErr::Custom(format!("Failed to write file: {e}")))?;
134
135        let mut model: ActiveModel = inserted.into();
136        model.path = Set(relative_path);
137        model.updated_at = Set(Utc::now());
138
139        model.update(db).await
140    }
141
142    /// Reads the contents of a memo output file from disk,
143    /// given the module_id, assignment_id, and the file id (filename).
144    pub fn read_memo_output_file(
145        module_id: i64,
146        assignment_id: i64,
147        file_id: i64,
148    ) -> Result<Vec<u8>, std::io::Error> {
149        let storage_root = Self::storage_root();
150
151        let dir_path = storage_root
152            .join(format!("module_{module_id}"))
153            .join(format!("assignment_{assignment_id}"))
154            .join("memo_output");
155
156        let file_path = dir_path.join(file_id.to_string());
157
158        std::fs::read(file_path)
159    }
160}