use actix_files::{Files, NamedFile}; use actix_web::{ body::MessageBody, dev::ServiceResponse, get, http::header::{HeaderValue, CACHE_CONTROL, PRAGMA}, middleware::{ErrorHandlerResponse, ErrorHandlers}, web::Data, App, HttpRequest, HttpResponse, HttpServer, ResponseError, }; use anyhow::Error; use askama::Template; use log::{error, info}; use nekrochan::{ cfg::Cfg, ctx::Ctx, db::{cache::init_cache, models::Banner}, error::NekrochanError, schedule::s_cleanup_files, web::{self, template_response}, }; use sqlx::migrate; use std::{env::var, time::Duration}; use tokio::time::sleep; #[actix_web::main] async fn main() { dotenv::dotenv().ok(); env_logger::init(); if let Err(err) = run().await { error!("{err:?}"); } } async fn run() -> Result<(), Error> { let cfg_path = var("NEKROCHAN_CONFIG").unwrap_or_else(|_| "Nekrochan.toml".into()); let cfg = Cfg::load(&cfg_path).await?; let ctx = Ctx::new(cfg).await?; migrate!().run(ctx.db()).await?; init_cache(&ctx).await?; let ctx_ = ctx.clone(); tokio::spawn(async move { loop { match s_cleanup_files(&ctx_).await { Ok(()) => info!("Routine file cleanup successful."), Err(err) => error!("Routine file cleanup failed: {err:?}"), }; sleep(Duration::from_secs(ctx_.cfg.files.cleanup_interval)).await; } }); let bind_addr = ctx.bind_addr(); HttpServer::new(move || { App::new() .app_data(Data::new(ctx.clone())) .service(web::board::board) .service(web::board_catalog::board_catalog) .service(web::index::index) .service(web::captcha::captcha) .service(web::edit_posts::edit_posts) .service(web::ip_posts::ip_posts) .service(web::live::live) .service(web::login::login_get) .service(web::login::login_post) .service(web::logout::logout) .service(web::news::news) .service(web::overboard::overboard) .service(web::overboard_catalog::overboard_catalog) .service(web::page::page) .service(web::search::search) .service(web::thread::thread) .service(web::thread_json::thread_json) .service(web::actions::appeal_ban::appeal_ban) .service(web::actions::create_post::create_post) .service(web::actions::edit_posts::edit_posts) .service(web::actions::report_posts::report_posts) .service(web::actions::staff_post_actions::staff_post_actions) .service(web::actions::user_post_actions::user_post_actions) .service(web::staff::account::account) .service(web::staff::accounts::accounts) .service(web::staff::bans::bans) .service(web::staff::banners::banners) .service(web::staff::board_config::board_config) .service(web::staff::boards::boards) .service(web::staff::edit_news::edit_news) .service(web::staff::news::news) .service(web::staff::permissions::permissions) .service(web::staff::reports::reports) .service(web::staff::actions::add_banners::add_banners) .service(web::staff::actions::change_password::change_password) .service(web::staff::actions::create_account::create_account) .service(web::staff::actions::create_board::create_board) .service(web::staff::actions::create_news::create_news) .service(web::staff::actions::delete_account::delete_account) .service(web::staff::actions::edit_news::edit_news) .service(web::staff::actions::remove_accounts::remove_accounts) .service(web::staff::actions::remove_banners::remove_banners) .service(web::staff::actions::remove_bans::remove_bans) .service(web::staff::actions::remove_boards::remove_boards) .service(web::staff::actions::remove_news::remove_news) .service(web::staff::actions::transfer_ownership::transfer_ownership) .service(web::staff::actions::update_board_config::update_board_config) .service(web::staff::actions::update_boards::update_boards) .service(web::staff::actions::update_permissions::update_permissions) .service(favicon) .service(random_banner) .service(Files::new("/static", "./static")) .service(Files::new("/uploads", "./uploads").disable_content_disposition()) .wrap(ErrorHandlers::new().default_handler(error_handler)) }) .bind(bind_addr)? .run() .await?; Ok(()) } #[get("/favicon.ico")] async fn favicon() -> Result { let favicon = NamedFile::open("./static/favicon.ico")?; Ok(favicon) } #[get("/random-banner")] async fn random_banner(ctx: Data, req: HttpRequest) -> Result { let file = if let Some(banner) = Banner::read_random(&ctx).await? { let timestamp = banner.banner.timestamp; let format = &banner.banner.format; NamedFile::open(format!("./uploads/{timestamp}.{format}"))? } else { NamedFile::open("./static/default-banner.png")? }; let mut res = file.into_response(&req); res.headers_mut().append( CACHE_CONTROL, HeaderValue::from_static("no-cache, no-store, must-revalidate"), ); res.headers_mut() .append(PRAGMA, HeaderValue::from_static("no-cache")); Ok(res) } #[derive(Template)] #[template(path = "error.html")] struct ErrorTempalate { error_code: u16, error_message: String, } fn error_handler(res: ServiceResponse) -> actix_web::Result> where B: MessageBody, ::Error: ResponseError + 'static, { let (req, res) = res.into_parts(); let status = res.status(); let error_code = status.as_u16(); let error_message = match res.into_body().try_into_bytes().ok() { Some(bytes) => String::from_utf8(bytes.to_vec()).unwrap_or_default(), None => String::default(), }; let template = ErrorTempalate { error_code, error_message, }; let mut res = template_response(&template)?; *(res.status_mut()) = status; let res = ServiceResponse::new(req, res).map_into_right_body(); Ok(ErrorHandlerResponse::Response(res)) }