Talon/src/server.rs
ThetaDev 36b80bbbd9
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
feat: purge file storage daily
2023-04-02 17:36:01 +02:00

166 lines
4.8 KiB
Rust

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<TalonInner>,
}
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<T> = std::result::Result<T, TalonError>;
impl Talon {
pub fn new<P: AsRef<Path>>(workdir: P) -> Result<Self> {
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<Info, crate::storage::StorageError> {
Ok(Info {
stats: self.storage.stats()?,
version: VersionInfo::get(),
start_time: self.start_time,
})
}
}