Skip to main content

rust_web_server/app/controller/static_resource/
mod.rs

1use std::env;
2use std::fs::{File, metadata};
3use file_ext::FileExt;
4use crate::controller::Controller;
5use crate::header::Header;
6use crate::mime_type::MimeType;
7use crate::range::{ContentRange, Range};
8use crate::request::{METHOD, Request};
9use crate::response::{Error, Response, STATUS_CODE_REASON_PHRASE};
10use crate::server::ConnectionInfo;
11use crate::symbol::SYMBOL;
12use crate::url::URL;
13
14#[cfg(test)]
15mod tests;
16
17pub struct StaticResourceController;
18
19impl Controller for StaticResourceController {
20    fn is_matching(request: &Request, _connection: &ConnectionInfo) -> bool {
21        let url_array = ["http://", "localhost", &request.request_uri];
22        let url = url_array.join(SYMBOL.empty_string);
23
24        let boxed_url_components = URL::parse(&url);
25        if boxed_url_components.is_err() {
26            let message = boxed_url_components.as_ref().err().unwrap().to_string();
27            // unfallable
28            println!("unexpected error, {}", message);
29        }
30
31        let components = boxed_url_components.unwrap();
32
33        let os_specific_separator : String = FileExt::get_path_separator();
34        let os_specific_path = &components.path.replace(SYMBOL.slash, os_specific_separator.as_str());
35
36        let boxed_static_filepath = FileExt::get_static_filepath(&os_specific_path);
37        if boxed_static_filepath.is_err() {
38            return false
39        }
40
41        let static_filepath = boxed_static_filepath.unwrap();
42
43        let mut is_directory_with_index_html = false;
44
45        let boxed_md = metadata(&static_filepath);
46        if boxed_md.is_ok() {
47            let md = boxed_md.unwrap();
48            if md.is_dir() {
49                let mut directory_index : String = "index.html".to_string();
50
51                let last_char = components.path.chars().last().unwrap();
52                if last_char != '/' {
53                    let index : String = "index.html".to_string();
54                    directory_index = format!("{}{}", os_specific_separator, index);
55
56                }
57                let index_html_in_directory = format!("{}{}", static_filepath, directory_index);
58
59
60                let boxed_file = File::open(&index_html_in_directory);
61                if boxed_file.is_err() {
62                    return false
63                }
64
65                is_directory_with_index_html = true;
66            }
67        }
68
69
70
71        let boxed_file = File::open(&static_filepath);
72
73        let is_get = request.method == METHOD.get;
74        let is_head = request.method == METHOD.head;
75        let is_options = request.method == METHOD.options;
76
77        let is_matching_method = (is_get || is_head || is_options) && (request.request_uri != SYMBOL.slash);
78
79        if boxed_file.is_ok() || is_directory_with_index_html {
80            is_matching_method
81        } else {
82            // check if file with same name and .html extension exists
83            if static_filepath.ends_with(".html") {
84                return false
85            }
86
87            let html_suffix = ".html";
88            let html_file = [&components.path.replace(SYMBOL.slash, &FileExt::get_path_separator()), html_suffix].join(SYMBOL.empty_string);
89            let boxed_static_filepath = FileExt::get_static_filepath(&html_file);
90            if boxed_static_filepath.is_err() {
91                return false
92            }
93
94            let static_filepath = boxed_static_filepath.unwrap();
95            let boxed_file = File::open(&static_filepath);
96
97            boxed_file.is_ok() && is_matching_method
98        }
99
100    }
101
102    fn process(request: &Request, mut response: Response, _connection: &ConnectionInfo) -> Response {
103        let boxed_content_range_list = StaticResourceController::process_static_resources(&request);
104        if boxed_content_range_list.is_ok() {
105            let content_range_list = boxed_content_range_list.unwrap();
106
107            if content_range_list.len() != 0 {
108
109                let mut status_code_reason_phrase = STATUS_CODE_REASON_PHRASE.n200_ok;
110
111                let does_request_include_range_header = request.get_header(Header::_RANGE.to_string()).is_some();
112                if does_request_include_range_header {
113                    status_code_reason_phrase = STATUS_CODE_REASON_PHRASE.n206_partial_content;
114                }
115
116                let is_options_request = request.method == METHOD.options;
117                if is_options_request {
118                    status_code_reason_phrase = STATUS_CODE_REASON_PHRASE.n204_no_content;
119                }
120
121
122                let dir = env::current_dir().unwrap();
123                let working_directory = dir.as_path().to_str().unwrap();
124
125                let url_array = ["http://", "localhost", &request.request_uri];
126                let url = url_array.join(SYMBOL.empty_string);
127
128                let boxed_url_components = URL::parse(&url);
129                if boxed_url_components.is_err() {
130                    let message = boxed_url_components.as_ref().err().unwrap().to_string();
131                    // unfallable
132                    println!("unexpected error, {}", message);
133                }
134
135                let components = boxed_url_components.unwrap();
136
137                let static_filepath = [working_directory, components.path.as_str()].join(SYMBOL.empty_string);
138                let boxed_modified_date_time = FileExt::file_modified_utc(&static_filepath);
139
140                if boxed_modified_date_time.is_ok() {
141                    let modified_date_time = boxed_modified_date_time.unwrap();
142                    let last_modified_unix_nanos = Header{ name: Header::_LAST_MODIFIED_UNIX_EPOCH_NANOS.to_string(), value: modified_date_time.to_string() };
143                    response.headers.push(last_modified_unix_nanos);
144
145                    let file_size = metadata(&static_filepath).map(|m| m.len()).unwrap_or(0);
146                    let etag_value = format!("\"{}-{}\"", modified_date_time, file_size);
147
148                    let if_none_match = request.get_header(Header::_IF_NONE_MATCH.to_string());
149                    if let Some(inm) = if_none_match {
150                        if inm.value == etag_value || inm.value == "*" {
151                            response.status_code = *STATUS_CODE_REASON_PHRASE.n304_not_modified.status_code;
152                            response.reason_phrase = STATUS_CODE_REASON_PHRASE.n304_not_modified.reason_phrase.to_string();
153                            response.headers.push(Header { name: Header::_ETAG.to_string(), value: etag_value });
154                            return response;
155                        }
156                    }
157
158                    response.headers.push(Header { name: Header::_ETAG.to_string(), value: etag_value });
159
160                    // Stream large files (> 8 MB) without loading into memory, unless it's
161                    // a range request (which needs precise byte slicing from the loaded body).
162                    const STREAM_THRESHOLD: u64 = 8 * 1024 * 1024;
163                    let is_range_request = request.get_header(Header::_RANGE.to_string()).is_some();
164                    if file_size > STREAM_THRESHOLD && !is_range_request {
165                        let mime = MimeType::detect_mime_type(&static_filepath);
166                        response.headers.push(Header {
167                            name: Header::_CONTENT_TYPE.to_string(),
168                            value: mime,
169                        });
170                        response.headers.push(Header {
171                            name: Header::_CONTENT_LENGTH.to_string(),
172                            value: file_size.to_string(),
173                        });
174                        response.status_code = *status_code_reason_phrase.status_code;
175                        response.reason_phrase = status_code_reason_phrase.reason_phrase.to_string();
176                        response.stream_file = Some(static_filepath);
177                        return response;
178                    }
179                }
180
181                response.status_code = *status_code_reason_phrase.status_code;
182                response.reason_phrase = status_code_reason_phrase.reason_phrase.to_string();
183                response.content_range_list = content_range_list;
184
185            }
186        } else {
187            let error : Error = boxed_content_range_list.err().unwrap();
188            let body = error.message;
189
190            let content_range = Range::get_content_range(
191                Vec::from(body.as_bytes()),
192                MimeType::TEXT_HTML.to_string()
193            );
194
195            let content_range_list = vec![content_range];
196
197            response.status_code = *error.status_code_reason_phrase.status_code;
198            response.reason_phrase = error.status_code_reason_phrase.reason_phrase.to_string();
199            response.content_range_list = content_range_list;
200
201        }
202
203
204        response
205    }
206}
207
208//backward compatability
209impl StaticResourceController {
210
211    pub fn is_matching_request(request: &Request) -> bool {
212        let boxed_static_filepath = FileExt::get_static_filepath(&request.request_uri);
213        if boxed_static_filepath.is_err() {
214            return false
215        }
216
217        let static_filepath = boxed_static_filepath.unwrap();
218
219        let boxed_md = metadata(&static_filepath);
220        if boxed_md.is_err() {
221            return false
222        }
223
224        let md = boxed_md.unwrap();
225        if md.is_dir() {
226            return false
227        }
228
229        let boxed_file = File::open(&static_filepath);
230
231        let is_get = request.method == METHOD.get;
232        let is_head = request.method == METHOD.head;
233        let is_options = request.method == METHOD.options;
234        boxed_file.is_ok() && (is_get || is_head || is_options && request.request_uri != SYMBOL.slash)
235    }
236
237    pub fn process_request(request: &Request, mut response: Response) -> Response {
238        let boxed_content_range_list = StaticResourceController::process_static_resources(&request);
239        if boxed_content_range_list.is_ok() {
240            let content_range_list = boxed_content_range_list.unwrap();
241
242            if content_range_list.len() != 0 {
243
244                let mut status_code_reason_phrase = STATUS_CODE_REASON_PHRASE.n200_ok;
245
246                let does_request_include_range_header = request.get_header(Header::_RANGE.to_string()).is_some();
247                if does_request_include_range_header {
248                    status_code_reason_phrase = STATUS_CODE_REASON_PHRASE.n206_partial_content;
249                }
250
251                let is_options_request = request.method == METHOD.options;
252                if is_options_request {
253                    status_code_reason_phrase = STATUS_CODE_REASON_PHRASE.n204_no_content;
254                }
255
256
257                let dir = env::current_dir().unwrap();
258                let working_directory = dir.as_path().to_str().unwrap();
259                let static_filepath = [working_directory, request.request_uri.as_str()].join(SYMBOL.empty_string);
260                let boxed_modified_date_time = FileExt::file_modified_utc(&static_filepath);
261
262                if boxed_modified_date_time.is_ok() {
263                    let modified_date_time = boxed_modified_date_time.unwrap();
264                    let last_modified_unix_nanos = Header{ name: Header::_LAST_MODIFIED_UNIX_EPOCH_NANOS.to_string(), value: modified_date_time.to_string() };
265                    response.headers.push(last_modified_unix_nanos);
266
267                    let file_size = metadata(&static_filepath).map(|m| m.len()).unwrap_or(0);
268                    let etag_value = format!("\"{}-{}\"", modified_date_time, file_size);
269
270                    let if_none_match = request.get_header(Header::_IF_NONE_MATCH.to_string());
271                    if let Some(inm) = if_none_match {
272                        if inm.value == etag_value || inm.value == "*" {
273                            response.status_code = *STATUS_CODE_REASON_PHRASE.n304_not_modified.status_code;
274                            response.reason_phrase = STATUS_CODE_REASON_PHRASE.n304_not_modified.reason_phrase.to_string();
275                            response.headers.push(Header { name: Header::_ETAG.to_string(), value: etag_value });
276                            return response;
277                        }
278                    }
279
280                    response.headers.push(Header { name: Header::_ETAG.to_string(), value: etag_value });
281
282                    const STREAM_THRESHOLD: u64 = 8 * 1024 * 1024;
283                    let is_range_request = request.get_header(Header::_RANGE.to_string()).is_some();
284                    if file_size > STREAM_THRESHOLD && !is_range_request {
285                        let mime = MimeType::detect_mime_type(&static_filepath);
286                        response.headers.push(Header {
287                            name: Header::_CONTENT_TYPE.to_string(),
288                            value: mime,
289                        });
290                        response.headers.push(Header {
291                            name: Header::_CONTENT_LENGTH.to_string(),
292                            value: file_size.to_string(),
293                        });
294                        response.status_code = *status_code_reason_phrase.status_code;
295                        response.reason_phrase = status_code_reason_phrase.reason_phrase.to_string();
296                        response.stream_file = Some(static_filepath);
297                        return response;
298                    }
299                }
300
301                response.status_code = *status_code_reason_phrase.status_code;
302                response.reason_phrase = status_code_reason_phrase.reason_phrase.to_string();
303                response.content_range_list = content_range_list;
304
305            }
306        } else {
307            let error : Error = boxed_content_range_list.err().unwrap();
308            let body = error.message;
309
310            let content_range = Range::get_content_range(
311                Vec::from(body.as_bytes()),
312                MimeType::TEXT_HTML.to_string()
313            );
314
315            let content_range_list = vec![content_range];
316
317            response.status_code = *error.status_code_reason_phrase.status_code;
318            response.reason_phrase = error.status_code_reason_phrase.reason_phrase.to_string();
319            response.content_range_list = content_range_list;
320
321        }
322
323
324        response
325    }
326
327    pub fn process_static_resources(request: &Request) -> Result<Vec<ContentRange>, Error> {
328        let dir = env::current_dir().unwrap();
329        let working_directory = dir.as_path().to_str().unwrap();
330
331        let url_array = ["http://", "localhost", &request.request_uri];
332        let url = url_array.join(SYMBOL.empty_string);
333
334        let boxed_url_components = URL::parse(&url);
335        if boxed_url_components.is_err() {
336            let message = boxed_url_components.as_ref().err().unwrap().to_string();
337            // unfallable
338            println!("unexpected error, {}", message);
339        }
340
341        let components = boxed_url_components.unwrap();
342
343        let os_specific_separator : String = FileExt::get_path_separator();
344        let os_specific_path = &components.path.replace(SYMBOL.slash, os_specific_separator.as_str());
345
346        let boxed_static_filepath = FileExt::get_static_filepath(&os_specific_path);
347
348        let static_filepath = boxed_static_filepath.unwrap();
349
350        let mut content_range_list = Vec::new();
351
352
353        let mut boxed_md = metadata(&static_filepath);
354        if boxed_md.is_err() {
355            let dot_html = format!("{}{}", &static_filepath, ".html");
356            boxed_md = metadata(&dot_html);
357
358            if boxed_md.is_err() {
359                let slash_index_html = format!("{}{}{}", &static_filepath, os_specific_separator,  "index.html");
360                boxed_md = metadata(&slash_index_html);
361            }
362        }
363        if boxed_md.is_ok() {
364            let md = boxed_md.unwrap();
365
366            if md.is_dir() {
367                let mut range_header = &Header {
368                    name: Header::_RANGE.to_string(),
369                    value: "bytes=0-".to_string()
370                };
371
372                let boxed_header = request.get_header(Header::_RANGE.to_string());
373                if boxed_header.is_some() {
374                    range_header = boxed_header.unwrap();
375                }
376
377                let mut directory_index : String = "index.html".to_string();
378
379                let last_char = components.path.chars().last().unwrap();
380                if last_char != '/' {
381                    let index : String = "index.html".to_string();
382                    directory_index = format!("{}{}", os_specific_separator, index);
383                }
384                let index_html_in_directory = format!("{}{}", os_specific_path, directory_index);
385
386
387                let boxed_content_range_list = Range::get_content_range_list(&index_html_in_directory, range_header);
388                if boxed_content_range_list.is_ok() {
389                    content_range_list = boxed_content_range_list.unwrap();
390                } else {
391                    let error = boxed_content_range_list.err().unwrap();
392                    return Err(error)
393                }
394
395                return Ok(content_range_list);
396            }
397
398            let boxed_file = File::open(&static_filepath);
399            if boxed_file.is_ok()  {
400                let md = metadata(&static_filepath).unwrap();
401                if md.is_dir() {
402                    let mut range_header = &Header {
403                        name: Header::_RANGE.to_string(),
404                        value: "bytes=0-".to_string()
405                    };
406
407                    let boxed_header = request.get_header(Header::_RANGE.to_string());
408                    if boxed_header.is_some() {
409                        range_header = boxed_header.unwrap();
410                    }
411
412                    let mut directory_index : String = "index.html".to_string();
413
414                    let last_char = components.path.chars().last().unwrap();
415                    if last_char != '/' {
416                        let index : String = "index.html".to_string();
417                        directory_index = format!("{}{}", os_specific_separator, index);
418                    }
419                    let index_html_in_directory = format!("{}{}", os_specific_path, directory_index);
420
421
422                    let boxed_content_range_list = Range::get_content_range_list(&index_html_in_directory, range_header);
423                    if boxed_content_range_list.is_ok() {
424                        content_range_list = boxed_content_range_list.unwrap();
425                    } else {
426                        let error = boxed_content_range_list.err().unwrap();
427                        return Err(error)
428                    }
429                }
430
431                if md.is_file() {
432                    let mut range_header = &Header {
433                        name: Header::_RANGE.to_string(),
434                        value: "bytes=0-".to_string()
435                    };
436
437                    let boxed_header = request.get_header(Header::_RANGE.to_string());
438                    if boxed_header.is_some() {
439                        range_header = boxed_header.unwrap();
440                    }
441
442                    let boxed_content_range_list = Range::get_content_range_list(&request.request_uri, range_header);
443                    if boxed_content_range_list.is_ok() {
444                        content_range_list = boxed_content_range_list.unwrap();
445                    } else {
446                        let error = boxed_content_range_list.err().unwrap();
447                        return Err(error)
448                    }
449                }
450            }
451
452
453            if boxed_file.is_err() {
454                //check if .html file exists
455                let static_filepath = [working_directory, components.path.as_str(), ".html"].join(SYMBOL.empty_string);
456
457                let boxed_file = File::open(&static_filepath);
458                if boxed_file.is_ok()  {
459                    let md = metadata(&static_filepath).unwrap();
460                    if md.is_file() {
461                        let mut range_header = &Header {
462                            name: Header::_RANGE.to_string(),
463                            value: "bytes=0-".to_string()
464                        };
465
466                        let boxed_header = request.get_header(Header::_RANGE.to_string());
467                        if boxed_header.is_some() {
468                            range_header = boxed_header.unwrap();
469                        }
470
471                        let url_array = ["http://", "localhost", &request.request_uri];
472                        let url = url_array.join(SYMBOL.empty_string);
473
474                        let boxed_url_components = URL::parse(&url);
475                        if boxed_url_components.is_err() {
476                            let message = boxed_url_components.as_ref().err().unwrap().to_string();
477                            // unfallable
478                            println!("unexpected error, {}", message);
479                        }
480
481                        let components = boxed_url_components.unwrap();
482
483                        // let html_file = [SYMBOL.slash, ].join(SYMBOL.empty_string);
484
485
486                        let html_file = [components.path.as_str(), ".html"].join(SYMBOL.empty_string);
487                        let boxed_content_range_list = Range::get_content_range_list(html_file.as_str(), range_header);
488                        if boxed_content_range_list.is_ok() {
489                            content_range_list = boxed_content_range_list.unwrap();
490                        } else {
491                            let error = boxed_content_range_list.err().unwrap();
492                            return Err(error)
493                        }
494                    }
495                }
496            }
497        }
498
499
500        Ok(content_range_list)
501    }
502}