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"
flate2 = "1.0.14"
tar = "0.4.28"
reqwest = { version = "0.10", features = ["blocking", "json"] }

View File

@ -1,6 +1,9 @@
# Albatross
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
```toml
[backup]
@ -10,8 +13,10 @@ minecraft_dir = "/home/mc/server"
output_dir = "/home/mc/backups"
# Number of backups to keep
backups_to_keep = 10
# Discord Webhook
discord_webhook = "https://discordapp.com/api/webhooks/"
# Work config option
# World config options
[[world_config]]
# world name
world_name = "world"

View File

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

View File

@ -10,10 +10,12 @@ use flate2::Compression;
use regex::Regex;
use std::fs::{copy, create_dir, create_dir_all, remove_dir_all, remove_file, DirEntry, File};
use std::path::PathBuf;
use std::time::Instant;
mod albatross_config;
use albatross_config::{AlbatrossConfig, WorldConfig, WorldType};
use std::collections::HashMap;
/// Struct to store information about the region
struct Region {
@ -48,22 +50,24 @@ fn backup_dir(
dir_name: &str,
world_path: &PathBuf,
backup_path: &PathBuf,
) -> Result<(), std::io::Error> {
) -> Result<u64, std::io::Error> {
let mut src_dir = world_path.clone();
src_dir.push(dir_name);
let mut backup_dir = backup_path.clone();
backup_dir.push(dir_name);
create_dir(&backup_dir)?;
for entry in src_dir.read_dir().unwrap() {
let entry = entry.unwrap();
let mut file_count = 0;
for entry in src_dir.read_dir()? {
let entry = entry?;
let mut target = backup_dir.clone();
target.push(entry.file_name());
copy(entry.path(), target)?;
file_count += 1;
}
Ok(())
Ok(file_count)
}
/// Backup the regions
@ -78,7 +82,8 @@ fn backup_region(
save_radius: u64,
world_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();
src_dir.push(dir_name);
let mut backup_dir = backup_path.clone();
@ -91,8 +96,8 @@ fn backup_region(
(save_radius / 512) as i64
};
for entry in src_dir.read_dir().unwrap() {
let entry = entry.unwrap();
for entry in src_dir.read_dir()? {
let entry = entry?;
let file_name = entry.file_name().to_str().unwrap().to_string();
if let Some(region) = Region::from_string(file_name) {
@ -101,11 +106,12 @@ fn backup_region(
target.push(entry.file_name());
copy(entry.path(), target)?;
count += 1;
}
}
}
Ok(())
Ok(count)
}
/// Backup a world
@ -118,13 +124,14 @@ fn backup_world(
world_path: PathBuf,
backup_path: PathBuf,
world_config: &WorldConfig,
) -> Result<(), std::io::Error> {
) -> Result<u64, std::io::Error> {
let mut backup_path = backup_path.clone();
let region_count;
backup_path.push(&world_config.world_name);
create_dir(backup_path.as_path())?;
backup_region("poi", world_config.save_radius, &world_path, &backup_path)?;
backup_region(
region_count = backup_region(
"region",
world_config.save_radius,
&world_path,
@ -132,7 +139,7 @@ fn backup_world(
)?;
backup_dir("data", &world_path, &backup_path)?;
Ok(())
Ok(region_count)
}
/// Backup the overworld
@ -145,9 +152,8 @@ fn backup_overworld(
world_path: PathBuf,
backup_path: PathBuf,
world_config: &WorldConfig,
) -> Result<(), std::io::Error> {
backup_world(world_path, backup_path, world_config)?;
Ok(())
) -> Result<u64, std::io::Error> {
backup_world(world_path, backup_path, world_config)
}
/// Backup the nether
@ -160,12 +166,11 @@ fn backup_nether(
world_path: PathBuf,
backup_path: PathBuf,
world_config: &WorldConfig,
) -> Result<(), std::io::Error> {
) -> Result<u64, std::io::Error> {
let mut nether_path = world_path.clone();
nether_path.push("DIM-1");
backup_world(nether_path, backup_path, world_config)?;
Ok(())
backup_world(nether_path, backup_path, world_config)
}
/// Backup the end
@ -178,12 +183,11 @@ fn backup_end(
world_path: PathBuf,
backup_path: PathBuf,
world_config: &WorldConfig,
) -> Result<(), std::io::Error> {
) -> Result<u64, std::io::Error> {
let mut end_path = world_path.clone();
end_path.push("DIM1");
backup_world(end_path, backup_path, world_config)?;
Ok(())
backup_world(end_path, backup_path, world_config)
}
/// 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(())
}
/// 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
///
/// # Params
/// * `cfg` - config file
fn do_backup(cfg: AlbatrossConfig) -> Result<(), std::io::Error> {
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 backup_name = format!("{}_backup.tar.gz", time_str);
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();
send_webhook("**Albatross is swooping in to backup your worlds!**", &cfg);
let timer = Instant::now();
for world in worlds {
let mut world_dir = server_base_dir.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());
if world_dir.exists() && world_dir.is_dir() {
send_webhook(
format!("Starting backup of **{}**", world_name).as_str(),
&cfg,
);
match world_type {
WorldType::OVERWORLD => {
backup_overworld(world_dir.clone(), tmp_dir.clone(), &world)?;
backup_dir("playerdata", &world_dir, &tmp_dir)?;
let region_count =
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 => {
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 => {
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 {
send_webhook(format!("Error: {} not found.", world_name).as_str(), &cfg);
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)?;
let secs = timer.elapsed().as_secs();
send_webhook(
format!("**Full backup completed in {}s**!", secs).as_str(),
&cfg,
);
Ok(())
}
@ -321,6 +370,7 @@ fn main() {
let cfg = AlbatrossConfig::new(cfg_path).expect("Config not found");
if cfg.world_config.is_some() {
println!("Starting backup");
match do_backup(cfg) {
Err(e) => println!("Error doing backup: {}", e),
_ => {}