fire_http/fs/
partial_file.rs

1use 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	/// zero index and inclusive
22	pub start: usize,
23	/// zero index and inclusive
24	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	// the size in bytes of the entire file
89	size: usize,
90	// where to start reading
91	start: usize,
92	// at which byte to stop reading (inclusive)
93	end: usize,
94}
95
96impl PartialFile {
97	/// returns not found if the path is not a directory
98	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		// make sure we open a file
112		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		// apache no-gzip
135		// content type
136
137		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// TODO NEED TO CHANGE u64
160// A File which streams a range
161#[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 finished reading
183		if self.remaining == 0 {
184			return Poll::Ready(Ok(()));
185		}
186
187		// take a max amount of buffer to not write to much
188		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		// this is safe since take returns a ReadBuf and it only returns
204		// bytes initializes that are.
205		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		// the file is already at the correct start
238		// since open did that
239
240		// if self.end points to the end of the file just return the file
241		// without limiting the reading
242		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}