backbone_bucket/domain/entity/
upload_session.rs1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use sqlx::FromRow;
4use uuid::Uuid;
5
6use super::UploadStatus;
7use super::StorageBackend;
8use super::AuditMetadata;
9
10use super::*;
11
12use crate::domain::state_machine::{UploadSessionStateMachine, UploadSessionState, StateMachineError};
13
14use thiserror::Error;
15
16#[derive(Debug, Clone, Error)]
18pub enum UploadError {
19 #[error("{0}")]
20 Message(String),
21
22 #[error("Not found: {0}")]
23 NotFound(String),
24
25 #[error("Validation failed: {0}")]
26 ValidationFailed(String),
27
28 #[error("Conflict: {0}")]
29 Conflict(String),
30}
31
32impl From<String> for UploadError {
33 fn from(msg: String) -> Self { Self::Message(msg) }
34}
35
36impl From<&str> for UploadError {
37 fn from(msg: &str) -> Self { Self::Message(msg.to_string()) }
38}
39
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
43#[serde(transparent)]
44pub struct UploadSessionId(pub Uuid);
45
46impl UploadSessionId {
47 pub fn new(id: Uuid) -> Self { Self(id) }
48 pub fn generate() -> Self { Self(Uuid::new_v4()) }
49 pub fn into_inner(self) -> Uuid { self.0 }
50}
51
52impl std::fmt::Display for UploadSessionId {
53 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54 write!(f, "{}", self.0)
55 }
56}
57
58impl std::str::FromStr for UploadSessionId {
59 type Err = uuid::Error;
60 fn from_str(s: &str) -> Result<Self, Self::Err> {
61 Ok(Self(Uuid::parse_str(s)?))
62 }
63}
64
65impl From<Uuid> for UploadSessionId {
66 fn from(id: Uuid) -> Self { Self(id) }
67}
68
69impl From<UploadSessionId> for Uuid {
70 fn from(id: UploadSessionId) -> Self { id.0 }
71}
72
73impl AsRef<Uuid> for UploadSessionId {
74 fn as_ref(&self) -> &Uuid { &self.0 }
75}
76
77impl std::ops::Deref for UploadSessionId {
78 type Target = Uuid;
79 fn deref(&self) -> &Self::Target { &self.0 }
80}
81
82#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
83pub struct UploadSession {
84 pub id: Uuid,
85 pub bucket_id: Uuid,
86 pub user_id: Uuid,
87 pub path: String,
88 pub filename: String,
89 pub mime_type: Option<String>,
90 pub file_size: i64,
91 pub chunk_size: i32,
92 pub total_chunks: i32,
93 pub uploaded_chunks: i32,
94 pub(crate) status: UploadStatus,
95 pub storage_backend: StorageBackend,
96 pub completed_parts: Vec<i32>,
97 pub part_etags: Option<serde_json::Value>,
98 pub expires_at: DateTime<Utc>,
99 #[serde(default)]
100 #[sqlx(json)]
101 pub metadata: AuditMetadata,
102}
103
104impl UploadSession {
105 pub fn builder() -> UploadSessionBuilder {
107 <UploadSessionBuilder as Default>::default()
108 }
109
110 pub fn new(bucket_id: Uuid, user_id: Uuid, path: String, filename: String, file_size: i64, chunk_size: i32, total_chunks: i32, uploaded_chunks: i32, status: UploadStatus, storage_backend: StorageBackend, completed_parts: Vec<i32>, expires_at: DateTime<Utc>) -> Self {
112 Self {
113 id: Uuid::new_v4(),
114 bucket_id,
115 user_id,
116 path,
117 filename,
118 mime_type: None,
119 file_size,
120 chunk_size,
121 total_chunks,
122 uploaded_chunks,
123 status,
124 storage_backend,
125 completed_parts,
126 part_etags: None,
127 expires_at,
128 metadata: AuditMetadata::default(),
129 }
130 }
131
132 pub fn id(&self) -> &Uuid {
134 &self.id
135 }
136
137 pub fn typed_id(&self) -> UploadSessionId {
139 UploadSessionId(self.id)
140 }
141
142 pub fn created_at(&self) -> Option<&DateTime<Utc>> {
144 self.metadata.created_at.as_ref()
145 }
146
147 pub fn updated_at(&self) -> Option<&DateTime<Utc>> {
149 self.metadata.updated_at.as_ref()
150 }
151
152 pub fn is_deleted(&self) -> bool {
154 self.metadata.deleted_at.is_some()
155 }
156
157 pub fn is_active(&self) -> bool {
159 self.metadata.deleted_at.is_none()
160 }
161
162 pub fn deleted_at(&self) -> Option<&DateTime<Utc>> {
164 self.metadata.deleted_at.as_ref()
165 }
166
167 pub fn created_by(&self) -> Option<&Uuid> {
169 self.metadata.created_by.as_ref()
170 }
171
172 pub fn updated_by(&self) -> Option<&Uuid> {
174 self.metadata.updated_by.as_ref()
175 }
176
177 pub fn deleted_by(&self) -> Option<&Uuid> {
179 self.metadata.deleted_by.as_ref()
180 }
181
182 pub fn status(&self) -> &UploadStatus {
184 &self.status
185 }
186
187
188 pub fn with_mime_type(mut self, value: String) -> Self {
194 self.mime_type = Some(value);
195 self
196 }
197
198 pub fn with_part_etags(mut self, value: serde_json::Value) -> Self {
200 self.part_etags = Some(value);
201 self
202 }
203
204 pub fn transition_to(&mut self, new_state: UploadSessionState) -> Result<(), StateMachineError> {
213 let current = self.status.to_string().parse::<UploadSessionState>()?;
214 let mut sm = UploadSessionStateMachine::from_state(current);
215 sm.transition_to_state(new_state)?;
216 self.status = new_state.to_string().parse::<UploadStatus>()
217 .map_err(|e| StateMachineError::InvalidState(e.to_string()))?;
218 Ok(())
219 }
220
221 pub fn apply_patch(&mut self, fields: std::collections::HashMap<String, serde_json::Value>) {
227 for (key, value) in fields {
228 match key.as_str() {
229 "bucket_id" => {
230 if let Ok(v) = serde_json::from_value(value) { self.bucket_id = v; }
231 }
232 "user_id" => {
233 if let Ok(v) = serde_json::from_value(value) { self.user_id = v; }
234 }
235 "path" => {
236 if let Ok(v) = serde_json::from_value(value) { self.path = v; }
237 }
238 "filename" => {
239 if let Ok(v) = serde_json::from_value(value) { self.filename = v; }
240 }
241 "mime_type" => {
242 if let Ok(v) = serde_json::from_value(value) { self.mime_type = v; }
243 }
244 "file_size" => {
245 if let Ok(v) = serde_json::from_value(value) { self.file_size = v; }
246 }
247 "chunk_size" => {
248 if let Ok(v) = serde_json::from_value(value) { self.chunk_size = v; }
249 }
250 "total_chunks" => {
251 if let Ok(v) = serde_json::from_value(value) { self.total_chunks = v; }
252 }
253 "uploaded_chunks" => {
254 if let Ok(v) = serde_json::from_value(value) { self.uploaded_chunks = v; }
255 }
256 "storage_backend" => {
257 if let Ok(v) = serde_json::from_value(value) { self.storage_backend = v; }
258 }
259 "completed_parts" => {
260 if let Ok(v) = serde_json::from_value(value) { self.completed_parts = v; }
261 }
262 "part_etags" => {
263 if let Ok(v) = serde_json::from_value(value) { self.part_etags = v; }
264 }
265 "expires_at" => {
266 if let Ok(v) = serde_json::from_value(value) { self.expires_at = v; }
267 }
268 _ => {} }
270 }
271 }
272
273}
274
275impl super::Entity for UploadSession {
276 type Id = Uuid;
277
278 fn entity_id(&self) -> &Self::Id {
279 &self.id
280 }
281
282 fn entity_type() -> &'static str {
283 "UploadSession"
284 }
285}
286
287impl backbone_core::PersistentEntity for UploadSession {
288 fn entity_id(&self) -> String {
289 self.id.to_string()
290 }
291 fn set_entity_id(&mut self, id: String) {
292 if let Ok(uuid) = uuid::Uuid::parse_str(&id) {
293 self.id = uuid;
294 }
295 }
296 fn created_at(&self) -> Option<chrono::DateTime<chrono::Utc>> {
297 self.metadata.created_at
298 }
299 fn set_created_at(&mut self, ts: chrono::DateTime<chrono::Utc>) {
300 self.metadata.created_at = Some(ts);
301 }
302 fn updated_at(&self) -> Option<chrono::DateTime<chrono::Utc>> {
303 self.metadata.updated_at
304 }
305 fn set_updated_at(&mut self, ts: chrono::DateTime<chrono::Utc>) {
306 self.metadata.updated_at = Some(ts);
307 }
308 fn deleted_at(&self) -> Option<chrono::DateTime<chrono::Utc>> {
309 self.metadata.deleted_at
310 }
311 fn set_deleted_at(&mut self, ts: Option<chrono::DateTime<chrono::Utc>>) {
312 self.metadata.deleted_at = ts;
313 }
314}
315
316impl backbone_orm::EntityRepoMeta for UploadSession {
317 fn column_types() -> std::collections::HashMap<String, String> {
318 let mut m = std::collections::HashMap::new();
319 m.insert("id".to_string(), "uuid".to_string());
320 m.insert("bucket_id".to_string(), "uuid".to_string());
321 m.insert("user_id".to_string(), "uuid".to_string());
322 m.insert("status".to_string(), "upload_status".to_string());
323 m.insert("storage_backend".to_string(), "storage_backend".to_string());
324 m.insert("expires_at".to_string(), "timestamptz".to_string());
325 m
326 }
327 fn search_fields() -> &'static [&'static str] {
328 &["path", "filename"]
329 }
330}
331
332#[derive(Debug, Clone, Default)]
337pub struct UploadSessionBuilder {
338 bucket_id: Option<Uuid>,
339 user_id: Option<Uuid>,
340 path: Option<String>,
341 filename: Option<String>,
342 mime_type: Option<String>,
343 file_size: Option<i64>,
344 chunk_size: Option<i32>,
345 total_chunks: Option<i32>,
346 uploaded_chunks: Option<i32>,
347 status: Option<UploadStatus>,
348 storage_backend: Option<StorageBackend>,
349 completed_parts: Option<Vec<i32>>,
350 part_etags: Option<serde_json::Value>,
351 expires_at: Option<DateTime<Utc>>,
352}
353
354impl UploadSessionBuilder {
355 pub fn bucket_id(mut self, value: Uuid) -> Self {
357 self.bucket_id = Some(value);
358 self
359 }
360
361 pub fn user_id(mut self, value: Uuid) -> Self {
363 self.user_id = Some(value);
364 self
365 }
366
367 pub fn path(mut self, value: String) -> Self {
369 self.path = Some(value);
370 self
371 }
372
373 pub fn filename(mut self, value: String) -> Self {
375 self.filename = Some(value);
376 self
377 }
378
379 pub fn mime_type(mut self, value: String) -> Self {
381 self.mime_type = Some(value);
382 self
383 }
384
385 pub fn file_size(mut self, value: i64) -> Self {
387 self.file_size = Some(value);
388 self
389 }
390
391 pub fn chunk_size(mut self, value: i32) -> Self {
393 self.chunk_size = Some(value);
394 self
395 }
396
397 pub fn total_chunks(mut self, value: i32) -> Self {
399 self.total_chunks = Some(value);
400 self
401 }
402
403 pub fn uploaded_chunks(mut self, value: i32) -> Self {
405 self.uploaded_chunks = Some(value);
406 self
407 }
408
409 pub fn status(mut self, value: UploadStatus) -> Self {
411 self.status = Some(value);
412 self
413 }
414
415 pub fn storage_backend(mut self, value: StorageBackend) -> Self {
417 self.storage_backend = Some(value);
418 self
419 }
420
421 pub fn completed_parts(mut self, value: Vec<i32>) -> Self {
423 self.completed_parts = Some(value);
424 self
425 }
426
427 pub fn part_etags(mut self, value: serde_json::Value) -> Self {
429 self.part_etags = Some(value);
430 self
431 }
432
433 pub fn expires_at(mut self, value: DateTime<Utc>) -> Self {
435 self.expires_at = Some(value);
436 self
437 }
438
439 pub fn build(self) -> Result<UploadSession, String> {
443 let bucket_id = self.bucket_id.ok_or_else(|| "bucket_id is required".to_string())?;
444 let user_id = self.user_id.ok_or_else(|| "user_id is required".to_string())?;
445 let path = self.path.ok_or_else(|| "path is required".to_string())?;
446 let filename = self.filename.ok_or_else(|| "filename is required".to_string())?;
447 let file_size = self.file_size.ok_or_else(|| "file_size is required".to_string())?;
448 let chunk_size = self.chunk_size.ok_or_else(|| "chunk_size is required".to_string())?;
449 let total_chunks = self.total_chunks.ok_or_else(|| "total_chunks is required".to_string())?;
450 let completed_parts = self.completed_parts.ok_or_else(|| "completed_parts is required".to_string())?;
451 let expires_at = self.expires_at.ok_or_else(|| "expires_at is required".to_string())?;
452
453 Ok(UploadSession {
454 id: Uuid::new_v4(),
455 bucket_id,
456 user_id,
457 path,
458 filename,
459 mime_type: self.mime_type,
460 file_size,
461 chunk_size,
462 total_chunks,
463 uploaded_chunks: self.uploaded_chunks.unwrap_or(0),
464 status: self.status.unwrap_or_default(),
465 storage_backend: self.storage_backend.unwrap_or_default(),
466 completed_parts,
467 part_etags: self.part_etags,
468 expires_at,
469 metadata: AuditMetadata::default(),
470 })
471 }
472}
473
474#[path = "upload_session.ext.rs"]
476mod upload_session_ext;
477pub use upload_session_ext::*;