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