Spawn an axum server
Concurrently spawn an axum server, which is not doing much yet, in order to prepare for the creation, etc. of reminders.
This commit is contained in:
parent
0434505b2d
commit
6b6f5aa48a
5 changed files with 519 additions and 84 deletions
107
src/config.rs
107
src/config.rs
|
@ -1,21 +1,25 @@
|
|||
use anyhow::{bail, Result};
|
||||
use anyhow::{bail, Error, Result};
|
||||
use serde::Deserialize;
|
||||
use std::{env, fs, path::PathBuf};
|
||||
use tracing::{error, info, trace};
|
||||
use std::{
|
||||
env, fs,
|
||||
net::{IpAddr, Ipv6Addr},
|
||||
path::PathBuf,
|
||||
};
|
||||
use tracing::{error, info, trace, warn};
|
||||
|
||||
fn default_host() -> String {
|
||||
fn default_database_host() -> String {
|
||||
"localhost".to_string()
|
||||
}
|
||||
|
||||
const fn default_port() -> u16 {
|
||||
const fn default_database_port() -> u16 {
|
||||
5432
|
||||
}
|
||||
|
||||
fn default_name() -> String {
|
||||
fn default_database_name() -> String {
|
||||
"whakarite".to_string()
|
||||
}
|
||||
|
||||
fn default_user() -> String {
|
||||
fn default_database_user() -> String {
|
||||
"whakarite".to_string()
|
||||
}
|
||||
|
||||
|
@ -23,68 +27,105 @@ fn default_user() -> String {
|
|||
#[serde(deny_unknown_fields)]
|
||||
pub struct Database {
|
||||
/// Host of the database
|
||||
#[serde(default = "default_host")]
|
||||
#[serde(default = "default_database_host")]
|
||||
pub host: String,
|
||||
|
||||
/// Port of the database
|
||||
#[serde(default = "default_port")]
|
||||
#[serde(default = "default_database_port")]
|
||||
pub port: u16,
|
||||
|
||||
/// Name of the database
|
||||
#[serde(default = "default_name")]
|
||||
#[serde(default = "default_database_name")]
|
||||
pub name: String,
|
||||
|
||||
/// Name of the user to connect to the database
|
||||
#[serde(default = "default_user")]
|
||||
#[serde(default = "default_database_user")]
|
||||
pub user: String,
|
||||
|
||||
/// Password of the user to connect to the database
|
||||
pub pass: String,
|
||||
}
|
||||
|
||||
const fn default_server_address() -> IpAddr {
|
||||
IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1))
|
||||
}
|
||||
|
||||
const fn default_server_port() -> u16 {
|
||||
8080
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Server {
|
||||
/// Listening address of the server
|
||||
#[serde(default = "default_server_address")]
|
||||
pub address: IpAddr,
|
||||
|
||||
/// Port of the database
|
||||
#[serde(default = "default_server_port")]
|
||||
pub port: u16,
|
||||
}
|
||||
|
||||
impl Default for Server {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
address: default_server_address(),
|
||||
port: default_server_port(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Config {
|
||||
/// Database configuration
|
||||
pub database: Database,
|
||||
|
||||
/// server configuration
|
||||
#[serde(default)]
|
||||
pub server: Server,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
fn try_paths() -> Result<(PathBuf, String)> {
|
||||
let file = "config.toml";
|
||||
fn load_file(file: &PathBuf) -> Result<Self> {
|
||||
info!(?file, "loading configuration");
|
||||
if let Ok(config) = fs::read_to_string(&file) {
|
||||
return Ok((file.into(), config));
|
||||
let config = toml::from_str(&fs::read_to_string(file)?)?;
|
||||
trace!(?file, ?config, "loaded configuration");
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
#[allow(clippy::cognitive_complexity)]
|
||||
fn try_paths() -> Result<Self> {
|
||||
let file = "config.toml";
|
||||
match Self::load_file(&file.into()) {
|
||||
Ok(config) => return Ok(config),
|
||||
Err(error) => warn!(?file, "cannot load configuration: {:#}", Error::msg(error)),
|
||||
}
|
||||
|
||||
let user_file =
|
||||
let user_config =
|
||||
xdg::BaseDirectories::with_prefix(env!("CARGO_BIN_NAME"))?.get_config_file(file);
|
||||
info!(file = ?user_file, "loading configuration");
|
||||
if let Ok(config) = fs::read_to_string(&user_file) {
|
||||
return Ok((user_file, config));
|
||||
match Self::load_file(&user_config) {
|
||||
Ok(config) => return Ok(config),
|
||||
Err(error) => warn!(file = ?user_config, "cannot load configuration: {:#}", Error::msg(error)),
|
||||
}
|
||||
|
||||
let global_file = format!("/etc/{}/{}", env!("CARGO_BIN_NAME"), file);
|
||||
info!(file = ?global_file, "loading configuration");
|
||||
if let Ok(config) = fs::read_to_string(&global_file) {
|
||||
return Ok((global_file.into(), config));
|
||||
let global_config = format!("/etc/{}/{}", env!("CARGO_BIN_NAME"), file);
|
||||
match Self::load_file(&global_config.clone().into()) {
|
||||
Ok(config) => return Ok(config),
|
||||
Err(error) => {
|
||||
warn!(file = ?global_config, "cannot load configuration: {:#}", Error::msg(error));
|
||||
}
|
||||
}
|
||||
|
||||
error!("no configuration file found");
|
||||
bail!("no configuration file found");
|
||||
error!("no valid configuration file found");
|
||||
bail!("no valid configuration file found");
|
||||
}
|
||||
|
||||
pub fn load_config(file: Option<PathBuf>) -> Result<Self> {
|
||||
if let Some(file) = file {
|
||||
info!(?file, "loading configuration");
|
||||
let config: Config = toml::from_str(&fs::read_to_string(&file)?)?;
|
||||
trace!(?file, ?config, "loaded configuration");
|
||||
Ok(config)
|
||||
Ok(Self::load_file(&file)?)
|
||||
} else {
|
||||
let (file, config) = Self::try_paths()?;
|
||||
let config = toml::from_str(&config)?;
|
||||
trace!(?file, ?config, "loaded configuration");
|
||||
Ok(config)
|
||||
Ok(Self::try_paths()?)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
83
src/main.rs
83
src/main.rs
|
@ -1,12 +1,15 @@
|
|||
use anyhow::Result;
|
||||
use axum::{http::StatusCode, response::IntoResponse, Router};
|
||||
use clap::Parser;
|
||||
use diesel::{
|
||||
prelude::*,
|
||||
r2d2::{ConnectionManager, Pool, PooledConnection},
|
||||
};
|
||||
use lettre::{Message, SmtpTransport, Transport};
|
||||
use std::{env, thread::park_timeout};
|
||||
use std::{env, net::SocketAddr};
|
||||
use time::{Duration, OffsetDateTime};
|
||||
use tower::ServiceBuilder;
|
||||
use tower_http::trace::TraceLayer;
|
||||
use tracing::{debug, info, trace};
|
||||
|
||||
mod args;
|
||||
|
@ -24,39 +27,51 @@ fn remind(
|
|||
) -> Result<()> {
|
||||
info!("checking for reminders");
|
||||
|
||||
for reminder in schema::reminders::dsl::reminders
|
||||
let result = schema::reminders::dsl::reminders
|
||||
.filter(schema::reminders::executed.is_null())
|
||||
.order(schema::reminders::planned.asc())
|
||||
.load::<Reminder>(db)?
|
||||
{
|
||||
trace!(?reminder, "checking reminder");
|
||||
if reminder.planned <= OffsetDateTime::now_utc() {
|
||||
let email = Message::builder()
|
||||
.from("whakarite@localhost".parse()?)
|
||||
.to(reminder.receiver.parse()?)
|
||||
.subject(&reminder.title)
|
||||
.body(reminder.message.to_string())?;
|
||||
.load::<Reminder>(db)?;
|
||||
|
||||
mailer.send(&email)?;
|
||||
if result.is_empty() {
|
||||
info!("no reminders present, parking indefinitely");
|
||||
std::thread::park();
|
||||
} else {
|
||||
for reminder in result {
|
||||
trace!(?reminder, "checking reminder");
|
||||
if reminder.planned <= OffsetDateTime::now_utc() {
|
||||
let email = Message::builder()
|
||||
.from("whakarite@localhost".parse()?)
|
||||
.to(reminder.receiver.parse()?)
|
||||
.subject(&reminder.title)
|
||||
.body(reminder.message.to_string())?;
|
||||
|
||||
diesel::update(&reminder)
|
||||
.set(schema::reminders::executed.eq(Some(OffsetDateTime::now_utc())))
|
||||
.execute(db)?;
|
||||
mailer.send(&email)?;
|
||||
|
||||
debug!("email sent to {}", reminder.receiver);
|
||||
} else {
|
||||
let duration = reminder.planned - OffsetDateTime::now_utc();
|
||||
info!(?duration, "parking reminder");
|
||||
diesel::update(&reminder)
|
||||
.set(schema::reminders::executed.eq(Some(OffsetDateTime::now_utc())))
|
||||
.execute(db)?;
|
||||
|
||||
park_timeout(<std::time::Duration>::try_from(duration)?);
|
||||
debug!("email sent to {}", reminder.receiver);
|
||||
} else {
|
||||
let duration = reminder.planned - OffsetDateTime::now_utc();
|
||||
info!(?duration, "parking reminder");
|
||||
|
||||
std::thread::park_timeout(<std::time::Duration>::try_from(duration)?);
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
// Check for another remind job if none dont loop forever
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
#[allow(clippy::unused_async)]
|
||||
async fn not_found() -> impl IntoResponse {
|
||||
(StatusCode::NOT_FOUND, "Page not found")
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
let args = Args::parse();
|
||||
|
||||
if env::var("RUST_LOG").is_err() {
|
||||
|
@ -93,9 +108,25 @@ fn main() -> Result<()> {
|
|||
.values(&test_reminder)
|
||||
.execute(&mut db_pool.get()?)?;
|
||||
|
||||
let mailer = SmtpTransport::unencrypted_localhost();
|
||||
std::thread::spawn(move || {
|
||||
let mailer = SmtpTransport::unencrypted_localhost();
|
||||
|
||||
loop {
|
||||
remind(&mut db_pool.get()?, &mailer)?;
|
||||
}
|
||||
loop {
|
||||
remind(&mut db_pool.get().unwrap(), &mailer).unwrap();
|
||||
}
|
||||
});
|
||||
|
||||
let app = Router::new()
|
||||
.layer(ServiceBuilder::new().layer(TraceLayer::new_for_http()))
|
||||
.fallback(not_found);
|
||||
|
||||
let addr = SocketAddr::from((config.server.address, config.server.port));
|
||||
|
||||
info!("{} listening on {}", env!("CARGO_PKG_NAME"), addr);
|
||||
|
||||
axum::Server::bind(&addr)
|
||||
.serve(app.into_make_service())
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue