use std::path::{Path, PathBuf};
use actix_web::http::header::{
CACHE_CONTROL, CONTENT_LENGTH, CONTENT_TYPE, LOCATION, X_CONTENT_TYPE_OPTIONS,
};
use actix_web::http::StatusCode;
use actix_web::{HttpRequest, HttpResponse};
use crate::livereload::LiveReload;
use crate::path::{resolve_safe_path, strip_base_path};
#[derive(Clone)]
pub enum NotFoundStrategy {
Plain,
File(PathBuf),
}
#[derive(Clone)]
pub struct StaticServeConfig {
pub root: PathBuf,
pub base_path: String,
pub livereload: LiveReload,
pub not_found: NotFoundStrategy,
}
pub async fn serve_static_file(req: &HttpRequest, cfg: &StaticServeConfig) -> HttpResponse {
let path = req.path();
let remainder = match strip_base_path(path, &cfg.base_path) {
Some(r) => r,
None => {
if path == "/" && cfg.base_path != "/" {
return HttpResponse::TemporaryRedirect()
.insert_header((LOCATION, cfg.base_path.clone()))
.finish();
}
return not_found_response(cfg).await;
}
};
if cfg.base_path != "/" && format!("{path}/") == cfg.base_path {
return HttpResponse::TemporaryRedirect()
.insert_header((LOCATION, cfg.base_path.clone()))
.finish();
}
let resolved = match resolve_safe_path(&cfg.root, remainder) {
Some(p) => p,
None => return not_found_response(cfg).await,
};
let final_path = if resolved.is_dir() {
if !path.ends_with('/') {
return HttpResponse::TemporaryRedirect()
.insert_header((LOCATION, format!("{path}/")))
.finish();
}
resolved.join("index.html")
} else {
resolved
};
match tokio::fs::read(&final_path).await {
Ok(bytes) => serve_file_response(&cfg.livereload, &final_path, bytes),
Err(_) => not_found_response(cfg).await,
}
}
#[must_use]
pub fn serve_file_response(livereload: &LiveReload, path: &Path, bytes: Vec<u8>) -> HttpResponse {
let mime = mime_guess::from_path(path)
.first_or_octet_stream()
.to_string();
let is_html = mime.starts_with("text/html");
let body = if is_html {
match std::str::from_utf8(&bytes) {
Ok(html) => livereload.inject(html).into_bytes(),
Err(_) => bytes,
}
} else {
bytes
};
let len = body.len();
HttpResponse::Ok()
.insert_header((CONTENT_TYPE, mime))
.insert_header((CONTENT_LENGTH, len.to_string()))
.insert_header((CACHE_CONTROL, "no-cache, no-store, must-revalidate"))
.insert_header((X_CONTENT_TYPE_OPTIONS, "nosniff"))
.body(body)
}
async fn not_found_response(cfg: &StaticServeConfig) -> HttpResponse {
if let NotFoundStrategy::File(rel) = &cfg.not_found {
let path = cfg.root.join(rel);
if let Ok(bytes) = tokio::fs::read(&path).await {
let mut resp = serve_file_response(&cfg.livereload, &path, bytes);
*resp.status_mut() = StatusCode::NOT_FOUND;
return resp;
}
}
HttpResponse::NotFound()
.content_type("text/plain; charset=utf-8")
.body("404 Not Found")
}