Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
d4c8625
feat(cli): added `mbm backup list` command
Nonolanlan1007 Feb 11, 2026
88f543f
feat(config): added backup identifier to Backup structure
Nonolanlan1007 Feb 14, 2026
f285899
feat(datastores): added .list_backups() and .check_backup_integrity()…
Nonolanlan1007 Feb 14, 2026
58637f6
feat(cli): added `mbm backup inspect` command
Nonolanlan1007 Feb 14, 2026
1442e06
refactor: fixed clippy errors
Nonolanlan1007 Feb 14, 2026
6097646
refactor(cli): fixed imports
Nonolanlan1007 Feb 14, 2026
494c77f
refactor!: moved backup related functions to BackupJob struct and mer…
Nonolanlan1007 Feb 14, 2026
177fae5
refactor: fixed clippy errors
Nonolanlan1007 Feb 15, 2026
f96b090
fix(backups): fixed finalize_backup() method in backupJob
Nonolanlan1007 Feb 16, 2026
0cb5ffc
fix(datastore): added open_read_stream() method
Nonolanlan1007 Feb 20, 2026
e55a569
fix(backup): added restore_backup method
Nonolanlan1007 Feb 20, 2026
54c634b
refactor(backup): fixed imports
Nonolanlan1007 Feb 20, 2026
61319d2
feat(cli): added `mbm backup restore` command
Nonolanlan1007 Feb 20, 2026
55a7178
fix(config): fixed config_parse_config test
Nonolanlan1007 Feb 20, 2026
0b9cac4
refactor: fixed clippy errors
Nonolanlan1007 Feb 20, 2026
e4d32e3
fix(config): fixed config_parse_config test
Nonolanlan1007 Feb 20, 2026
84eb027
fix(config): brought back optional schedule
Nonolanlan1007 Feb 20, 2026
e5eb9a4
fix(config): fixed config_parse_config test
Nonolanlan1007 Feb 20, 2026
896b64b
refactor(datastore): added `.as_str()` method to `Datastore` trait
Nonolanlan1007 Feb 21, 2026
d676974
refactor(datastore): replaced `.unwrap_or_else(Vec::new())` by `.unwr…
Nonolanlan1007 Feb 21, 2026
ac35f7a
refactor(datastore): enhanced filesystem datastore
Nonolanlan1007 Feb 21, 2026
6f558ca
chore(crates): removed default features from `async-stream`
Nonolanlan1007 Feb 21, 2026
ae90990
refactor(logger): removed useless brackets
Nonolanlan1007 Feb 21, 2026
fff87b1
refactor(config): removed useless `.clone()`
Nonolanlan1007 Feb 21, 2026
5caa317
refactor: fixed clippy errors
Nonolanlan1007 Feb 21, 2026
ef997d8
refactor(cli): removed some unwraps from `mbm backup inspect` command
Nonolanlan1007 Feb 23, 2026
597a055
refactor(datastore): merged multiple `.filer_map()` in one
Nonolanlan1007 Feb 23, 2026
4500055
refactor(backups): merged `.map()` + `.unwrap_or()` into `.map_or()`
Nonolanlan1007 Feb 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

23 changes: 12 additions & 11 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,30 +12,31 @@ name = "mbm"
path = "src/main.rs"

[dependencies]
async-stream = { version = "0.3.6", default-features = false }
chrono = { version = "0.4.43", default-features = false, features = ["clock"] }
clap = { version = "4.5.54", default-features = false, features = [
"derive",
"help",
"std",
"usage",
"derive",
"help",
"std",
"usage",
] }
dotenvy = "0.15.7"
mongodb = { version = "3.5.0", default-features = false, features = [
"bson-3",
"compat-3-3-0",
"dns-resolver",
"rustls-tls",
"bson-3",
"compat-3-3-0",
"dns-resolver",
"rustls-tls",
] }
ratatui = { version = "0.30.0", default-features = false, features = [
"crossterm",
"crossterm",
] }
tokio = { version = "1.49.0", features = ["full"] }
regex = "1.12.2"
cronexpr = "1.4.0"
futures = { version = "0.3.31", default-features = false }
regex = "1.12.2"
serde = { version = "1.0.228", features = ["derive"] }
serde_json = { version = "1.0.149", default-features = false, features = ["std"] }
sha2 = { version = "0.10.9", default-features = false }
tokio = { version = "1.49.0", features = ["full"] }

[profile.release]
opt-level = "z"
Expand Down
102 changes: 102 additions & 0 deletions src/cli/commands/backup/inspect.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
use std::process::exit;

use chrono::DateTime;

use crate::{
datastores::{Datastore, DatastoreTrait},
utils::{Config, Log},
};

pub fn inspect(name: String) {
let config = Config::new();
let Some(backup_job) = config
.backups
.values()
.find(|backup_job| backup_job.identifier == name)
else {
Log::error("Backup not found");
exit(1);
};

let backup_dir_prefix = format!("backup_{name}_");
let backups = match &backup_job.datastore {
Datastore::FileSystem(store) => store
.list_backups()
.unwrap_or_default()
.into_iter()
.filter_map(|store| {
let dir_name = store.base_path.file_name()?.to_str()?.to_string();

if dir_name.starts_with(&backup_dir_prefix) {
Some((dir_name, Datastore::FileSystem(store)))
} else {
None
}
})
.collect::<Vec<_>>(),

Datastore::S3(store) => store
.list_backups()
.unwrap_or_default()
.into_iter()
.filter_map(|store| {
let dir_name = store.base_path.file_name()?.to_str()?.to_string();

if dir_name.starts_with(&backup_dir_prefix) {
Some((dir_name, Datastore::S3(store)))
} else {
None
}
})
.collect::<Vec<_>>(),
};

let datastore_type = backup_job.datastore.as_str();
let datastore_base_path = match &backup_job.datastore {
Datastore::FileSystem(store) => store.base_path.display().to_string(),
Datastore::S3(store) => store.base_path.display().to_string(),
};

println!("-- {} --", backup_job.display_name);
println!("Datastore:");
println!("\tType: {}", datastore_type);
println!("\tPath: {}", datastore_base_path);
println!(
"Schedule: {:?}",
if let Some(schedule) = backup_job.clone().raw_schedule {
schedule
} else {
"disabled".to_string()
}
);
if let Some(next) = backup_job.get_next_run() {
println!("Next run: {:?}", next);
}
println!(
"Encryption: {}",
if backup_job.is_encryption_enabled() {
"enabled"
} else {
"disabled"
}
);
println!("Available backups:");
for (dir_name, datastore) in backups {
let health_state = datastore.check_backup_integrity().is_ok_and(|res| res);
let timestamp = dir_name
Comment thread
Nonolanlan1007 marked this conversation as resolved.
.rsplit("_")
.next()
.and_then(|s| s.parse::<i64>().ok())
.unwrap_or(0);
let date = match DateTime::from_timestamp_secs(timestamp) {
Some(value) => value.to_rfc3339(),
None => "Unknown date".to_string(),
};
println!(
"\t{} - {} - {}",
dir_name,
date,
if health_state { "Healthy" } else { "Unhealthy" }
)
}
}
21 changes: 21 additions & 0 deletions src/cli/commands/backup/list.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
use crate::utils::Config;
use crate::utils::backup_manager::BackupJob;

pub fn list() {
let config = Config::new();

if config.backups.is_empty() {
println!("No backup jobs found")
}

let mut backup_jobs: Vec<&BackupJob> = config.backups.values().collect();
backup_jobs.sort_by_key(|a| a.get_next_run());

for backup_job in backup_jobs {
println!(
"- {} - Next run: {:?}",
backup_job.identifier,
backup_job.get_next_run()
);
}
}
6 changes: 6 additions & 0 deletions src/cli/commands/backup/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
mod inspect;
mod list;
mod restore;
mod start;

pub use inspect::inspect;
pub use list::list;
pub use restore::restore;
pub use start::start;
37 changes: 37 additions & 0 deletions src/cli/commands/backup/restore.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
use std::process::exit;

use futures::StreamExt;

use crate::utils::{Config, Log, logger::StreamEvent};

pub async fn restore(name: String, target: Option<String>) {
let config = Config::new();
let Some(backup_job) = config
.backups
.values()
.find(|backup_job| name.starts_with(&format!("backup_{}", &backup_job.identifier)))
else {
Log::error("Backup not found");
exit(1);
};

let mut events = Box::pin(backup_job.restore_backup_to_database(name, target));

let mut has_errors = false;
while let Some(event) = events.next().await {
if matches!(event, StreamEvent::Error(_)) && !has_errors {
has_errors = true;
}
Log::from_stream_event(event);
}

if !has_errors {
Log::highlight(
format!(
"Backup job {} successfully executed",
backup_job.display_name
)
.as_str(),
)
}
}
39 changes: 24 additions & 15 deletions src/cli/commands/backup/start.rs
Original file line number Diff line number Diff line change
@@ -1,30 +1,39 @@
use std::process::exit;

use crate::{
Daemon,
utils::{config::Config, logger::Logger},
};
use futures::StreamExt;

use crate::utils::{Config, Log, logger::StreamEvent};

pub async fn start(name: String) {
let config = Config::new();
let Some((_, backup_config)) = config
let Some(backup_job) = config
.backups
.iter()
.find(|(backup_name, _)| **backup_name == format!("backup.{name}"))
.values()
.find(|backup_job| backup_job.identifier == name)
else {
Logger::error("Backup not found");
Log::error("Backup not found");
exit(1);
};

Logger::info(format!("Starting backup job {}...", backup_config.display_name).as_str());
match Daemon::start_backup_job(backup_config).await {
Ok(_) => Logger::highlight(
Log::info(format!("Starting backup job {}...", backup_job.display_name).as_str());

let mut events = Box::pin(backup_job.execute());

let mut has_errors = false;
while let Some(event) = events.next().await {
if matches!(event, StreamEvent::Error(_)) && !has_errors {
has_errors = true;
}
Log::from_stream_event(event);
}

if !has_errors {
Log::highlight(
format!(
"Backup job {} successfully executed",
backup_config.display_name
backup_job.display_name
)
.as_str(),
),
Err(err) => Logger::error(err.as_str()),
};
)
}
}
Loading