use std::sync::Arc;
use crate::effect::{FileUploadRequest, FileUploadResponse};
use crate::error::PortError;
use crate::platform::{MaybeSend, MaybeSync};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct FileUploadProgress {
pub completed_bytes: u64,
pub total_bytes: u64,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum FileUploadProgressPolicy {
#[default]
Disabled,
PercentStep(u8),
}
pub trait FileUploadProgressReporter: MaybeSend + MaybeSync + 'static {
fn report(&self, progress: FileUploadProgress);
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FileUploadUrls {
upload_url: String,
public_url: String,
progress_policy: FileUploadProgressPolicy,
}
impl FileUploadUrls {
pub fn new(upload_url: String, public_url: String) -> Result<Self, &'static str> {
if upload_url.trim().is_empty() || public_url.trim().is_empty() {
return Err("upload_url and public_url must not be empty");
}
Ok(Self {
upload_url,
public_url,
progress_policy: FileUploadProgressPolicy::Disabled,
})
}
pub fn with_progress_policy(mut self, policy: FileUploadProgressPolicy) -> Self {
self.progress_policy = policy;
self
}
pub fn progress_policy(&self) -> FileUploadProgressPolicy {
self.progress_policy
}
pub fn upload_url(&self) -> &str {
&self.upload_url
}
pub fn public_url(&self) -> &str {
&self.public_url
}
pub fn into_parts(self) -> (String, String) {
(self.upload_url, self.public_url)
}
}
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
pub trait FileUploader: MaybeSend + MaybeSync + 'static {
async fn upload(&self, req: FileUploadRequest) -> Result<FileUploadResponse, PortError>;
async fn upload_with_progress(
&self,
req: FileUploadRequest,
progress: Option<Arc<dyn FileUploadProgressReporter>>,
) -> Result<FileUploadResponse, PortError> {
let _ = progress;
self.upload(req).await
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::effect::{Correlation, Effect, FileUploadRequest};
#[test]
fn upload_effect_keeps_distinct_validated_urls() {
let req = FileUploadRequest {
local_path: "/tmp/a.png".into(),
object_key: "im/a.png".into(),
method: "PUT".into(),
urls: FileUploadUrls::new(
"https://upload/a?signature=secret".into(),
"https://cdn/a.png".into(),
)
.expect("valid urls")
.with_progress_policy(FileUploadProgressPolicy::PercentStep(5)),
headers: Vec::new(),
content_type: Some("image/png".into()),
size: Some(123),
};
let Effect::UploadFile { corr, req } = (Effect::UploadFile {
corr: Correlation::from_raw(9),
req,
}) else {
panic!("expected upload effect");
};
assert_eq!(corr.raw(), 9);
assert!(req.urls.upload_url().contains("signature=secret"));
assert_eq!(req.urls.public_url(), "https://cdn/a.png");
assert_eq!(
req.progress_policy(),
FileUploadProgressPolicy::PercentStep(5)
);
}
#[test]
fn upload_urls_require_write_and_public_addresses() {
assert!(FileUploadUrls::new(String::new(), "https://cdn/a".into()).is_err());
assert!(FileUploadUrls::new("https://upload/a".into(), String::new()).is_err());
}
}