1use crate::error::StorageError;
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use std::sync::atomic::{AtomicU64, Ordering};
10use std::sync::Mutex;
11use std::time::{Duration, SystemTime, UNIX_EPOCH};
12
13static UPLOAD_ID_COUNTER: AtomicU64 = AtomicU64::new(0);
15
16static REFRESH_ID_COUNTER: AtomicU64 = AtomicU64::new(0);
18
19#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct Part {
26 pub number: u32,
28 pub data: Vec<u8>,
30 pub size: usize,
32 pub etag: String,
34}
35
36impl Part {
37 pub fn new(number: u32, data: Vec<u8>) -> Self {
38 let size = data.len();
39 let etag = format!("etag-{:x}-{:x}", number, size);
40 Self {
41 number,
42 data,
43 size,
44 etag,
45 }
46 }
47}
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
51pub enum UploadStatus {
52 Initiated,
54 InProgress,
56 Completed,
58 Aborted,
60}
61
62#[derive(Debug, Serialize, Deserialize)]
64pub struct MultipartUpload {
65 pub upload_id: String,
67 pub key: String,
69 pub content_type: String,
71 pub parts: Vec<Part>,
73 pub expected_parts: u32,
75 pub part_size: usize,
77 pub status: UploadStatus,
79 pub created_at: u64,
81}
82
83impl MultipartUpload {
84 pub fn new(key: impl Into<String>, content_type: impl Into<String>, part_size: usize) -> Self {
86 let now = SystemTime::now()
87 .duration_since(UNIX_EPOCH)
88 .unwrap_or_default()
89 .as_secs();
90 let counter = UPLOAD_ID_COUNTER.fetch_add(1, Ordering::SeqCst);
91 Self {
92 upload_id: format!("upload-{}-{}", now, counter),
93 key: key.into(),
94 content_type: content_type.into(),
95 parts: Vec::new(),
96 expected_parts: 0,
97 part_size,
98 status: UploadStatus::Initiated,
99 created_at: now,
100 }
101 }
102
103 pub fn upload_part(&mut self, number: u32, data: Vec<u8>) -> Result<String, StorageError> {
105 if self.status == UploadStatus::Completed || self.status == UploadStatus::Aborted {
106 return Err(StorageError::Put(format!(
107 "upload {} already {:?}",
108 self.upload_id, self.status
109 )));
110 }
111 let next_number = self.parts.last().map(|p| p.number + 1).unwrap_or(1);
113 if number != next_number {
114 return Err(StorageError::Put(format!(
115 "expected part {}, got {}",
116 next_number, number
117 )));
118 }
119 if number < self.expected_parts && data.len() != self.part_size {
121 return Err(StorageError::Put(format!(
122 "part {} size {} != expected {}",
123 number,
124 data.len(),
125 self.part_size
126 )));
127 }
128 let part = Part::new(number, data);
129 let etag = part.etag.clone();
130 self.parts.push(part);
131 self.status = UploadStatus::InProgress;
132 Ok(etag)
133 }
134
135 pub fn complete(&mut self) -> Result<Vec<u8>, StorageError> {
137 if self.status == UploadStatus::Completed {
138 return Err(StorageError::Put(format!(
139 "upload {} already completed",
140 self.upload_id
141 )));
142 }
143 if self.status == UploadStatus::Aborted {
144 return Err(StorageError::Put(format!(
145 "upload {} was aborted",
146 self.upload_id
147 )));
148 }
149 if self.parts.is_empty() {
150 return Err(StorageError::Put(format!(
151 "upload {} has no parts",
152 self.upload_id
153 )));
154 }
155 for (idx, part) in self.parts.iter().enumerate() {
157 if part.number as usize != idx + 1 {
158 return Err(StorageError::Put(format!(
159 "part numbering gap: expected {}, got {}",
160 idx + 1,
161 part.number
162 )));
163 }
164 }
165 let mut combined = Vec::new();
166 for part in &self.parts {
167 combined.extend_from_slice(&part.data);
168 }
169 self.status = UploadStatus::Completed;
170 Ok(combined)
171 }
172
173 pub fn abort(&mut self) -> Result<(), StorageError> {
175 if self.status == UploadStatus::Completed {
176 return Err(StorageError::Put(format!(
177 "upload {} already completed, cannot abort",
178 self.upload_id
179 )));
180 }
181 self.status = UploadStatus::Aborted;
182 self.parts.clear();
183 Ok(())
184 }
185
186 pub fn uploaded_part_count(&self) -> usize {
188 self.parts.len()
189 }
190
191 pub fn uploaded_bytes(&self) -> usize {
193 self.parts.iter().map(|p| p.size).sum()
194 }
195
196 pub fn progress_percent(&self) -> u8 {
198 if self.expected_parts == 0 {
199 return if self.status == UploadStatus::Completed {
200 100
201 } else {
202 0
203 };
204 }
205 ((self.parts.len() as f64 / self.expected_parts as f64) * 100.0) as u8
206 }
207
208 pub fn is_completed(&self) -> bool {
210 self.status == UploadStatus::Completed
211 }
212}
213
214pub struct ResumableUploadManager {
220 sessions: Mutex<HashMap<String, MultipartUpload>>,
222 completed: Mutex<HashMap<String, Vec<u8>>>,
224}
225
226impl ResumableUploadManager {
227 pub fn new() -> Self {
228 Self {
229 sessions: Mutex::new(HashMap::new()),
230 completed: Mutex::new(HashMap::new()),
231 }
232 }
233
234 pub fn initiate(
236 &self,
237 key: &str,
238 content_type: &str,
239 total_size: usize,
240 part_size: usize,
241 ) -> Result<String, StorageError> {
242 let mut upload = MultipartUpload::new(key, content_type, part_size);
243 upload.expected_parts = total_size.div_ceil(part_size) as u32;
244 let upload_id = upload.upload_id.clone();
245 let mut sessions = self
246 .sessions
247 .lock()
248 .map_err(|e| StorageError::Connection(format!("lock error: {}", e)))?;
249 sessions.insert(upload_id.clone(), upload);
250 Ok(upload_id)
251 }
252
253 pub fn upload_part(
255 &self,
256 upload_id: &str,
257 number: u32,
258 data: Vec<u8>,
259 ) -> Result<String, StorageError> {
260 let mut sessions = self
261 .sessions
262 .lock()
263 .map_err(|e| StorageError::Connection(format!("lock error: {}", e)))?;
264 let upload = sessions
265 .get_mut(upload_id)
266 .ok_or_else(|| StorageError::NotFound(format!("upload {}", upload_id)))?;
267 upload.upload_part(number, data)
268 }
269
270 pub fn complete(&self, upload_id: &str) -> Result<Vec<u8>, StorageError> {
272 let mut sessions = self
273 .sessions
274 .lock()
275 .map_err(|e| StorageError::Connection(format!("lock error: {}", e)))?;
276 let upload = sessions
277 .get_mut(upload_id)
278 .ok_or_else(|| StorageError::NotFound(format!("upload {}", upload_id)))?;
279 let combined = upload.complete()?;
280 let key = upload.key.clone();
281 if let Ok(mut completed) = self.completed.lock() {
283 completed.insert(key, combined.clone());
284 }
285 Ok(combined)
286 }
287
288 pub fn abort(&self, upload_id: &str) -> Result<(), StorageError> {
290 let mut sessions = self
291 .sessions
292 .lock()
293 .map_err(|e| StorageError::Connection(format!("lock error: {}", e)))?;
294 let upload = sessions
295 .get_mut(upload_id)
296 .ok_or_else(|| StorageError::NotFound(format!("upload {}", upload_id)))?;
297 upload.abort()
298 }
299
300 pub fn get_session(&self, upload_id: &str) -> Option<MultipartUploadSnapshot> {
302 let sessions = self.sessions.lock().ok()?;
303 let upload = sessions.get(upload_id)?;
304 Some(MultipartUploadSnapshot {
305 upload_id: upload.upload_id.clone(),
306 key: upload.key.clone(),
307 content_type: upload.content_type.clone(),
308 uploaded_part_numbers: upload.parts.iter().map(|p| p.number).collect(),
309 expected_parts: upload.expected_parts,
310 part_size: upload.part_size,
311 status: upload.status,
312 uploaded_bytes: upload.uploaded_bytes(),
313 progress_percent: upload.progress_percent(),
314 })
315 }
316
317 pub fn list_uploads(&self) -> Vec<String> {
319 self.sessions
320 .lock()
321 .map(|s| {
322 s.iter()
323 .filter(|(_, u)| {
324 u.status != UploadStatus::Completed && u.status != UploadStatus::Aborted
325 })
326 .map(|(k, _)| k.clone())
327 .collect()
328 })
329 .unwrap_or_default()
330 }
331
332 pub fn get_completed_data(&self, key: &str) -> Option<Vec<u8>> {
334 self.completed.lock().ok().and_then(|c| c.get(key).cloned())
335 }
336
337 pub fn cleanup(&self) -> usize {
339 let mut sessions = match self.sessions.lock() {
340 Ok(s) => s,
341 Err(_) => return 0,
342 };
343 let before = sessions.len();
344 sessions.retain(|_, u| {
345 u.status == UploadStatus::Initiated || u.status == UploadStatus::InProgress
346 });
347 before - sessions.len()
348 }
349}
350
351impl Default for ResumableUploadManager {
352 fn default() -> Self {
353 Self::new()
354 }
355}
356
357#[derive(Debug, Clone, Serialize, Deserialize)]
359pub struct MultipartUploadSnapshot {
360 pub upload_id: String,
361 pub key: String,
362 pub content_type: String,
363 pub uploaded_part_numbers: Vec<u32>,
365 pub expected_parts: u32,
366 pub part_size: usize,
367 pub status: UploadStatus,
368 pub uploaded_bytes: usize,
369 pub progress_percent: u8,
370}
371
372#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
378pub enum LifecycleAction {
379 Transition {
381 storage_class: String,
383 days: u32,
385 },
386 Expiration {
388 days: u32,
390 },
391 AbortIncompleteMultipartUpload {
393 days: u32,
395 },
396}
397
398#[derive(Debug, Clone, Serialize, Deserialize)]
400pub struct LifecycleRule {
401 pub id: String,
403 pub enabled: bool,
405 pub prefix: String,
407 pub action: LifecycleAction,
409}
410
411impl LifecycleRule {
412 pub fn new(id: impl Into<String>, action: LifecycleAction) -> Self {
413 Self {
414 id: id.into(),
415 enabled: true,
416 prefix: String::new(),
417 action,
418 }
419 }
420
421 pub fn with_prefix(mut self, prefix: impl Into<String>) -> Self {
422 self.prefix = prefix.into();
423 self
424 }
425
426 pub fn disabled(mut self) -> Self {
427 self.enabled = false;
428 self
429 }
430
431 pub fn matches(&self, key: &str) -> bool {
433 self.enabled && (self.prefix.is_empty() || key.starts_with(&self.prefix))
434 }
435}
436
437pub struct BucketLifecycle {
439 rules: Vec<LifecycleRule>,
441}
442
443impl BucketLifecycle {
444 pub fn new() -> Self {
445 Self { rules: Vec::new() }
446 }
447
448 pub fn add_rule(&mut self, rule: LifecycleRule) {
450 self.rules.push(rule);
451 }
452
453 pub fn remove_rule(&mut self, id: &str) -> bool {
455 let before = self.rules.len();
456 self.rules.retain(|r| r.id != id);
457 self.rules.len() < before
458 }
459
460 pub fn rules(&self) -> &[LifecycleRule] {
462 &self.rules
463 }
464
465 pub fn matching_rules(&self, key: &str) -> Vec<&LifecycleRule> {
467 self.rules.iter().filter(|r| r.matches(key)).collect()
468 }
469
470 pub fn evaluate(
472 &self,
473 key: &str,
474 object_age_days: u32,
475 has_incomplete_upload: bool,
476 ) -> Vec<LifecycleEvaluationResult> {
477 let mut results = Vec::new();
478 for rule in self.matching_rules(key) {
479 match &rule.action {
480 LifecycleAction::Transition {
481 storage_class,
482 days,
483 } => {
484 if object_age_days >= *days {
485 results.push(LifecycleEvaluationResult {
486 rule_id: rule.id.clone(),
487 key: key.to_string(),
488 action: LifecycleAction::Transition {
489 storage_class: storage_class.clone(),
490 days: *days,
491 },
492 });
493 }
494 }
495 LifecycleAction::Expiration { days } => {
496 if object_age_days >= *days {
497 results.push(LifecycleEvaluationResult {
498 rule_id: rule.id.clone(),
499 key: key.to_string(),
500 action: LifecycleAction::Expiration { days: *days },
501 });
502 }
503 }
504 LifecycleAction::AbortIncompleteMultipartUpload { days } => {
505 if has_incomplete_upload {
506 results.push(LifecycleEvaluationResult {
507 rule_id: rule.id.clone(),
508 key: key.to_string(),
509 action: LifecycleAction::AbortIncompleteMultipartUpload { days: *days },
510 });
511 }
512 }
513 }
514 }
515 results
516 }
517
518 pub fn rule_count(&self) -> usize {
520 self.rules.len()
521 }
522
523 pub fn enable_rule(&mut self, id: &str) -> bool {
525 let mut found = false;
526 for rule in &mut self.rules {
527 if rule.id == id {
528 rule.enabled = true;
529 found = true;
530 }
531 }
532 found
533 }
534
535 pub fn disable_rule(&mut self, id: &str) -> bool {
537 let mut found = false;
538 for rule in &mut self.rules {
539 if rule.id == id {
540 rule.enabled = false;
541 found = true;
542 }
543 }
544 found
545 }
546}
547
548impl Default for BucketLifecycle {
549 fn default() -> Self {
550 Self::new()
551 }
552}
553
554#[derive(Debug, Clone, Serialize, Deserialize)]
556pub struct LifecycleEvaluationResult {
557 pub rule_id: String,
558 pub key: String,
559 pub action: LifecycleAction,
560}
561
562#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
568pub enum RefreshType {
569 Url,
571 Directory,
573}
574
575#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
577pub enum RefreshStatus {
578 Pending,
580 Done,
582 Failed,
584}
585
586#[derive(Debug, Clone, Serialize, Deserialize)]
588pub struct RefreshRequest {
589 pub request_id: String,
591 pub refresh_type: RefreshType,
593 pub targets: Vec<String>,
595 pub submitted_at: u64,
597 pub status: RefreshStatus,
599 pub error: Option<String>,
601}
602
603impl RefreshRequest {
604 pub fn new(refresh_type: RefreshType, targets: Vec<String>) -> Self {
605 let now = SystemTime::now()
606 .duration_since(UNIX_EPOCH)
607 .unwrap_or_default()
608 .as_secs();
609 let counter = REFRESH_ID_COUNTER.fetch_add(1, Ordering::SeqCst);
610 Self {
611 request_id: format!("refresh-{}-{}", now, counter),
612 refresh_type,
613 targets,
614 submitted_at: now,
615 status: RefreshStatus::Pending,
616 error: None,
617 }
618 }
619}
620
621pub struct CdnRefresher {
623 history: Mutex<Vec<RefreshRequest>>,
625 rate_limit: Mutex<Vec<u64>>,
627 pub max_requests_per_minute: u32,
629 pub max_targets_per_request: u32,
631}
632
633impl CdnRefresher {
634 pub fn new() -> Self {
635 Self {
636 history: Mutex::new(Vec::new()),
637 rate_limit: Mutex::new(Vec::new()),
638 max_requests_per_minute: 100,
639 max_targets_per_request: 1000,
640 }
641 }
642
643 pub fn with_rate_limit(mut self, max_per_minute: u32) -> Self {
645 self.max_requests_per_minute = max_per_minute;
646 self
647 }
648
649 pub fn refresh_urls(&self, urls: Vec<String>) -> Result<String, StorageError> {
651 self.submit(RefreshType::Url, urls)
652 }
653
654 pub fn refresh_dirs(&self, dirs: Vec<String>) -> Result<String, StorageError> {
656 self.submit(RefreshType::Directory, dirs)
657 }
658
659 fn submit(
661 &self,
662 refresh_type: RefreshType,
663 targets: Vec<String>,
664 ) -> Result<String, StorageError> {
665 if targets.is_empty() {
666 return Err(StorageError::InvalidConfig(
667 "refresh targets cannot be empty".to_string(),
668 ));
669 }
670 if targets.len() as u32 > self.max_targets_per_request {
671 return Err(StorageError::InvalidConfig(format!(
672 "too many targets: {} > {}",
673 targets.len(),
674 self.max_targets_per_request
675 )));
676 }
677 self.check_rate_limit()?;
679 let mut request = RefreshRequest::new(refresh_type, targets);
680 request.status = RefreshStatus::Done;
682 let request_id = request.request_id.clone();
683 let mut history = self
684 .history
685 .lock()
686 .map_err(|e| StorageError::Connection(format!("lock error: {}", e)))?;
687 history.push(request);
688 Ok(request_id)
689 }
690
691 fn check_rate_limit(&self) -> Result<(), StorageError> {
693 let now = SystemTime::now()
694 .duration_since(UNIX_EPOCH)
695 .unwrap_or_default()
696 .as_secs();
697 let mut rate = self
698 .rate_limit
699 .lock()
700 .map_err(|e| StorageError::Connection(format!("lock error: {}", e)))?;
701 rate.retain(|&t| now - t < 60);
703 if rate.len() as u32 >= self.max_requests_per_minute {
704 return Err(StorageError::InvalidConfig(format!(
705 "rate limit exceeded: {} requests in last minute",
706 rate.len()
707 )));
708 }
709 rate.push(now);
710 Ok(())
711 }
712
713 pub fn get_status(&self, request_id: &str) -> Option<RefreshStatus> {
715 let history = self.history.lock().ok()?;
716 history
717 .iter()
718 .find(|r| r.request_id == request_id)
719 .map(|r| r.status)
720 }
721
722 pub fn get_request(&self, request_id: &str) -> Option<RefreshRequest> {
724 let history = self.history.lock().ok()?;
725 history.iter().find(|r| r.request_id == request_id).cloned()
726 }
727
728 pub fn history(&self) -> Vec<RefreshRequest> {
730 self.history.lock().map(|h| h.clone()).unwrap_or_default()
731 }
732
733 pub fn total_requests(&self) -> usize {
735 self.history.lock().map(|h| h.len()).unwrap_or(0)
736 }
737
738 pub fn requests_in_last(&self, seconds: u64) -> usize {
740 let now = SystemTime::now()
741 .duration_since(UNIX_EPOCH)
742 .unwrap_or_default()
743 .as_secs();
744 self.history
745 .lock()
746 .map(|h| h.iter().filter(|r| now - r.submitted_at < seconds).count())
747 .unwrap_or(0)
748 }
749
750 pub fn prefetch(&self, urls: Vec<String>) -> Result<String, StorageError> {
752 if urls.is_empty() {
753 return Err(StorageError::InvalidConfig(
754 "prefetch urls cannot be empty".to_string(),
755 ));
756 }
757 self.check_rate_limit()?;
758 let mut request = RefreshRequest::new(RefreshType::Url, urls);
759 request.status = RefreshStatus::Done;
760 let request_id = request.request_id.clone();
761 let mut history = self
762 .history
763 .lock()
764 .map_err(|e| StorageError::Connection(format!("lock error: {}", e)))?;
765 history.push(request);
766 Ok(request_id)
767 }
768}
769
770impl Default for CdnRefresher {
771 fn default() -> Self {
772 Self::new()
773 }
774}
775
776pub fn object_age_days(created_at: SystemTime) -> u32 {
782 let now = SystemTime::now();
783 match now.duration_since(created_at) {
784 Ok(d) => (d.as_secs() / 86400) as u32,
785 Err(_) => 0,
786 }
787}
788
789pub fn format_size(bytes: usize) -> String {
791 const UNITS: &[&str] = &["B", "KB", "MB", "GB", "TB"];
792 let mut size = bytes as f64;
793 let mut unit_idx = 0;
794 while size >= 1024.0 && unit_idx < UNITS.len() - 1 {
795 size /= 1024.0;
796 unit_idx += 1;
797 }
798 format!("{:.2} {}", size, UNITS[unit_idx])
799}
800
801pub fn estimate_remaining_seconds(
803 uploaded_bytes: usize,
804 total_bytes: usize,
805 elapsed: Duration,
806) -> Option<u64> {
807 if uploaded_bytes == 0 || total_bytes == 0 {
808 return None;
809 }
810 let elapsed_secs = elapsed.as_secs();
811 if elapsed_secs == 0 {
812 return None;
813 }
814 let speed = uploaded_bytes as f64 / elapsed_secs as f64;
815 if speed < 1.0 {
816 return None;
817 }
818 let remaining = (total_bytes - uploaded_bytes) as f64 / speed;
819 Some(remaining as u64)
820}
821
822#[cfg(test)]
823mod tests {
824 use super::*;
825
826 #[test]
831 fn test_multipart_upload_init() {
832 let upload = MultipartUpload::new("test.txt", "text/plain", 1024);
833 assert!(!upload.upload_id.is_empty());
834 assert_eq!(upload.key, "test.txt");
835 assert_eq!(upload.content_type, "text/plain");
836 assert_eq!(upload.part_size, 1024);
837 assert_eq!(upload.status, UploadStatus::Initiated);
838 assert_eq!(upload.uploaded_part_count(), 0);
839 assert_eq!(upload.uploaded_bytes(), 0);
840 }
841
842 #[test]
843 fn test_multipart_upload_single_part() {
844 let mut upload = MultipartUpload::new("file.txt", "text/plain", 100);
845 upload.expected_parts = 1;
846 let etag = upload.upload_part(1, b"hello".to_vec()).unwrap();
847 assert!(!etag.is_empty());
848 assert_eq!(upload.uploaded_part_count(), 1);
849 assert_eq!(upload.uploaded_bytes(), 5);
850 assert_eq!(upload.status, UploadStatus::InProgress);
851
852 let combined = upload.complete().unwrap();
853 assert_eq!(combined, b"hello");
854 assert_eq!(upload.status, UploadStatus::Completed);
855 assert!(upload.is_completed());
856 }
857
858 #[test]
859 fn test_multipart_upload_multiple_parts() {
860 let mut upload = MultipartUpload::new("big.bin", "application/octet-stream", 10);
861 upload.expected_parts = 3;
862 upload.upload_part(1, vec![0u8; 10]).unwrap();
863 upload.upload_part(2, vec![1u8; 10]).unwrap();
864 upload.upload_part(3, vec![2u8; 5]).unwrap(); assert_eq!(upload.uploaded_part_count(), 3);
866 assert_eq!(upload.uploaded_bytes(), 25);
867
868 let combined = upload.complete().unwrap();
869 assert_eq!(combined.len(), 25);
870 assert_eq!(upload.progress_percent(), 100);
871 }
872
873 #[test]
874 fn test_multipart_upload_wrong_part_number() {
875 let mut upload = MultipartUpload::new("f", "text/plain", 10);
876 upload.expected_parts = 2;
877 upload.upload_part(1, vec![0u8; 10]).unwrap();
878 let result = upload.upload_part(3, vec![0u8; 10]);
880 assert!(result.is_err());
881 }
882
883 #[test]
884 fn test_multipart_upload_wrong_part_size() {
885 let mut upload = MultipartUpload::new("f", "text/plain", 10);
886 upload.expected_parts = 2;
887 let result = upload.upload_part(1, vec![0u8; 5]);
889 assert!(result.is_err());
890 }
891
892 #[test]
893 fn test_multipart_upload_complete_after_complete() {
894 let mut upload = MultipartUpload::new("f", "text/plain", 10);
895 upload.expected_parts = 1;
896 upload.upload_part(1, b"data".to_vec()).unwrap();
897 upload.complete().unwrap();
898 let result = upload.complete();
899 assert!(result.is_err());
900 }
901
902 #[test]
903 fn test_multipart_upload_abort() {
904 let mut upload = MultipartUpload::new("f", "text/plain", 10);
905 upload.expected_parts = 2;
906 upload.upload_part(1, vec![0u8; 10]).unwrap();
907 upload.abort().unwrap();
908 assert_eq!(upload.status, UploadStatus::Aborted);
909 assert_eq!(upload.uploaded_part_count(), 0);
910 let result = upload.upload_part(2, vec![0u8; 10]);
912 assert!(result.is_err());
913 }
914
915 #[test]
916 fn test_multipart_upload_complete_empty() {
917 let mut upload = MultipartUpload::new("f", "text/plain", 10);
918 let result = upload.complete();
919 assert!(result.is_err());
920 }
921
922 #[test]
923 fn test_multipart_upload_progress() {
924 let mut upload = MultipartUpload::new("f", "text/plain", 10);
925 upload.expected_parts = 4;
926 assert_eq!(upload.progress_percent(), 0);
927 upload.upload_part(1, vec![0u8; 10]).unwrap();
928 assert_eq!(upload.progress_percent(), 25);
929 upload.upload_part(2, vec![0u8; 10]).unwrap();
930 assert_eq!(upload.progress_percent(), 50);
931 upload.upload_part(3, vec![0u8; 10]).unwrap();
932 assert_eq!(upload.progress_percent(), 75);
933 }
934
935 #[test]
936 fn test_multipart_upload_abort_after_complete_fails() {
937 let mut upload = MultipartUpload::new("f", "text/plain", 10);
938 upload.expected_parts = 1;
939 upload.upload_part(1, b"data".to_vec()).unwrap();
940 upload.complete().unwrap();
941 let result = upload.abort();
942 assert!(result.is_err());
943 }
944
945 #[test]
950 fn test_resumable_initiate() {
951 let mgr = ResumableUploadManager::new();
952 let upload_id = mgr
953 .initiate("file.bin", "application/octet-stream", 1000, 100)
954 .unwrap();
955 assert!(!upload_id.is_empty());
956 let session = mgr.get_session(&upload_id).unwrap();
957 assert_eq!(session.key, "file.bin");
958 assert_eq!(session.expected_parts, 10);
959 assert_eq!(session.part_size, 100);
960 assert_eq!(session.uploaded_part_numbers.len(), 0);
961 }
962
963 #[test]
964 fn test_resumable_upload_and_complete() {
965 let mgr = ResumableUploadManager::new();
966 let upload_id = mgr
967 .initiate("file.bin", "application/octet-stream", 200, 100)
968 .unwrap();
969 mgr.upload_part(&upload_id, 1, vec![0u8; 100]).unwrap();
970 mgr.upload_part(&upload_id, 2, vec![1u8; 100]).unwrap();
971 let combined = mgr.complete(&upload_id).unwrap();
972 assert_eq!(combined.len(), 200);
973 let data = mgr.get_completed_data("file.bin").unwrap();
974 assert_eq!(data.len(), 200);
975 }
976
977 #[test]
978 fn test_resumable_abort() {
979 let mgr = ResumableUploadManager::new();
980 let upload_id = mgr
981 .initiate("file.bin", "application/octet-stream", 200, 100)
982 .unwrap();
983 mgr.upload_part(&upload_id, 1, vec![0u8; 100]).unwrap();
984 mgr.abort(&upload_id).unwrap();
985 let session = mgr.get_session(&upload_id).unwrap();
986 assert_eq!(session.status, UploadStatus::Aborted);
987 }
988
989 #[test]
990 fn test_resumable_list_uploads() {
991 let mgr = ResumableUploadManager::new();
992 let id1 = mgr.initiate("a", "text/plain", 100, 50).unwrap();
993 let _id2 = mgr.initiate("b", "text/plain", 100, 50).unwrap();
994 assert_eq!(mgr.list_uploads().len(), 2);
995 mgr.upload_part(&id1, 1, vec![0u8; 50]).unwrap();
997 mgr.upload_part(&id1, 2, vec![0u8; 50]).unwrap();
998 mgr.complete(&id1).unwrap();
999 assert_eq!(mgr.list_uploads().len(), 1);
1001 }
1002
1003 #[test]
1004 fn test_resumable_get_session_not_found() {
1005 let mgr = ResumableUploadManager::new();
1006 assert!(mgr.get_session("nonexistent").is_none());
1007 }
1008
1009 #[test]
1010 fn test_resumable_upload_part_invalid_session() {
1011 let mgr = ResumableUploadManager::new();
1012 let result = mgr.upload_part("nonexistent", 1, vec![0u8; 10]);
1013 assert!(result.is_err());
1014 }
1015
1016 #[test]
1017 fn test_resumable_complete_invalid_session() {
1018 let mgr = ResumableUploadManager::new();
1019 let result = mgr.complete("nonexistent");
1020 assert!(result.is_err());
1021 }
1022
1023 #[test]
1024 fn test_resumable_cleanup() {
1025 let mgr = ResumableUploadManager::new();
1026 let id1 = mgr.initiate("a", "text/plain", 100, 50).unwrap();
1027 let id2 = mgr.initiate("b", "text/plain", 100, 50).unwrap();
1028 mgr.upload_part(&id1, 1, vec![0u8; 50]).unwrap();
1030 mgr.upload_part(&id1, 2, vec![0u8; 50]).unwrap();
1031 mgr.complete(&id1).unwrap();
1032 mgr.abort(&id2).unwrap();
1033 let cleaned = mgr.cleanup();
1034 assert_eq!(cleaned, 2);
1035 }
1036
1037 #[test]
1038 fn test_resumable_snapshot_has_uploaded_parts() {
1039 let mgr = ResumableUploadManager::new();
1040 let upload_id = mgr.initiate("f", "text/plain", 300, 100).unwrap();
1041 mgr.upload_part(&upload_id, 1, vec![0u8; 100]).unwrap();
1042 mgr.upload_part(&upload_id, 2, vec![0u8; 100]).unwrap();
1043 let snapshot = mgr.get_session(&upload_id).unwrap();
1044 assert_eq!(snapshot.uploaded_part_numbers, vec![1, 2]);
1045 assert_eq!(snapshot.uploaded_bytes, 200);
1046 assert_eq!(snapshot.progress_percent, 66);
1047 }
1048
1049 #[test]
1054 fn test_lifecycle_add_and_remove_rule() {
1055 let mut lc = BucketLifecycle::new();
1056 lc.add_rule(LifecycleRule::new(
1057 "expire-30d",
1058 LifecycleAction::Expiration { days: 30 },
1059 ));
1060 assert_eq!(lc.rule_count(), 1);
1061 assert!(lc.remove_rule("expire-30d"));
1062 assert_eq!(lc.rule_count(), 0);
1063 assert!(!lc.remove_rule("nonexistent"));
1064 }
1065
1066 #[test]
1067 fn test_lifecycle_rule_matches_prefix() {
1068 let rule = LifecycleRule::new(
1069 "logs",
1070 LifecycleAction::Transition {
1071 storage_class: "IA".to_string(),
1072 days: 30,
1073 },
1074 )
1075 .with_prefix("logs/");
1076 assert!(rule.matches("logs/app.log"));
1077 assert!(!rule.matches("images/photo.jpg"));
1078 }
1079
1080 #[test]
1081 fn test_lifecycle_rule_empty_prefix_matches_all() {
1082 let rule = LifecycleRule::new("all", LifecycleAction::Expiration { days: 365 });
1083 assert!(rule.matches("anything"));
1084 assert!(rule.matches("path/to/file"));
1085 }
1086
1087 #[test]
1088 fn test_lifecycle_rule_disabled_does_not_match() {
1089 let rule = LifecycleRule::new("r", LifecycleAction::Expiration { days: 30 }).disabled();
1090 assert!(!rule.matches("any"));
1091 }
1092
1093 #[test]
1094 fn test_lifecycle_evaluate_expiration() {
1095 let mut lc = BucketLifecycle::new();
1096 lc.add_rule(LifecycleRule::new(
1097 "expire-30d",
1098 LifecycleAction::Expiration { days: 30 },
1099 ));
1100 let results = lc.evaluate("file.txt", 20, false);
1102 assert!(results.is_empty());
1103 let results = lc.evaluate("file.txt", 35, false);
1105 assert_eq!(results.len(), 1);
1106 assert!(matches!(
1107 results[0].action,
1108 LifecycleAction::Expiration { .. }
1109 ));
1110 }
1111
1112 #[test]
1113 fn test_lifecycle_evaluate_transition() {
1114 let mut lc = BucketLifecycle::new();
1115 lc.add_rule(LifecycleRule::new(
1116 "to-ia",
1117 LifecycleAction::Transition {
1118 storage_class: "IA".to_string(),
1119 days: 30,
1120 },
1121 ));
1122 let results = lc.evaluate("data.bin", 40, false);
1123 assert_eq!(results.len(), 1);
1124 if let LifecycleAction::Transition { storage_class, .. } = &results[0].action {
1125 assert_eq!(storage_class, "IA");
1126 } else {
1127 panic!("expected Transition action");
1128 }
1129 }
1130
1131 #[test]
1132 fn test_lifecycle_evaluate_abort_incomplete() {
1133 let mut lc = BucketLifecycle::new();
1134 lc.add_rule(LifecycleRule::new(
1135 "abort-multipart",
1136 LifecycleAction::AbortIncompleteMultipartUpload { days: 7 },
1137 ));
1138 let results = lc.evaluate("file", 0, false);
1140 assert!(results.is_empty());
1141 let results = lc.evaluate("file", 0, true);
1143 assert_eq!(results.len(), 1);
1144 }
1145
1146 #[test]
1147 fn test_lifecycle_enable_disable_rule() {
1148 let mut lc = BucketLifecycle::new();
1149 lc.add_rule(LifecycleRule::new(
1150 "r",
1151 LifecycleAction::Expiration { days: 30 },
1152 ));
1153 assert!(lc.disable_rule("r"));
1154 assert!(!lc.matching_rules("file").iter().any(|r| r.id == "r"));
1155 assert!(lc.enable_rule("r"));
1156 assert!(lc.matching_rules("file").iter().any(|r| r.id == "r"));
1157 }
1158
1159 #[test]
1160 fn test_lifecycle_matching_rules_filtered_by_prefix() {
1161 let mut lc = BucketLifecycle::new();
1162 lc.add_rule(
1163 LifecycleRule::new("logs", LifecycleAction::Expiration { days: 30 })
1164 .with_prefix("logs/"),
1165 );
1166 lc.add_rule(
1167 LifecycleRule::new("imgs", LifecycleAction::Expiration { days: 60 })
1168 .with_prefix("images/"),
1169 );
1170 let matches = lc.matching_rules("logs/app.log");
1171 assert_eq!(matches.len(), 1);
1172 assert_eq!(matches[0].id, "logs");
1173 }
1174
1175 #[test]
1176 fn test_lifecycle_multiple_rules_match_same_key() {
1177 let mut lc = BucketLifecycle::new();
1178 lc.add_rule(LifecycleRule::new(
1179 "to-ia",
1180 LifecycleAction::Transition {
1181 storage_class: "IA".to_string(),
1182 days: 30,
1183 },
1184 ));
1185 lc.add_rule(LifecycleRule::new(
1186 "expire",
1187 LifecycleAction::Expiration { days: 365 },
1188 ));
1189 let results = lc.evaluate("file", 400, false);
1190 assert_eq!(results.len(), 2);
1191 }
1192
1193 #[test]
1198 fn test_cdn_refresh_urls() {
1199 let refresher = CdnRefresher::new();
1200 let id = refresher
1201 .refresh_urls(vec![
1202 "https://cdn.example.com/a.js".to_string(),
1203 "https://cdn.example.com/b.css".to_string(),
1204 ])
1205 .unwrap();
1206 assert!(!id.is_empty());
1207 assert_eq!(refresher.total_requests(), 1);
1208 let status = refresher.get_status(&id).unwrap();
1209 assert_eq!(status, RefreshStatus::Done);
1210 }
1211
1212 #[test]
1213 fn test_cdn_refresh_dirs() {
1214 let refresher = CdnRefresher::new();
1215 let id = refresher
1216 .refresh_dirs(vec!["https://cdn.example.com/static/".to_string()])
1217 .unwrap();
1218 let request = refresher.get_request(&id).unwrap();
1219 assert_eq!(request.refresh_type, RefreshType::Directory);
1220 assert_eq!(request.targets.len(), 1);
1221 }
1222
1223 #[test]
1224 fn test_cdn_refresh_empty_targets() {
1225 let refresher = CdnRefresher::new();
1226 let result = refresher.refresh_urls(vec![]);
1227 assert!(result.is_err());
1228 }
1229
1230 #[test]
1231 fn test_cdn_refresh_too_many_targets() {
1232 let refresher = CdnRefresher::new().with_rate_limit(100);
1233 let urls: Vec<String> = (0..2000)
1234 .map(|i| format!("https://cdn.example.com/{}.js", i))
1235 .collect();
1236 let result = refresher.refresh_urls(urls);
1237 assert!(result.is_err());
1238 }
1239
1240 #[test]
1241 fn test_cdn_refresh_history() {
1242 let refresher = CdnRefresher::new();
1243 refresher
1244 .refresh_urls(vec!["https://cdn.example.com/a".to_string()])
1245 .unwrap();
1246 refresher
1247 .refresh_urls(vec!["https://cdn.example.com/b".to_string()])
1248 .unwrap();
1249 refresher
1250 .refresh_dirs(vec!["https://cdn.example.com/static/".to_string()])
1251 .unwrap();
1252 let history = refresher.history();
1253 assert_eq!(history.len(), 3);
1254 }
1255
1256 #[test]
1257 fn test_cdn_get_status_not_found() {
1258 let refresher = CdnRefresher::new();
1259 assert!(refresher.get_status("nonexistent").is_none());
1260 }
1261
1262 #[test]
1263 fn test_cdn_get_request_not_found() {
1264 let refresher = CdnRefresher::new();
1265 assert!(refresher.get_request("nonexistent").is_none());
1266 }
1267
1268 #[test]
1269 fn test_cdn_prefetch() {
1270 let refresher = CdnRefresher::new();
1271 let id = refresher
1272 .prefetch(vec!["https://cdn.example.com/big-file.zip".to_string()])
1273 .unwrap();
1274 assert_eq!(refresher.get_status(&id).unwrap(), RefreshStatus::Done);
1275 }
1276
1277 #[test]
1278 fn test_cdn_prefetch_empty() {
1279 let refresher = CdnRefresher::new();
1280 let result = refresher.prefetch(vec![]);
1281 assert!(result.is_err());
1282 }
1283
1284 #[test]
1285 fn test_cdn_rate_limit() {
1286 let refresher = CdnRefresher::new().with_rate_limit(3);
1287 refresher
1288 .refresh_urls(vec!["https://a.com".to_string()])
1289 .unwrap();
1290 refresher
1291 .refresh_urls(vec!["https://b.com".to_string()])
1292 .unwrap();
1293 refresher
1294 .refresh_urls(vec!["https://c.com".to_string()])
1295 .unwrap();
1296 let result = refresher.refresh_urls(vec!["https://d.com".to_string()]);
1298 assert!(result.is_err());
1299 }
1300
1301 #[test]
1302 fn test_cdn_requests_in_last() {
1303 let refresher = CdnRefresher::new();
1304 refresher
1305 .refresh_urls(vec!["https://a.com".to_string()])
1306 .unwrap();
1307 refresher
1308 .refresh_urls(vec!["https://b.com".to_string()])
1309 .unwrap();
1310 assert_eq!(refresher.requests_in_last(60), 2);
1311 }
1312
1313 #[test]
1318 fn test_format_size() {
1319 assert_eq!(format_size(0), "0.00 B");
1320 assert_eq!(format_size(512), "512.00 B");
1321 assert_eq!(format_size(1024), "1.00 KB");
1322 assert_eq!(format_size(1048576), "1.00 MB");
1323 assert_eq!(format_size(1073741824), "1.00 GB");
1324 }
1325
1326 #[test]
1327 fn test_estimate_remaining_seconds() {
1328 let remaining = estimate_remaining_seconds(100, 200, Duration::from_secs(10));
1330 assert_eq!(remaining, Some(10));
1331 }
1332
1333 #[test]
1334 fn test_estimate_remaining_seconds_zero_uploaded() {
1335 let remaining = estimate_remaining_seconds(0, 100, Duration::from_secs(5));
1336 assert_eq!(remaining, None);
1337 }
1338
1339 #[test]
1340 fn test_estimate_remaining_seconds_zero_elapsed() {
1341 let remaining = estimate_remaining_seconds(50, 100, Duration::from_secs(0));
1342 assert_eq!(remaining, None);
1343 }
1344
1345 #[test]
1346 fn test_object_age_days() {
1347 let one_day_ago = SystemTime::now() - Duration::from_secs(86400);
1348 let age = object_age_days(one_day_ago);
1349 assert!(age >= 1);
1350 }
1351
1352 #[test]
1353 fn test_part_etag_unique() {
1354 let p1 = Part::new(1, vec![0u8; 10]);
1355 let p2 = Part::new(2, vec![0u8; 10]);
1356 let p3 = Part::new(1, vec![0u8; 20]);
1357 assert_ne!(p1.etag, p2.etag);
1358 assert_ne!(p1.etag, p3.etag);
1359 }
1360}