http_fs/
file.rs

1//! File service module
2use bytes::Bytes;
3
4use crate::config::{FileServeConfig, FsTaskSpawner};
5use crate::headers::cd::{self, ContentDisposition, DispositionType};
6use crate::utils;
7use crate::headers::range::HttpRange;
8use crate::body::Body;
9
10use std::io::{self, Seek, Read};
11use std::path::Path;
12use std::fs;
13use core::{mem, cmp, task};
14use core::str::FromStr;
15use core::marker::PhantomData;
16use core::future::Future;
17use core::pin::Pin;
18
19///Result of file read.
20pub type FileReadResult = Result<Bytes, io::Error>;
21
22///Performs match of `ETag` against `If-Match`
23///
24///Returns `true` if has no `If-Match` header or the `Etag` doesn't match it
25pub fn if_match(etag: &etag::EntityTag, headers: &http::header::HeaderMap) -> bool {
26    match headers.get(http::header::IF_MATCH).and_then(|header| header.to_str().ok()) {
27        Some(header) => {
28            for header_tag in header.split(',').map(|tag| tag.trim()) {
29                match header_tag.parse::<etag::EntityTag>() {
30                    Ok(header_tag) => match etag.strong_eq(&header_tag) {
31                        true => return true,
32                        false => (),
33                    },
34                    Err(_) => ()
35                }
36            }
37            false
38        },
39        None => true
40    }
41}
42
43///Matches given ETag against `If-None-Match` header.
44///
45///Returns `true` if matching value found is not found or misses the header
46pub fn if_none_match(etag: &etag::EntityTag, headers: &http::header::HeaderMap) -> bool {
47    match headers.get(http::header::IF_NONE_MATCH).and_then(|header| header.to_str().ok()) {
48        Some(header) => {
49            for header_tag in header.split(',').map(|tag| tag.trim()) {
50                match header_tag.parse::<etag::EntityTag>() {
51                    Ok(header_tag) => match etag.weak_eq(&header_tag) {
52                        true => return false,
53                        false => (),
54                    },
55                    Err(_) => ()
56                }
57            }
58            true
59        },
60        None => true
61    }
62}
63
64///Matches `Last-Modified` against `If-Unmodified-Since`
65///
66///Returns true if `Last-Modified` is not after `If-Unmodified-Since`
67///Or the header is missing
68pub fn if_unmodified_since(last_modified: httpdate::HttpDate, headers: &http::header::HeaderMap) -> bool {
69    match headers.get(http::header::IF_UNMODIFIED_SINCE).and_then(|header| header.to_str().ok()).and_then(|header| httpdate::HttpDate::from_str(header.trim()).ok()) {
70        Some(header) => last_modified <= header,
71        None => true,
72    }
73}
74
75///Matches `Last-Modified` against `If-Modified-Since`
76///
77///Returns true if `Last-Modified` is before `If-Modified-Since`, header is missing or
78///`If-None-Matches` is present
79pub fn if_modified_since(last_modified: httpdate::HttpDate, headers: &http::header::HeaderMap) -> bool {
80    if headers.contains_key(http::header::IF_NONE_MATCH) {
81        return true;
82    }
83
84    match headers.get(http::header::IF_MODIFIED_SINCE).and_then(|header| header.to_str().ok()).and_then(|header| httpdate::HttpDate::from_str(header.trim()).ok()) {
85        Some(header) => last_modified > header,
86        None => true,
87    }
88}
89
90///File service helper
91pub struct ServeFile<W, C> {
92    file: fs::File,
93    meta: fs::Metadata,
94    ///File's MIME type
95    pub content_type: mime::Mime,
96    ///File's Content-Disposition
97    pub content_disposition: ContentDisposition,
98    _config: PhantomData<(W, C)>,
99}
100
101impl<W: FsTaskSpawner, C: FileServeConfig> ServeFile<W, C> {
102    ///Opens file to serve.
103    pub fn open(path: &Path) -> io::Result<Self> {
104        let file = fs::File::open(path)?;
105        let meta = file.metadata()?;
106
107        if let Some(file_name) = path.file_name().and_then(|file_name| file_name.to_str()) {
108            Ok(Self::from_parts_with_cfg(file_name, file, meta))
109        } else {
110            Err(io::Error::new(io::ErrorKind::InvalidInput, "Provided path has no filename"))
111        }
112    }
113
114    #[inline]
115    ///Creates new instance from already opened file
116    pub fn from_parts(file_name: &str, file: fs::File, meta: fs::Metadata) -> Self {
117        Self::from_parts_with_cfg(file_name, file, meta)
118    }
119
120    ///Creates new instance from already opened file
121    pub fn from_parts_with_cfg(file_name: &str, file: fs::File, meta: fs::Metadata) -> Self {
122        let (content_type, content_disposition) = {
123            let content_type = mime_guess::from_path(&file_name).first_or_octet_stream();
124            let content_disposition = match C::content_disposition_map(content_type.type_()) {
125                DispositionType::Inline => ContentDisposition::Inline,
126                DispositionType::Attachment => ContentDisposition::Attachment(cd::Filename::with_encoded_name(file_name.into())),
127            };
128
129            (content_type, content_disposition)
130        };
131
132        Self {
133            file,
134            meta,
135            content_type,
136            content_disposition,
137            _config: PhantomData,
138        }
139    }
140
141    #[inline]
142    ///Creates `EntityTag` for file
143    pub fn etag(&self) -> etag::EntityTag {
144        etag::EntityTag::from_file_meta(&self.meta)
145    }
146
147    #[inline]
148    ///Creates `HttpDate` instance for file, if possible
149    pub fn last_modified(&self) -> Option<httpdate::HttpDate> {
150        self.meta.modified().map(|modified| modified.into()).ok()
151    }
152
153    #[inline]
154    ///Returns length of File.
155    pub fn len(&self) -> u64 {
156        self.meta.len()
157    }
158
159    ///Prepares file for service
160    pub fn prepare(self, path: &Path, method: http::Method, headers: &http::HeaderMap, out_headers: &mut http::HeaderMap) -> (http::StatusCode, Body<W, C>) {
161        //ETag is more reliable so it is given priority.
162        if C::is_use_etag(&path) {
163            let etag = self.etag();
164
165            //As per RFC we must send useful cache related headers.
166            //Since cache is from ETag let's send ETag only
167            out_headers.insert(http::header::ETAG, utils::display_to_header(&etag));
168
169            if !if_match(&etag, headers) {
170                return (http::StatusCode::PRECONDITION_FAILED, Body::empty());
171            } else if !if_none_match(&etag, headers) {
172                return (http::StatusCode::NOT_MODIFIED, Body::empty());
173            }
174        }
175
176        if C::is_use_last_modifier(&path) {
177            if let Some(last_modified) = self.last_modified() {
178                out_headers.insert(http::header::LAST_MODIFIED, utils::display_to_header(&last_modified));
179
180                if !if_unmodified_since(last_modified, headers) {
181                    return (http::StatusCode::PRECONDITION_FAILED, Body::empty());
182                } else if !if_modified_since(last_modified, headers) {
183                    return (http::StatusCode::NOT_MODIFIED, Body::empty());
184                }
185            }
186        }
187
188        out_headers.insert(http::header::CONTENT_TYPE, utils::display_to_header(&self.content_type));
189        out_headers.insert(http::header::CONTENT_DISPOSITION, utils::display_to_header(&self.content_disposition));
190        out_headers.insert(http::header::ACCEPT_RANGES, http::header::HeaderValue::from_static("bytes"));
191
192        let mut length = self.len();
193        let mut offset = 0;
194
195        // check for range header
196        if let Some(ranges) = headers.get(http::header::RANGE) {
197            if let Ok(ranges_header) = ranges.to_str() {
198                if let Ok(ranges_vec) = HttpRange::parse(ranges_header, length) {
199                    length = ranges_vec[0].length;
200                    offset = ranges_vec[0].start;
201                    let content_range = utils::display_to_header(&format_args!("bytes {}-{}/{}", offset, offset + length - 1, self.len()));
202                    out_headers.insert(http::header::CONTENT_RANGE, content_range);
203                } else {
204                    let content_range = utils::display_to_header(&format_args!("bytes */{}", length));
205                    out_headers.insert(http::header::CONTENT_RANGE, content_range);
206                    return (http::StatusCode::RANGE_NOT_SATISFIABLE, Body::empty());
207                }
208            } else {
209                return (http::StatusCode::BAD_REQUEST, Body::empty());
210            };
211        };
212
213        out_headers.insert(http::header::CONTENT_LENGTH, utils::display_to_header(&length));
214
215        match method {
216            http::Method::HEAD => (http::StatusCode::OK, Body::empty()),
217            _ => {
218                let code = if offset != 0 || length != self.len() {
219                    http::StatusCode::PARTIAL_CONTENT
220                } else {
221                    http::StatusCode::OK
222                };
223
224                let reader = ChunkedReadFile::<W, C>::new(length, offset, self.into());
225                (code, Body::Chunked(reader))
226            },
227        }
228    }
229}
230
231impl<W, C> Into<fs::File> for ServeFile<W, C> {
232    fn into(self) -> fs::File {
233        self.file
234    }
235}
236
237#[cold]
238#[inline(never)]
239fn map_spawn_error<T: Into<Box<dyn std::error::Error + Send + Sync>>>(error: T) -> io::Error {
240    io::Error::new(io::ErrorKind::Other, error)
241}
242
243///Stream to read chunks of file
244pub struct ChunkedReadFile<W: FsTaskSpawner, C> {
245    ///Size of file to read
246    pub size: u64,
247    offset: u64,
248    ongoing: Option<W::FileReadFut>,
249    file: fs::File,
250    counter: u64,
251    _config: PhantomData<(W, C)>,
252}
253
254impl<W: FsTaskSpawner, C: FileServeConfig> ChunkedReadFile<W, C> {
255    ///Creates new instance
256    pub fn new(size: u64, offset: u64, file: fs::File) -> Self {
257        Self {
258            size,
259            offset,
260            ongoing: None,
261            file,
262            counter: 0,
263            _config: PhantomData
264        }
265    }
266
267    fn next_read(&mut self) -> W::FileReadFut {
268        #[cfg(not(windows))]
269        use std::os::fd::{AsRawFd, FromRawFd};
270        #[cfg(windows)]
271        use std::os::windows::io::{AsRawHandle, FromRawHandle};
272
273        #[cfg(windows)]
274        pub struct Handle(std::os::windows::io::RawHandle);
275        #[cfg(windows)]
276        unsafe impl Send for Handle {}
277
278        #[cfg(not(windows))]
279        let fd = self.file.as_raw_fd();
280        #[cfg(windows)]
281        let fd = Handle(self.file.as_raw_handle());
282
283        let size = self.size;
284        let offset = self.offset;
285        let counter = self.counter;
286
287        W::spawn_file_read(move || {
288            #[cfg(not(windows))]
289            let mut file = unsafe {
290                fs::File::from_raw_fd(fd)
291            };
292            #[cfg(windows)]
293            let mut file = unsafe {
294                fs::File::from_raw_handle(fd.0)
295            };
296
297            let max_bytes = cmp::min(size.saturating_sub(counter), C::max_buffer_size());
298            let mut buf = Vec::with_capacity(max_bytes as usize);
299            let result = match file.seek(io::SeekFrom::Start(offset)) {
300                Ok(_) => match file.by_ref().take(max_bytes).read_to_end(&mut buf) {
301                    Ok(0) => Err(io::ErrorKind::UnexpectedEof.into()),
302                    Ok(_) => Ok(Bytes::from(buf)),
303                    Err(error) => Err(error),
304                },
305                Err(error) => Err(error),
306            };
307            mem::forget(file);
308            result
309        })
310    }
311
312    ///Fetches next chunk
313    pub async fn next(&mut self) -> Result<Option<Bytes>, io::Error> {
314        self.await
315    }
316
317    #[inline(always)]
318    ///Returns `true` if reading is finished
319    pub fn is_finished(&self) -> bool {
320        self.size == self.counter
321    }
322
323    #[inline(always)]
324    ///Returns number of bytes left to read.
325    pub fn remaining(&self) -> u64 {
326        self.size.saturating_sub(self.offset)
327    }
328}
329
330impl<W: FsTaskSpawner, C: FileServeConfig> Future for ChunkedReadFile<W, C> {
331    type Output = Result<Option<Bytes>, io::Error>;
332
333    fn poll(self: Pin<&mut Self>, ctx: &mut task::Context<'_>) -> task::Poll<Self::Output> {
334        if self.is_finished() {
335            return task::Poll::Ready(Ok(None));
336        }
337        let this = self.get_mut();
338
339        loop {
340            if let Some(ongoing) = this.ongoing.as_mut() {
341                break match Future::poll(Pin::new(ongoing), ctx) {
342                    task::Poll::Pending => task::Poll::Pending,
343                    task::Poll::Ready(result) => {
344                        this.ongoing = None;
345                        match result {
346                            Ok(Ok(bytes)) => task::Poll::Ready(Ok(Some(bytes))),
347                            Ok(Err(error)) => task::Poll::Ready(Err(error)),
348                            Err(error) => task::Poll::Ready(Err(map_spawn_error(error))),
349                        }
350                    }
351                }
352            } else {
353                this.ongoing = Some(this.next_read());
354            }
355        }
356    }
357}