All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
166 lines
4.8 KiB
Rust
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,
|
|
})
|
|
}
|
|
}
|