Use data directory and add config defaults
This commit is contained in:
parent
4adc01c49c
commit
9f195fffce
4 changed files with 80 additions and 30 deletions
|
@ -14,7 +14,7 @@ RUN cargo build --release --bin contact-form
|
||||||
|
|
||||||
FROM docker.io/library/debian:stable-slim AS runtime
|
FROM docker.io/library/debian:stable-slim AS runtime
|
||||||
WORKDIR app
|
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/target/release/contact-form /usr/local/bin/contact-form
|
||||||
COPY --from=builder /app/static/ ./static/
|
COPY --from=builder /app/static/ ./static/
|
||||||
CMD ["contact-form"]
|
CMD ["contact-form"]
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::{env, fs::File, io::BufReader};
|
use std::{fs::File, io::BufReader, path::Path};
|
||||||
|
|
||||||
/// Email server credentials.
|
/// Email server credentials.
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
|
@ -20,7 +20,9 @@ pub struct Email {
|
||||||
/// UTC offset for time formatting.
|
/// UTC offset for time formatting.
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct UtcOffset {
|
pub struct UtcOffset {
|
||||||
|
#[serde(default)]
|
||||||
pub hours: i8,
|
pub hours: i8,
|
||||||
|
#[serde(default)]
|
||||||
pub minutes: i8,
|
pub minutes: i8,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -40,10 +42,18 @@ pub struct CustomField {
|
||||||
pub field_type: CustomFieldType,
|
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.
|
/// Error messages for localization.
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct ErrorMessages {
|
pub struct ErrorMessages {
|
||||||
|
#[serde(default = "default_captcha_error")]
|
||||||
pub captcha_error: String,
|
pub captcha_error: String,
|
||||||
|
#[serde(default = "default_email_error")]
|
||||||
pub email_error: String,
|
pub email_error: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -79,7 +89,7 @@ fn default_captcha_label() -> String {
|
||||||
"Enter the code above".to_string()
|
"Enter the code above".to_string()
|
||||||
}
|
}
|
||||||
fn default_captcha_required_feedback() -> 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)]
|
#[derive(Deserialize)]
|
||||||
pub struct CaptchaField {
|
pub struct CaptchaField {
|
||||||
|
@ -89,14 +99,35 @@ pub struct CaptchaField {
|
||||||
pub required_feedback: String,
|
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.
|
/// Localization strings.
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct Strings {
|
pub struct Strings {
|
||||||
|
#[serde(default)]
|
||||||
pub description: String,
|
pub description: String,
|
||||||
|
#[serde(default = "default_title")]
|
||||||
pub title: String,
|
pub title: String,
|
||||||
|
#[serde(default = "default_optional")]
|
||||||
pub optional: String,
|
pub optional: String,
|
||||||
|
#[serde(default = "default_submit")]
|
||||||
pub submit: String,
|
pub submit: String,
|
||||||
|
#[serde(default = "default_success")]
|
||||||
pub success: String,
|
pub success: String,
|
||||||
|
#[serde(default = "default_message_from")]
|
||||||
pub message_from: String,
|
pub message_from: String,
|
||||||
pub name_field: NameField,
|
pub name_field: NameField,
|
||||||
pub email_field: EmailField,
|
pub email_field: EmailField,
|
||||||
|
@ -104,13 +135,16 @@ pub struct Strings {
|
||||||
pub error_messages: ErrorMessages,
|
pub error_messages: ErrorMessages,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn default_path_prefix() -> String {
|
||||||
|
"/".to_string()
|
||||||
|
}
|
||||||
fn default_lang() -> String {
|
fn default_lang() -> String {
|
||||||
"en".to_string()
|
"en".to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct StateConfig {
|
pub struct StateConfig {
|
||||||
/// The path prefix of all routes.
|
/// The path prefix of all routes.
|
||||||
|
#[serde(default = "default_path_prefix")]
|
||||||
pub path_prefix: String,
|
pub path_prefix: String,
|
||||||
pub custom_fields: Vec<CustomField>,
|
pub custom_fields: Vec<CustomField>,
|
||||||
/// The language tag of the HTML file.
|
/// The language tag of the HTML file.
|
||||||
|
@ -119,29 +153,35 @@ pub struct StateConfig {
|
||||||
pub strings: Strings,
|
pub strings: Strings,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn default_socket_address() -> String {
|
||||||
|
"0.0.0.0:80".to_string()
|
||||||
|
}
|
||||||
/// Configuration.
|
/// Configuration.
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
/// The server socket address including port.
|
/// The server socket address including port.
|
||||||
|
#[serde(default = "default_socket_address")]
|
||||||
pub socket_address: String,
|
pub socket_address: String,
|
||||||
pub email: Email,
|
pub email: Email,
|
||||||
pub log_file: String,
|
|
||||||
pub utc_offset: UtcOffset,
|
pub utc_offset: UtcOffset,
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
pub state_config: StateConfig,
|
pub state_config: StateConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
/// Parses the configuration from the config path in the environment variable.
|
/// Parses the configuration in the given data directory.
|
||||||
pub fn build() -> Result<Self> {
|
pub fn build(data_dir: &Path) -> Result<Self> {
|
||||||
// The environment variable with the path to the config file.
|
let reader = {
|
||||||
let config_file_var = "CF_CONFIG_FILE";
|
let mut buf = data_dir.to_path_buf();
|
||||||
let config_path = env::var(config_file_var)
|
buf.push("config.yaml");
|
||||||
.with_context(|| format!("Environment variable {config_file_var} missing!"))?;
|
|
||||||
|
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 =
|
let mut config: Self =
|
||||||
serde_yaml::from_reader(reader).context("Can not parse the YAML config file!")?;
|
serde_yaml::from_reader(reader).context("Can not parse the YAML config file!")?;
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use std::fs::OpenOptions;
|
use std::{fs::OpenOptions, path::Path};
|
||||||
use time::{format_description::well_known::Rfc3339, UtcOffset};
|
use time::{format_description::well_known::Rfc3339, UtcOffset};
|
||||||
use tracing_subscriber::{
|
use tracing_subscriber::{
|
||||||
filter::LevelFilter,
|
filter::LevelFilter,
|
||||||
|
@ -9,11 +9,13 @@ use tracing_subscriber::{
|
||||||
Layer,
|
Layer,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use crate::config;
|
||||||
|
|
||||||
/// Initializes the logger.
|
/// 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.
|
// Set UTC offset for time formatting.
|
||||||
let timer = OffsetTime::new(
|
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!")?,
|
.context("Failed to set the time offset from the given utc_hours_offset!")?,
|
||||||
Rfc3339,
|
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);
|
.with_filter(stdout_level_filter);
|
||||||
|
|
||||||
// Log file.
|
// Log file.
|
||||||
let log_file = OpenOptions::new()
|
let log_file = {
|
||||||
|
let mut buf = data_dir.to_path_buf();
|
||||||
|
buf.push("log.txt");
|
||||||
|
|
||||||
|
OpenOptions::new()
|
||||||
.create(true)
|
.create(true)
|
||||||
.append(true)
|
.append(true)
|
||||||
.open(log_file)
|
.open(buf)
|
||||||
.context("Failed to open the log file in append mode!")?;
|
.context("Failed to open the log file in append mode!")?
|
||||||
|
};
|
||||||
let file_layer = fmt::layer()
|
let file_layer = fmt::layer()
|
||||||
.with_writer(log_file)
|
.with_writer(log_file)
|
||||||
.with_ansi(false)
|
.with_ansi(false)
|
||||||
|
|
19
src/main.rs
19
src/main.rs
|
@ -13,20 +13,23 @@ use axum::{
|
||||||
routing::{get, get_service, Router},
|
routing::{get, get_service, Router},
|
||||||
Server,
|
Server,
|
||||||
};
|
};
|
||||||
use std::{net::SocketAddr, process};
|
use std::{env, net::SocketAddr, path::PathBuf, process};
|
||||||
use tower_http::services::ServeDir;
|
use tower_http::services::ServeDir;
|
||||||
use tracing::{error, info};
|
use tracing::{error, info};
|
||||||
|
|
||||||
use crate::{config::Config, states::AppState};
|
use crate::{config::Config, states::AppState};
|
||||||
|
|
||||||
async fn init(logger_initialized: &mut bool) -> Result<()> {
|
const DATA_DIR_ENV_VAR: &str = "CF_DATA_DIR";
|
||||||
let config = Config::build()?;
|
|
||||||
|
|
||||||
logging::init_logger(
|
async fn init(logger_initialized: &mut bool) -> Result<()> {
|
||||||
&config.log_file,
|
let data_dir = PathBuf::from(
|
||||||
config.utc_offset.hours,
|
env::var(DATA_DIR_ENV_VAR)
|
||||||
config.utc_offset.minutes,
|
.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;
|
*logger_initialized = true;
|
||||||
|
|
||||||
// The path prefix of all routes.
|
// The path prefix of all routes.
|
||||||
|
|
Loading…
Reference in a new issue