rocket_raw_response/
lib.rs

1/*!
2# Raw Response for Rocket Framework
3
4This crate provides a response struct used for responding raw data.
5
6See `examples`.
7*/
8
9pub extern crate mime;
10
11#[macro_use]
12extern crate educe;
13
14mod temp_file_async_reader;
15
16use std::{
17    io::{self, Cursor},
18    marker::Unpin,
19    path::Path,
20    sync::Arc,
21};
22
23use mime::Mime;
24use rocket::{
25    fs::TempFile,
26    http::Status,
27    request::Request,
28    response::{self, Responder, Response},
29    tokio::{fs::File as AsyncFile, io::AsyncRead},
30};
31use temp_file_async_reader::TempFileAsyncReader;
32
33#[derive(Educe)]
34#[educe(Debug)]
35enum RawResponseData<'o> {
36    Slice(&'o [u8]),
37    Vec(Vec<u8>),
38    Reader {
39        #[educe(Debug(ignore))]
40        data:           Box<dyn AsyncRead + Send + Unpin + 'o>,
41        content_length: Option<u64>,
42    },
43    File(Arc<Path>, AsyncFile),
44    TempFile(Box<TempFile<'o>>),
45}
46
47pub type RawResponse = RawResponsePro<'static>;
48
49#[derive(Debug)]
50pub struct RawResponsePro<'o> {
51    file_name:    Option<String>,
52    content_type: Option<Mime>,
53    data:         RawResponseData<'o>,
54}
55
56impl<'o> RawResponsePro<'o> {
57    /// Create a `RawResponse` instance from a `&'o [u8]`.
58    pub fn from_slice<S: Into<String>>(
59        data: &'o [u8],
60        file_name: Option<S>,
61        content_type: Option<Mime>,
62    ) -> RawResponsePro<'o> {
63        let file_name = file_name.map(|file_name| file_name.into());
64
65        let data = RawResponseData::Slice(data);
66
67        RawResponsePro {
68            file_name,
69            content_type,
70            data,
71        }
72    }
73
74    /// Create a `RawResponse` instance from a `Vec<u8>`.
75    pub fn from_vec<S: Into<String>>(
76        vec: Vec<u8>,
77        file_name: Option<S>,
78        content_type: Option<Mime>,
79    ) -> RawResponsePro<'o> {
80        let file_name = file_name.map(|file_name| file_name.into());
81
82        let data = RawResponseData::Vec(vec);
83
84        RawResponsePro {
85            file_name,
86            content_type,
87            data,
88        }
89    }
90
91    /// Create a `RawResponse` instance from a reader.
92    pub fn from_reader<R: AsyncRead + Send + Unpin + 'o, S: Into<String>>(
93        reader: R,
94        file_name: Option<S>,
95        content_type: Option<Mime>,
96        content_length: Option<u64>,
97    ) -> RawResponsePro<'o> {
98        let file_name = file_name.map(|file_name| file_name.into());
99
100        let data = RawResponseData::Reader {
101            data: Box::new(reader),
102            content_length,
103        };
104
105        RawResponsePro {
106            file_name,
107            content_type,
108            data,
109        }
110    }
111
112    /// Create a `RawResponse` instance from a path of a file.
113    pub async fn from_file<P: Into<Arc<Path>>, S: Into<String>>(
114        path: P,
115        file_name: Option<S>,
116        content_type: Option<Mime>,
117    ) -> Result<RawResponsePro<'o>, io::Error> {
118        let path = path.into();
119
120        let file = AsyncFile::open(path.as_ref()).await?;
121
122        let file_name = file_name.map(|file_name| file_name.into());
123
124        let data = RawResponseData::File(path, file);
125
126        Ok(RawResponsePro {
127            file_name,
128            content_type,
129            data,
130        })
131    }
132
133    /// Create a `RawResponse` instance from a `TempFile`.
134    pub fn from_temp_file<S: Into<String>>(
135        temp_file: TempFile<'o>,
136        file_name: Option<S>,
137        content_type: Option<Mime>,
138    ) -> RawResponsePro<'o> {
139        let file_name = file_name.map(|file_name| file_name.into());
140
141        let data = RawResponseData::TempFile(Box::new(temp_file));
142
143        RawResponsePro {
144            file_name,
145            content_type,
146            data,
147        }
148    }
149}
150
151macro_rules! file_name {
152    ($s:expr, $res:expr) => {
153        if let Some(file_name) = $s.file_name {
154            if file_name.is_empty() {
155                $res.raw_header("Content-Disposition", "inline");
156            } else {
157                let mut v = String::from("inline; filename*=UTF-8''");
158
159                url_escape::encode_component_to_string(file_name, &mut v);
160
161                $res.raw_header("Content-Disposition", v);
162            }
163        }
164    };
165}
166
167macro_rules! content_type {
168    ($s:expr, $res:expr) => {
169        if let Some(content_type) = $s.content_type {
170            $res.raw_header("Content-Type", content_type.to_string());
171        }
172    };
173}
174
175impl<'r, 'o: 'r> Responder<'r, 'o> for RawResponsePro<'o> {
176    fn respond_to(self, _: &'r Request<'_>) -> response::Result<'o> {
177        let mut response = Response::build();
178
179        match self.data {
180            RawResponseData::Slice(data) => {
181                file_name!(self, response);
182                content_type!(self, response);
183
184                response.sized_body(data.len(), Cursor::new(data));
185            },
186            RawResponseData::Vec(data) => {
187                file_name!(self, response);
188                content_type!(self, response);
189
190                response.sized_body(data.len(), Cursor::new(data));
191            },
192            RawResponseData::Reader {
193                data,
194                content_length,
195            } => {
196                file_name!(self, response);
197                content_type!(self, response);
198
199                if let Some(content_length) = content_length {
200                    response.raw_header("Content-Length", content_length.to_string());
201                }
202
203                response.streamed_body(data);
204            },
205            RawResponseData::File(path, file) => {
206                if let Some(file_name) = self.file_name {
207                    if file_name.is_empty() {
208                        response.raw_header("Content-Disposition", "inline");
209                    } else {
210                        let mut v = String::from("inline; filename*=UTF-8''");
211
212                        url_escape::encode_component_to_string(file_name, &mut v);
213
214                        response.raw_header("Content-Disposition", v);
215                    }
216                } else if let Some(file_name) =
217                    path.file_name().map(|file_name| file_name.to_string_lossy())
218                {
219                    let mut v = String::from("inline; filename*=UTF-8''");
220
221                    url_escape::encode_component_to_string(file_name, &mut v);
222
223                    response.raw_header("Content-Disposition", v);
224                } else {
225                    response.raw_header("Content-Disposition", "inline");
226                }
227
228                if let Some(content_type) = self.content_type {
229                    response.raw_header("Content-Type", content_type.to_string());
230                } else if let Some(extension) = path.extension() {
231                    if let Some(extension) = extension.to_str() {
232                        let content_type = mime_guess::from_ext(extension).first_or_octet_stream();
233
234                        response.raw_header("Content-Type", content_type.to_string());
235                    }
236                }
237
238                response.sized_body(None, file);
239            },
240            RawResponseData::TempFile(file) => {
241                if let Some(file_name) = self.file_name {
242                    if file_name.is_empty() {
243                        response.raw_header("Content-Disposition", "inline");
244                    } else {
245                        let mut v = String::from("inline; filename*=UTF-8''");
246
247                        url_escape::encode_component_to_string(file_name, &mut v);
248
249                        response.raw_header("Content-Disposition", v);
250                    }
251                } else if let Some(file_name) = file.name() {
252                    if file_name.is_empty() {
253                        response.raw_header("Content-Disposition", "inline");
254                    } else {
255                        let mut v = String::from("attachment; filename*=UTF-8''");
256
257                        url_escape::encode_component_to_string(file_name, &mut v);
258
259                        response.raw_header("Content-Disposition", v);
260                    }
261                } else {
262                    response.raw_header("Content-Disposition", "inline");
263                }
264
265                if let Some(content_type) = self.content_type {
266                    response.raw_header("Content-Type", content_type.to_string());
267                } else if let Some(content_type) = file.content_type() {
268                    response.raw_header("Content-Type", content_type.to_string());
269                } else if let Some(extension) = file.name().map(Path::new).and_then(Path::extension)
270                {
271                    if let Some(extension) = extension.to_str() {
272                        let content_type = mime_guess::from_ext(extension).first_or_octet_stream();
273
274                        response.raw_header("Content-Type", content_type.to_string());
275                    }
276                }
277
278                response.raw_header("Content-Length", file.len().to_string());
279
280                response.streamed_body(
281                    TempFileAsyncReader::from(file).map_err(|_| Status::InternalServerError)?,
282                );
283            },
284        }
285
286        response.ok()
287    }
288}