use axum::body::{Body, Bytes};
use axum::http::{StatusCode, header};
use axum::response::{IntoResponse, Response};
use futures_util::{Stream, StreamExt, TryStream};
pub struct StreamingResponse {
body: Body,
content_type: String,
content_disposition: Option<String>,
status: StatusCode,
}
impl StreamingResponse {
pub fn new<S>(stream: S) -> Self
where
S: TryStream + Send + 'static,
S::Ok: Into<Bytes>,
S::Error: Into<axum::BoxError>,
{
Self {
body: Body::from_stream(stream),
content_type: "application/octet-stream".to_string(),
content_disposition: None,
status: StatusCode::OK,
}
}
pub fn from_chunks<S, T>(stream: S) -> Self
where
S: Stream<Item = T> + Send + 'static,
T: Into<Bytes>,
{
let try_stream = stream.map(|chunk| Ok::<Bytes, std::convert::Infallible>(chunk.into()));
Self::new(try_stream)
}
pub fn content_type(mut self, content_type: impl Into<String>) -> Self {
self.content_type = content_type.into();
self
}
pub fn attachment(mut self, filename: impl Into<String>) -> Self {
self.content_disposition = Some(format!(
"attachment; filename=\"{}\"",
sanitize_filename(&filename.into())
));
self
}
pub fn inline(mut self, filename: impl Into<String>) -> Self {
self.content_disposition = Some(format!(
"inline; filename=\"{}\"",
sanitize_filename(&filename.into())
));
self
}
pub fn status(mut self, status: StatusCode) -> Self {
self.status = status;
self
}
}
impl IntoResponse for StreamingResponse {
fn into_response(self) -> Response {
let mut builder = Response::builder()
.status(self.status)
.header(header::CONTENT_TYPE, self.content_type);
if let Some(cd) = self.content_disposition {
builder = builder.header(header::CONTENT_DISPOSITION, cd);
}
builder
.body(self.body)
.expect("content-type / content-disposition are always valid header values")
}
}
fn sanitize_filename(name: &str) -> String {
name.chars()
.filter(|c| !matches!(c, '\r' | '\n' | '"'))
.collect()
}