fs::{self, File},
};
+use chrono::NaiveTime;
use ron::{
- de::from_reader,
+ de::{from_reader, from_str},
ser::{PrettyConfig, to_string_pretty},
};
use serde::{Deserialize, Serialize};
#[derive(Deserialize, Serialize, Clone)]
pub struct Config {
+ #[serde(default = "port_default")]
pub port: u16,
+
+ #[serde(default = "smtp_relay_address_default")]
pub smtp_relay_address: String,
+
+ #[serde(default = "smtp_login_default")]
pub smtp_login: String,
+
+ #[serde(default = "smtp_password_default")]
pub smtp_password: String,
+
+ #[serde(default)]
+ pub backup_time: Option<NaiveTime>, // If not set, no backup will be done.
+
+ #[serde(default = "backup_directory_default")]
+ pub backup_directory: String,
+}
+
+fn port_default() -> u16 {
+ 8082
+}
+
+fn smtp_relay_address_default() -> String {
+ "mail.something.com".to_string()
+}
+
+fn smtp_login_default() -> String {
+ "login".to_string()
+}
+
+fn smtp_password_default() -> String {
+ "password".to_string()
+}
+
+fn backup_directory_default() -> String {
+ "data".to_string()
}
impl Config {
pub fn default() -> Self {
- Config {
- port: 8082,
- smtp_relay_address: "mail.something.com".to_string(),
- smtp_login: "login".to_string(),
- smtp_password: "password".to_string(),
- }
+ from_str("()").unwrap()
}
}
.field("smtp_relay_address", &self.smtp_relay_address)
.field("smtp_login", &self.smtp_login)
.field("smtp_password", &"*****")
+ .field("backup_time", &self.backup_time)
+ .field("backup_directory", &self.backup_directory)
.finish()
}
}
pub fn load() -> Config {
- match File::open(consts::FILE_CONF) {
+ let config = match File::open(consts::FILE_CONF) {
Ok(file) => from_reader(file).unwrap_or_else(|error| {
panic!(
"Failed to open configuration file {}: {}",
error
)
}),
- Err(_) => {
- let default_config = Config::default();
+ Err(_) => Config::default(),
+ };
- let ron_string = to_string_pretty(&default_config, PrettyConfig::new())
- .unwrap_or_else(|error| panic!("Failed to serialize ron configuration: {}", error));
+ // Rewrite the whole config, useful in the case
+ // when some fields are missing in the original or no config file at all.
+ // FIXME: It will remove any manually added comments.
+ let ron_string = to_string_pretty(&config, PrettyConfig::new())
+ .unwrap_or_else(|error| panic!("Failed to serialize ron configuration: {}", error));
- fs::write(consts::FILE_CONF, ron_string).unwrap_or_else(|error| {
- panic!("Failed to write default configuration file: {}", error)
- });
+ fs::write(consts::FILE_CONF, ron_string)
+ .unwrap_or_else(|error| panic!("Failed to write default configuration file: {}", error));
- default_config
- }
- }
+ config
}
--- /dev/null
+use chrono::{NaiveTime, TimeDelta};
+use tracing::{Level, event};
+
+use super::db;
+
+/// This function starts a backup process that runs at a specified time of day forever.
+/// It creates a backup of the database at the specified directory.
+/// The backup file is named with the date and time at the time of the backup.
+pub fn start<P>(
+ directory: P,
+ db_connection: db::Connection,
+ time_of_day: NaiveTime,
+) -> tokio::task::JoinHandle<()>
+where
+ P: AsRef<std::path::Path> + Send + Sync + 'static,
+{
+ if !directory.as_ref().is_dir() {
+ panic!(
+ "Path must be a directory: {}",
+ directory
+ .as_ref()
+ .to_str()
+ .unwrap_or("<Unable to convert directory to string>")
+ );
+ }
+
+ // Can also user tokio::spawn_blocking if needed.
+ tokio::task::spawn(async move {
+ loop {
+ let mut time_to_wait = time_of_day - chrono::Local::now().time();
+ if time_to_wait < TimeDelta::zero() {
+ time_to_wait += TimeDelta::days(1);
+ }
+ event!(Level::DEBUG, "Backup in {}s", time_to_wait.num_seconds());
+ tokio::time::sleep(time_to_wait.to_std().unwrap()).await;
+
+ let path = directory.as_ref().join(format!(
+ "recipes_backup_{}.sqlite",
+ chrono::Local::now().format("%Y-%m-%d_%H%M%S")
+ ));
+
+ event!(
+ Level::INFO,
+ "Starting backup process to {}...",
+ path.display()
+ );
+
+ if let Err(error) = db_connection.backup(path).await {
+ event!(Level::ERROR, "Error when backing up database: {}", error);
+ }
+
+ event!(Level::INFO, "Backup done");
+ }
+ })
+}
};
use sqlx::{
- sqlite::{SqliteConnectOptions, SqliteJournalMode, SqlitePoolOptions, SqliteSynchronous},
Pool, Sqlite, Transaction,
+ sqlite::{SqliteConnectOptions, SqliteJournalMode, SqlitePoolOptions, SqliteSynchronous},
};
-use tracing::{event, Level};
+use tracing::{Level, event};
use crate::consts;
Ok(connection)
}
+ pub async fn backup<P>(&self, file: P) -> Result<()>
+ where
+ P: AsRef<Path>,
+ {
+ sqlx::query("VACUUM INTO $1")
+ .bind(file.as_ref().to_str().unwrap())
+ .execute(&self.pool)
+ .await
+ .map(|_| ()) // Ignore the result.
+ .map_err(DBError::from)
+ }
+
async fn tx(&self) -> Result<Transaction<Sqlite>> {
self.pool.begin().await.map_err(DBError::from)
}
+pub mod backup;
pub mod db;
pub mod model;
};
use tracing::{Level, event};
-use data::{db, model};
+use data::{backup, db, model};
use translation::Tr;
mod config;
event!(Level::INFO, "Configuration: {:?}", config);
- let db_connection = db::Connection::new().await.unwrap();
+ let Ok(db_connection) = db::Connection::new().await else {
+ event!(Level::ERROR, "Unable to connect to the database");
+ return;
+ };
+
+ backup::start(
+ "data",
+ db_connection.clone(),
+ // TODO: take from config.
+ NaiveTime::from_hms_opt(4, 0, 0).expect("Invalid time of day"),
+ );
let state = AppState {
config,
use axum::{
body::Bytes,
extract::{FromRequest, Request},
- http::{header, StatusCode},
+ http::{StatusCode, header},
response::{IntoResponse, Response},
};
use serde::de::DeserializeOwned;
return Err(
ron_utils::ron_error(StatusCode::BAD_REQUEST, "No content type given")
.into_response(),
- )
+ );
}
}