feat: implemented the /backup/create [POST] endpoint and the 'NginxConfig' backup configuration

This commit is contained in:
antifallobst 2023-11-12 20:49:04 +01:00
parent 3f12b92e9e
commit 05ee74eb7b
Signed by: antifallobst
GPG Key ID: 2B4F402172791BAF
12 changed files with 466 additions and 31 deletions

2
.env
View File

@ -1 +1,3 @@
DATABASE_URL="sqlite:store.db" DATABASE_URL="sqlite:store.db"
NC_AW_BACKUP_PATH=test
NC_AW_HOST_PATH=/

1
.gitignore vendored
View File

@ -1,3 +1,4 @@
.idea .idea
/target /target
store.db store.db
test/

147
Cargo.lock generated
View File

@ -55,7 +55,7 @@ dependencies = [
"tokio", "tokio",
"tokio-util", "tokio-util",
"tracing", "tracing",
"zstd", "zstd 0.12.4",
] ]
[[package]] [[package]]
@ -218,12 +218,26 @@ dependencies = [
"actix-web", "actix-web",
"actix-web-httpauth", "actix-web-httpauth",
"anyhow", "anyhow",
"chrono",
"env_logger", "env_logger",
"log", "log",
"serde", "serde",
"serde_json", "serde_json",
"sqlx", "sqlx",
"tokio", "tokio",
"walkdir",
"zip",
]
[[package]]
name = "aes"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac1f845298e95f983ff1944b728ae08b8cebab80d684f0a832ed0fc74dfa27e2"
dependencies = [
"cfg-if",
"cipher",
"cpufeatures",
] ]
[[package]] [[package]]
@ -404,6 +418,27 @@ dependencies = [
"bytes", "bytes",
] ]
[[package]]
name = "bzip2"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8"
dependencies = [
"bzip2-sys",
"libc",
]
[[package]]
name = "bzip2-sys"
version = "0.1.11+1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc"
dependencies = [
"cc",
"libc",
"pkg-config",
]
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.0.83" version = "1.0.83"
@ -428,16 +463,34 @@ checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38"
dependencies = [ dependencies = [
"android-tzdata", "android-tzdata",
"iana-time-zone", "iana-time-zone",
"js-sys",
"num-traits", "num-traits",
"wasm-bindgen",
"windows-targets", "windows-targets",
] ]
[[package]]
name = "cipher"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
dependencies = [
"crypto-common",
"inout",
]
[[package]] [[package]]
name = "const-oid" name = "const-oid"
version = "0.9.5" version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28c122c3980598d243d63d9a704629a2d748d101f278052ff068be5a4423ab6f" checksum = "28c122c3980598d243d63d9a704629a2d748d101f278052ff068be5a4423ab6f"
[[package]]
name = "constant_time_eq"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc"
[[package]] [[package]]
name = "convert_case" name = "convert_case"
version = "0.4.0" version = "0.4.0"
@ -959,6 +1012,15 @@ dependencies = [
"hashbrown 0.14.2", "hashbrown 0.14.2",
] ]
[[package]]
name = "inout"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5"
dependencies = [
"generic-array",
]
[[package]] [[package]]
name = "is-terminal" name = "is-terminal"
version = "0.4.9" version = "0.4.9"
@ -1235,12 +1297,35 @@ dependencies = [
"windows-targets", "windows-targets",
] ]
[[package]]
name = "password-hash"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7676374caaee8a325c9e7a2ae557f216c5563a171d6997b0ef8a65af35147700"
dependencies = [
"base64ct",
"rand_core",
"subtle",
]
[[package]] [[package]]
name = "paste" name = "paste"
version = "1.0.14" version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c"
[[package]]
name = "pbkdf2"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917"
dependencies = [
"digest",
"hmac",
"password-hash",
"sha2",
]
[[package]] [[package]]
name = "pem-rfc7468" name = "pem-rfc7468"
version = "0.7.0" version = "0.7.0"
@ -1447,6 +1532,15 @@ version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741"
[[package]]
name = "same-file"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
dependencies = [
"winapi-util",
]
[[package]] [[package]]
name = "scopeguard" name = "scopeguard"
version = "1.2.0" version = "1.2.0"
@ -2077,6 +2171,16 @@ version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
[[package]]
name = "walkdir"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee"
dependencies = [
"same-file",
"winapi-util",
]
[[package]] [[package]]
name = "wasi" name = "wasi"
version = "0.11.0+wasi-snapshot-preview1" version = "0.11.0+wasi-snapshot-preview1"
@ -2275,13 +2379,52 @@ version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9" checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9"
[[package]]
name = "zip"
version = "0.6.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261"
dependencies = [
"aes",
"byteorder",
"bzip2",
"constant_time_eq",
"crc32fast",
"crossbeam-utils",
"flate2",
"hmac",
"pbkdf2",
"sha1",
"time",
"zstd 0.11.2+zstd.1.5.2",
]
[[package]]
name = "zstd"
version = "0.11.2+zstd.1.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20cc960326ece64f010d2d2107537f26dc589a6573a316bd5b1dba685fa5fde4"
dependencies = [
"zstd-safe 5.0.2+zstd.1.5.2",
]
[[package]] [[package]]
name = "zstd" name = "zstd"
version = "0.12.4" version = "0.12.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a27595e173641171fc74a1232b7b1c7a7cb6e18222c11e9dfb9888fa424c53c" checksum = "1a27595e173641171fc74a1232b7b1c7a7cb6e18222c11e9dfb9888fa424c53c"
dependencies = [ dependencies = [
"zstd-safe", "zstd-safe 6.0.6",
]
[[package]]
name = "zstd-safe"
version = "5.0.2+zstd.1.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d2a5585e04f9eea4b2a3d1eca508c4dee9592a89ef6f450c11719da0726f4db"
dependencies = [
"libc",
"zstd-sys",
] ]
[[package]] [[package]]

View File

@ -12,6 +12,9 @@ serde = { version = "1.0.183", features = ["derive"] }
serde_json = "1.0.108" serde_json = "1.0.108"
env_logger = "0.10" env_logger = "0.10"
log = "0.4" log = "0.4"
chrono = "0.4.31"
zip = "0.6.6"
walkdir = "2.4.0"
sqlx = { version = "0.7.1", features = ["runtime-tokio", "sqlite", "chrono"] } sqlx = { version = "0.7.1", features = ["runtime-tokio", "sqlite", "chrono"] }
actix-web = "4" actix-web = "4"
actix-web-httpauth = "0.8.0" actix-web-httpauth = "0.8.0"

View File

@ -3,21 +3,21 @@ use actix_web::{delete, get, post, web, HttpResponse, Responder};
use actix_web_httpauth::extractors::bearer::BearerAuth; use actix_web_httpauth::extractors::bearer::BearerAuth;
use log::error; use log::error;
// #[post("/backup/create")] #[post("/backup/create")]
// async fn backup_create( async fn backup_create(
// data: web::Data<State>, data: web::Data<State>,
// body: web::Json<BackupCreateRequest>, body: web::Json<BackupCreateRequest>,
// ) -> impl Responder { ) -> impl Responder {
// match handlers::backup_create(&data.pool, body.into_inner()).await { match handlers::backup_create(&data.pool, data.backup_tx.clone(), body.into_inner()).await {
// Ok(resp) => match resp { Ok(resp) => match resp {
// BackupCreateResponse::Success => HttpResponse::Ok().finish(), BackupCreateResponse::Success => HttpResponse::Ok().finish(),
// }, },
// Err(e) => { Err(e) => {
// error!("While handling /backup/create [POST] request: {e}"); error!("While handling /backup/create [POST] request: {e}");
// HttpResponse::InternalServerError().finish() HttpResponse::InternalServerError().finish()
// } }
// } }
// } }
#[post("/backup/preset")] #[post("/backup/preset")]
async fn backup_preset_post( async fn backup_preset_post(

View File

@ -1,15 +1,28 @@
use crate::api::data::*; use crate::api::data::*;
use crate::backend::backup; use crate::backend::backup;
use anyhow::Result; use anyhow::{Context, Result};
use log::warn; use log::warn;
use sqlx::sqlite::SqlitePool; use sqlx::sqlite::SqlitePool;
use tokio::sync::mpsc;
// pub async fn backup_create( pub async fn backup_create(
// pool: &SqlitePool, pool: &SqlitePool,
// request: BackupCreateRequest, tx: mpsc::Sender<backup::Backup>,
// ) -> Result<BackupCreateResponse> { request: BackupCreateRequest,
// Ok(BackupCreateResponse::Success) ) -> Result<BackupCreateResponse> {
// } let config = match request {
BackupCreateRequest::Preset(preset) => backup::preset::Preset::load(pool, &preset)
.await?
.context(format!("Failed to look up preset '{preset}' !"))?
.config()
.to_owned(),
BackupCreateRequest::Config(cfg) => cfg.into(),
};
let backup = backup::Backup::new(pool, tx, config).await?;
Ok(BackupCreateResponse::Success)
}
pub async fn backup_preset_post( pub async fn backup_preset_post(
pool: &SqlitePool, pool: &SqlitePool,

View File

@ -2,19 +2,27 @@ pub mod calls;
pub mod data; pub mod data;
mod handlers; mod handlers;
use crate::backend::backup;
use actix_web::{web, App, HttpServer}; use actix_web::{web, App, HttpServer};
use anyhow::Result; use anyhow::Result;
use sqlx::sqlite::SqlitePool; use sqlx::sqlite::SqlitePool;
use tokio::sync::mpsc;
pub struct State { pub struct State {
pool: SqlitePool, pool: SqlitePool,
token: String, token: String,
backup_tx: mpsc::Sender<backup::Backup>,
} }
pub async fn start(port: u16, pool: SqlitePool, token: String) -> Result<()> { pub async fn start(
port: u16,
pool: SqlitePool,
token: String,
backup_tx: mpsc::Sender<backup::Backup>,
) -> Result<()> {
let _ = HttpServer::new(move || { let _ = HttpServer::new(move || {
App::new() App::new()
// .service(calls::backup_create) .service(calls::backup_create)
.service(calls::backup_preset_post) .service(calls::backup_preset_post)
.service(calls::backup_preset_get) .service(calls::backup_preset_get)
.service(calls::backup_preset_id_get) .service(calls::backup_preset_id_get)
@ -22,6 +30,7 @@ pub async fn start(port: u16, pool: SqlitePool, token: String) -> Result<()> {
.app_data(web::Data::new(State { .app_data(web::Data::new(State {
pool: pool.clone(), pool: pool.clone(),
token: token.to_owned(), token: token.to_owned(),
backup_tx: backup_tx.to_owned(),
})) }))
}) })
.bind(("0.0.0.0", port))? .bind(("0.0.0.0", port))?

View File

@ -1,12 +1,18 @@
pub mod preset; pub mod preset;
pub mod worker;
use crate::api; use crate::api;
use anyhow::{Context, Error, Result};
use chrono::NaiveDateTime;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sqlx::SqlitePool;
use std::path::PathBuf;
use tokio::sync::mpsc;
#[derive(Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize, Clone)]
struct DockerConfig {} struct DockerConfig {}
#[derive(Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Config { pub struct Config {
nginx_config: bool, nginx_config: bool,
mail_server: bool, mail_server: bool,
@ -52,3 +58,109 @@ impl From<Config> for api::data::BackupConfig {
} }
} }
} }
#[derive(Debug, Clone, Deserialize, Serialize)]
pub enum Status {
Pending,
Processing,
Finished,
Failed,
}
pub struct RawBackup {
id: i64,
time: i64,
config: String,
path: String,
status: String,
}
#[derive(Debug, Clone)]
pub struct Backup {
id: u64,
time: NaiveDateTime,
config: Config,
path: PathBuf,
status: Status,
}
impl TryFrom<RawBackup> for Backup {
type Error = Error;
fn try_from(value: RawBackup) -> std::result::Result<Self, Self::Error> {
Ok(Self {
id: value.id as u64,
time: NaiveDateTime::from_timestamp_opt(value.time, 0)
.context("Failed to convert timestamp into NaiveTime")?,
config: serde_json::from_str(&value.config)?,
path: value.path.into(),
status: serde_json::from_str(&value.status)?,
})
}
}
impl Backup {
pub async fn new(pool: &SqlitePool, tx: mpsc::Sender<Backup>, config: Config) -> Result<Self> {
let time = chrono::Utc::now().naive_utc();
let timestamp = time.timestamp();
let config = serde_json::to_string(&config)?;
let status = serde_json::to_string(&Status::Pending)?;
let id = sqlx::query!(
r#"INSERT INTO Backups (time, config, path, status) VALUES($1, $2, $3, $4) RETURNING id;"#,
timestamp,
config,
"",
status
)
.fetch_one(pool)
.await?
.id;
let path = format!(
"{}/{}.bak",
env!("NC_AW_BACKUP_PATH"),
time.format("%H-%M-%S_%d-%m-%Y")
);
sqlx::query!(r#"UPDATE Backups SET path = $1 WHERE id = $2;"#, path, id)
.execute(pool)
.await?;
let backup = Self::load(pool, id)
.await?
.context("The just created backup record can't be found in the database!")?;
tx.send(backup.clone()).await?;
Ok(backup)
}
pub async fn load(pool: &SqlitePool, id: i64) -> Result<Option<Backup>> {
let query_result =
sqlx::query_as!(RawBackup, r#"SELECT * FROM Backups WHERE id = $1;"#, id)
.fetch_one(pool)
.await;
match query_result {
Ok(raw) => Ok(Some(raw.try_into()?)),
Err(sqlx::Error::RowNotFound) => Ok(None),
Err(e) => Err(Error::new(e)),
}
}
pub async fn set_status(&self, pool: &SqlitePool, status: Status) -> Result<()> {
let status = serde_json::to_string(&status)?;
let id = self.id as i64;
sqlx::query!(
r#"UPDATE Backups SET status = $1 WHERE id = $2;"#,
status,
id
)
.execute(pool)
.await?;
Ok(())
}
}

View File

@ -87,6 +87,10 @@ impl Preset {
} }
} }
pub fn config(&self) -> &backup::Config {
&self.config
}
pub async fn delete(&self, pool: &SqlitePool) -> Result<()> { pub async fn delete(&self, pool: &SqlitePool) -> Result<()> {
sqlx::query!(r#"DELETE FROM Presets WHERE id = $1;"#, self.id) sqlx::query!(r#"DELETE FROM Presets WHERE id = $1;"#, self.id)
.execute(pool) .execute(pool)

View File

@ -0,0 +1,126 @@
use crate::backend::backup;
use anyhow::{bail, Context, Result};
use log::{error, info};
use sqlx::SqlitePool;
use std::fs::File;
use std::io::{copy, BufReader, Write};
use std::path::Path;
use tokio::sync::mpsc;
use walkdir::{DirEntry, WalkDir};
use zip::{write::FileOptions, CompressionMethod, ZipWriter};
fn add_dir_to_archive(
archive: &mut ZipWriter<File>,
dir: &Path,
options: FileOptions,
) -> Result<()> {
for entry in WalkDir::new(dir)
.follow_links(true)
.into_iter()
.filter_map(|e| e.ok())
{
let fname = entry.file_name();
let ftype = entry.file_type();
if ftype.is_dir() {
archive.add_directory(entry.path().to_string_lossy(), options)?;
} else if ftype.is_file() {
archive.start_file(entry.path().to_string_lossy(), options)?;
let mut reader = BufReader::new(File::open(entry.path())?);
copy(&mut reader, archive)?;
}
}
Ok(())
}
fn perform_backup(backup: backup::Backup, pool: SqlitePool) -> Result<()> {
let host = env!("NC_AW_HOST_PATH");
let archive_file = File::create(&backup.path)?;
let mut archive = ZipWriter::new(archive_file);
let options = FileOptions::default()
.compression_method(CompressionMethod::Bzip2)
.compression_level(Some(9));
info!("Starting backup...");
if backup.config.nginx_config {
info!("Starting NGINX config backup...");
add_dir_to_archive(
&mut archive,
&Path::new(&format!("{host}/etc/nginx")),
options,
)?;
}
if backup.config.mail_server {
info!("Starting mail server backup...");
bail!("The config option 'mail_server' is not implemented yet!");
}
if let Some(cfg) = &backup.config.docker {
info!("Starting docker backup...");
bail!("The config option 'docker' is not implemented yet!");
}
archive.finish()?;
info!("Finished backup.");
Ok(())
}
pub async fn start(pool: SqlitePool) -> Result<mpsc::Sender<backup::Backup>> {
let (tx, mut rx) = mpsc::channel(128);
tokio::task::spawn(async move {
loop {
let backup: backup::Backup = rx
.recv()
.await
.expect("Failed to read from the backup workers internal MPSC channel!");
let cloned_backup = backup.clone();
let result: Result<()> = async {
backup
.set_status(&pool, backup::Status::Processing)
.await
.expect("Failed to set backup status!");
let cloned_backup = backup.clone();
let cloned_pool = pool.clone();
match tokio::task::spawn_blocking(|| perform_backup(cloned_backup, cloned_pool))
.await?
{
Ok(_) => (),
Err(e) => {
tokio::fs::remove_file(backup.path).await?;
return Err(e);
}
}
backup
.set_status(&pool, backup::Status::Finished)
.await
.context("Failed to set backup status!")?;
Ok(())
}
.await;
match result {
Ok(_) => (),
Err(e) => {
match cloned_backup
.set_status(&pool, backup::Status::Failed)
.await
{
Ok(_) => (),
Err(e) => error!("Failed to set backup failure status: {e}"),
}
error!("The backup worker thread failed: {e}");
}
}
}
});
Ok(tx)
}

View File

@ -16,5 +16,24 @@ pub async fn prepare(pool: &SqlitePool) -> Result<()> {
) )
.execute(pool) .execute(pool)
.await?; .await?;
sqlx::query!(
r#"
CREATE TABLE IF NOT EXISTS Backups (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
time INTEGER NOT NULL,
config TEXT NOT NULL,
path TEXT NOT NULL,
status TEXT NOT NULL
);
"#
)
.execute(pool)
.await?;
if !tokio::fs::try_exists(env!("NC_AW_BACKUP_PATH")).await? {
tokio::fs::create_dir(env!("NC_AW_BACKUP_PATH")).await?;
}
Ok(()) Ok(())
} }

View File

@ -1,6 +1,7 @@
mod api; mod api;
mod backend; mod backend;
use crate::backend::backup;
use anyhow::Result; use anyhow::Result;
use log::info; use log::info;
use sqlx::SqlitePool; use sqlx::SqlitePool;
@ -24,7 +25,9 @@ async fn main() -> Result<()> {
.await?; .await?;
backend::prepare(&pool).await?; backend::prepare(&pool).await?;
api::start(6969, pool, token.to_owned()).await?; let backup_tx = backup::worker::start(pool.clone()).await?;
api::start(6969, pool, token.to_owned(), backup_tx).await?;
Ok(()) Ok(())
} }