Use data directory and add config defaults

This commit is contained in:
Mo 2023-02-26 14:51:57 +01:00
parent 4adc01c49c
commit 9f195fffce
4 changed files with 80 additions and 30 deletions

View file

@ -14,7 +14,7 @@ RUN cargo build --release --bin contact-form
FROM docker.io/library/debian:stable-slim AS runtime
WORKDIR app
ENV CF_CONFIG_FILE=/volumes/data/config.yaml
ENV CF_DATA_DIR=/volumes/data
COPY --from=builder /app/target/release/contact-form /usr/local/bin/contact-form
COPY --from=builder /app/static/ ./static/
CMD ["contact-form"]

View file

@ -1,6 +1,6 @@
use anyhow::{Context, Result};
use serde::Deserialize;
use std::{env, fs::File, io::BufReader};
use std::{fs::File, io::BufReader, path::Path};
/// Email server credentials.
#[derive(Deserialize)]
@ -20,7 +20,9 @@ pub struct Email {
/// UTC offset for time formatting.
#[derive(Deserialize)]
pub struct UtcOffset {
#[serde(default)]
pub hours: i8,
#[serde(default)]
pub minutes: i8,
}
@ -40,10 +42,18 @@ pub struct CustomField {
pub field_type: CustomFieldType,
}
fn default_captcha_error() -> String {
"You did enter the wrong code at the end of the form. Please try again.".to_string()
}
fn default_email_error() -> String {
"An internal error occurred while sending your request. Please try again later.".to_string()
}
/// Error messages for localization.
#[derive(Deserialize)]
pub struct ErrorMessages {
#[serde(default = "default_captcha_error")]
pub captcha_error: String,
#[serde(default = "default_email_error")]
pub email_error: String,
}
@ -79,7 +89,7 @@ fn default_captcha_label() -> String {
"Enter the code above".to_string()
}
fn default_captcha_required_feedback() -> String {
"Please enter code from the image above".to_string()
"Please enter the code from the image above".to_string()
}
#[derive(Deserialize)]
pub struct CaptchaField {
@ -89,14 +99,35 @@ pub struct CaptchaField {
pub required_feedback: String,
}
fn default_title() -> String {
"Contact form".to_string()
}
fn default_optional() -> String {
"optional".to_string()
}
fn default_submit() -> String {
"Submit".to_string()
}
fn default_success() -> String {
"Your request has been successfully submitted. We will get back to you soon.".to_string()
}
fn default_message_from() -> String {
"Message from".to_string()
}
/// Localization strings.
#[derive(Deserialize)]
pub struct Strings {
#[serde(default)]
pub description: String,
#[serde(default = "default_title")]
pub title: String,
#[serde(default = "default_optional")]
pub optional: String,
#[serde(default = "default_submit")]
pub submit: String,
#[serde(default = "default_success")]
pub success: String,
#[serde(default = "default_message_from")]
pub message_from: String,
pub name_field: NameField,
pub email_field: EmailField,
@ -104,13 +135,16 @@ pub struct Strings {
pub error_messages: ErrorMessages,
}
fn default_path_prefix() -> String {
"/".to_string()
}
fn default_lang() -> String {
"en".to_string()
}
#[derive(Deserialize)]
pub struct StateConfig {
/// The path prefix of all routes.
#[serde(default = "default_path_prefix")]
pub path_prefix: String,
pub custom_fields: Vec<CustomField>,
/// The language tag of the HTML file.
@ -119,29 +153,35 @@ pub struct StateConfig {
pub strings: Strings,
}
fn default_socket_address() -> String {
"0.0.0.0:80".to_string()
}
/// Configuration.
#[derive(Deserialize)]
pub struct Config {
/// The server socket address including port.
#[serde(default = "default_socket_address")]
pub socket_address: String,
pub email: Email,
pub log_file: String,
pub utc_offset: UtcOffset,
#[serde(flatten)]
pub state_config: StateConfig,
}
impl Config {
/// Parses the configuration from the config path in the environment variable.
pub fn build() -> Result<Self> {
// The environment variable with the path to the config file.
let config_file_var = "CF_CONFIG_FILE";
let config_path = env::var(config_file_var)
.with_context(|| format!("Environment variable {config_file_var} missing!"))?;
/// Parses the configuration in the given data directory.
pub fn build(data_dir: &Path) -> Result<Self> {
let reader = {
let mut buf = data_dir.to_path_buf();
buf.push("config.yaml");
let file = File::open(&buf).with_context(|| {
format!("Can not open the config file at the path {}", buf.display())
})?;
BufReader::new(file)
};
let file = File::open(&config_path)
.with_context(|| format!("Can not open the config file at the path {config_path}"))?;
let reader = BufReader::new(file);
let mut config: Self =
serde_yaml::from_reader(reader).context("Can not parse the YAML config file!")?;

View file

@ -1,5 +1,5 @@
use anyhow::{Context, Result};
use std::fs::OpenOptions;
use std::{fs::OpenOptions, path::Path};
use time::{format_description::well_known::Rfc3339, UtcOffset};
use tracing_subscriber::{
filter::LevelFilter,
@ -9,11 +9,13 @@ use tracing_subscriber::{
Layer,
};
use crate::config;
/// Initializes the logger.
pub fn init_logger(log_file: &str, utc_offset_hours: i8, utc_offset_minutes: i8) -> Result<()> {
pub fn init_logger(data_dir: &Path, utc_offset: &config::UtcOffset) -> Result<()> {
// Set UTC offset for time formatting.
let timer = OffsetTime::new(
UtcOffset::from_hms(utc_offset_hours, utc_offset_minutes, 0)
UtcOffset::from_hms(utc_offset.hours, utc_offset.minutes, 0)
.context("Failed to set the time offset from the given utc_hours_offset!")?,
Rfc3339,
);
@ -31,11 +33,16 @@ pub fn init_logger(log_file: &str, utc_offset_hours: i8, utc_offset_minutes: i8)
.with_filter(stdout_level_filter);
// Log file.
let log_file = OpenOptions::new()
.create(true)
.append(true)
.open(log_file)
.context("Failed to open the log file in append mode!")?;
let log_file = {
let mut buf = data_dir.to_path_buf();
buf.push("log.txt");
OpenOptions::new()
.create(true)
.append(true)
.open(buf)
.context("Failed to open the log file in append mode!")?
};
let file_layer = fmt::layer()
.with_writer(log_file)
.with_ansi(false)

View file

@ -13,20 +13,23 @@ use axum::{
routing::{get, get_service, Router},
Server,
};
use std::{net::SocketAddr, process};
use std::{env, net::SocketAddr, path::PathBuf, process};
use tower_http::services::ServeDir;
use tracing::{error, info};
use crate::{config::Config, states::AppState};
async fn init(logger_initialized: &mut bool) -> Result<()> {
let config = Config::build()?;
const DATA_DIR_ENV_VAR: &str = "CF_DATA_DIR";
logging::init_logger(
&config.log_file,
config.utc_offset.hours,
config.utc_offset.minutes,
)?;
async fn init(logger_initialized: &mut bool) -> Result<()> {
let data_dir = PathBuf::from(
env::var(DATA_DIR_ENV_VAR)
.with_context(|| format!("Environment variable {DATA_DIR_ENV_VAR} missing!"))?,
);
let config = Config::build(&data_dir)?;
logging::init_logger(&data_dir, &config.utc_offset)?;
*logger_initialized = true;
// The path prefix of all routes.