feat: added flexible configuration system using figment
parent
f16cdfdfff
commit
f9ffc21a3f
|
@ -14,3 +14,5 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
* subscriptions
|
||||
* episode changes
|
||||
* devices
|
||||
* Flexible configuration via a TOML config file, environment variables or CLI
|
||||
arguments
|
||||
|
|
|
@ -94,6 +94,15 @@ dependencies = [
|
|||
"password-hash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "atomic"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8d818003e740b63afc82337e3160717f4f63078720a810b7b903e70a5d1d2994"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.4.0"
|
||||
|
@ -246,6 +255,12 @@ version = "3.17.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf"
|
||||
|
||||
[[package]]
|
||||
name = "bytemuck"
|
||||
version = "1.22.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6b1fc10dbac614ebc03540c9dbd60e83887fda27794998c6528f1782047d540"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
version = "1.5.0"
|
||||
|
@ -496,6 +511,20 @@ version = "1.0.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||
|
||||
[[package]]
|
||||
name = "figment"
|
||||
version = "0.10.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8cb01cd46b0cf372153850f4c6c272d9cbea2da513e07538405148f95bd789f3"
|
||||
dependencies = [
|
||||
"atomic",
|
||||
"pear",
|
||||
"serde",
|
||||
"toml",
|
||||
"uncased",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fnv"
|
||||
version = "1.0.7"
|
||||
|
@ -727,6 +756,12 @@ dependencies = [
|
|||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inlinable_string"
|
||||
version = "0.1.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8fae54786f62fb2918dcfae3d568594e50eb9b5c25bf04371af6fe7516452fb"
|
||||
|
||||
[[package]]
|
||||
name = "is_terminal_polyfill"
|
||||
version = "1.70.1"
|
||||
|
@ -899,6 +934,7 @@ dependencies = [
|
|||
"cookie",
|
||||
"diesel",
|
||||
"diesel_migrations",
|
||||
"figment",
|
||||
"libsqlite3-sys",
|
||||
"rand",
|
||||
"serde",
|
||||
|
@ -948,6 +984,29 @@ dependencies = [
|
|||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pear"
|
||||
version = "0.2.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bdeeaa00ce488657faba8ebf44ab9361f9365a97bd39ffb8a60663f57ff4b467"
|
||||
dependencies = [
|
||||
"inlinable_string",
|
||||
"pear_codegen",
|
||||
"yansi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pear_codegen"
|
||||
version = "0.2.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4bab5b985dc082b345f812b7df84e1bef27e7207b39e448439ba8bd69c93f147"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"proc-macro2-diagnostics",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "percent-encoding"
|
||||
version = "2.3.1"
|
||||
|
@ -996,6 +1055,19 @@ dependencies = [
|
|||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2-diagnostics"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"version_check",
|
||||
"yansi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.38"
|
||||
|
@ -1443,6 +1515,15 @@ version = "1.18.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f"
|
||||
|
||||
[[package]]
|
||||
name = "uncased"
|
||||
version = "0.9.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e1b88fcfe09e89d3866a5c11019378088af2d24c3fbd4f0543f96b479ec90697"
|
||||
dependencies = [
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.17"
|
||||
|
@ -1659,6 +1740,12 @@ dependencies = [
|
|||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yansi"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.7.35"
|
||||
|
|
|
@ -12,6 +12,7 @@ clap = { version = "4.5.30", features = ["derive", "env"] }
|
|||
cookie = "0.18.1"
|
||||
diesel = { version = "2.2.7", features = ["r2d2", "sqlite", "returning_clauses_for_sqlite_3_35"] }
|
||||
diesel_migrations = { version = "2.2.0", features = ["sqlite"] }
|
||||
figment = { version = "0.10.19", features = ["env", "toml"] }
|
||||
libsqlite3-sys = { version = "0.31.0", features = ["bundled"] }
|
||||
rand = "0.8.5"
|
||||
serde = { version = "1.0.218", features = ["derive"] }
|
||||
|
|
21
README.md
21
README.md
|
@ -11,3 +11,24 @@ Gpodder, suitable for low-power devices.
|
|||
* Devices API
|
||||
* Subscriptions API
|
||||
* Episode actions API
|
||||
|
||||
## Configuration
|
||||
|
||||
Otter is configurable via a TOML config file, environment variables and CLI
|
||||
arguments thanks to the [Figment](https://docs.rs/figment/latest/figment/)
|
||||
library. Each variable in the configuration file below can be provided via an
|
||||
environment variable (prefixed using `OTTER_`) or a CLI argument.
|
||||
|
||||
```toml
|
||||
data_dir = "./data"
|
||||
domain = "127.0.0.1"
|
||||
port = 8080
|
||||
```
|
||||
|
||||
Otter can be told to read a config variable using the `--config/-c` flag (see
|
||||
the `help` command for other useful CLI commands). Each variable needs to be
|
||||
provided either from the config file, as an environment variable (e.g.
|
||||
`OTTER_DATA_DIR`) or a CLI argument (e.g. `--data-dir`).
|
||||
|
||||
If a variable is provided in multiple ways, environment variables overwrite
|
||||
configuration file variables, and CLI arguments overwrite both.
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
data_dir = "./data"
|
||||
domain = "127.0.0.1"
|
||||
port = 8080
|
|
@ -16,16 +16,16 @@ pub enum AddCommand {
|
|||
}
|
||||
|
||||
impl DbCommand {
|
||||
pub fn run(&self, cli: &super::Cli) -> u8 {
|
||||
pub fn run(&self, config: &crate::config::Config) -> u8 {
|
||||
match self {
|
||||
DbCommand::Add(cmd) => cmd.run(cli),
|
||||
DbCommand::Add(cmd) => cmd.run(config),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AddCommand {
|
||||
pub fn run(&self, cli: &super::Cli) -> u8 {
|
||||
match self.run_err(cli) {
|
||||
pub fn run(&self, config: &crate::config::Config) -> u8 {
|
||||
match self.run_err(config) {
|
||||
Ok(()) => 0,
|
||||
Err(err) => {
|
||||
eprintln!("An error occured: {}", err.stack());
|
||||
|
@ -35,8 +35,8 @@ impl AddCommand {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn run_err(&self, cli: &super::Cli) -> DbResult<()> {
|
||||
let pool = crate::db::initialize_db(cli.data_dir.join(crate::DB_FILENAME), false)?;
|
||||
pub fn run_err(&self, config: &crate::config::Config) -> DbResult<()> {
|
||||
let pool = crate::db::initialize_db(config.data_dir.join(crate::DB_FILENAME), false)?;
|
||||
|
||||
match self {
|
||||
Self::User { username, password } => {
|
||||
|
|
|
@ -3,36 +3,78 @@ mod serve;
|
|||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use clap::{Parser, Subcommand};
|
||||
use clap::{Args, Parser, Subcommand};
|
||||
use figment::{
|
||||
providers::{Env, Format, Serialized, Toml},
|
||||
Figment,
|
||||
};
|
||||
use serde::Serialize;
|
||||
|
||||
/// Otter is a lightweight implementation of the Gpodder API, designed to be used for small
|
||||
/// personal deployments.
|
||||
#[derive(Parser)]
|
||||
pub struct Cli {
|
||||
#[arg(
|
||||
long = "data",
|
||||
default_value = "./data",
|
||||
value_name = "DATA_DIR",
|
||||
env = "OTTER_DATA_DIR"
|
||||
)]
|
||||
pub data_dir: PathBuf,
|
||||
#[command(flatten)]
|
||||
pub config: ClapConfig,
|
||||
|
||||
#[command(subcommand)]
|
||||
pub cmd: Command,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Args, Clone)]
|
||||
pub struct ClapConfig {
|
||||
#[arg(
|
||||
short,
|
||||
long = "config",
|
||||
env = "OTTER_CONFIG_FILE",
|
||||
value_name = "CONFIG_FILE"
|
||||
)]
|
||||
config_file: Option<PathBuf>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[arg(long = "data", value_name = "DATA_DIR")]
|
||||
data_dir: Option<PathBuf>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[arg(short, long, value_name = "DOMAIN")]
|
||||
domain: Option<String>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[arg(short, long, value_name = "PORT")]
|
||||
port: Option<u16>,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub enum Command {
|
||||
Serve(serve::ServeCommand),
|
||||
Serve,
|
||||
#[command(subcommand)]
|
||||
Db(db::DbCommand),
|
||||
}
|
||||
|
||||
impl Cli {
|
||||
pub fn run(&self) -> u8 {
|
||||
let mut figment = Figment::new();
|
||||
|
||||
if let Some(config_path) = &self.config.config_file {
|
||||
figment = figment.merge(Toml::file(config_path));
|
||||
}
|
||||
|
||||
let config: crate::config::Config = match figment
|
||||
.merge(Env::prefixed("OTTER_"))
|
||||
.merge(Serialized::defaults(self.config.clone()))
|
||||
.extract()
|
||||
{
|
||||
Ok(config) => config,
|
||||
Err(err) => {
|
||||
eprintln!("{}", err);
|
||||
|
||||
return 1;
|
||||
}
|
||||
};
|
||||
|
||||
match &self.cmd {
|
||||
Command::Serve(cmd) => cmd.run(self),
|
||||
Command::Db(cmd) => cmd.run(self),
|
||||
Command::Serve => serve::serve(&config),
|
||||
Command::Db(cmd) => cmd.run(&config),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,57 +1,31 @@
|
|||
use clap::Args;
|
||||
|
||||
use crate::{db, server};
|
||||
|
||||
/// Run the Otter web server
|
||||
#[derive(Args)]
|
||||
pub struct ServeCommand {
|
||||
#[arg(
|
||||
short,
|
||||
long,
|
||||
default_value = "127.0.0.1",
|
||||
value_name = "DOMAIN",
|
||||
env = "OTTER_DOMAIN"
|
||||
)]
|
||||
domain: String,
|
||||
pub fn serve(config: &crate::config::Config) -> u8 {
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
#[arg(
|
||||
short,
|
||||
long,
|
||||
default_value = "8080",
|
||||
value_name = "PORT",
|
||||
env = "OTTER_PORT"
|
||||
)]
|
||||
port: u16,
|
||||
}
|
||||
|
||||
impl ServeCommand {
|
||||
pub fn run(&self, cli: &super::Cli) -> u8 {
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
tracing::info!("Initializing database and running migrations");
|
||||
|
||||
let pool = db::initialize_db(cli.data_dir.join(crate::DB_FILENAME), true).unwrap();
|
||||
|
||||
let ctx = server::Context {
|
||||
repo: db::SqliteRepository::from(pool),
|
||||
};
|
||||
let app = server::app(ctx);
|
||||
|
||||
let rt = tokio::runtime::Builder::new_multi_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
let address = format!("{}:{}", self.domain, self.port);
|
||||
tracing::info!("Starting server on {address}");
|
||||
|
||||
rt.block_on(async {
|
||||
let listener = tokio::net::TcpListener::bind(address).await.unwrap();
|
||||
axum::serve(listener, app.into_make_service())
|
||||
.await
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
0
|
||||
}
|
||||
tracing::info!("Initializing database and running migrations");
|
||||
|
||||
let pool = db::initialize_db(config.data_dir.join(crate::DB_FILENAME), true).unwrap();
|
||||
|
||||
let ctx = server::Context {
|
||||
repo: db::SqliteRepository::from(pool),
|
||||
};
|
||||
let app = server::app(ctx);
|
||||
|
||||
let rt = tokio::runtime::Builder::new_multi_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
let address = format!("{}:{}", config.domain, config.port);
|
||||
tracing::info!("Starting server on {address}");
|
||||
|
||||
rt.block_on(async {
|
||||
let listener = tokio::net::TcpListener::bind(address).await.unwrap();
|
||||
axum::serve(listener, app.into_make_service())
|
||||
.await
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
0
|
||||
}
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct Config {
|
||||
#[serde(default = "default_data_dir")]
|
||||
pub data_dir: PathBuf,
|
||||
#[serde(default = "default_domain")]
|
||||
pub domain: String,
|
||||
#[serde(default = "default_port")]
|
||||
pub port: u16,
|
||||
}
|
||||
|
||||
fn default_data_dir() -> PathBuf {
|
||||
PathBuf::from("./data")
|
||||
}
|
||||
|
||||
fn default_domain() -> String {
|
||||
"127.0.0.1".to_string()
|
||||
}
|
||||
|
||||
fn default_port() -> u16 {
|
||||
8080
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
mod cli;
|
||||
mod config;
|
||||
mod db;
|
||||
mod gpodder;
|
||||
mod server;
|
||||
|
|
|
@ -11,10 +11,7 @@ use axum::{
|
|||
RequestExt, Router,
|
||||
};
|
||||
use axum_extra::{
|
||||
extract::{
|
||||
cookie::{Cookie, Expiration},
|
||||
CookieJar,
|
||||
},
|
||||
extract::{cookie::Cookie, CookieJar},
|
||||
headers::{authorization::Basic, Authorization},
|
||||
TypedHeader,
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue