Add a simple daily backup module
This commit is contained in:
parent
b8a8af3979
commit
d22617538e
6 changed files with 133 additions and 24 deletions
|
|
@ -3,8 +3,9 @@ use std::{
|
||||||
fs::{self, File},
|
fs::{self, File},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use chrono::NaiveTime;
|
||||||
use ron::{
|
use ron::{
|
||||||
de::from_reader,
|
de::{from_reader, from_str},
|
||||||
ser::{PrettyConfig, to_string_pretty},
|
ser::{PrettyConfig, to_string_pretty},
|
||||||
};
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
@ -13,20 +14,48 @@ use crate::consts;
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Clone)]
|
#[derive(Deserialize, Serialize, Clone)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
|
#[serde(default = "port_default")]
|
||||||
pub port: u16,
|
pub port: u16,
|
||||||
|
|
||||||
|
#[serde(default = "smtp_relay_address_default")]
|
||||||
pub smtp_relay_address: String,
|
pub smtp_relay_address: String,
|
||||||
|
|
||||||
|
#[serde(default = "smtp_login_default")]
|
||||||
pub smtp_login: String,
|
pub smtp_login: String,
|
||||||
|
|
||||||
|
#[serde(default = "smtp_password_default")]
|
||||||
pub smtp_password: String,
|
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 {
|
impl Config {
|
||||||
pub fn default() -> Self {
|
pub fn default() -> Self {
|
||||||
Config {
|
from_str("()").unwrap()
|
||||||
port: 8082,
|
|
||||||
smtp_relay_address: "mail.something.com".to_string(),
|
|
||||||
smtp_login: "login".to_string(),
|
|
||||||
smtp_password: "password".to_string(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -38,12 +67,14 @@ impl fmt::Debug for Config {
|
||||||
.field("smtp_relay_address", &self.smtp_relay_address)
|
.field("smtp_relay_address", &self.smtp_relay_address)
|
||||||
.field("smtp_login", &self.smtp_login)
|
.field("smtp_login", &self.smtp_login)
|
||||||
.field("smtp_password", &"*****")
|
.field("smtp_password", &"*****")
|
||||||
|
.field("backup_time", &self.backup_time)
|
||||||
|
.field("backup_directory", &self.backup_directory)
|
||||||
.finish()
|
.finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn load() -> Config {
|
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| {
|
Ok(file) => from_reader(file).unwrap_or_else(|error| {
|
||||||
panic!(
|
panic!(
|
||||||
"Failed to open configuration file {}: {}",
|
"Failed to open configuration file {}: {}",
|
||||||
|
|
@ -51,17 +82,17 @@ pub fn load() -> Config {
|
||||||
error
|
error
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
Err(_) => {
|
Err(_) => Config::default(),
|
||||||
let default_config = Config::default();
|
};
|
||||||
|
|
||||||
let ron_string = to_string_pretty(&default_config, PrettyConfig::new())
|
// 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));
|
.unwrap_or_else(|error| panic!("Failed to serialize ron configuration: {}", error));
|
||||||
|
|
||||||
fs::write(consts::FILE_CONF, ron_string).unwrap_or_else(|error| {
|
fs::write(consts::FILE_CONF, ron_string)
|
||||||
panic!("Failed to write default configuration file: {}", error)
|
.unwrap_or_else(|error| panic!("Failed to write default configuration file: {}", error));
|
||||||
});
|
|
||||||
|
|
||||||
default_config
|
config
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
55
backend/src/data/backup.rs
Normal file
55
backend/src/data/backup.rs
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -8,10 +8,10 @@ use std::{
|
||||||
};
|
};
|
||||||
|
|
||||||
use sqlx::{
|
use sqlx::{
|
||||||
sqlite::{SqliteConnectOptions, SqliteJournalMode, SqlitePoolOptions, SqliteSynchronous},
|
|
||||||
Pool, Sqlite, Transaction,
|
Pool, Sqlite, Transaction,
|
||||||
|
sqlite::{SqliteConnectOptions, SqliteJournalMode, SqlitePoolOptions, SqliteSynchronous},
|
||||||
};
|
};
|
||||||
use tracing::{event, Level};
|
use tracing::{Level, event};
|
||||||
|
|
||||||
use crate::consts;
|
use crate::consts;
|
||||||
|
|
||||||
|
|
@ -93,6 +93,18 @@ impl Connection {
|
||||||
Ok(connection)
|
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>> {
|
async fn tx(&self) -> Result<Transaction<Sqlite>> {
|
||||||
self.pool.begin().await.map_err(DBError::from)
|
self.pool.begin().await.map_err(DBError::from)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1,3 @@
|
||||||
|
pub mod backup;
|
||||||
pub mod db;
|
pub mod db;
|
||||||
pub mod model;
|
pub mod model;
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ use tower_http::{
|
||||||
};
|
};
|
||||||
use tracing::{Level, event};
|
use tracing::{Level, event};
|
||||||
|
|
||||||
use data::{db, model};
|
use data::{backup, db, model};
|
||||||
use translation::Tr;
|
use translation::Tr;
|
||||||
|
|
||||||
mod config;
|
mod config;
|
||||||
|
|
@ -103,7 +103,17 @@ async fn main() {
|
||||||
|
|
||||||
event!(Level::INFO, "Configuration: {:?}", 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 {
|
let state = AppState {
|
||||||
config,
|
config,
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
use axum::{
|
use axum::{
|
||||||
body::Bytes,
|
body::Bytes,
|
||||||
extract::{FromRequest, Request},
|
extract::{FromRequest, Request},
|
||||||
http::{header, StatusCode},
|
http::{StatusCode, header},
|
||||||
response::{IntoResponse, Response},
|
response::{IntoResponse, Response},
|
||||||
};
|
};
|
||||||
use serde::de::DeserializeOwned;
|
use serde::de::DeserializeOwned;
|
||||||
|
|
@ -35,7 +35,7 @@ where
|
||||||
return Err(
|
return Err(
|
||||||
ron_utils::ron_error(StatusCode::BAD_REQUEST, "No content type given")
|
ron_utils::ron_error(StatusCode::BAD_REQUEST, "No content type given")
|
||||||
.into_response(),
|
.into_response(),
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue