use std::{ops::Deref, path::Path, sync::Arc, time::Duration}; use crate::{ assets, config::Config, db::Db, icons::{icon, Icons}, model::{Info, VersionInfo}, page::page, storage::Storage, util, }; use clokwerk::{Interval, Scheduler}; use path_macro::path; use poem::{ http::header, listener::TcpListener, middleware, Endpoint, EndpointExt, Route, RouteDomain, Server, }; use time::OffsetDateTime; use tokio::signal; #[derive(Clone)] pub struct Talon { i: Arc, } pub struct TalonInner { pub cfg: Config, pub db: Db, pub storage: Storage, pub icons: Icons, pub start_time: OffsetDateTime, } impl Deref for Talon { type Target = TalonInner; fn deref(&self) -> &Self::Target { &self.i } } #[derive(thiserror::Error, Debug)] pub enum TalonError { #[error("db error: {0}")] Db(#[from] crate::db::DbError), #[error("db error: {0}")] Storage(#[from] crate::storage::StorageError), #[error("db error: {0}")] Config(#[from] crate::config::ConfigError), #[error("io error: {0}")] Io(#[from] std::io::Error), } pub type Result = std::result::Result; impl Talon { pub fn new>(workdir: P) -> Result { let db_dir = path!(workdir / "db"); let storage_dir = path!(workdir / "storage"); let icons_dir = path!(workdir / "icons"); util::create_dir_ne(&workdir)?; util::create_dir_ne(&db_dir)?; util::create_dir_ne(&storage_dir)?; util::create_dir_ne(&icons_dir)?; let cfg = Config::from_file(path!(workdir / "config.toml"))?; let db = Db::new(db_dir)?; let storage = Storage::new(storage_dir, db.clone(), cfg.clone()); let icons = Icons::new(icons_dir); let talon = Self { i: TalonInner { cfg, db, storage, icons, start_time: OffsetDateTime::now_utc(), } .into(), }; Ok(talon) } fn scheduler(&self) -> Scheduler { let mut scheduler = Scheduler::new(); let talon = self.clone(); scheduler .every(Interval::Minutes(self.cfg.server.purge_interval)) .run(move || { log::info!("Starting purge"); match talon.storage.purge() { Ok((files, freed)) => log::info!("{files} files purged, {freed} bytes freed"), Err(e) => log::error!("purge error: {e}"), } }); scheduler } pub fn endpoint(&self) -> impl Endpoint { let api_service = crate::api::api_service().server(format!("{}/api", self.i.cfg.server.internal_url)); let swagger_ui = api_service.swagger_ui(); let spec = api_service.spec(); let route_internal = Route::new() .at( "/", poem::endpoint::make_sync(|_| "Hello World, I am Talon"), ) .nest( "/api", api_service .with(middleware::Cors::new()) .with(crate::middleware::LastModified), ) .nest("/api/swagger", swagger_ui) .at( "/api/spec", poem::endpoint::make_sync(move |_| spec.clone()), ) .at("/assets/menu/*path", assets::menu_assets) .at("/icons/:subdomain", icon); let internal_domain = format!( "{}.{}", self.i.cfg.server.internal_subdomain, self.i.cfg.server.root_domain ); let site_domains = format!("+.{}", self.i.cfg.server.root_domain); RouteDomain::new() .at(internal_domain, route_internal) .at(site_domains, page) .at(&self.i.cfg.server.root_domain, page) .with(middleware::Tracing) .with( middleware::SetHeader::new().overriding(header::X_CONTENT_TYPE_OPTIONS, "nosniff"), ) .data(self.clone()) } async fn shutdown_signal() { if let Err(e) = signal::ctrl_c().await { log::error!("failed to listen to shutdown signal: {e}"); } } pub async fn launch(&self) -> Result<()> { let scheduler = self.scheduler(); let _scheduler_handle = scheduler.watch_thread(Duration::from_secs(1)); Server::new(TcpListener::bind(&self.i.cfg.server.address)) .run_with_graceful_shutdown(self.endpoint(), Self::shutdown_signal(), None) .await?; Ok(()) } pub fn info(&self) -> std::result::Result { Ok(Info { stats: self.storage.stats()?, version: VersionInfo::get(), start_time: self.start_time, }) } }