parent
c2cdbd56ac
commit
0752877345
File diff suppressed because it is too large
Load Diff
|
@ -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"] }
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
100
src/main.rs
100
src/main.rs
|
@ -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),
|
||||
_ => {}
|
||||
|
|
Loading…
Reference in New Issue