From 8ab9c61b0f55aecf5e082813e4abd3d66634482c Mon Sep 17 00:00:00 2001 From: Lennart K <18233294+lennart-k@users.noreply.github.com> Date: Tue, 27 Jan 2026 23:06:57 +0100 Subject: [PATCH] Move commands to lib.rs --- Cargo.toml | 6 ++ src/app.rs | 7 +- src/commands/health.rs | 1 + src/commands/membership.rs | 3 +- src/commands/mod.rs | 8 +- src/commands/principals.rs | 7 +- src/lib.rs | 155 +++++++++++++++++++++++++++++++++++ src/main.rs | 163 ++----------------------------------- 8 files changed, 187 insertions(+), 163 deletions(-) create mode 100644 src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index e5ecc72..a045c46 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,6 +35,12 @@ opentelemetry = [ [profile.dev] debug = 0 +[lib] +doc = true +name = "rustical" +path = "src/lib.rs" +test = true + [workspace.dependencies] rustical_dav = { path = "./crates/dav/", features = ["ical"] } rustical_dav_push = { path = "./crates/dav_push/" } diff --git a/src/app.rs b/src/app.rs index 44691e8..ecb05dc 100644 --- a/src/app.rs +++ b/src/app.rs @@ -32,7 +32,8 @@ use tracing::field::display; #[allow( clippy::too_many_arguments, clippy::too_many_lines, - clippy::cognitive_complexity + clippy::cognitive_complexity, + clippy::missing_panics_doc )] pub fn make_app< AS: AddressbookStore + PrefixedCalendarStore, @@ -109,9 +110,9 @@ pub fn make_app< options(async || { let mut resp = Response::builder().status(StatusCode::OK); resp.headers_mut() - .unwrap() + .expect("this always works") .insert("DAV", HeaderValue::from_static("1")); - resp.body(Body::empty()).unwrap() + resp.body(Body::empty()).expect("empty body always works") }), ); diff --git a/src/commands/health.rs b/src/commands/health.rs index f5cfe58..d74f40e 100644 --- a/src/commands/health.rs +++ b/src/commands/health.rs @@ -7,6 +7,7 @@ pub struct HealthArgs {} /// Healthcheck for running rustical instance /// Currently just pings to see if it's reachable via HTTP +#[allow(clippy::missing_errors_doc, clippy::missing_panics_doc)] pub async fn cmd_health(http_config: HttpConfig, _health_args: HealthArgs) -> anyhow::Result<()> { let client = reqwest::ClientBuilder::new().build().unwrap(); diff --git a/src/commands/membership.rs b/src/commands/membership.rs index 8e893b3..0f6f994 100644 --- a/src/commands/membership.rs +++ b/src/commands/membership.rs @@ -33,7 +33,8 @@ pub struct MembershipArgs { command: MembershipCommand, } -pub async fn handle_membership_command( +#[allow(clippy::missing_errors_doc, clippy::missing_panics_doc)] +pub async fn cmd_membership( user_store: &impl AuthenticationProvider, MembershipArgs { command }: MembershipArgs, ) -> anyhow::Result<()> { diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 072bb8c..e02dadb 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -6,13 +6,17 @@ use clap::Parser; use rustical_caldav::CalDavConfig; use rustical_frontend::FrontendConfig; -pub mod health; +mod health; pub mod membership; -pub mod principals; +mod principals; + +pub use health::{HealthArgs, cmd_health}; +pub use principals::{PrincipalsArgs, cmd_principals}; #[derive(Debug, Parser)] pub struct GenConfigArgs {} +#[allow(clippy::missing_errors_doc, clippy::missing_panics_doc)] pub fn cmd_gen_config(_args: GenConfigArgs) -> anyhow::Result<()> { let config = Config { http: HttpConfig::default(), diff --git a/src/commands/principals.rs b/src/commands/principals.rs index 70d8aa8..4c7353c 100644 --- a/src/commands/principals.rs +++ b/src/commands/principals.rs @@ -1,5 +1,5 @@ -use super::membership::{MembershipArgs, handle_membership_command}; -use crate::{config::Config, get_data_stores}; +use super::membership::MembershipArgs; +use crate::{config::Config, get_data_stores, membership::cmd_membership}; use clap::{Parser, Subcommand}; use figment::{ Figment, @@ -58,6 +58,7 @@ enum Command { Membership(MembershipArgs), } +#[allow(clippy::missing_errors_doc, clippy::missing_panics_doc)] pub async fn cmd_principals(args: PrincipalsArgs) -> anyhow::Result<()> { let config: Config = Figment::new() .merge(Toml::file(&args.config_file)) @@ -152,7 +153,7 @@ pub async fn cmd_principals(args: PrincipalsArgs) -> anyhow::Result<()> { println!("Principal {id} updated"); } Command::Membership(args) => { - handle_membership_command(principal_store.as_ref(), args).await?; + cmd_membership(principal_store.as_ref(), args).await?; } } Ok(()) diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..d54050f --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,155 @@ +#![warn(clippy::all, clippy::pedantic, clippy::nursery)] +use crate::config::Config; +use anyhow::Result; +use app::make_app; +use axum::ServiceExt; +use axum::extract::Request; +use clap::{Parser, Subcommand}; +use config::{DataStoreConfig, SqliteDataStoreConfig}; +use rustical_dav_push::DavPushController; +use rustical_store::auth::AuthenticationProvider; +use rustical_store::{ + AddressbookStore, CalendarStore, CollectionOperation, PrefixedCalendarStore, SubscriptionStore, +}; +use rustical_store_sqlite::addressbook_store::SqliteAddressbookStore; +use rustical_store_sqlite::calendar_store::SqliteCalendarStore; +use rustical_store_sqlite::principal_store::SqlitePrincipalStore; +use rustical_store_sqlite::{SqliteStore, create_db_pool}; +use setup_tracing::setup_tracing; +use std::sync::Arc; +use tokio::sync::mpsc::Receiver; +use tower::Layer; +use tower_http::normalize_path::NormalizePathLayer; +use tracing::{info, warn}; + +pub mod app; +mod commands; +pub use commands::*; +pub mod config; +#[cfg(test)] +pub mod integration_tests; +mod setup_tracing; + +#[derive(Parser, Debug)] +#[command(author, version, about, long_about = None)] +pub struct Args { + #[arg(short, long, env, default_value = "/etc/rustical/config.toml")] + pub config_file: String, + #[arg(long, env, help = "Do no run database migrations (only for sql store)")] + pub no_migrations: bool, + + #[command(subcommand)] + pub command: Option, +} + +#[derive(Debug, Subcommand)] +pub enum Command { + GenConfig(commands::GenConfigArgs), + Principals(PrincipalsArgs), + #[command( + about = "Healthcheck for running instance (Used for HEALTHCHECK in Docker container)" + )] + Health(HealthArgs), +} + +#[allow(clippy::missing_errors_doc)] +pub async fn get_data_stores( + migrate: bool, + config: &DataStoreConfig, +) -> Result<( + Arc, + Arc, + Arc, + Arc, + Receiver, +)> { + Ok(match &config { + DataStoreConfig::Sqlite(SqliteDataStoreConfig { + db_url, + run_repairs, + skip_broken, + }) => { + let db = create_db_pool(db_url, migrate).await?; + // Channel to watch for changes (for DAV Push) + let (send, recv) = tokio::sync::mpsc::channel(1000); + + let addressbook_store = Arc::new(SqliteAddressbookStore::new( + db.clone(), + send.clone(), + *skip_broken, + )); + let cal_store = Arc::new(SqliteCalendarStore::new(db.clone(), send, *skip_broken)); + if *run_repairs { + info!("Running repair tasks"); + addressbook_store.repair_orphans().await?; + cal_store.repair_invalid_version_4_0().await?; + cal_store.repair_orphans().await?; + } + let subscription_store = Arc::new(SqliteStore::new(db.clone())); + let principal_store = Arc::new(SqlitePrincipalStore::new(db)); + + // Validate all calendar objects + for principal in principal_store.get_principals().await? { + cal_store.validate_objects(&principal.id).await?; + addressbook_store.validate_objects(&principal.id).await?; + } + + ( + addressbook_store, + cal_store, + subscription_store, + principal_store, + recv, + ) + } + }) +} + +#[allow(clippy::missing_errors_doc, clippy::missing_panics_doc)] +pub async fn cmd_default(args: Args, config: Config) -> Result<()> { + setup_tracing(&config.tracing); + + let (addr_store, cal_store, subscription_store, principal_store, update_recv) = + get_data_stores(!args.no_migrations, &config.data_store).await?; + + let mut tasks = vec![]; + + if config.dav_push.enabled { + let dav_push_controller = DavPushController::new( + config.dav_push.allowed_push_servers, + subscription_store.clone(), + ); + tasks.push(tokio::spawn(async move { + dav_push_controller.notifier(update_recv).await; + })); + } + + let app = make_app( + addr_store.clone(), + cal_store.clone(), + subscription_store.clone(), + principal_store.clone(), + config.frontend.clone(), + config.oidc.clone(), + config.caldav, + &config.nextcloud_login, + config.dav_push.enabled, + config.http.session_cookie_samesite_strict, + config.http.payload_limit_mb, + ); + let app = ServiceExt::::into_make_service( + NormalizePathLayer::trim_trailing_slash().layer(app), + ); + + let address = format!("{}:{}", config.http.host, config.http.port); + let listener = tokio::net::TcpListener::bind(&address).await?; + tasks.push(tokio::spawn(async move { + info!("RustiCal serving on http://{address}"); + axum::serve(listener, app).await.unwrap(); + })); + + for task in tasks { + task.await?; + } + Ok(()) +} diff --git a/src/main.rs b/src/main.rs index 8743d1a..7c0011c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,112 +1,12 @@ #![warn(clippy::all, clippy::pedantic, clippy::nursery)] -use crate::commands::health::{HealthArgs, cmd_health}; -use crate::config::Config; use anyhow::Result; -use app::make_app; -use axum::ServiceExt; -use axum::extract::Request; -use clap::{Parser, Subcommand}; -use commands::cmd_gen_config; -use commands::principals::{PrincipalsArgs, cmd_principals}; -use config::{DataStoreConfig, SqliteDataStoreConfig}; +use clap::Parser; use figment::Figment; use figment::providers::{Env, Format, Toml}; -use rustical_dav_push::DavPushController; -use rustical_store::auth::AuthenticationProvider; -use rustical_store::{ - AddressbookStore, CalendarStore, CollectionOperation, PrefixedCalendarStore, SubscriptionStore, -}; -use rustical_store_sqlite::addressbook_store::SqliteAddressbookStore; -use rustical_store_sqlite::calendar_store::SqliteCalendarStore; -use rustical_store_sqlite::principal_store::SqlitePrincipalStore; -use rustical_store_sqlite::{SqliteStore, create_db_pool}; -use setup_tracing::setup_tracing; -use std::sync::Arc; -use tokio::sync::mpsc::Receiver; -use tower::Layer; -use tower_http::normalize_path::NormalizePathLayer; -use tracing::{info, warn}; - -mod app; -mod commands; -mod config; -#[cfg(test)] -pub mod integration_tests; -mod setup_tracing; - -#[derive(Parser, Debug)] -#[command(author, version, about, long_about = None)] -struct Args { - #[arg(short, long, env, default_value = "/etc/rustical/config.toml")] - config_file: String, - #[arg(long, env, help = "Do no run database migrations (only for sql store)")] - no_migrations: bool, - - #[command(subcommand)] - command: Option, -} - -#[derive(Debug, Subcommand)] -enum Command { - GenConfig(commands::GenConfigArgs), - Principals(PrincipalsArgs), - #[command( - about = "Healthcheck for running instance (Used for HEALTHCHECK in Docker container)" - )] - Health(HealthArgs), -} - -async fn get_data_stores( - migrate: bool, - config: &DataStoreConfig, -) -> Result<( - Arc, - Arc, - Arc, - Arc, - Receiver, -)> { - Ok(match &config { - DataStoreConfig::Sqlite(SqliteDataStoreConfig { - db_url, - run_repairs, - skip_broken, - }) => { - let db = create_db_pool(db_url, migrate).await?; - // Channel to watch for changes (for DAV Push) - let (send, recv) = tokio::sync::mpsc::channel(1000); - - let addressbook_store = Arc::new(SqliteAddressbookStore::new( - db.clone(), - send.clone(), - *skip_broken, - )); - let cal_store = Arc::new(SqliteCalendarStore::new(db.clone(), send, *skip_broken)); - if *run_repairs { - info!("Running repair tasks"); - addressbook_store.repair_orphans().await?; - cal_store.repair_invalid_version_4_0().await?; - cal_store.repair_orphans().await?; - } - let subscription_store = Arc::new(SqliteStore::new(db.clone())); - let principal_store = Arc::new(SqlitePrincipalStore::new(db)); - - // Validate all calendar objects - for principal in principal_store.get_principals().await? { - cal_store.validate_objects(&principal.id).await?; - addressbook_store.validate_objects(&principal.id).await?; - } - - ( - addressbook_store, - cal_store, - subscription_store, - principal_store, - recv, - ) - } - }) -} +use rustical::config::Config; +use rustical::{Args, Command}; +use rustical::{cmd_default, cmd_gen_config, cmd_health, cmd_principals}; +use tracing::warn; #[tokio::main(flavor = "multi_thread")] async fn main() -> Result<()> { @@ -120,60 +20,15 @@ async fn main() -> Result<()> { }; match args.command { - Some(Command::GenConfig(gen_config_args)) => cmd_gen_config(gen_config_args)?, - Some(Command::Principals(principals_args)) => cmd_principals(principals_args).await?, + Some(Command::GenConfig(gen_config_args)) => cmd_gen_config(gen_config_args), + Some(Command::Principals(principals_args)) => cmd_principals(principals_args).await, Some(Command::Health(health_args)) => { let config: Config = parse_config()?; - cmd_health(config.http, health_args).await?; + cmd_health(config.http, health_args).await } None => { let config: Config = parse_config()?; - - setup_tracing(&config.tracing); - - let (addr_store, cal_store, subscription_store, principal_store, update_recv) = - get_data_stores(!args.no_migrations, &config.data_store).await?; - - let mut tasks = vec![]; - - if config.dav_push.enabled { - let dav_push_controller = DavPushController::new( - config.dav_push.allowed_push_servers, - subscription_store.clone(), - ); - tasks.push(tokio::spawn(async move { - dav_push_controller.notifier(update_recv).await; - })); - } - - let app = make_app( - addr_store.clone(), - cal_store.clone(), - subscription_store.clone(), - principal_store.clone(), - config.frontend.clone(), - config.oidc.clone(), - config.caldav, - &config.nextcloud_login, - config.dav_push.enabled, - config.http.session_cookie_samesite_strict, - config.http.payload_limit_mb, - ); - let app = ServiceExt::::into_make_service( - NormalizePathLayer::trim_trailing_slash().layer(app), - ); - - let address = format!("{}:{}", config.http.host, config.http.port); - let listener = tokio::net::TcpListener::bind(&address).await?; - tasks.push(tokio::spawn(async move { - info!("RustiCal serving on http://{address}"); - axum::serve(listener, app).await.unwrap(); - })); - - for task in tasks { - task.await?; - } + cmd_default(args, config).await } } - Ok(()) }