axum_extra/response/
attachment.rs

1use axum::response::IntoResponse;
2use http::{header, HeaderMap, HeaderValue};
3use tracing::error;
4
5/// A file attachment response.
6///
7/// This type will set the `Content-Disposition` header to `attachment`. In response a webbrowser
8/// will offer to download the file instead of displaying it directly.
9///
10/// Use the `filename` and `content_type` methods to set the filename or content-type of the
11/// attachment. If these values are not set they will not be sent.
12///
13///
14/// # Example
15///
16/// ```rust
17///  use axum::{http::StatusCode, routing::get, Router};
18///  use axum_extra::response::Attachment;
19///
20///  async fn cargo_toml() -> Result<Attachment<String>, (StatusCode, String)> {
21///      let file_contents = tokio::fs::read_to_string("Cargo.toml")
22///          .await
23///          .map_err(|err| (StatusCode::NOT_FOUND, format!("File not found: {err}")))?;
24///      Ok(Attachment::new(file_contents)
25///          .filename("Cargo.toml")
26///          .content_type("text/x-toml"))
27///  }
28///
29///  let app = Router::new().route("/Cargo.toml", get(cargo_toml));
30///  let _: Router = app;
31/// ```
32///
33/// # Note
34///
35/// If you use axum with hyper, hyper will set the `Content-Length` if it is known.
36#[derive(Debug)]
37#[must_use]
38pub struct Attachment<T> {
39    inner: T,
40    filename: Option<HeaderValue>,
41    content_type: Option<HeaderValue>,
42}
43
44impl<T: IntoResponse> Attachment<T> {
45    /// Creates a new [`Attachment`].
46    pub fn new(inner: T) -> Self {
47        Self {
48            inner,
49            filename: None,
50            content_type: None,
51        }
52    }
53
54    /// Sets the filename of the [`Attachment`].
55    ///
56    /// This updates the `Content-Disposition` header to add a filename.
57    pub fn filename<H: TryInto<HeaderValue>>(mut self, value: H) -> Self {
58        self.filename = if let Ok(filename) = value.try_into() {
59            Some(filename)
60        } else {
61            error!("Attachment filename contains invalid characters");
62            None
63        };
64        self
65    }
66
67    /// Sets the content-type of the [`Attachment`]
68    pub fn content_type<H: TryInto<HeaderValue>>(mut self, value: H) -> Self {
69        if let Ok(content_type) = value.try_into() {
70            self.content_type = Some(content_type);
71        } else {
72            error!("Attachment content-type contains invalid characters");
73        }
74        self
75    }
76}
77
78impl<T> IntoResponse for Attachment<T>
79where
80    T: IntoResponse,
81{
82    fn into_response(self) -> axum::response::Response {
83        let mut headers = HeaderMap::new();
84
85        if let Some(content_type) = self.content_type {
86            headers.append(header::CONTENT_TYPE, content_type);
87        }
88
89        let content_disposition = if let Some(filename) = self.filename {
90            let mut bytes = b"attachment; filename=\"".to_vec();
91            bytes.extend_from_slice(filename.as_bytes());
92            bytes.push(b'\"');
93
94            HeaderValue::from_bytes(&bytes).expect("This was a HeaderValue so this can not fail")
95        } else {
96            HeaderValue::from_static("attachment")
97        };
98
99        headers.append(header::CONTENT_DISPOSITION, content_disposition);
100
101        (headers, self.inner).into_response()
102    }
103}