volo_http/server/utils/
file_response.rs

1use std::{
2    fs::File,
3    io,
4    path::Path,
5    pin::Pin,
6    task::{Context, Poll, ready},
7};
8
9use bytes::Bytes;
10use futures::Stream;
11use http::header::{self, HeaderValue};
12use http_body::{Frame, SizeHint};
13use pin_project::pin_project;
14use tokio::io::AsyncRead;
15use tokio_util::io::ReaderStream;
16
17use crate::{body::Body, response::Response, server::IntoResponse};
18
19const BUF_SIZE: usize = 4096;
20
21/// Response for sending a file.
22pub struct FileResponse {
23    file: File,
24    size: u64,
25    content_type: HeaderValue,
26}
27
28impl FileResponse {
29    /// Create a new [`FileResponse`] with given path and `Content-Type`
30    pub fn new<P>(path: P, content_type: HeaderValue) -> io::Result<Self>
31    where
32        P: AsRef<Path>,
33    {
34        let file = File::open(path)?;
35        let metadata = file.metadata()?;
36
37        Ok(Self {
38            file,
39            size: metadata.len(),
40            content_type,
41        })
42    }
43
44    /// Create a new [`FileResponse`] with guessing `Content-Type` through file name
45    pub fn new_with_guess_type<P>(path: P) -> io::Result<Self>
46    where
47        P: AsRef<Path>,
48    {
49        let path = path.as_ref();
50        Self::new(path, super::serve_dir::guess_mime(path))
51    }
52}
53
54impl IntoResponse for FileResponse {
55    fn into_response(self) -> Response {
56        let file = tokio::fs::File::from_std(self.file);
57        Response::builder()
58            .header(header::CONTENT_TYPE, self.content_type)
59            .body(Body::from_body(FileBody {
60                reader: ReaderStream::with_capacity(file, BUF_SIZE),
61                size: self.size,
62            }))
63            .unwrap()
64    }
65}
66
67#[pin_project]
68struct FileBody<R> {
69    #[pin]
70    reader: ReaderStream<R>,
71    size: u64,
72}
73
74impl<R> http_body::Body for FileBody<R>
75where
76    R: AsyncRead,
77{
78    type Data = Bytes;
79    type Error = io::Error;
80
81    fn poll_frame(
82        self: Pin<&mut Self>,
83        cx: &mut Context<'_>,
84    ) -> Poll<Option<Result<Frame<Self::Data>, Self::Error>>> {
85        match ready!(self.project().reader.poll_next(cx)) {
86            Some(Ok(chunk)) => Poll::Ready(Some(Ok(Frame::data(chunk)))),
87            Some(Err(err)) => Poll::Ready(Some(Err(err))),
88            None => Poll::Ready(None),
89        }
90    }
91
92    fn size_hint(&self) -> SizeHint {
93        SizeHint::with_exact(self.size)
94    }
95}