spacegate_kernel/backend_service/
static_file_service.rs

1use std::{fs::Metadata, os::unix::fs::MetadataExt, path::Path};
2
3use chrono::{DateTime, Utc};
4use hyper::{
5    header::{HeaderValue, CONTENT_TYPE, IF_MODIFIED_SINCE, IF_UNMODIFIED_SINCE, LOCATION},
6    HeaderMap, Response, StatusCode,
7};
8use tokio::io::AsyncReadExt;
9use tracing::{instrument, trace};
10
11use crate::{extension::Reflect, SgBody, SgRequest, SgResponse};
12
13fn header_value_to_rfc3339(header: &HeaderValue) -> Option<DateTime<Utc>> {
14    let header = header.to_str().ok()?;
15    Some(DateTime::parse_from_rfc3339(header).ok()?.to_utc())
16}
17fn predict(headers: &HeaderMap, last_modified: Option<DateTime<Utc>>) -> Option<StatusCode> {
18    if let Some(since) = headers.get(IF_UNMODIFIED_SINCE).and_then(header_value_to_rfc3339) {
19        if let Some(last_modified) = last_modified {
20            if last_modified > since {
21                return Some(StatusCode::PRECONDITION_FAILED);
22            }
23        }
24    }
25    if let Some(since) = headers.get(IF_MODIFIED_SINCE).and_then(header_value_to_rfc3339) {
26        if let Some(last_modified) = last_modified {
27            if last_modified <= since {
28                return Some(StatusCode::NOT_MODIFIED);
29            }
30        }
31    }
32    None
33}
34
35// temporary implementation
36pub fn cache_policy(metadata: &Metadata) -> bool {
37    let size = metadata.size();
38    // cache file less than 1MB
39    size < (1 << 20)
40}
41
42#[instrument()]
43pub async fn static_file_service(mut request: SgRequest, dir: &Path) -> SgResponse {
44    let mut response = Response::builder().body(SgBody::empty()).expect("failed to build response");
45    if let Some(reflect) = request.extensions_mut().remove::<Reflect>() {
46        *response.extensions_mut() = reflect.into_inner();
47    }
48
49    let path = dir.join(request.uri().path().trim_start_matches('/')).canonicalize().unwrap_or(dir.to_owned());
50    trace!("resolved static file path: {:?}", path);
51    if !path.starts_with(dir) {
52        *response.body_mut() = SgBody::full("file is not under the path");
53        *response.status_mut() = StatusCode::NOT_FOUND;
54        return response;
55    }
56    let mut file = match tokio::fs::File::open(&path).await {
57        Ok(file) => file,
58        Err(e) => match e.kind() {
59            std::io::ErrorKind::NotFound => {
60                *response.status_mut() = StatusCode::NOT_FOUND;
61                return response;
62            }
63            std::io::ErrorKind::PermissionDenied => {
64                *response.body_mut() = SgBody::full("access permission denied");
65                *response.status_mut() = StatusCode::FORBIDDEN;
66                return response;
67            }
68            e => {
69                tracing::error!("failed to read file: {:?}", e);
70                *response.status_mut() = StatusCode::INTERNAL_SERVER_ERROR;
71                return response;
72            }
73        },
74    };
75
76    if let Ok(metadata) = file.metadata().await {
77        let last_modified: Option<DateTime<Utc>> = metadata.modified().ok().map(|t| t.into());
78        if let Some(code) = predict(request.headers(), last_modified) {
79            *response.status_mut() = code;
80            return response;
81        }
82        if metadata.is_dir() {
83            // we may return dir page in the future
84            *response.status_mut() = StatusCode::SEE_OTHER;
85            // redirect to index.html
86            response.headers_mut().insert(LOCATION, HeaderValue::from_static("/index.html"));
87            return response;
88        }
89        let cache_this = cache_policy(&metadata);
90        if cache_this {
91            // TODO: cache
92        }
93    }
94    let mimes = mime_guess::from_path(path).into_iter().filter_map(|mime| HeaderValue::from_str(mime.essence_str()).ok());
95    for mime_value in mimes {
96        response.headers_mut().append(CONTENT_TYPE, mime_value);
97    }
98    let mut buffer = Vec::new();
99    let _read = file.read_to_end(&mut buffer).await;
100    *response.status_mut() = StatusCode::OK;
101    *response.body_mut() = SgBody::full(buffer);
102    response
103}