volo_http/server/utils/
file_response.rs1use 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
21pub struct FileResponse {
23 file: File,
24 size: u64,
25 content_type: HeaderValue,
26}
27
28impl FileResponse {
29 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 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}