mangadex-home-rs/src/config.rs

417 lines
14 KiB
Rust
Raw Normal View History

2021-05-12 01:01:01 +00:00
use std::fmt::{Display, Formatter};
2021-06-16 19:31:03 +00:00
use std::fs::{File, OpenOptions};
2021-07-09 21:18:43 +00:00
use std::hint::unreachable_unchecked;
2021-06-16 19:31:03 +00:00
use std::io::{ErrorKind, Write};
2021-07-09 23:14:53 +00:00
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::num::NonZeroU16;
2021-07-09 21:18:43 +00:00
use std::path::{Path, PathBuf};
2021-05-12 01:01:01 +00:00
use std::str::FromStr;
2021-07-09 21:18:43 +00:00
use std::sync::atomic::{AtomicBool, Ordering};
2021-03-26 01:06:54 +00:00
2021-04-18 02:13:36 +00:00
use clap::{crate_authors, crate_description, crate_version, Clap};
2021-07-09 21:18:43 +00:00
use log::LevelFilter;
2021-06-16 19:31:03 +00:00
use serde::{Deserialize, Serialize};
2021-07-09 23:48:25 +00:00
use thiserror::Error;
2021-07-13 03:23:51 +00:00
use tracing::level_filters::LevelFilter as TracingLevelFilter;
2021-04-19 03:06:18 +00:00
use url::Url;
2021-03-26 01:06:54 +00:00
2021-07-09 21:18:43 +00:00
use crate::units::{KilobitsPerSecond, Mebibytes, Port};
2021-06-16 19:31:03 +00:00
2021-03-26 02:58:07 +00:00
// Validate tokens is an atomic because it's faster than locking on rwlock.
pub static VALIDATE_TOKENS: AtomicBool = AtomicBool::new(false);
2021-05-23 03:06:05 +00:00
pub static OFFLINE_MODE: AtomicBool = AtomicBool::new(false);
2021-07-09 23:48:25 +00:00
#[derive(Error, Debug)]
pub enum ConfigError {
#[error("No config found. One has been created for you to modify.")]
NotInitialized,
#[error(transparent)]
Io(#[from] std::io::Error),
#[error(transparent)]
Parse(#[from] serde_yaml::Error),
}
pub fn load_config() -> Result<Config, ConfigError> {
2021-07-09 21:18:43 +00:00
// Load cli args first
let cli_args: CliArgs = CliArgs::parse();
// Load yaml file next
let config_file: Result<YamlArgs, _> = {
let config_path = cli_args
.config_path
2021-07-09 21:20:15 +00:00
.as_deref()
.unwrap_or_else(|| Path::new("./settings.yaml"));
2021-07-09 21:18:43 +00:00
match File::open(config_path) {
Ok(file) => serde_yaml::from_reader(file),
Err(e) if e.kind() == ErrorKind::NotFound => {
let mut file = OpenOptions::new()
.write(true)
.create_new(true)
.open(config_path)
.unwrap();
let default_config = include_str!("../settings.sample.yaml");
file.write_all(default_config.as_bytes()).unwrap();
2021-07-09 23:48:25 +00:00
return Err(ConfigError::NotInitialized);
2021-07-09 21:18:43 +00:00
}
2021-07-09 23:48:25 +00:00
Err(e) => return Err(e.into()),
2021-06-16 19:31:03 +00:00
}
};
2021-07-09 21:18:43 +00:00
// generate config
let config = Config::from_cli_and_file(cli_args, config_file?);
// initialize globals
OFFLINE_MODE.store(
config
.unstable_options
.contains(&UnstableOptions::OfflineMode),
Ordering::Release,
);
Ok(config)
}
2021-07-09 23:51:48 +00:00
#[derive(Debug)]
2021-07-09 23:14:53 +00:00
/// Represents a fully parsed config, from a variety of sources.
2021-07-09 21:18:43 +00:00
pub struct Config {
pub cache_type: CacheType,
pub cache_path: PathBuf,
pub shutdown_timeout: NonZeroU16,
2021-07-13 03:23:51 +00:00
pub log_level: TracingLevelFilter,
2021-07-09 21:18:43 +00:00
pub client_secret: ClientSecret,
pub port: Port,
pub bind_address: SocketAddr,
pub external_address: Option<SocketAddr>,
pub ephemeral_disk_encryption: bool,
pub network_speed: KilobitsPerSecond,
pub disk_quota: Mebibytes,
pub memory_quota: Mebibytes,
2021-07-09 23:14:53 +00:00
pub unstable_options: Vec<UnstableOptions>,
2021-07-09 21:18:43 +00:00
pub override_upstream: Option<Url>,
2021-07-09 23:14:53 +00:00
pub enable_metrics: bool,
2021-06-16 19:31:03 +00:00
}
2021-07-09 21:18:43 +00:00
impl Config {
fn from_cli_and_file(cli_args: CliArgs, file_args: YamlArgs) -> Self {
2021-07-09 23:14:53 +00:00
let file_extended_options = file_args.extended_options.unwrap_or_default();
2021-07-09 21:18:43 +00:00
let log_level = match (cli_args.quiet, cli_args.verbose) {
2021-07-13 03:23:51 +00:00
(n, _) if n > 2 => TracingLevelFilter::OFF,
(2, _) => TracingLevelFilter::ERROR,
(1, _) => TracingLevelFilter::WARN,
2021-07-09 21:18:43 +00:00
// Use log level from file if no flags were provided to CLI
2021-07-14 03:12:29 +00:00
(0, 0) => {
file_extended_options
.logging_level
.map_or(TracingLevelFilter::INFO, |filter| match filter {
LevelFilter::Off => TracingLevelFilter::OFF,
LevelFilter::Error => TracingLevelFilter::ERROR,
LevelFilter::Warn => TracingLevelFilter::WARN,
LevelFilter::Info => TracingLevelFilter::INFO,
LevelFilter::Debug => TracingLevelFilter::DEBUG,
LevelFilter::Trace => TracingLevelFilter::TRACE,
})
}
2021-07-13 03:23:51 +00:00
(_, 1) => TracingLevelFilter::DEBUG,
(_, n) if n > 1 => TracingLevelFilter::TRACE,
2021-07-09 21:18:43 +00:00
// compiler can't figure it out
_ => unsafe { unreachable_unchecked() },
};
2021-07-09 23:14:53 +00:00
let bind_port = cli_args
.port
.unwrap_or(file_args.server_settings.port)
.get();
// This needs to be outside because rust isn't smart enough yet to
// realize a disjointed borrow of a moved value is ok. This will be
// fixed in Rust 2021.
let external_port = file_args
.server_settings
.external_port
.map_or(bind_port, Port::get);
Self {
cache_type: cli_args
.cache_type
.or(file_extended_options.cache_type)
.unwrap_or_default(),
cache_path: cli_args
.cache_path
.or(file_extended_options.cache_path)
.unwrap_or_else(|| PathBuf::from_str("./cache").unwrap()),
shutdown_timeout: file_args
.server_settings
.graceful_shutdown_wait_seconds
.unwrap_or(unsafe { NonZeroU16::new_unchecked(60) }),
log_level,
// secret should never be in CLI
2021-07-09 23:48:25 +00:00
client_secret: if let Ok(v) = std::env::var("CLIENT_SECRET") {
ClientSecret(v)
} else {
file_args.server_settings.secret
},
2021-07-09 23:14:53 +00:00
port: cli_args.port.unwrap_or(file_args.server_settings.port),
bind_address: SocketAddr::new(
file_args
.server_settings
.hostname
.unwrap_or_else(|| IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0))),
bind_port,
),
external_address: file_args
.server_settings
.external_ip
.map(|ip_addr| SocketAddr::new(ip_addr, external_port)),
2021-07-10 22:53:28 +00:00
ephemeral_disk_encryption: cli_args.ephemeral_disk_encryption
|| file_extended_options
.ephemeral_disk_encryption
.unwrap_or_default(),
2021-07-09 23:14:53 +00:00
network_speed: cli_args
.network_speed
.unwrap_or(file_args.server_settings.external_max_kilobits_per_second),
disk_quota: cli_args
.disk_quota
.unwrap_or(file_args.max_cache_size_in_mebibytes),
memory_quota: cli_args
.memory_quota
.or(file_extended_options.memory_quota)
.unwrap_or_default(),
enable_metrics: file_extended_options.enable_metrics.unwrap_or_default(),
// Unstable options (and related) should never be in yaml config
unstable_options: cli_args.unstable_options,
override_upstream: cli_args.override_upstream,
}
2021-07-09 21:18:43 +00:00
}
}
2021-06-16 19:31:03 +00:00
2021-07-09 23:51:48 +00:00
// this intentionally does not implement display
2021-07-09 23:48:25 +00:00
#[derive(Deserialize, Serialize, Clone)]
2021-07-09 21:18:43 +00:00
pub struct ClientSecret(String);
2021-06-16 19:31:03 +00:00
2021-07-09 23:51:48 +00:00
impl std::fmt::Debug for ClientSecret {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "[client secret]")
}
}
2021-07-15 01:56:29 +00:00
#[derive(Deserialize, Copy, Clone, Debug, PartialEq, Eq)]
2021-07-09 21:18:43 +00:00
#[serde(rename_all = "snake_case")]
pub enum CacheType {
2021-06-16 19:31:03 +00:00
OnDisk,
Lru,
Lfu,
}
2021-07-09 21:18:43 +00:00
impl FromStr for CacheType {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"on_disk" => Ok(Self::OnDisk),
"lru" => Ok(Self::Lru),
"lfu" => Ok(Self::Lfu),
_ => Err(format!("Unknown option: {}", s)),
}
}
}
impl Default for CacheType {
2021-06-16 19:31:03 +00:00
fn default() -> Self {
Self::OnDisk
}
}
2021-07-10 01:25:08 +00:00
#[derive(Deserialize)]
struct YamlArgs {
// Naming is legacy
max_cache_size_in_mebibytes: Mebibytes,
server_settings: YamlServerSettings,
// This implementation custom options
extended_options: Option<YamlExtendedOptions>,
}
// Naming is legacy
#[derive(Deserialize)]
struct YamlServerSettings {
secret: ClientSecret,
#[serde(default)]
port: Port,
external_max_kilobits_per_second: KilobitsPerSecond,
external_port: Option<Port>,
graceful_shutdown_wait_seconds: Option<NonZeroU16>,
hostname: Option<IpAddr>,
external_ip: Option<IpAddr>,
}
#[derive(Deserialize, Default)]
struct YamlExtendedOptions {
memory_quota: Option<Mebibytes>,
cache_type: Option<CacheType>,
ephemeral_disk_encryption: Option<bool>,
enable_metrics: Option<bool>,
logging_level: Option<LevelFilter>,
cache_path: Option<PathBuf>,
}
2021-03-26 04:07:32 +00:00
#[derive(Clap, Clone)]
2021-04-18 02:13:36 +00:00
#[clap(version = crate_version!(), author = crate_authors!(), about = crate_description!())]
2021-07-09 21:18:43 +00:00
struct CliArgs {
2021-03-26 01:06:54 +00:00
/// The port to listen on.
2021-07-09 23:14:53 +00:00
#[clap(short, long)]
pub port: Option<Port>,
/// How large, in mebibytes, the in-memory cache should be. Note that this
/// does not include runtime memory usage.
2021-07-09 21:18:43 +00:00
#[clap(long)]
2021-07-09 23:14:53 +00:00
pub memory_quota: Option<Mebibytes>,
/// How large, in mebibytes, the on-disk cache should be. Note that actual
2021-03-26 01:06:54 +00:00
/// values may be larger for metadata information.
2021-06-16 19:31:03 +00:00
#[clap(long)]
2021-07-09 23:14:53 +00:00
pub disk_quota: Option<Mebibytes>,
2021-03-26 01:06:54 +00:00
/// Sets the location of the disk cache.
2021-07-09 23:14:53 +00:00
#[clap(long)]
pub cache_path: Option<PathBuf>,
2021-03-26 01:06:54 +00:00
/// The network speed to advertise to Mangadex@Home control server.
2021-06-16 19:31:03 +00:00
#[clap(long)]
2021-07-09 23:14:53 +00:00
pub network_speed: Option<KilobitsPerSecond>,
2021-04-20 18:12:20 +00:00
/// Changes verbosity. Default verbosity is INFO, while increasing counts of
/// verbose flags increases the verbosity to DEBUG and TRACE, respectively.
2021-07-09 23:14:53 +00:00
#[clap(short, long, parse(from_occurrences), conflicts_with = "quiet")]
2021-04-18 03:19:27 +00:00
pub verbose: usize,
2021-04-20 18:12:20 +00:00
/// Changes verbosity. Default verbosity is INFO, while increasing counts of
/// quiet flags decreases the verbosity to WARN, ERROR, and no logs,
/// respectively.
#[clap(short, long, parse(from_occurrences), conflicts_with = "verbose")]
pub quiet: usize,
2021-04-25 16:55:31 +00:00
#[clap(short = 'Z', long)]
pub unstable_options: Vec<UnstableOptions>,
2021-04-19 03:06:18 +00:00
#[clap(long)]
pub override_upstream: Option<Url>,
2021-05-12 01:01:01 +00:00
/// Enables ephemeral disk encryption. Items written to disk are first
/// encrypted with a key generated at runtime. There are implications to
/// performance, privacy, and usability with this flag enabled.
#[clap(short, long)]
2021-07-10 22:53:28 +00:00
pub ephemeral_disk_encryption: bool,
2021-06-16 19:31:03 +00:00
#[clap(short, long)]
pub config_path: Option<PathBuf>,
2021-07-09 23:17:56 +00:00
#[clap(short = 't', long)]
2021-07-09 23:14:53 +00:00
pub cache_type: Option<CacheType>,
2021-04-25 16:55:31 +00:00
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum UnstableOptions {
/// Overrides the upstream URL to fetch images from. Don't use this unless
/// you know what you're dealing with.
OverrideUpstream,
/// Disables token validation. Don't use this unless you know the
/// ramifications of this command.
DisableTokenValidation,
2021-05-23 03:06:05 +00:00
/// Tries to run without communication to MangaDex.
OfflineMode,
/// Serves HTTP in plaintext
DisableTls,
2021-04-25 16:55:31 +00:00
}
impl FromStr for UnstableOptions {
2021-05-23 03:06:05 +00:00
type Err = String;
2021-04-25 16:55:31 +00:00
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"override-upstream" => Ok(Self::OverrideUpstream),
"disable-token-validation" => Ok(Self::DisableTokenValidation),
2021-05-23 03:06:05 +00:00
"offline-mode" => Ok(Self::OfflineMode),
"disable-tls" => Ok(Self::DisableTls),
_ => Err(format!("Unknown unstable option '{}'", s)),
2021-04-25 16:55:31 +00:00
}
}
}
impl Display for UnstableOptions {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::OverrideUpstream => write!(f, "override-upstream"),
Self::DisableTokenValidation => write!(f, "disable-token-validation"),
2021-05-23 03:06:05 +00:00
Self::OfflineMode => write!(f, "offline-mode"),
Self::DisableTls => write!(f, "disable-tls"),
2021-04-25 16:55:31 +00:00
}
}
2021-03-26 01:06:54 +00:00
}
2021-07-10 01:25:08 +00:00
#[cfg(test)]
mod sample_yaml {
use crate::config::YamlArgs;
#[test]
2021-07-15 01:56:29 +00:00
fn parses() {
2021-07-11 17:19:37 +00:00
assert!(serde_yaml::from_str::<YamlArgs>(include_str!("../settings.sample.yaml")).is_ok());
2021-07-10 01:25:08 +00:00
}
}
2021-07-15 01:56:29 +00:00
#[cfg(test)]
mod config {
use std::path::PathBuf;
use log::LevelFilter;
use tracing::level_filters::LevelFilter as TracingLevelFilter;
use crate::config::{CacheType, ClientSecret, Config, YamlExtendedOptions, YamlServerSettings};
use crate::units::{KilobitsPerSecond, Mebibytes, Port};
use super::{CliArgs, YamlArgs};
#[test]
fn cli_has_priority() {
let cli_config = CliArgs {
port: Port::new(1234),
memory_quota: Some(Mebibytes::new(10)),
disk_quota: Some(Mebibytes::new(10)),
cache_path: Some(PathBuf::from("a")),
network_speed: KilobitsPerSecond::new(10),
verbose: 1,
quiet: 0,
unstable_options: vec![],
override_upstream: None,
ephemeral_disk_encryption: true,
config_path: None,
cache_type: Some(CacheType::Lfu),
};
let yaml_args = YamlArgs {
max_cache_size_in_mebibytes: Mebibytes::new(50),
server_settings: YamlServerSettings {
secret: ClientSecret(String::new()),
port: Port::new(4321).expect("to work?"),
external_max_kilobits_per_second: KilobitsPerSecond::new(50).expect("to work?"),
external_port: None,
graceful_shutdown_wait_seconds: None,
hostname: None,
external_ip: None,
},
extended_options: Some(YamlExtendedOptions {
memory_quota: Some(Mebibytes::new(50)),
cache_type: Some(CacheType::Lru),
ephemeral_disk_encryption: Some(false),
enable_metrics: None,
logging_level: Some(LevelFilter::Error),
cache_path: Some(PathBuf::from("b")),
}),
};
let config = Config::from_cli_and_file(cli_config, yaml_args);
assert_eq!(Some(config.port), Port::new(1234));
assert_eq!(config.memory_quota, Mebibytes::new(10));
assert_eq!(config.disk_quota, Mebibytes::new(10));
assert_eq!(config.cache_path, PathBuf::from("a"));
assert_eq!(Some(config.network_speed), KilobitsPerSecond::new(10));
assert_eq!(config.log_level, TracingLevelFilter::DEBUG);
assert_eq!(config.ephemeral_disk_encryption, true);
assert_eq!(config.cache_type, CacheType::Lfu);
}
}