net/
local_directory_listing.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::{DirEntry, Metadata, ReadDir};
6use std::path::PathBuf;
7
8use chrono::{DateTime, Local};
9use embedder_traits::resources::{Resource, read_string};
10use headers::{ContentType, HeaderMapExt};
11use net_traits::request::Request;
12use net_traits::response::{Response, ResponseBody};
13use net_traits::{NetworkError, ResourceFetchTiming};
14use servo_config::pref;
15use servo_url::ServoUrl;
16use url::Url;
17
18pub fn fetch(request: &mut Request, url: ServoUrl, path_buf: PathBuf) -> Response {
19    if !pref!(network_local_directory_listing_enabled) {
20        // If you want to be able to browse local directories, configure Servo prefs so that
21        // "network.local_directory_listing.enabled" is set to true.
22        return Response::network_error(NetworkError::Internal(
23            "Local directory listing feature has not been enabled in preferences".into(),
24        ));
25    }
26
27    if !request.origin.is_opaque() {
28        // Checking for an opaque origin as a shorthand for user activation
29        // as opposed to a request originating from a script.
30        // TODO(32534): carefully consider security of this approach.
31        return Response::network_error(NetworkError::Internal(
32            "Cannot request local directory listing from non-local origin.".into(),
33        ));
34    }
35
36    let directory_contents = match std::fs::read_dir(path_buf.clone()) {
37        Ok(directory_contents) => directory_contents,
38        Err(error) => {
39            return Response::network_error(NetworkError::Internal(format!(
40                "Unable to access directory: {error}"
41            )));
42        },
43    };
44
45    let output = build_html_directory_listing(url.as_url(), path_buf, directory_contents);
46
47    let mut response = Response::new(url, ResourceFetchTiming::new(request.timing_type()));
48    response.headers.typed_insert(ContentType::html());
49    *response.body.lock().unwrap() = ResponseBody::Done(output.into_bytes());
50
51    response
52}
53
54/// Returns an the string of an JavaScript `<script>` tag calling the `setData` function with the
55/// contents of the given [`ReadDir`] directory listing.
56///
57/// # Arguments
58///
59/// * `url` - the original URL of the request that triggered this directory listing.
60/// * `path` - the full path to the local directory.
61/// * `directory_contents` - a [`ReadDir`] with the contents of the directory.
62pub fn build_html_directory_listing(
63    url: &Url,
64    path: PathBuf,
65    directory_contents: ReadDir,
66) -> String {
67    let mut page_html = String::with_capacity(1024);
68    page_html.push_str("<!DOCTYPE html>");
69
70    let mut parent_url_string = String::new();
71    if path.parent().is_some() {
72        let mut parent_url = url.clone();
73        if let Ok(mut path_segments) = parent_url.path_segments_mut() {
74            path_segments.pop();
75        }
76        parent_url.as_str().clone_into(&mut parent_url_string);
77    }
78
79    page_html.push_str(&read_string(Resource::DirectoryListingHTML));
80
81    page_html.push_str("<script>\n");
82    page_html.push_str(&format!(
83        "setData({:?}, {:?}, [",
84        url.as_str(),
85        parent_url_string
86    ));
87
88    for directory_entry in directory_contents {
89        let Ok(directory_entry) = directory_entry else {
90            continue;
91        };
92        let Ok(metadata) = directory_entry.metadata() else {
93            continue;
94        };
95        write_directory_entry(directory_entry, metadata, url, &mut page_html);
96    }
97
98    page_html.push_str("]);");
99    page_html.push_str("</script>\n");
100
101    page_html
102}
103
104fn write_directory_entry(entry: DirEntry, metadata: Metadata, url: &Url, output: &mut String) {
105    let Ok(name) = entry.file_name().into_string() else {
106        return;
107    };
108
109    let mut file_url = url.clone();
110    {
111        let Ok(mut path_segments) = file_url.path_segments_mut() else {
112            return;
113        };
114        path_segments.push(&name);
115    }
116
117    let class = if metadata.is_dir() {
118        "directory"
119    } else if metadata.is_symlink() {
120        "symlink"
121    } else {
122        "file"
123    };
124
125    let file_url_string = &file_url.to_string();
126    let file_size = metadata_to_file_size_string(&metadata);
127    let last_modified = metadata
128        .modified()
129        .map(DateTime::<Local>::from)
130        .map(|time| time.format("%F %r").to_string())
131        .unwrap_or_default();
132
133    output.push_str(&format!(
134        "[{class:?}, {name:?}, {file_url_string:?}, {file_size:?}, {last_modified:?}],"
135    ));
136}
137
138pub fn metadata_to_file_size_string(metadata: &Metadata) -> String {
139    if !metadata.is_file() {
140        return String::new();
141    }
142
143    let mut float_size = metadata.len() as f64;
144    let mut prefix_power = 0;
145    while float_size > 1000.0 && prefix_power < 3 {
146        float_size /= 1000.0;
147        prefix_power += 1;
148    }
149
150    let prefix = match prefix_power {
151        0 => "B",
152        1 => "KB",
153        2 => "MB",
154        _ => "GB",
155    };
156
157    format!("{:.2} {prefix}", float_size)
158}