poem_openapi/payload/
attachment.rs

1use std::fmt::Write;
2
3use poem::{http::header::CONTENT_DISPOSITION, Body, IntoResponse, Response};
4
5use crate::{
6    payload::{Binary, Payload},
7    registry::{MetaHeader, MetaMediaType, MetaResponse, MetaResponses, MetaSchemaRef, Registry},
8    types::Type,
9    ApiResponse,
10};
11
12const CONTENT_DISPOSITION_DESC: &str = "Indicate if the content is expected to be displayed inline in the browser, that is, as a Web page or as part of a Web page, or as an attachment, that is downloaded and saved locally.";
13
14/// Attachment type
15#[derive(Debug, Copy, Clone, Eq, PartialEq)]
16pub enum AttachmentType {
17    /// Indicate it can be displayed inside the Web page, or as the Web page
18    Inline,
19    /// Indicate it should be downloaded; most browsers presenting a 'Save as'
20    /// dialog
21    Attachment,
22}
23
24impl AttachmentType {
25    #[inline]
26    fn as_str(&self) -> &'static str {
27        match self {
28            AttachmentType::Inline => "inline",
29            AttachmentType::Attachment => "attachment",
30        }
31    }
32}
33
34/// A binary payload for download file.
35#[derive(Debug, Clone, Eq, PartialEq)]
36pub struct Attachment<T> {
37    data: Binary<T>,
38    ty: AttachmentType,
39    filename: Option<String>,
40}
41
42impl<T: Into<Body> + Send> Attachment<T> {
43    /// Create an attachment with data.
44    pub fn new(data: T) -> Self {
45        Self {
46            data: Binary(data),
47            ty: AttachmentType::Attachment,
48            filename: None,
49        }
50    }
51
52    /// Specify the attachment. (defaults to: [`AttachmentType::Inline`])
53    #[must_use]
54    pub fn attachment_type(self, ty: AttachmentType) -> Self {
55        Self { ty, ..self }
56    }
57
58    /// Specify the file name.
59    #[must_use]
60    pub fn filename(self, filename: impl Into<String>) -> Self {
61        Self {
62            filename: Some(filename.into()),
63            ..self
64        }
65    }
66
67    fn content_disposition(&self) -> String {
68        let mut content_disposition = self.ty.as_str().to_string();
69
70        if let Some(legal_filename) = self.filename.as_ref().map(|filename| {
71            filename
72                .replace('\\', "\\\\")
73                .replace('\"', "\\\"")
74                .replace('\r', "\\\r")
75                .replace('\n', "\\\n")
76        }) {
77            _ = write!(content_disposition, "; filename=\"{legal_filename}\"");
78        }
79
80        content_disposition
81    }
82}
83
84impl<T: Into<Body> + Send> Payload for Attachment<T> {
85    const CONTENT_TYPE: &'static str = Binary::<T>::CONTENT_TYPE;
86
87    fn schema_ref() -> MetaSchemaRef {
88        Binary::<T>::schema_ref()
89    }
90}
91
92impl<T: Into<Body> + Send> IntoResponse for Attachment<T> {
93    fn into_response(self) -> Response {
94        let content_disposition = self.content_disposition();
95        self.data
96            .with_header(CONTENT_DISPOSITION, content_disposition)
97            .into_response()
98    }
99}
100
101impl<T: Into<Body> + Send> ApiResponse for Attachment<T> {
102    fn meta() -> MetaResponses {
103        MetaResponses {
104            responses: vec![MetaResponse {
105                description: "",
106                status: Some(200),
107                content: vec![MetaMediaType {
108                    content_type: Self::CONTENT_TYPE,
109                    schema: Self::schema_ref(),
110                }],
111                headers: vec![MetaHeader {
112                    name: "Content-Disposition".to_string(),
113                    description: Some(CONTENT_DISPOSITION_DESC.to_string()),
114                    required: true,
115                    deprecated: false,
116                    schema: String::schema_ref(),
117                }],
118            }],
119        }
120    }
121
122    fn register(_registry: &mut Registry) {}
123}