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"
|
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"] }
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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
|
||||||
|
|
98
src/main.rs
98
src/main.rs
|
@ -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 => {
|
||||||
|
let region_count =
|
||||||
backup_overworld(world_dir.clone(), tmp_dir.clone(), &world)?;
|
backup_overworld(world_dir.clone(), tmp_dir.clone(), &world)?;
|
||||||
backup_dir("playerdata", &world_dir, &tmp_dir)?;
|
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),
|
||||||
_ => {}
|
_ => {}
|
||||||
|
|
Loading…
Reference in New Issue