fire_http/fs/
partial_file.rs1use crate::header::{
2 Mime, RequestHeader, StatusCode, ACCEPT_RANGES, CONTENT_LENGTH,
3 CONTENT_RANGE, RANGE,
4};
5use crate::into::IntoResponse;
6use crate::{Body, Response};
7
8use std::fmt;
9use std::io::SeekFrom;
10use std::path::Path;
11use std::pin::Pin;
12use std::task::{Context, Poll};
13
14use io::AsyncSeekExt;
15use tokio::{fs, io};
16
17use bytes::Bytes;
18
19#[derive(Debug, Clone)]
20pub struct Range {
21 pub start: usize,
23 pub end: Option<usize>,
25}
26
27impl Range {
28 pub fn parse(header: &RequestHeader) -> Option<Self> {
29 let range = header.value(RANGE)?;
30 if !range.starts_with("bytes=") {
31 return None;
32 }
33
34 let mut range = range[6..].split('-');
35
36 let start: usize = range.next()?.parse().ok()?;
37
38 let end = range.next()?;
39 let end: Option<usize> = if end != "" {
40 Some(end.parse().ok()?)
41 } else {
42 None
43 };
44
45 Some(Self { start, end })
46 }
47}
48
49pub fn serve_memory_partial_file(
50 path: &'static str,
51 bytes: &'static [u8],
52 range: Range,
53) -> io::Result<Response> {
54 let mime_type = path
55 .rsplit('.')
56 .next()
57 .and_then(Mime::from_extension)
58 .unwrap_or(Mime::BINARY);
59
60 let size = bytes.len();
61 let start = range.start;
62 let end = range.end.unwrap_or(size - 1);
63
64 if end >= size || start >= end {
65 return Err(io::Error::new(
66 io::ErrorKind::Other,
67 RangeIncorrect(range),
68 ));
69 }
70
71 let len = (end + 1) - start;
72
73 let response = Response::builder()
74 .status_code(StatusCode::PARTIAL_CONTENT)
75 .content_type(mime_type)
76 .header(ACCEPT_RANGES, "bytes")
77 .header(CONTENT_LENGTH, len)
78 .header(CONTENT_RANGE, format!("bytes {}-{}/{}", start, end, size))
79 .body(Bytes::from_static(&bytes[start..=end]))
80 .build();
81
82 Ok(response)
83}
84
85pub struct PartialFile {
86 file: fs::File,
87 mime_type: Mime,
88 size: usize,
90 start: usize,
92 end: usize,
94}
95
96impl PartialFile {
97 pub async fn open<P>(path: P, range: Range) -> io::Result<Self>
99 where
100 P: AsRef<Path>,
101 {
102 let extension = path.as_ref().extension().and_then(|f| f.to_str());
103
104 let mime_type = extension
105 .and_then(Mime::from_extension)
106 .unwrap_or(Mime::BINARY);
107
108 let mut file = fs::File::open(path).await?;
109 let metadata = file.metadata().await?;
110
111 if !metadata.is_file() {
113 return Err(io::Error::new(
114 io::ErrorKind::NotFound,
115 "expected file found folder",
116 ));
117 }
118
119 let size: usize = metadata.len().try_into().map_err(|_| {
120 io::Error::new(io::ErrorKind::NotFound, "file to large")
121 })?;
122 let start = range.start;
123 let end = range.end.unwrap_or(size - 1);
124
125 if end >= size || start >= end {
126 return Err(io::Error::new(
127 io::ErrorKind::Other,
128 RangeIncorrect(range),
129 ));
130 }
131
132 file.seek(SeekFrom::Start(start as u64)).await?;
133
134 Ok(Self {
138 file,
139 mime_type,
140 size,
141 start,
142 end,
143 })
144 }
145}
146
147#[derive(Debug)]
148#[allow(dead_code)]
149pub struct RangeIncorrect(pub Range);
150
151impl fmt::Display for RangeIncorrect {
152 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
153 fmt::Debug::fmt(self, f)
154 }
155}
156
157impl std::error::Error for RangeIncorrect {}
158
159#[derive(Debug)]
162struct FixedFile {
163 file: fs::File,
164 remaining: u64,
165}
166
167impl FixedFile {
168 pub fn new(file: fs::File, len: u64) -> Self {
169 Self {
170 file,
171 remaining: len,
172 }
173 }
174}
175
176impl io::AsyncRead for FixedFile {
177 fn poll_read(
178 mut self: Pin<&mut Self>,
179 cx: &mut Context,
180 buf: &mut io::ReadBuf,
181 ) -> Poll<io::Result<()>> {
182 if self.remaining == 0 {
184 return Poll::Ready(Ok(()));
185 }
186
187 let (initialized, filled) = {
189 let mut buf =
190 buf.take(self.remaining.try_into().unwrap_or(usize::MAX));
191 debug_assert!(buf.filled().is_empty());
192
193 let res = Pin::new(&mut self.file).poll_read(cx, &mut buf);
194 match res {
195 Poll::Ready(Ok(())) => {}
196 Poll::Ready(Err(e)) => return Poll::Ready(Err(e)),
197 Poll::Pending => return Poll::Pending,
198 }
199
200 (buf.initialized().len(), buf.filled().len())
201 };
202
203 unsafe {
206 buf.assume_init(buf.filled().len() + initialized);
207 }
208 buf.advance(filled);
209
210 if filled == 0 {
211 return Poll::Ready(Err(io::Error::new(
212 io::ErrorKind::UnexpectedEof,
213 "The file is to short",
214 )));
215 }
216
217 self.remaining = self.remaining.checked_sub(filled as u64).unwrap();
218
219 Poll::Ready(Ok(()))
220 }
221}
222
223impl IntoResponse for PartialFile {
224 fn into_response(self) -> Response {
225 let len = (self.end + 1) - self.start;
226
227 let response = Response::builder()
228 .status_code(StatusCode::PARTIAL_CONTENT)
229 .content_type(self.mime_type)
230 .header(ACCEPT_RANGES, "bytes")
231 .header(CONTENT_LENGTH, len)
232 .header(
233 CONTENT_RANGE,
234 format!("bytes {}-{}/{}", self.start, self.end, self.size),
235 );
236
237 if self.end + 1 == self.size {
243 response.body(Body::from_async_reader(self.file)).build()
244 } else {
245 let fixed_file = FixedFile::new(self.file, len as u64);
246 response.body(Body::from_async_reader(fixed_file)).build()
247 }
248 }
249}