This commit is contained in:
nora 2022-03-06 16:31:22 +01:00
parent d7b574cef4
commit 85ef2aa7fd
6 changed files with 395 additions and 52 deletions

View file

@ -0,0 +1,149 @@
use axum::{
body::Body,
http::{header, Request, Response, StatusCode},
};
use mime_guess::mime;
use std::{
collections::HashMap,
fmt::{Debug, Formatter},
future,
io::Cursor,
path::Path,
task::{Context, Poll},
};
use tracing::trace;
use zip::ZipArchive;
#[derive(Debug, Clone)]
enum StaticFileKind {
File { mime: mime::Mime },
Directory,
}
#[derive(Clone)]
struct StaticFile {
data: Vec<u8>,
kind: StaticFileKind,
}
impl Debug for StaticFile {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_struct("StaticFile")
.field("kind", &self.kind)
.field("data", &format!("[{} bytes]", self.data.len()))
.finish()
}
}
#[derive(Debug, Clone)]
pub struct StaticFileService {
files: HashMap<String, StaticFile>,
}
impl StaticFileService {
pub fn new(zip: &[u8]) -> Self {
let mut archive = ZipArchive::new(Cursor::new(zip)).unwrap();
let mut files = HashMap::with_capacity(archive.len());
for i in 0..archive.len() {
let mut file = archive.by_index(i).unwrap();
let mut data = Vec::with_capacity(usize::try_from(file.size()).unwrap());
std::io::copy(&mut file, &mut data).unwrap();
trace!(name = %file.name(), size = %file.size(),"Unpacking dashboard frontend file");
let path = Path::new(file.name());
let kind = if file.is_dir() {
StaticFileKind::Directory
} else {
let mime = match path.extension() {
Some(ext) => {
mime_guess::from_ext(&ext.to_string_lossy()).first_or_octet_stream()
}
None => mime::APPLICATION_OCTET_STREAM,
};
StaticFileKind::File { mime }
};
files.insert(
file.name().trim_start_matches('/').to_owned(),
StaticFile { data, kind },
);
}
trace!(?files, "Created StaticFileService");
Self { files }
}
fn call_inner(&mut self, req: Request<Body>) -> Result<Response<Body>, anyhow::Error> {
trace!(?req, "Got request for static file");
let path = req.uri().path().trim_start_matches('/');
let entry = self.files.get(path);
match entry {
Some(file) => {
if let StaticFileKind::File { mime } = &file.kind {
trace!(%path, "Found file");
Ok(Response::builder()
.status(StatusCode::OK)
.header(header::CONTENT_TYPE, mime.essence_str())
.body(Body::from(file.data.clone()))?)
} else {
trace!(%path, "Found directory, trying to append index.html");
let new_path = if path.is_empty() {
"index.html".to_owned()
} else {
let new_path = path.trim_end_matches('/');
format!("{}/index.html", new_path)
};
match self.files.get(&new_path) {
Some(file) => {
trace!(%new_path, "Found index.html");
if let StaticFileKind::File { mime } = &file.kind {
Ok(Response::builder()
.status(StatusCode::OK)
.header(header::CONTENT_TYPE, mime.essence_str())
.body(Body::from(file.data.clone()))?)
} else {
unreachable!()
}
}
None => {
trace!(%new_path, "Did not find index.html");
Ok(Response::builder()
.status(StatusCode::NOT_FOUND)
.body(Body::empty())?)
}
}
}
}
None => {
trace!(%path, "Did not find file");
Ok(Response::builder()
.status(StatusCode::NOT_FOUND)
.body(Body::empty())?)
}
}
}
}
impl tower::Service<Request<Body>> for StaticFileService {
type Response = Response<Body>;
type Error = anyhow::Error;
type Future = future::Ready<Result<Self::Response, Self::Error>>;
fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
fn call(&mut self, req: Request<Body>) -> Self::Future {
future::ready(self.call_inner(req))
}
}

View file

@ -1,31 +1,42 @@
#![warn(rust_2018_idioms)]
mod archive;
use crate::archive::StaticFileService;
use amqp_core::GlobalData;
use axum::{
body::{boxed, Full},
http::Method,
response::{Html, IntoResponse, Response},
routing::get,
http::{Method, StatusCode},
response::IntoResponse,
routing::{get, get_service},
Json, Router,
};
use serde::Serialize;
use tower_http::cors::{Any, CorsLayer};
use tracing::info;
use tracing::{error, info};
const INDEX_HTML: &str = include_str!("../assets/index.html");
const SCRIPT_JS: &str = include_str!("../assets/script.js");
const STYLE_CSS: &str = include_str!("../assets/style.css");
// const INDEX_HTML: &str = include_str!("../assets/index.html");
// const SCRIPT_JS: &str = include_str!("../assets/script.js");
// const STYLE_CSS: &str = include_str!("../assets/style.css");
const DATA_ZIP: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/frontend.zip"));
pub async fn dashboard(global_data: GlobalData) {
let cors = CorsLayer::new()
.allow_methods(vec![Method::GET])
.allow_origin(Any);
let static_file_service =
get_service(StaticFileService::new(DATA_ZIP)).handle_error(|error| async move {
error!(?error, "Error in static file service");
StatusCode::INTERNAL_SERVER_ERROR
});
let app = Router::new()
.route("/", get(get_index_html))
.route("/script.js", get(get_script_js))
.route("/style.css", get(get_style_css))
.route("/api/data", get(move || get_data(global_data)).layer(cors));
//.route("/", get(get_index_html))
//.route("/script.js", get(get_script_js))
//.route("/style.css", get(get_style_css))
.route("/api/data", get(move || get_data(global_data)).layer(cors))
.fallback(static_file_service);
let socket_addr = "0.0.0.0:8080".parse().unwrap();
@ -37,24 +48,24 @@ pub async fn dashboard(global_data: GlobalData) {
.unwrap();
}
async fn get_index_html() -> impl IntoResponse {
info!("Requesting index.html");
Html(INDEX_HTML)
}
async fn get_script_js() -> Response {
Response::builder()
.header("content-type", "application/javascript")
.body(boxed(Full::from(SCRIPT_JS)))
.unwrap()
}
async fn get_style_css() -> Response {
Response::builder()
.header("content-type", "text/css")
.body(boxed(Full::from(STYLE_CSS)))
.unwrap()
}
//async fn get_index_html() -> impl IntoResponse {
// info!("Requesting index.html");
// Html(INDEX_HTML)
//}
//
//async fn get_script_js() -> Response {
// Response::builder()
// .header("content-type", "application/javascript")
// .body(boxed(Full::from(SCRIPT_JS)))
// .unwrap()
//}
//
//async fn get_style_css() -> Response {
// Response::builder()
// .header("content-type", "text/css")
// .body(boxed(Full::from(STYLE_CSS)))
// .unwrap()
//}
#[derive(Serialize)]
struct Data {