Skip to main content

backbone_bucket/domain/entity/
upload_session.ext.rs

1//! The extension of `upload_session.rs` (hand-written; ADR-0031).
2
3#[allow(unused_imports)]
4use super::*;
5
6impl UploadSession {
7
8    // ==========================================================
9    // DDD Entity Methods
10    // ==========================================================
11
12    /// Check if session has expired
13    pub fn is_expired(&self) -> bool {
14        self.expires_at <= Utc::now()
15    }
16
17    /// Add completed part
18    pub fn add_part(&mut self, part_number: i32, etag: String) -> Result<(), UploadError> {
19        if self.is_expired() {
20            return Err(UploadError::ValidationFailed("Upload session has expired".to_string()));
21        }
22
23        if self.completed_parts.contains(&part_number) {
24            return Err(UploadError::Conflict(format!("Part {} already uploaded", part_number)));
25        }
26
27        if self.uploaded_chunks >= self.total_chunks {
28            return Err(UploadError::ValidationFailed("All chunks already uploaded".to_string()));
29        }
30
31        self.completed_parts.push(part_number);
32        self.uploaded_chunks += 1;
33
34        // Store etag in part_etags JSON
35        let etags = self.part_etags.get_or_insert_with(|| serde_json::json!({}));
36        if let Some(obj) = etags.as_object_mut() {
37            obj.insert(part_number.to_string(), serde_json::Value::String(etag));
38        }
39
40        self.status = UploadStatus::Uploading;
41        self.metadata.updated_at = Some(Utc::now());
42
43        Ok(())
44    }
45
46    /// Check if all parts uploaded
47    pub fn is_complete(&self) -> bool {
48        self.uploaded_chunks >= self.total_chunks
49    }
50
51    /// Get upload progress percentage
52    pub fn calculate_progress(&self) -> i32 {
53        if self.total_chunks <= 0 {
54            return 0;
55        }
56        ((self.uploaded_chunks as f64 / self.total_chunks as f64) * 100.0) as i32
57    }
58
59    /// Get number of remaining chunks
60    pub fn remaining_chunks(&self) -> i32 {
61        (self.total_chunks - self.uploaded_chunks).max(0)
62    }
63
64    /// Check if upload can be resumed
65    pub fn can_resume(&self) -> bool {
66        if self.is_expired() {
67            return false;
68        }
69        matches!(self.status, UploadStatus::Initiated | UploadStatus::Uploading)
70    }
71
72    /// Mark session as completing
73    pub fn mark_complete(&mut self) -> Result<(), UploadError> {
74        if !self.is_complete() {
75            return Err(UploadError::ValidationFailed(
76                format!("Upload not complete: {}/{} chunks uploaded", self.uploaded_chunks, self.total_chunks),
77            ));
78        }
79        self.status = UploadStatus::Completed;
80        self.metadata.updated_at = Some(Utc::now());
81        Ok(())
82    }
83
84    /// Mark session as failed
85    pub fn mark_failed(&mut self, error: String) -> Result<(), UploadError> {
86        self.status = UploadStatus::Failed;
87        // Store error message in part_etags JSON under "_error" key
88        let etags = self.part_etags.get_or_insert_with(|| serde_json::json!({}));
89        if let Some(obj) = etags.as_object_mut() {
90            obj.insert("_error".to_string(), serde_json::Value::String(error));
91        }
92        self.metadata.updated_at = Some(Utc::now());
93        Ok(())
94    }
95
96    /// Check all business invariants
97    pub fn check_invariants(&self) -> Result<(), Vec<&'static str>> {
98        let mut errors = Vec::new();
99        // Invariant 1: uploaded_chunks <= total_chunks
100        if self.uploaded_chunks > self.total_chunks {
101            errors.push("uploaded_chunks must not exceed total_chunks");
102        }
103        // Invariant 2: uploaded_chunks == completed_parts.length
104        if self.uploaded_chunks != self.completed_parts.len() as i32 {
105            errors.push("uploaded_chunks must equal completed_parts length");
106        }
107        // Invariant 3: file_size > 0 and total_chunks > 0
108        if self.file_size <= 0 {
109            errors.push("file_size must be greater than 0");
110        }
111        if self.total_chunks <= 0 {
112            errors.push("total_chunks must be greater than 0");
113        }
114        // Invariant 4: chunk_size >= 5 * 1024 * 1024 (minimum 5MB)
115        if self.chunk_size < 5 * 1024 * 1024 {
116            errors.push("chunk_size must be at least 5MB (5242880 bytes)");
117        }
118        if errors.is_empty() { Ok(()) } else { Err(errors) }
119    }
120}