spacegate_kernel/backend_service/
static_file_service.rs1use 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
35pub fn cache_policy(metadata: &Metadata) -> bool {
37 let size = metadata.size();
38 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 *response.status_mut() = StatusCode::SEE_OTHER;
85 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 }
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}