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
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
//! Configuration module

use percent_encoding::{utf8_percent_encode};
use crate::utils::PATH_ENCODE_SET;

use std::fs;
use std::path::Path;
use std::fmt::Write;

use crate::headers::cd::DispositionType;

///Default serve directory
pub const DEFAULT_SERVE_DIR: &'static str = ".";

///Describes how to serve file
pub trait FileServeConfig: std::marker::Send {
    ///Returns maximum size for buffer to read file.
    ///
    ///By default 64kb
    fn max_buffer_size() -> u64 {
        65_536
    }

    ///Describes mapping for mime type to content disposition header
    ///
    ///By default `IMAGE`, `TEXT` and `VIDEO` are mapped to Inline.
    ///Others are mapped to Attachment
    fn content_disposition_map(typ: mime::Name) -> DispositionType {
        match typ {
            mime::IMAGE | mime::TEXT | mime::VIDEO => DispositionType::Inline,
            _ => DispositionType::Attachment,
        }
    }

    ///Specifies whether `ETag` should be used for caching
    ///
    ///Provides path to the file, relative to File Service directory
    ///
    ///By default it is true
    fn is_use_etag(_path: &Path) -> bool {
        true
    }

    ///Specifies whether `Last-Modified` should be used for caching
    ///
    ///Provides path to the file, relative to File Service directory
    ///
    ///By default it is true
    fn is_use_last_modifier(_path: &Path) -> bool {
        true
    }
}

///Describes how to serve directory
pub trait DirectoryListingConfig {
    ///Describes how to create HTML text for Directory listing
    ///
    ///`base` is root directory for file service.
    ///`path` is relative to `base` path to directory.
    ///`dir` is result of `fs::read_dir`
    ///
    ///Default implementation provides trivial listing
    fn create_body(base: &Path, path: &Path, dir: fs::ReadDir) -> bytes::Bytes {
        let mut body = crate::utils::BytesWriter::with_capacity(256);

        let _ = write!(body, "<html>\n");
        let _ = write!(body, "    <head>\n");
        let _ = write!(body, "        <title>Index of {}/</title>\n", path.display());
        let _ = write!(body, "    </head>\n");
        let _ = write!(body, "    <body>\n");
        let _ = write!(body, "        <h1>Index of {}/</h1>\n", path.display());
        let _ = write!(body, "        <ul>\n");

        let _ = write!(body, "<li><a href=\"/{}\">.</a></li>\n", utf8_percent_encode(&path.to_string_lossy(), PATH_ENCODE_SET));
        if let Some(parent) = path.parent() {
            let _ = write!(body, "<li><a href=\"/{}\">..</a></li>\n", utf8_percent_encode(&parent.to_string_lossy(), PATH_ENCODE_SET));
        }

        for entry in dir.filter_map(|entry| entry.ok()) {
            if entry.file_name().to_str().map(|entry| entry.starts_with(".")).unwrap_or(false) {
                continue;
            }

            let is_dir = match entry.metadata().map(|meta| meta.file_type()) {
                Ok(meta) => if !meta.is_dir() && !meta.is_file() && !meta.is_symlink() {
                    continue;
                } else {
                    meta.is_dir()
                },
                Err(_) => continue,
            };

            let entry_path = entry.path();
            let entry_path = match entry_path.strip_prefix(base) {
                Ok(res) => res,
                Err(_) => &entry_path,
            };

            let _ = write!(body, "<li><a href=\"");
            for component in entry_path.components().map(|component| component.as_os_str()) {
                let _ = write!(body, "/{}", utf8_percent_encode(&component.to_string_lossy(), PATH_ENCODE_SET));
            }

            let _ = write!(body, "\">{}", v_htmlescape::escape(&entry.file_name().to_string_lossy()));

            if is_dir {
                let _ = write!(body, "/");
            }

            let _ = write!(body, "</a></li>\n");
        }

        let _ = write!(body, "        </ul>\n");
        let _ = write!(body, "    </body>\n");
        let _ = write!(body, "</html>\n");

        body.freeze()
    }
}

///Configuration description
pub trait StaticFileConfig {
    ///File serve configuration
    type FileService: FileServeConfig + 'static;
    ///Directory serve configuration
    type DirService: DirectoryListingConfig;

    ///Returns whether specified method is allowed for use.
    ///By default allows `HEAD` and `GET`
    fn is_method_allowed(method: &http::Method) -> bool {
        match method {
            &http::Method::GET | &http::Method::HEAD => true,
            _ => false,
        }
    }

    ///Returns directory from where to serve files.
    ///
    ///By default returns `.`
    fn serve_dir(&self) -> &Path {
        Path::new(DEFAULT_SERVE_DIR)
    }

    ///Specifies router prefix.
    ///
    ///To be used by frameworks such as Actix
    ///
    ///Defaults to `/`
    fn router_prefix(&self) -> &str {
        "/"
    }

    ///Returns name of index file to show.
    ///
    ///`path` points to directory relative to `StaticFileConfig::serve_dir`.
    ///
    ///By default returns `None`
    fn index_file(&self, _path: &Path) -> Option<&Path> {
        None
    }

    ///Returns whether directory should be listed on access
    ///
    ///`path` points to directory relative to `StaticFileConfig::serve_dir`.
    ///
    ///By default returns `false`
    fn handle_directory(&self, _path: &Path) -> bool {
        false
    }

    ///Handles entry that hasn't been found.
    ///
    ///`path` points to entry relative to `StaticFileConfig::serve_dir`.
    ///
    ///By default prepares empty `NotFound` response with empty body
    fn handle_not_found(&self, _path: &Path, _out_headers: &mut http::HeaderMap) -> (http::StatusCode, bytes::Bytes) {
        (http::StatusCode::NOT_FOUND, bytes::Bytes::with_capacity(0))
    }

    ///Describes how to get instance of `threadpool::ThreadPool`
    ///
    ///By default sets prefix `http-fs` and leaves everything else by default
    fn thread_pool_builder() -> threadpool::ThreadPool {
        threadpool::Builder::new().thread_name("http-fs".to_owned()).build()
    }
}

#[derive(Clone)]
///Default configuration
pub struct DefaultConfig;
impl FileServeConfig for DefaultConfig {}
impl DirectoryListingConfig for DefaultConfig {}
impl StaticFileConfig for DefaultConfig {
    type FileService = Self;
    type DirService = Self;
}