use base64::Engine;
use base64::engine::general_purpose::STANDARD;
use bytes::Bytes;
use url::Url;
use crate::client::{Client, MAX_RESPONSE_BODY_BYTES, read_body};
use crate::error::Error;
use crate::generated::types::{
CreateDirectUploadRequestContent, DirectUpload, DirectUploadBlob, DirectUploadTarget,
};
use crate::http::header::AUTHORIZATION;
use crate::http::{HeaderMap, HeaderName, HeaderValue, Method, Request};
use crate::security::require_secure_endpoint;
pub use crate::generated::services::attachments::*;
const DEFAULT_CONTENT_TYPE: &str = "application/octet-stream";
impl<'a> Attachments<'a> {
pub async fn upload(
&self,
filename: &str,
content_type: Option<&str>,
content: impl Into<Bytes>,
) -> Result<DirectUpload, Error> {
if filename.is_empty() {
return Err(Error::usage("an attachment needs a filename"));
}
let content = content.into();
let body = CreateDirectUploadRequestContent {
blob: DirectUploadBlob {
filename: filename.to_string(),
byte_size: content.len() as i64,
checksum: STANDARD.encode(md5::compute(&content).0),
content_type: content_type.unwrap_or(DEFAULT_CONTENT_TYPE).to_string(),
},
};
let upload = reserved(self.create_direct_upload(&body).await?)?;
store(self.client(), &upload.direct_upload, content).await?;
Ok(upload)
}
}
fn reserved(upload: DirectUpload) -> Result<DirectUpload, Error> {
if upload.signed_id.is_empty()
|| upload.attachable_sgid.is_empty()
|| upload.direct_upload.url.is_empty()
{
Err(Error::api(
0,
"HEY returned an empty attachment upload response",
))
} else {
Ok(upload)
}
}
async fn store(client: &Client, target: &DirectUploadTarget, content: Bytes) -> Result<(), Error> {
let url = Url::parse(&target.url)?;
require_secure_endpoint(&url)
.map_err(|error| Error::usage(format!("unsafe attachment upload target: {error}")))?;
let path = url.path().to_string();
let mut request = Request::builder()
.method(Method::PUT)
.uri(url.as_str())
.body(content)
.map_err(Error::from_std)?;
*request.headers_mut() = storage_headers(target)?;
let answered = client.http().send(request).await?;
let status = answered.status();
let headers = answered.headers().clone();
let body = read_body(
answered.into_body(),
MAX_RESPONSE_BODY_BYTES,
&Method::PUT,
&path,
)
.await?;
if status.is_success() {
Ok(())
} else {
Err(Error::from_response(status, &Method::PUT, &headers, &body))
}
}
fn storage_headers(target: &DirectUploadTarget) -> Result<HeaderMap, Error> {
let mut headers = HeaderMap::new();
for (name, value) in target.headers.iter().flatten() {
let name = HeaderName::from_bytes(name.as_bytes())
.map_err(|_| Error::api(0, format!("{name:?} is not a valid header name")))?;
let value = HeaderValue::from_str(value)
.map_err(|_| Error::api(0, format!("{name} carries an unsendable value")))?;
headers.insert(name, value);
}
headers.remove(AUTHORIZATION);
Ok(headers)
}