net/protocols/
file.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use std::fs::File;
6use std::future::{Future, ready};
7use std::io::{BufReader, Seek, SeekFrom};
8use std::pin::Pin;
9
10use headers::{ContentType, HeaderMapExt, Range};
11use http::Method;
12use net_traits::request::Request;
13use net_traits::response::{Response, ResponseBody};
14use net_traits::{NetworkError, ResourceFetchTiming};
15use tokio::sync::mpsc::unbounded_channel;
16
17use crate::fetch::methods::{DoneChannel, FetchContext};
18use crate::filemanager_thread::FILE_CHUNK_SIZE;
19use crate::local_directory_listing;
20use crate::protocols::{
21    ProtocolHandler, get_range_request_bounds, partial_content, range_not_satisfiable_error,
22};
23
24#[derive(Default)]
25pub struct FileProtocolHander {}
26
27impl ProtocolHandler for FileProtocolHander {
28    fn load(
29        &self,
30        request: &mut Request,
31        done_chan: &mut DoneChannel,
32        context: &FetchContext,
33    ) -> Pin<Box<dyn Future<Output = Response> + Send>> {
34        let url = request.current_url();
35
36        if request.method != Method::GET {
37            return Box::pin(ready(Response::network_error(NetworkError::Internal(
38                "Unexpected method for file".into(),
39            ))));
40        }
41        let response = if let Ok(file_path) = url.to_file_path() {
42            if file_path.is_dir() {
43                return Box::pin(ready(local_directory_listing::fetch(
44                    request, url, file_path,
45                )));
46            }
47
48            if let Ok(file) = File::open(file_path.clone()) {
49                // Get range bounds (if any) and try to seek to the requested offset.
50                // If seeking fails, bail out with a NetworkError.
51                let file_size = match file.metadata() {
52                    Ok(metadata) => Some(metadata.len()),
53                    Err(_) => None,
54                };
55
56                let mut response =
57                    Response::new(url, ResourceFetchTiming::new(request.timing_type()));
58
59                let range_header = request.headers.typed_get::<Range>();
60                let is_range_request = range_header.is_some();
61                let Ok(range) = get_range_request_bounds(range_header, file_size.unwrap_or(0))
62                    .get_final(file_size)
63                else {
64                    range_not_satisfiable_error(&mut response);
65                    return Box::pin(ready(response));
66                };
67                let mut reader = BufReader::with_capacity(FILE_CHUNK_SIZE, file);
68                if reader.seek(SeekFrom::Start(range.start as u64)).is_err() {
69                    return Box::pin(ready(Response::network_error(NetworkError::Internal(
70                        "Unexpected method for file".into(),
71                    ))));
72                }
73
74                // Set response status to 206 if Range header is present.
75                // At this point we should have already validated the header.
76                if is_range_request {
77                    partial_content(&mut response);
78                }
79
80                // Set Content-Type header.
81                let mime = mime_guess::from_path(file_path).first_or_octet_stream();
82                response.headers.typed_insert(ContentType::from(mime));
83
84                // Setup channel to receive cross-thread messages about the file fetch
85                // operation.
86                let (mut done_sender, done_receiver) = unbounded_channel();
87                *done_chan = Some((done_sender.clone(), done_receiver));
88
89                *response.body.lock().unwrap() = ResponseBody::Receiving(vec![]);
90
91                context.filemanager.lock().unwrap().fetch_file_in_chunks(
92                    &mut done_sender,
93                    reader,
94                    response.body.clone(),
95                    context.cancellation_listener.clone(),
96                    range,
97                );
98
99                response
100            } else {
101                Response::network_error(NetworkError::Internal("Opening file failed".into()))
102            }
103        } else {
104            Response::network_error(NetworkError::Internal(
105                "Constructing file path failed".into(),
106            ))
107        };
108
109        Box::pin(ready(response))
110    }
111}