Added discord webhooks and more status messages

+ Updated readme
backup_error_fix
Joey Hines 2020-06-07 14:21:26 -05:00
parent c2cdbd56ac
commit 0752877345
5 changed files with 952 additions and 31 deletions

874
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -16,3 +16,4 @@ chrono = "0.4"
regex = "1.3.9" regex = "1.3.9"
flate2 = "1.0.14" flate2 = "1.0.14"
tar = "0.4.28" tar = "0.4.28"
reqwest = { version = "0.10", features = ["blocking", "json"] }

View File

@ -1,6 +1,9 @@
# Albatross # Albatross
Back up what you care about in your Minecraft worlds. Back up what you care about in your Minecraft worlds.
Albatross backs up player files and region files within a certain configurable radius. It can also send Discord
webhooks. Backups are compressed and stored as `tar.gz` archives.
## Config ## Config
```toml ```toml
[backup] [backup]
@ -10,8 +13,10 @@ minecraft_dir = "/home/mc/server"
output_dir = "/home/mc/backups" output_dir = "/home/mc/backups"
# Number of backups to keep # Number of backups to keep
backups_to_keep = 10 backups_to_keep = 10
# Discord Webhook
discord_webhook = "https://discordapp.com/api/webhooks/"
# Work config option # World config options
[[world_config]] [[world_config]]
# world name # world name
world_name = "world" world_name = "world"

View File

@ -37,6 +37,7 @@ pub struct BackupConfig {
pub minecraft_dir: PathBuf, pub minecraft_dir: PathBuf,
pub output_dir: PathBuf, pub output_dir: PathBuf,
pub backups_to_keep: u64, pub backups_to_keep: u64,
pub discord_webhook: Option<String>,
} }
/// Configs /// Configs

View File

