Compare commits
2 commits
cd63f87f1b
...
e579a47e0f
Author | SHA1 | Date | |
---|---|---|---|
e579a47e0f | |||
3b75cfdbf8 |
5 changed files with 654 additions and 518 deletions
912
Cargo.lock
generated
912
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
43
Cargo.toml
43
Cargo.toml
|
@ -4,34 +4,31 @@ version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
tower-service = "0.3.2"
|
|
||||||
console_error_panic_hook = { version = "0.1.1" }
|
|
||||||
anyhow = "1.0.91"
|
anyhow = "1.0.91"
|
||||||
serde_json = "1.0.132"
|
askama = { version = "0.12.1", features = ["urlencode"] }
|
||||||
oauth2 = "4.4.2"
|
|
||||||
config = "0.14.1"
|
|
||||||
dotenvy = "0.15.7"
|
|
||||||
serde = { version = "1.0.213", features = ["derive"] }
|
|
||||||
reqwest = { version = "0.12.8", features = ["json"] }
|
|
||||||
tracing = "0.1.40"
|
|
||||||
async-session = "3.0.0"
|
async-session = "3.0.0"
|
||||||
askama_axum = { version = "0.4.0", features = ["urlencode"] }
|
|
||||||
askama = { version = "0.12.1", features = ["with-axum"] }
|
|
||||||
futures = "0.3.31"
|
|
||||||
uuid = { version = "1.11.0", features = ["js", "serde", "v4", "v7"] }
|
|
||||||
rand = "0.8.5"
|
|
||||||
tracing-subscriber = { version = "0.3.19", features = ["chrono", "env-filter"] }
|
|
||||||
tower-http = { version = "0.6.2", features = ["compression-gzip", "fs", "normalize-path", "trace"] }
|
|
||||||
tokio = { version = "1.42.0", features = ["macros", "rt-multi-thread", "tracing"] }
|
|
||||||
deadpool-diesel = { version = "0.6.1", features = ["postgres", "serde"] }
|
|
||||||
axum = { version = "0.8.1", features = ["macros"] }
|
axum = { version = "0.8.1", features = ["macros"] }
|
||||||
axum-extra = { version = "0.10.0", features = ["cookie", "form", "typed-header"] }
|
axum-extra = { version = "0.10.0", features = ["cookie", "form", "typed-header"] }
|
||||||
chrono = { version = "0.4.39", features = ["serde"] }
|
|
||||||
base64 = "0.22.1"
|
base64 = "0.22.1"
|
||||||
diesel = { version = "2.2.6", features = ["postgres", "chrono", "uuid", "serde_json"] }
|
chrono = { version = "0.4.39", features = ["serde"] }
|
||||||
tower = "0.5.2"
|
|
||||||
regex = "1.11.1"
|
|
||||||
lettre = { version = "0.11.12", features = ["tokio1", "serde", "tracing", "tokio1-native-tls"] }
|
|
||||||
clap = { version = "4.5.31", features = ["derive"] }
|
clap = { version = "4.5.31", features = ["derive"] }
|
||||||
|
config = "0.14.1"
|
||||||
|
deadpool-diesel = { version = "0.6.1", features = ["postgres", "serde"] }
|
||||||
|
diesel = { version = "2.2.6", features = ["postgres", "chrono", "uuid", "serde_json"] }
|
||||||
diesel_migrations = { version = "2.2.0", features = ["postgres"] }
|
diesel_migrations = { version = "2.2.0", features = ["postgres"] }
|
||||||
|
dotenvy = "0.15.7"
|
||||||
|
futures = "0.3.31"
|
||||||
|
lettre = { version = "0.11.12", features = ["tokio1", "serde", "tracing", "tokio1-native-tls"] }
|
||||||
|
oauth2 = "4.4.2"
|
||||||
|
rand = "0.8.5"
|
||||||
|
regex = "1.11.1"
|
||||||
|
reqwest = { version = "0.12.8", features = ["json"] }
|
||||||
|
serde = { version = "1.0.213", features = ["derive"] }
|
||||||
|
serde_json = "1.0.132"
|
||||||
|
tokio = { version = "1.42.0", features = ["full"] }
|
||||||
|
tower = "0.5.2"
|
||||||
|
tower-http = { version = "0.6.2", features = ["compression-gzip", "fs", "normalize-path", "trace"] }
|
||||||
|
tracing = "0.1.40"
|
||||||
|
tracing-subscriber = { version = "0.3.19", features = ["chrono", "env-filter"] }
|
||||||
|
uuid = { version = "1.11.0", features = ["serde", "v4", "v7"] }
|
||||||
validator = { version = "0.20.0", features = ["derive"] }
|
validator = { version = "0.20.0", features = ["derive"] }
|
||||||
|
|
83
src/cli.rs
Normal file
83
src/cli.rs
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
use anyhow::Result;
|
||||||
|
use axum::middleware::map_request;
|
||||||
|
use chrono::{TimeDelta, Utc};
|
||||||
|
use clap::{Parser, Subcommand};
|
||||||
|
use tokio::time::sleep;
|
||||||
|
use tower::ServiceBuilder;
|
||||||
|
use tower_http::{
|
||||||
|
compression::CompressionLayer, normalize_path::NormalizePathLayer, trace::TraceLayer,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
app_state::AppState, middleware::lowercase_uri_path, router::new_router, worker::run_worker,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Parser)]
|
||||||
|
#[command(version, about, long_about = None)]
|
||||||
|
pub struct Cli {
|
||||||
|
#[command(subcommand)]
|
||||||
|
pub command: Commands,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Parser)]
|
||||||
|
pub struct WorkerArgs {
|
||||||
|
/// Loop the every n seconds instead of exiting after execution
|
||||||
|
#[arg(long)]
|
||||||
|
auto_loop_seconds: Option<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Subcommand)]
|
||||||
|
pub enum Commands {
|
||||||
|
/// Run web server
|
||||||
|
Serve,
|
||||||
|
/// Run background worker
|
||||||
|
Worker(WorkerArgs),
|
||||||
|
// TODO: add a low-frequency worker task exclusively for self-healing
|
||||||
|
// mechanisms like Governor::reset_all()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn serve_command(state: AppState) -> Result<()> {
|
||||||
|
let router = new_router(state.clone()).layer(
|
||||||
|
ServiceBuilder::new()
|
||||||
|
.layer(map_request(lowercase_uri_path))
|
||||||
|
.layer(TraceLayer::new_for_http())
|
||||||
|
.layer(CompressionLayer::new())
|
||||||
|
.layer(NormalizePathLayer::trim_trailing_slash()),
|
||||||
|
);
|
||||||
|
|
||||||
|
let listener =
|
||||||
|
tokio::net::TcpListener::bind((state.settings.host.clone(), state.settings.port))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
tracing::info!(
|
||||||
|
"App running at http://{}:{}{}",
|
||||||
|
state.settings.host,
|
||||||
|
state.settings.port,
|
||||||
|
state.settings.base_path
|
||||||
|
);
|
||||||
|
|
||||||
|
axum::serve(listener, router).await.map_err(Into::into)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn worker_command(args: &WorkerArgs, state: AppState) -> Result<()> {
|
||||||
|
if let Some(loop_seconds) = args.auto_loop_seconds {
|
||||||
|
let loop_delta = TimeDelta::seconds(i64::from(loop_seconds));
|
||||||
|
loop {
|
||||||
|
let t_next_loop = Utc::now() + loop_delta;
|
||||||
|
|
||||||
|
if let Err(err) = run_worker(state.clone()).await {
|
||||||
|
tracing::error!("{}", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
let sleep_delta = t_next_loop - Utc::now();
|
||||||
|
match sleep_delta.to_std() {
|
||||||
|
Ok(duration) => {
|
||||||
|
sleep(duration).await;
|
||||||
|
}
|
||||||
|
Err(_) => { /* sleep_delta was < 0, so don't sleep */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
run_worker(state).await
|
||||||
|
}
|
||||||
|
}
|
131
src/main.rs
131
src/main.rs
|
@ -1,66 +1,43 @@
|
||||||
use app_state::App;
|
use clap::Parser as _;
|
||||||
use axum::middleware::map_request;
|
use diesel_migrations::MigrationHarness;
|
||||||
use chrono::{TimeDelta, Utc};
|
|
||||||
use clap::{Parser, Subcommand};
|
|
||||||
use diesel_migrations::{embed_migrations, EmbeddedMigrations, MigrationHarness};
|
|
||||||
use dotenvy::dotenv;
|
use dotenvy::dotenv;
|
||||||
use middleware::lowercase_uri_path;
|
use migrations::MIGRATIONS;
|
||||||
use tokio::time::sleep;
|
|
||||||
use tower::ServiceBuilder;
|
|
||||||
use tower_http::{
|
|
||||||
compression::CompressionLayer, normalize_path::NormalizePathLayer, trace::TraceLayer,
|
|
||||||
};
|
|
||||||
use tracing_subscriber::EnvFilter;
|
use tracing_subscriber::EnvFilter;
|
||||||
|
|
||||||
use crate::{app_state::AppState, router::new_router, settings::Settings, worker::run_worker};
|
use crate::{
|
||||||
|
app_state::{App, AppState},
|
||||||
|
cli::{serve_command, worker_command, Cli, Commands},
|
||||||
|
settings::Settings,
|
||||||
|
};
|
||||||
|
|
||||||
pub mod api_keys;
|
mod api_keys;
|
||||||
pub mod app_error;
|
mod app_error;
|
||||||
pub mod app_state;
|
mod app_state;
|
||||||
pub mod auth;
|
mod auth;
|
||||||
pub mod channel_selections;
|
mod channel_selections;
|
||||||
pub mod channels;
|
mod channels;
|
||||||
mod channels_router;
|
mod channels_router;
|
||||||
pub mod csrf;
|
mod cli;
|
||||||
pub mod email;
|
mod csrf;
|
||||||
pub mod governors;
|
mod email;
|
||||||
pub mod guards;
|
mod governors;
|
||||||
pub mod messages;
|
mod guards;
|
||||||
pub mod middleware;
|
mod messages;
|
||||||
|
mod middleware;
|
||||||
|
mod migrations;
|
||||||
mod nav_state;
|
mod nav_state;
|
||||||
pub mod projects;
|
mod projects;
|
||||||
mod projects_router;
|
mod projects_router;
|
||||||
pub mod router;
|
mod router;
|
||||||
pub mod schema;
|
mod schema;
|
||||||
pub mod sessions;
|
mod sessions;
|
||||||
pub mod settings;
|
mod settings;
|
||||||
pub mod team_memberships;
|
mod team_memberships;
|
||||||
pub mod teams;
|
mod teams;
|
||||||
mod teams_router;
|
mod teams_router;
|
||||||
pub mod users;
|
mod users;
|
||||||
mod v0_router;
|
mod v0_router;
|
||||||
pub mod worker;
|
mod worker;
|
||||||
|
|
||||||
#[derive(Parser)]
|
|
||||||
#[command(version, about, long_about = None)]
|
|
||||||
struct Cli {
|
|
||||||
#[command(subcommand)]
|
|
||||||
command: Commands,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Subcommand)]
|
|
||||||
enum Commands {
|
|
||||||
/// Run web server
|
|
||||||
Serve,
|
|
||||||
/// Run background worker
|
|
||||||
Worker {
|
|
||||||
/// Loop the every n seconds instead of exiting after execution
|
|
||||||
#[arg(long)]
|
|
||||||
auto_loop_seconds: Option<u32>,
|
|
||||||
},
|
|
||||||
// TODO: add a low-frequency worker task exclusively for self-healing
|
|
||||||
// mechanisms like Governor::reset_all()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Run CLI
|
/// Run CLI
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
|
@ -76,7 +53,6 @@ async fn main() {
|
||||||
let state: AppState = App::from_settings(settings.clone()).await.unwrap().into();
|
let state: AppState = App::from_settings(settings.clone()).await.unwrap().into();
|
||||||
|
|
||||||
if settings.run_database_migrations == Some(1) {
|
if settings.run_database_migrations == Some(1) {
|
||||||
const MIGRATIONS: EmbeddedMigrations = embed_migrations!("migrations/");
|
|
||||||
// Run migrations on server startup
|
// Run migrations on server startup
|
||||||
let conn = state.db_pool.get().await.unwrap();
|
let conn = state.db_pool.get().await.unwrap();
|
||||||
conn.interact(|conn| conn.run_pending_migrations(MIGRATIONS).map(|_| ()))
|
conn.interact(|conn| conn.run_pending_migrations(MIGRATIONS).map(|_| ()))
|
||||||
|
@ -87,48 +63,7 @@ async fn main() {
|
||||||
|
|
||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
match &cli.command {
|
match &cli.command {
|
||||||
Commands::Serve => {
|
Commands::Serve => serve_command(state).await.unwrap(),
|
||||||
let router = new_router(state.clone()).layer(
|
Commands::Worker(args) => worker_command(args, state).await.unwrap(),
|
||||||
ServiceBuilder::new()
|
|
||||||
.layer(map_request(lowercase_uri_path))
|
|
||||||
.layer(TraceLayer::new_for_http())
|
|
||||||
.layer(CompressionLayer::new())
|
|
||||||
.layer(NormalizePathLayer::trim_trailing_slash()),
|
|
||||||
);
|
|
||||||
|
|
||||||
let listener = tokio::net::TcpListener::bind((settings.host.clone(), settings.port))
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
tracing::info!(
|
|
||||||
"App running at http://{}:{}{}",
|
|
||||||
settings.host,
|
|
||||||
settings.port,
|
|
||||||
settings.base_path
|
|
||||||
);
|
|
||||||
|
|
||||||
axum::serve(listener, router).await.unwrap();
|
|
||||||
}
|
|
||||||
Commands::Worker { auto_loop_seconds } => {
|
|
||||||
if let Some(loop_seconds) = auto_loop_seconds {
|
|
||||||
let loop_delta = TimeDelta::seconds(i64::from(*loop_seconds));
|
|
||||||
loop {
|
|
||||||
let t_next_loop = Utc::now() + loop_delta;
|
|
||||||
|
|
||||||
if let Err(err) = run_worker(state.clone()).await {
|
|
||||||
tracing::error!("{}", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
let sleep_delta = t_next_loop - Utc::now();
|
|
||||||
match sleep_delta.to_std() {
|
|
||||||
Ok(duration) => {
|
|
||||||
sleep(duration).await;
|
|
||||||
}
|
|
||||||
Err(_) => { /* sleep_delta was < 0, so don't sleep */ }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
run_worker(state).await.unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
3
src/migrations.rs
Normal file
3
src/migrations.rs
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
use diesel_migrations::{embed_migrations, EmbeddedMigrations};
|
||||||
|
|
||||||
|
pub const MIGRATIONS: EmbeddedMigrations = embed_migrations!("migrations/");
|
Loading…
Add table
Reference in a new issue