db/models/
assignment_overwrite_file.rs1use 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#[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 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 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}