1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */

use std::fs::File;
use std::future::{ready, Future};
use std::io::{BufReader, Seek, SeekFrom};
use std::pin::Pin;

use headers::{ContentType, HeaderMapExt, Range};
use http::Method;
use net_traits::request::Request;
use net_traits::response::{Response, ResponseBody};
use net_traits::{NetworkError, ResourceFetchTiming};
use tokio::sync::mpsc::unbounded_channel;

use crate::fetch::methods::{DoneChannel, FetchContext};
use crate::filemanager_thread::FILE_CHUNK_SIZE;
use crate::local_directory_listing;
use crate::protocols::{
    get_range_request_bounds, partial_content, range_not_satisfiable_error, ProtocolHandler,
};

#[derive(Default)]
pub struct FileProtocolHander {}

impl ProtocolHandler for FileProtocolHander {
    fn load(
        &self,
        request: &mut Request,
        done_chan: &mut DoneChannel,
        context: &FetchContext,
    ) -> Pin<Box<dyn Future<Output = Response> + Send>> {
        let url = request.current_url();

        if request.method != Method::GET {
            return Box::pin(ready(Response::network_error(NetworkError::Internal(
                "Unexpected method for file".into(),
            ))));
        }
        let response = if let Ok(file_path) = url.to_file_path() {
            if file_path.is_dir() {
                return Box::pin(ready(local_directory_listing::fetch(
                    request, url, file_path,
                )));
            }

            if let Ok(file) = File::open(file_path.clone()) {
                // Get range bounds (if any) and try to seek to the requested offset.
                // If seeking fails, bail out with a NetworkError.
                let file_size = match file.metadata() {
                    Ok(metadata) => Some(metadata.len()),
                    Err(_) => None,
                };

                let mut response =
                    Response::new(url, ResourceFetchTiming::new(request.timing_type()));

                let range_header = request.headers.typed_get::<Range>();
                let is_range_request = range_header.is_some();
                let Ok(range) = get_range_request_bounds(range_header).get_final(file_size) else {
                    range_not_satisfiable_error(&mut response);
                    return Box::pin(ready(response));
                };
                let mut reader = BufReader::with_capacity(FILE_CHUNK_SIZE, file);
                if reader.seek(SeekFrom::Start(range.start as u64)).is_err() {
                    return Box::pin(ready(Response::network_error(NetworkError::Internal(
                        "Unexpected method for file".into(),
                    ))));
                }

                // Set response status to 206 if Range header is present.
                // At this point we should have already validated the header.
                if is_range_request {
                    partial_content(&mut response);
                }

                // Set Content-Type header.
                let mime = mime_guess::from_path(file_path).first_or_octet_stream();
                response.headers.typed_insert(ContentType::from(mime));

                // Setup channel to receive cross-thread messages about the file fetch
                // operation.
                let (mut done_sender, done_receiver) = unbounded_channel();
                *done_chan = Some((done_sender.clone(), done_receiver));

                *response.body.lock().unwrap() = ResponseBody::Receiving(vec![]);

                context.filemanager.lock().unwrap().fetch_file_in_chunks(
                    &mut done_sender,
                    reader,
                    response.body.clone(),
                    context.cancellation_listener.clone(),
                    range,
                );

                response
            } else {
                Response::network_error(NetworkError::Internal("Opening file failed".into()))
            }
        } else {
            Response::network_error(NetworkError::Internal(
                "Constructing file path failed".into(),
            ))
        };

        Box::pin(ready(response))
    }
}