helix_core/ports/
upload.rs1use std::sync::Arc;
7
8use crate::effect::{FileUploadRequest, FileUploadResponse};
9use crate::error::PortError;
10use crate::platform::{MaybeSend, MaybeSync};
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub struct FileUploadProgress {
15 pub completed_bytes: u64,
16 pub total_bytes: u64,
17}
18
19#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
21pub enum FileUploadProgressPolicy {
22 #[default]
23 Disabled,
24 PercentStep(u8),
26}
27
28pub trait FileUploadProgressReporter: MaybeSend + MaybeSync + 'static {
32 fn report(&self, progress: FileUploadProgress);
33}
34
35#[derive(Debug, Clone, PartialEq, Eq)]
37pub struct FileUploadUrls {
38 upload_url: String,
39 public_url: String,
40 progress_policy: FileUploadProgressPolicy,
42}
43
44impl FileUploadUrls {
45 pub fn new(upload_url: String, public_url: String) -> Result<Self, &'static str> {
46 if upload_url.trim().is_empty() || public_url.trim().is_empty() {
47 return Err("upload_url and public_url must not be empty");
48 }
49 Ok(Self {
50 upload_url,
51 public_url,
52 progress_policy: FileUploadProgressPolicy::Disabled,
53 })
54 }
55
56 pub fn with_progress_policy(mut self, policy: FileUploadProgressPolicy) -> Self {
57 self.progress_policy = policy;
58 self
59 }
60
61 pub fn progress_policy(&self) -> FileUploadProgressPolicy {
62 self.progress_policy
63 }
64
65 pub fn upload_url(&self) -> &str {
66 &self.upload_url
67 }
68
69 pub fn public_url(&self) -> &str {
70 &self.public_url
71 }
72
73 pub fn into_parts(self) -> (String, String) {
74 (self.upload_url, self.public_url)
75 }
76}
77
78#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
79#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
80pub trait FileUploader: MaybeSend + MaybeSync + 'static {
81 async fn upload(&self, req: FileUploadRequest) -> Result<FileUploadResponse, PortError>;
82
83 async fn upload_with_progress(
85 &self,
86 req: FileUploadRequest,
87 progress: Option<Arc<dyn FileUploadProgressReporter>>,
88 ) -> Result<FileUploadResponse, PortError> {
89 let _ = progress;
90 self.upload(req).await
91 }
92}
93
94#[cfg(test)]
95mod tests {
96 use super::*;
97 use crate::effect::{Correlation, Effect, FileUploadRequest};
98
99 #[test]
100 fn upload_effect_keeps_distinct_validated_urls() {
101 let req = FileUploadRequest {
102 local_path: "/tmp/a.png".into(),
103 object_key: "im/a.png".into(),
104 method: "PUT".into(),
105 urls: FileUploadUrls::new(
106 "https://upload/a?signature=secret".into(),
107 "https://cdn/a.png".into(),
108 )
109 .expect("valid urls")
110 .with_progress_policy(FileUploadProgressPolicy::PercentStep(5)),
111 headers: Vec::new(),
112 content_type: Some("image/png".into()),
113 size: Some(123),
114 };
115 let Effect::UploadFile { corr, req } = (Effect::UploadFile {
116 corr: Correlation::from_raw(9),
117 req,
118 }) else {
119 panic!("expected upload effect");
120 };
121 assert_eq!(corr.raw(), 9);
122 assert!(req.urls.upload_url().contains("signature=secret"));
123 assert_eq!(req.urls.public_url(), "https://cdn/a.png");
124 assert_eq!(
125 req.progress_policy(),
126 FileUploadProgressPolicy::PercentStep(5)
127 );
128 }
129
130 #[test]
131 fn upload_urls_require_write_and_public_addresses() {
132 assert!(FileUploadUrls::new(String::new(), "https://cdn/a".into()).is_err());
133 assert!(FileUploadUrls::new("https://upload/a".into(), String::new()).is_err());
134 }
135}