@ -10,10 +10,12 @@ use flate2::Compression;
use regex::Regex; use regex::Regex;
use std::fs::{copy, create_dir, create_dir_all, remove_dir_all, remove_file, DirEntry, File}; use std::fs::{copy, create_dir, create_dir_all, remove_dir_all, remove_file, DirEntry, File};
use std::path::PathBuf; use std::path::PathBuf;
use std::time::Instant;
mod albatross_config; mod albatross_config;
use albatross_config::{AlbatrossConfig, WorldConfig, WorldType}; use albatross_config::{AlbatrossConfig, WorldConfig, WorldType};
use std::collections::HashMap;
/// Struct to store information about the region /// Struct to store information about the region
struct Region { struct Region {
@ -48,22 +50,24 @@ fn backup_dir(
dir_name: &str, dir_name: &str,
world_path: &PathBuf, world_path: &PathBuf,
backup_path: &PathBuf, backup_path: &PathBuf,
) -> Result<(), std::io::Error> { ) -> Result<u64, std::io::Error> {
let mut src_dir = world_path.clone(); let mut src_dir = world_path.clone();
src_dir.push(dir_name); src_dir.push(dir_name);
let mut backup_dir = backup_path.clone(); let mut backup_dir = backup_path.clone();
backup_dir.push(dir_name); backup_dir.push(dir_name);
create_dir(&backup_dir)?; create_dir(&backup_dir)?;
for entry in src_dir.read_dir().unwrap() { let mut file_count = 0;
let entry = entry.unwrap(); for entry in src_dir.read_dir()? {
let entry = entry?;
let mut target = backup_dir.clone(); let mut target = backup_dir.clone();
target.push(entry.file_name()); target.push(entry.file_name());
copy(entry.path(), target)?; copy(entry.path(), target)?;
file_count += 1;
} }
Ok(()) Ok(file_count)
} }
/// Backup the regions /// Backup the regions
@ -78,7 +82,8 @@ fn backup_region(
save_radius: u64, save_radius: u64,
world_path: &PathBuf, world_path: &PathBuf,
backup_path: &PathBuf, backup_path: &PathBuf,
) -> Result<(), std::io::Error> { ) -> Result<u64, std::io::Error> {
let mut count: u64 = 0;
let mut src_dir = world_path.clone(); let mut src_dir = world_path.clone();
src_dir.push(dir_name); src_dir.push(dir_name);
let mut backup_dir = backup_path.clone(); let mut backup_dir = backup_path.clone();
@ -91,8 +96,8 @@ fn backup_region(
(save_radius / 512) as i64 (save_radius / 512) as i64
}; };
for entry in src_dir.read_dir().unwrap() { for entry in src_dir.read_dir()? {
let entry = entry.unwrap(); let entry = entry?;
let file_name = entry.file_name().to_str().unwrap().to_string(); let file_name = entry.file_name().to_str().unwrap().to_string();
if let Some(region) = Region::from_string(file_name) { if let Some(region) = Region::from_string(file_name) {
@ -101,11 +106,12 @@ fn backup_region(
target.push(entry.file_name()); target.push(entry.file_name());
copy(entry.path(), target)?; copy(entry.path(), target)?;
count += 1;
} }
} }
} }
Ok(()) Ok(count)
} }
/// Backup a world /// Backup a world
@ -118,13 +124,14 @@ fn backup_world(
world_path: PathBuf, world_path: PathBuf,
backup_path: PathBuf, backup_path: PathBuf,
world_config: &WorldConfig, world_config: &WorldConfig,
) -> Result<(), std::io::Error> { ) -> Result<u64, std::io::Error> {
let mut backup_path = backup_path.clone(); let mut backup_path = backup_path.clone();
let region_count;
backup_path.push(&world_config.world_name); backup_path.push(&world_config.world_name);
create_dir(backup_path.as_path())?; create_dir(backup_path.as_path())?;
backup_region("poi", world_config.save_radius, &world_path, &backup_path)?; backup_region("poi", world_config.save_radius, &world_path, &backup_path)?;
backup_region( region_count = backup_region(
"region", "region",
world_config.save_radius, world_config.save_radius,
&world_path, &world_path,
@ -132,7 +139,7 @@ fn backup_world(
)?; )?;
backup_dir("data", &world_path, &backup_path)?; backup_dir("data", &world_path, &backup_path)?;
Ok(()) Ok(region_count)
} }
/// Backup the overworld /// Backup the overworld
@ -145,9 +152,8 @@ fn backup_overworld(
world_path: PathBuf, world_path: PathBuf,
backup_path: PathBuf, backup_path: PathBuf,
world_config: &WorldConfig, world_config: &WorldConfig,
) -> Result<(), std::io::Error> { ) -> Result<u64, std::io::Error> {
backup_world(world_path, backup_path, world_config)?; backup_world(world_path, backup_path, world_config)
Ok(())
} }
/// Backup the nether /// Backup the nether
@ -160,12 +166,11 @@ fn backup_nether(
world_path: PathBuf, world_path: PathBuf,
backup_path: PathBuf, backup_path: PathBuf,
world_config: &WorldConfig, world_config: &WorldConfig,
) -> Result<(), std::io::Error> { ) -> Result<u64, std::io::Error> {
let mut nether_path = world_path.clone(); let mut nether_path = world_path.clone();
nether_path.push("DIM-1"); nether_path.push("DIM-1");
backup_world(nether_path, backup_path, world_config)?; backup_world(nether_path, backup_path, world_config)
Ok(())
} }
/// Backup the end /// Backup the end
@ -178,12 +183,11 @@ fn backup_end(
world_path: PathBuf, world_path: PathBuf,
backup_path: PathBuf, backup_path: PathBuf,
world_config: &WorldConfig, world_config: &WorldConfig,
) -> Result<(), std::io::Error> { ) -> Result<u64, std::io::Error> {
let mut end_path = world_path.clone(); let mut end_path = world_path.clone();
end_path.push("DIM1"); end_path.push("DIM1");
backup_world(end_path, backup_path, world_config)?; backup_world(end_path, backup_path, world_config)
Ok(())
} }
/// Compress the backup after the files have been copied /// Compress the backup after the files have been copied
@ -249,13 +253,29 @@ fn remove_old_backups(output_dir: &PathBuf, keep: u64) -> Result<(), std::io::Er
Ok(()) Ok(())
} }
/// Sends a webhook to Discord if its configured
///
/// # Params
/// * `msg` - Message to send to discord
/// * `cfg` - Albatross config
fn send_webhook(msg: &str, cfg: &AlbatrossConfig) {
if let Some(webhook) = &cfg.backup.discord_webhook {
let mut map = HashMap::new();
map.insert("content".to_string(), msg.to_string());
let client = reqwest::blocking::Client::new();
client.post(webhook).json(&map).send().ok();
}
}
/// Backup the configured worlds from a minecraft server /// Backup the configured worlds from a minecraft server
/// ///
/// # Params /// # Params
/// * `cfg` - config file /// * `cfg` - config file
fn do_backup(cfg: AlbatrossConfig) -> Result<(), std::io::Error> { fn do_backup(cfg: AlbatrossConfig) -> Result<(), std::io::Error> {
let server_base_dir = cfg.backup.minecraft_dir.clone(); let server_base_dir = cfg.backup.minecraft_dir.clone();
let worlds = cfg.world_config.unwrap().clone(); let worlds = cfg.world_config.clone().expect("No worlds configured");
let time_str = Utc::now().format("%d-%m-%y-%H:%M:%S").to_string(); let time_str = Utc::now().format("%d-%m-%y-%H:%M:%S").to_string();
let backup_name = format!("{}_backup.tar.gz", time_str); let backup_name = format!("{}_backup.tar.gz", time_str);
let mut output_archive = cfg.backup.output_dir.clone(); let mut output_archive = cfg.backup.output_dir.clone();
@ -266,6 +286,8 @@ fn do_backup(cfg: AlbatrossConfig) -> Result<(), std::io::Error> {
create_dir_all(tmp_dir.clone()).unwrap(); create_dir_all(tmp_dir.clone()).unwrap();
send_webhook("**Albatross is swooping in to backup your worlds!**", &cfg);
let timer = Instant::now();
for world in worlds { for world in worlds {
let mut world_dir = server_base_dir.clone(); let mut world_dir = server_base_dir.clone();
let world_name = world.world_name.clone(); let world_name = world.world_name.clone();
@ -276,19 +298,41 @@ fn do_backup(cfg: AlbatrossConfig) -> Result<(), std::io::Error> {
world_dir.push(world_name.clone()); world_dir.push(world_name.clone());
if world_dir.exists() && world_dir.is_dir() { if world_dir.exists() && world_dir.is_dir() {
send_webhook(
format!("Starting backup of **{}**", world_name).as_str(),
&cfg,
);
match world_type { match world_type {
WorldType::OVERWORLD => { WorldType::OVERWORLD => {
backup_overworld(world_dir.clone(), tmp_dir.clone(), &world)?; let region_count =
backup_dir("playerdata", &world_dir, &tmp_dir)?; backup_overworld(world_dir.clone(), tmp_dir.clone(), &world)?;
let player_count = backup_dir("playerdata", &world_dir, &tmp_dir)?;
send_webhook(
format!(
"{} regions and {} player files backed up.",
region_count, player_count
)
.as_str(),
&cfg,
);
} }
WorldType::NETHER => { WorldType::NETHER => {
backup_nether(world_dir, tmp_dir.clone(), &world)?; let region_count = backup_nether(world_dir, tmp_dir.clone(), &world)?;
send_webhook(
format!("{} regions backed up.", region_count).as_str(),
&cfg,
);
} }
WorldType::END => { WorldType::END => {
backup_end(world_dir, tmp_dir.clone(), &world)?; let region_count = backup_end(world_dir, tmp_dir.clone(), &world)?;
send_webhook(
format!("{} regions backed up.", region_count).as_str(),
&cfg,
);
} }
}; };
} else { } else {
send_webhook(format!("Error: {} not found.", world_name).as_str(), &cfg);
println!("World \"{}\" not found", world_name.clone()); println!("World \"{}\" not found", world_name.clone());
} }
} }
@ -299,6 +343,11 @@ fn do_backup(cfg: AlbatrossConfig) -> Result<(), std::io::Error> {
remove_old_backups(&cfg.backup.output_dir, cfg.backup.backups_to_keep)?; remove_old_backups(&cfg.backup.output_dir, cfg.backup.backups_to_keep)?;
let secs = timer.elapsed().as_secs();
send_webhook(
format!("**Full backup completed in {}s**!", secs).as_str(),
&cfg,
);
Ok(()) Ok(())
} }
@ -321,6 +370,7 @@ fn main() {
let cfg = AlbatrossConfig::new(cfg_path).expect("Config not found"); let cfg = AlbatrossConfig::new(cfg_path).expect("Config not found");
if cfg.world_config.is_some() { if cfg.world_config.is_some() {
println!("Starting backup");
match do_backup(cfg) { match do_backup(cfg) {
Err(e) => println!("Error doing backup: {}", e), Err(e) => println!("Error doing backup: {}", e),
_ => {} _ => {}