diff --git a/src/api.rs b/src/api.rs index 3c30ac6..b44e7e5 100644 --- a/src/api.rs +++ b/src/api.rs @@ -1,67 +1,17 @@ -use crate::display::{create_display_thread, EInkPanel}; use crate::dither::{DitherMethod, DitheredImage}; use crate::eink::Palette; use axum::async_trait; use axum::extract::{FromRequest, Multipart, State}; use axum::http::{header, StatusCode}; use axum::response::IntoResponse; -use axum::{response::Response, routing::post, Router}; +use axum::{routing::post, Router}; use image::{imageops::resize, imageops::FilterType, ImageReader, RgbImage}; use std::io::Cursor; use std::str; use std::str::FromStr; -use std::sync::mpsc; -use std::sync::Arc; -use std::thread::JoinHandle; -use tracing::{error, info, instrument}; +use tracing::{info, instrument}; -#[derive(thiserror::Error, Debug)] -pub enum ApiError { - #[error("missing image field")] - MissingImage, -} - -#[derive(Clone)] -pub struct AppState { - display_channel: mpsc::Sender>, - display_task: Arc>, -} - -impl AppState { - #[must_use] - pub fn new(disp: Box) -> Self { - let (handle, tx) = create_display_thread(disp); - Self { - display_channel: tx, - display_task: Arc::new(handle), - } - } -} - -// Make our own error that wraps `anyhow::Error`. -struct AppError(anyhow::Error); - -// Tell axum how to convert `AppError` into a response. -impl IntoResponse for AppError { - fn into_response(self) -> Response { - ( - StatusCode::INTERNAL_SERVER_ERROR, - format!("Something went wrong: {}", self.0), - ) - .into_response() - } -} - -// This enables using `?` on functions that return `Result<_, anyhow::Error>` to turn them into -// `Result<_, AppError>`. That way you don't need to do that manually. -impl From for AppError -where - E: Into, -{ - fn from(err: E) -> Self { - Self(err.into()) - } -} +use crate::app::{ApiError, AppError, AppState}; /// API routes for axum /// Start with the basics: Send an image, crop it, dither, and upload. @@ -132,7 +82,6 @@ async fn set_image( State(ctx): State, img_req: ImageRequest, ) -> Result { - // FIXME: resize image to 800x480 to match the eink panel. info!("Got image"); let mut buf = DitheredImage::new(800, 480, img_req.palette.value().to_vec()); let resized = resize(&*img_req.image, 800, 480, FilterType::Lanczos3); diff --git a/src/app.rs b/src/app.rs new file mode 100644 index 0000000..39aa535 --- /dev/null +++ b/src/app.rs @@ -0,0 +1,136 @@ +use crate::api; +use crate::display::create_display_thread; +use crate::dither::DitheredImage; +use axum::extract::{Path, State}; +use axum::http::{header, StatusCode}; +use axum::response::{IntoResponse, Response}; +use axum::routing::get; +use axum::Router; +use include_dir::{include_dir, Dir, DirEntry}; +use minijinja::Environment; +use std::sync::mpsc; +use std::sync::Arc; +use std::thread::JoinHandle; +use tracing::{debug, warn}; +use tracing::{error, info, instrument}; + +#[derive(thiserror::Error, Debug)] +pub enum ApiError { + #[error("missing image field")] + MissingImage, +} + +#[derive(Clone)] +pub struct AppState { + pub display_channel: mpsc::Sender>, + pub display_task: Arc>, + pub templates: Arc>, +} + +impl Default for AppState { + #[must_use] + fn default() -> Self { + let (handle, tx) = create_display_thread(); + Self { + display_channel: tx, + display_task: Arc::new(handle), + templates: Arc::new(make_environment().expect("valid")), + } + } +} + +/// Represents an error that can be returned to the requester by implementing ``IntoResponse``. +/// This wraps ``anyhow::Error`` so that it can accept any error object. +pub struct AppError(anyhow::Error); + +// Tell axum how to convert `AppError` into a response. +impl IntoResponse for AppError { + fn into_response(self) -> Response { + ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("Something went wrong: {}", self.0), + ) + .into_response() + } +} + +// This enables using `?` on functions that return `Result<_, anyhow::Error>` to turn them into +// `Result<_, AppError>`. That way you don't need to do that manually. +impl From for AppError +where + E: Into, +{ + fn from(err: E) -> Self { + Self(err.into()) + } +} + +static TEMPLATES_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/templates"); +static ASSETS_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/static"); + +#[instrument] +fn make_environment() -> Result, anyhow::Error> { + let mut env = Environment::new(); + let mut entries = vec![&TEMPLATES_DIR]; + + while let Some(dir) = entries.pop() { + for entry in dir.entries() { + match entry { + DirEntry::Dir(d) => { + entries.push(d); + } + DirEntry::File(f) => { + debug!("adding {:?} to minijinja environment", f.path()); + env.add_template( + f.path().to_str().expect("valid pathname"), + f.contents_utf8().expect("contents are valid"), + )?; + } + } + } + } + + info!( + "loaded {} templates into environment", + env.templates().count() + ); + + Ok(env) +} + +pub fn make_app_router() -> Router { + Router::new() + .route("/app/*path", get(app_handler)) + .nest("/api", api::router()) + .route("/assets", get(asset_handler)) + .with_state(AppState::default()) +} + +#[instrument(skip(ctx))] +async fn app_handler( + State(ctx): State, + Path(path): Path, +) -> Result { + let template = ctx.templates.get_template(&path.to_string())?; + + let content = template.render("")?; + + let headers = [(header::CONTENT_TYPE, mime::TEXT_HTML_UTF_8.to_string())]; + + Ok((StatusCode::OK, headers, content)) +} + +async fn asset_handler() -> Result { + Ok(StatusCode::OK) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_environment() -> anyhow::Result<()> { + make_environment()?; + Ok(()) + } +} diff --git a/src/main.rs b/src/main.rs index 3da3e52..a4fbaf8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,12 +1,12 @@ pub mod api; +pub mod app; pub mod display; pub mod dither; pub mod eink; -pub mod app; use serde::Deserialize; -use tower_http::trace::TraceLayer; use std::path::PathBuf; +use tower_http::trace::TraceLayer; use crate::display::get_display; use crate::dither::{DitherMethod, DitheredImage}; @@ -84,10 +84,9 @@ async fn main() -> anyhow::Result<()> { display.display(&eink_buf)?; } Command::Serve => { - let display = get_display(); - let ctx = api::AppState::new(display); - let app = api::router().with_state(ctx).layer(TraceLayer::new_for_http()); + let app = app::make_app_router() + .layer(TraceLayer::new_for_http()); let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?; info!("Listening on 0.0.0.0:3000"); axum::serve(listener, app).await?;