hey_sdk/services/
attachments.rs1use base64::Engine;
6use base64::engine::general_purpose::STANDARD;
7use bytes::Bytes;
8use url::Url;
9
10use crate::client::{Client, MAX_RESPONSE_BODY_BYTES, read_body};
11use crate::error::Error;
12use crate::generated::types::{
13 CreateDirectUploadRequestContent, DirectUpload, DirectUploadBlob, DirectUploadTarget,
14};
15use crate::http::header::AUTHORIZATION;
16use crate::http::{HeaderMap, HeaderName, HeaderValue, Method, Request};
17use crate::security::require_secure_endpoint;
18
19pub use crate::generated::services::attachments::*;
20
21const DEFAULT_CONTENT_TYPE: &str = "application/octet-stream";
23
24impl Attachments<'_> {
25 pub async fn upload(
31 &self,
32 filename: &str,
33 content_type: Option<&str>,
34 content: impl Into<Bytes>,
35 ) -> Result<DirectUpload, Error> {
36 if filename.is_empty() {
37 return Err(Error::usage("an attachment needs a filename"));
38 }
39
40 let content = content.into();
41 #[allow(clippy::cast_possible_wrap)]
42 let body = CreateDirectUploadRequestContent {
44 blob: DirectUploadBlob {
45 filename: filename.to_string(),
46 byte_size: content.len() as i64,
47 checksum: STANDARD.encode(md5::compute(&content).0),
48 content_type: content_type.unwrap_or(DEFAULT_CONTENT_TYPE).to_string(),
49 },
50 };
51 self.client()
53 .within_limit(Box::pin(async move {
54 let upload = reserved(self.create_direct_upload(&body).await?)?;
55 store(self.client(), &upload.direct_upload, content).await?;
56 Ok(upload)
57 }))
58 .await
59 }
60}
61
62fn reserved(upload: DirectUpload) -> Result<DirectUpload, Error> {
66 if upload.signed_id.is_empty()
67 || upload.attachable_sgid.is_empty()
68 || upload.direct_upload.url.is_empty()
69 {
70 Err(Error::api(
71 0,
72 "HEY returned an empty attachment upload response",
73 ))
74 } else {
75 Ok(upload)
76 }
77}
78
79async fn store(client: &Client, target: &DirectUploadTarget, content: Bytes) -> Result<(), Error> {
92 let url = Url::parse(&target.url)?;
93 require_secure_endpoint(&url)
94 .map_err(|error| Error::usage(format!("unsafe attachment upload target: {error}")))?;
95
96 let path = url.path().to_string();
97 let mut request = Request::builder()
98 .method(Method::PUT)
99 .uri(url.as_str())
100 .body(content)
101 .map_err(Error::from_std)?;
102 *request.headers_mut() = storage_headers(target)?;
103 let (status, headers, body) = client
105 .within_deadline(client.deadline(), async {
106 let answered = client.http().send(request).await?;
107 let status = answered.status();
108 let headers = answered.headers().clone();
109 let body = read_body(
110 answered.into_body(),
111 MAX_RESPONSE_BODY_BYTES,
112 &Method::PUT,
113 &path,
114 )
115 .await?;
116 Ok((status, headers, body))
117 })
118 .await?;
119 if status.is_success() {
120 Ok(())
121 } else {
122 Err(Error::from_response(status, &Method::PUT, &headers, &body))
123 }
124}
125
126fn storage_headers(target: &DirectUploadTarget) -> Result<HeaderMap, Error> {
129 let mut headers = HeaderMap::new();
130 for (name, value) in target.headers.iter().flatten() {
131 let name = HeaderName::from_bytes(name.as_bytes())
132 .map_err(|_| Error::api(0, format!("{name:?} is not a valid header name")))?;
133 let value = HeaderValue::from_str(value)
134 .map_err(|_| Error::api(0, format!("{name} carries an unsendable value")))?;
135 headers.insert(name, value);
136 }
137 headers.remove(AUTHORIZATION);
138 Ok(headers)
139}