mirror of https://github.com/tokio-rs/axum
Add an Attachment type to axum-extra (#2789)
This commit is contained in:
parent
806bc26e62
commit
fcb45b8d32
|
@ -15,6 +15,7 @@ version = "0.9.3"
|
|||
default = ["tracing"]
|
||||
|
||||
async-read-body = ["dep:tokio-util", "tokio-util?/io", "dep:tokio"]
|
||||
attachment = ["dep:tracing"]
|
||||
cookie = ["dep:cookie"]
|
||||
cookie-private = ["cookie", "cookie?/private"]
|
||||
cookie-signed = ["cookie", "cookie?/signed"]
|
||||
|
|
|
@ -23,8 +23,7 @@ use std::marker::PhantomData;
|
|||
/// Additionally, a `JsonRejection` error will be returned, when calling `deserialize` if:
|
||||
///
|
||||
/// - The body doesn't contain syntactically valid JSON.
|
||||
/// - The body contains syntactically valid JSON, but it couldn't be deserialized into the target
|
||||
/// type.
|
||||
/// - The body contains syntactically valid JSON, but it couldn't be deserialized into the target type.
|
||||
/// - Attempting to deserialize escaped JSON into a type that must be borrowed (e.g. `&'a str`).
|
||||
///
|
||||
/// ⚠️ `serde` will implicitly try to borrow for `&str` and `&[u8]` types, but will error if the
|
||||
|
|
|
@ -0,0 +1,103 @@
|
|||
use axum::response::IntoResponse;
|
||||
use http::{header, HeaderMap, HeaderValue};
|
||||
use tracing::error;
|
||||
|
||||
/// A file attachment response.
|
||||
///
|
||||
/// This type will set the `Content-Disposition` header to `attachment`. In response a webbrowser
|
||||
/// will offer to download the file instead of displaying it directly.
|
||||
///
|
||||
/// Use the `filename` and `content_type` methods to set the filename or content-type of the
|
||||
/// attachment. If these values are not set they will not be sent.
|
||||
///
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use axum::{http::StatusCode, routing::get, Router};
|
||||
/// use axum_extra::response::Attachment;
|
||||
///
|
||||
/// async fn cargo_toml() -> Result<Attachment<String>, (StatusCode, String)> {
|
||||
/// let file_contents = tokio::fs::read_to_string("Cargo.toml")
|
||||
/// .await
|
||||
/// .map_err(|err| (StatusCode::NOT_FOUND, format!("File not found: {err}")))?;
|
||||
/// Ok(Attachment::new(file_contents)
|
||||
/// .filename("Cargo.toml")
|
||||
/// .content_type("text/x-toml"))
|
||||
/// }
|
||||
///
|
||||
/// let app = Router::new().route("/Cargo.toml", get(cargo_toml));
|
||||
/// let _: Router = app;
|
||||
/// ```
|
||||
///
|
||||
/// # Note
|
||||
///
|
||||
/// If you use axum with hyper, hyper will set the `Content-Length` if it is known.
|
||||
///
|
||||
#[derive(Debug)]
|
||||
pub struct Attachment<T> {
|
||||
inner: T,
|
||||
filename: Option<HeaderValue>,
|
||||
content_type: Option<HeaderValue>,
|
||||
}
|
||||
|
||||
impl<T: IntoResponse> Attachment<T> {
|
||||
/// Creates a new [`Attachment`].
|
||||
pub fn new(inner: T) -> Self {
|
||||
Self {
|
||||
inner,
|
||||
filename: None,
|
||||
content_type: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the filename of the [`Attachment`].
|
||||
///
|
||||
/// This updates the `Content-Disposition` header to add a filename.
|
||||
pub fn filename<H: TryInto<HeaderValue>>(mut self, value: H) -> Self {
|
||||
self.filename = if let Ok(filename) = value.try_into() {
|
||||
Some(filename)
|
||||
} else {
|
||||
error!("Attachment filename contains invalid characters");
|
||||
None
|
||||
};
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the content-type of the [`Attachment`]
|
||||
pub fn content_type<H: TryInto<HeaderValue>>(mut self, value: H) -> Self {
|
||||
if let Ok(content_type) = value.try_into() {
|
||||
self.content_type = Some(content_type);
|
||||
} else {
|
||||
error!("Attachment content-type contains invalid characters");
|
||||
}
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> IntoResponse for Attachment<T>
|
||||
where
|
||||
T: IntoResponse,
|
||||
{
|
||||
fn into_response(self) -> axum::response::Response {
|
||||
let mut headers = HeaderMap::new();
|
||||
|
||||
if let Some(content_type) = self.content_type {
|
||||
headers.append(header::CONTENT_TYPE, content_type);
|
||||
}
|
||||
|
||||
let content_disposition = if let Some(filename) = self.filename {
|
||||
let mut bytes = b"attachment; filename=\"".to_vec();
|
||||
bytes.extend_from_slice(filename.as_bytes());
|
||||
bytes.push(b'\"');
|
||||
|
||||
HeaderValue::from_bytes(&bytes).expect("This was a HeaderValue so this can not fail")
|
||||
} else {
|
||||
HeaderValue::from_static("attachment")
|
||||
};
|
||||
|
||||
headers.append(header::CONTENT_DISPOSITION, content_disposition);
|
||||
|
||||
(headers, self.inner).into_response()
|
||||
}
|
||||
}
|
|
@ -3,6 +3,9 @@
|
|||
#[cfg(feature = "erased-json")]
|
||||
mod erased_json;
|
||||
|
||||
#[cfg(feature = "attachment")]
|
||||
mod attachment;
|
||||
|
||||
#[cfg(feature = "erased-json")]
|
||||
pub use erased_json::ErasedJson;
|
||||
|
||||
|
@ -10,6 +13,9 @@ pub use erased_json::ErasedJson;
|
|||
#[doc(no_inline)]
|
||||
pub use crate::json_lines::JsonLines;
|
||||
|
||||
#[cfg(feature = "attachment")]
|
||||
pub use attachment::Attachment;
|
||||
|
||||
macro_rules! mime_response {
|
||||
(
|
||||
$(#[$m:meta])*
|
||||
|
|
|
@ -85,12 +85,12 @@ use serde::Serialize;
|
|||
///
|
||||
/// - A `TypedPath` implementation.
|
||||
/// - A [`FromRequest`] implementation compatible with [`RouterExt::typed_get`],
|
||||
/// [`RouterExt::typed_post`], etc. This implementation uses [`Path`] and thus your struct must
|
||||
/// also implement [`serde::Deserialize`], unless it's a unit struct.
|
||||
/// [`RouterExt::typed_post`], etc. This implementation uses [`Path`] and thus your struct must
|
||||
/// also implement [`serde::Deserialize`], unless it's a unit struct.
|
||||
/// - A [`Display`] implementation that interpolates the captures. This can be used to, among other
|
||||
/// things, create links to known paths and have them verified statically. Note that the
|
||||
/// [`Display`] implementation for each field must return something that's compatible with its
|
||||
/// [`Deserialize`] implementation.
|
||||
/// things, create links to known paths and have them verified statically. Note that the
|
||||
/// [`Display`] implementation for each field must return something that's compatible with its
|
||||
/// [`Deserialize`] implementation.
|
||||
///
|
||||
/// Additionally the macro will verify the captures in the path matches the fields of the struct.
|
||||
/// For example this fails to compile since the struct doesn't have a `team_id` field:
|
||||
|
|
|
@ -103,6 +103,7 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub(crate) struct MakeErasedRouter<S> {
|
||||
pub(crate) router: Router<S>,
|
||||
pub(crate) into_route: fn(Router<S>, S) -> Route,
|
||||
|
|
|
@ -181,7 +181,7 @@ router.
|
|||
# Panics
|
||||
|
||||
- If the route overlaps with another route. See [`Router::route`]
|
||||
for more details.
|
||||
for more details.
|
||||
- If the route contains a wildcard (`*`).
|
||||
- If `path` is empty.
|
||||
|
||||
|
|
|
@ -17,8 +17,7 @@ use serde::{de::DeserializeOwned, Serialize};
|
|||
///
|
||||
/// - The request doesn't have a `Content-Type: application/json` (or similar) header.
|
||||
/// - The body doesn't contain syntactically valid JSON.
|
||||
/// - The body contains syntactically valid JSON, but it couldn't be deserialized into the target
|
||||
/// type.
|
||||
/// - The body contains syntactically valid JSON, but it couldn't be deserialized into the target type.
|
||||
/// - Buffering the request body fails.
|
||||
///
|
||||
/// ⚠️ Since parsing JSON requires consuming the request body, the `Json` extractor must be
|
||||
|
|
Loading…
Reference in New Issue