394 lines
12 KiB
Rust
394 lines
12 KiB
Rust
use crate::backup;
|
|
use crate::config::{AlbatrossConfig, RemoteBackupConfig, WorldConfig, WorldType};
|
|
use crate::discord::send_webhook;
|
|
use crate::error::Result;
|
|
use crate::region::Region;
|
|
use crate::remote::file::FileBackup;
|
|
use crate::remote::ftp::FTPBackup;
|
|
use crate::remote::sftp::SFTPBackup;
|
|
use crate::remote::RemoteBackupSite;
|
|
use chrono::Utc;
|
|
use flate2::read::GzDecoder;
|
|
use flate2::write::GzEncoder;
|
|
use flate2::Compression;
|
|
use std::convert::TryFrom;
|
|
use std::fs::{copy, create_dir, create_dir_all, remove_dir_all, rename, File};
|
|
use std::path::PathBuf;
|
|
use std::time::Instant;
|
|
use tar::Archive;
|
|
|
|
/// Backup a file
|
|
///
|
|
/// # Param
|
|
/// * `file_name` - file name
|
|
/// * `world_path` - path to the world folder
|
|
/// * `backup_path` - path to the backup folder
|
|
pub fn backup_file(
|
|
file_name: &str,
|
|
mut world_path: PathBuf,
|
|
mut backup_path: PathBuf,
|
|
) -> Result<u64> {
|
|
world_path.push(file_name);
|
|
backup_path.push(file_name);
|
|
|
|
Ok(copy(world_path, backup_path)?)
|
|
}
|
|
|
|
/// Backup a directory
|
|
///
|
|
/// # Param
|
|
/// * `dir_name` - directory name
|
|
/// * `world_path` - path to the world folder
|
|
/// * `backup_path` - path to the backup folder
|
|
pub fn backup_dir(dir_name: &str, world_path: &PathBuf, backup_path: &PathBuf) -> Result<u64> {
|
|
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)?;
|
|
|
|
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(file_count)
|
|
}
|
|
|
|
/// Backup the regions
|
|
///
|
|
/// # Param
|
|
/// * `dir_name` - name of the backup folder
|
|
/// * `save_radius` - block radius to save
|
|
/// * `world_path` - path to the world folder
|
|
/// * `backup_path` - path to the backup folder
|
|
pub fn backup_region(
|
|
dir_name: &str,
|
|
save_radius: u64,
|
|
world_path: &PathBuf,
|
|
backup_path: &PathBuf,
|
|
) -> Result<u64> {
|
|
let mut count: u64 = 0;
|
|
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)?;
|
|
|
|
let save_radius = (save_radius as f64 / 512.0).ceil() as i64;
|
|
|
|
for entry in src_dir.read_dir()? {
|
|
let entry = entry?;
|
|
let file_name = entry.file_name().to_str().unwrap().to_string();
|
|
|
|
if let Ok(region) = Region::try_from(file_name) {
|
|
if region.x.abs() <= save_radius && region.y.abs() <= save_radius {
|
|
let mut target = backup_dir.clone();
|
|
target.push(entry.file_name());
|
|
|
|
copy(entry.path(), target)?;
|
|
count += 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(count)
|
|
}
|
|
|
|
/// Backup a world
|
|
///
|
|
/// # Param
|
|
/// * `world_path` - path to the world folder
|
|
/// * `backup_path` - path to the backup folder
|
|
/// * `world_config` - world config options
|
|
pub fn backup_world(
|
|
world_path: PathBuf,
|
|
mut backup_path: PathBuf,
|
|
world_config: &WorldConfig,
|
|
) -> Result<u64> {
|
|
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)?;
|
|
region_count = backup_region(
|
|
"region",
|
|
world_config.save_radius,
|
|
&world_path,
|
|
&backup_path,
|
|
)?;
|
|
Ok(region_count)
|
|
}
|
|
|
|
/// Backup the overworld
|
|
///
|
|
/// # Param
|
|
/// * `world_path` - path to the world folder
|
|
/// * `backup_path` - path to the backup folder
|
|
/// * `world_config` - world config options
|
|
pub fn backup_overworld(
|
|
world_path: PathBuf,
|
|
backup_path: PathBuf,
|
|
world_config: &WorldConfig,
|
|
) -> Result<(u64, u64)> {
|
|
backup_dir("data", &world_path, &backup_path)?;
|
|
backup_dir("stats", &world_path, &backup_path)?;
|
|
|
|
backup_file("level.dat", world_path.clone(), backup_path.clone())?;
|
|
backup_file("level.dat_old", world_path.clone(), backup_path.clone())?;
|
|
backup_file("session.lock", world_path.clone(), backup_path.clone())?;
|
|
backup_file("uid.dat", world_path.clone(), backup_path.clone())?;
|
|
|
|
let player_count = backup_dir("playerdata", &world_path, &backup_path)?;
|
|
let region_count = backup_world(world_path, backup_path, world_config)?;
|
|
|
|
Ok((region_count, player_count))
|
|
}
|
|
|
|
/// Backup the nether
|
|
///
|
|
/// # Param
|
|
/// * `world_path` - path to the world folder
|
|
/// * `backup_path` - path to the backup folder
|
|
/// * `world_config` - world config options
|
|
pub fn backup_nether(
|
|
world_path: PathBuf,
|
|
backup_path: PathBuf,
|
|
world_config: &WorldConfig,
|
|
) -> Result<u64> {
|
|
let mut nether_path = world_path;
|
|
nether_path.push("DIM-1");
|
|
|
|
backup_world(nether_path, backup_path, world_config)
|
|
}
|
|
|
|
/// Backup the end
|
|
///
|
|
/// # Param
|
|
/// * `world_path` - path to the world folder
|
|
/// * `backup_path` - path to the backup folder
|
|
/// * `world_config` - world config options
|
|
pub fn backup_end(
|
|
world_path: PathBuf,
|
|
backup_path: PathBuf,
|
|
world_config: &WorldConfig,
|
|
) -> Result<u64> {
|
|
let mut end_path = world_path;
|
|
end_path.push("DIM1");
|
|
|
|
backup_world(end_path, backup_path, world_config)
|
|
}
|
|
|
|
/// Compress the backup after the files have been copied
|
|
///
|
|
/// # Param
|
|
/// * `tmp_dir`: tmp directory with the backed up files
|
|
/// * `output_file`: output archive
|
|
pub fn compress_backup(tmp_dir: &PathBuf, output_file: &PathBuf) -> Result<()> {
|
|
let archive = File::create(output_file)?;
|
|
let enc = GzEncoder::new(archive, Compression::default());
|
|
let mut tar_builder = tar::Builder::new(enc);
|
|
tar_builder.append_dir_all(".", tmp_dir)?;
|
|
Ok(())
|
|
}
|
|
|
|
pub fn uncompress_backup(backup: &PathBuf) -> Result<PathBuf> {
|
|
let backup_file = File::open(backup)?;
|
|
let dec = GzDecoder::new(backup_file);
|
|
let mut extract = Archive::new(dec);
|
|
let extract_path = PathBuf::from("tmp");
|
|
extract.unpack(&extract_path)?;
|
|
|
|
Ok(extract_path)
|
|
}
|
|
|
|
/// Takes an existing backup and converts it to a singleplayer world
|
|
///
|
|
/// # Param
|
|
/// * config - Albatross config
|
|
/// * backup - path of the backup to convert
|
|
/// * output - output path
|
|
pub fn convert_backup_to_sp(
|
|
config: &AlbatrossConfig,
|
|
backup: &PathBuf,
|
|
output: &PathBuf,
|
|
) -> Result<()> {
|
|
let extract_path = uncompress_backup(backup)?;
|
|
|
|
if let Some(worlds) = &config.world_config {
|
|
for world in worlds {
|
|
let world_type = match world.world_type.clone() {
|
|
Some(world_type) => world_type,
|
|
None => WorldType::OVERWORLD,
|
|
};
|
|
let src = PathBuf::from(&extract_path).join(&world.world_name);
|
|
let dest = PathBuf::from(&extract_path);
|
|
match world_type {
|
|
WorldType::OVERWORLD => {
|
|
rename(src.clone().join("poi"), dest.clone().join("poi"))?;
|
|
rename(src.clone().join("region"), dest.clone().join("region"))?;
|
|
}
|
|
WorldType::NETHER => {
|
|
rename(src, dest.clone().join("DIM-1"))?;
|
|
}
|
|
WorldType::END => {
|
|
rename(src, dest.clone().join("DIM1"))?;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
compress_backup(&extract_path, output)?;
|
|
remove_dir_all(&extract_path)?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Preform a remote backup, if configured
|
|
pub fn do_remote_backup(
|
|
remote_backup_cfg: &RemoteBackupConfig,
|
|
backup_path: PathBuf,
|
|
) -> Result<()> {
|
|
if remote_backup_cfg.sftp.is_some() {
|
|
let mut sftp_backup = SFTPBackup::new(&remote_backup_cfg)?;
|
|
sftp_backup.backup_to_remote(backup_path)?;
|
|
sftp_backup.cleanup()?;
|
|
} else if remote_backup_cfg.ftp.is_some() {
|
|
let mut ftps_backup = FTPBackup::new(&remote_backup_cfg)?;
|
|
ftps_backup.backup_to_remote(backup_path)?;
|
|
ftps_backup.cleanup()?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Backup the configured worlds from a minecraft server
|
|
///
|
|
/// # Params
|
|
/// * `cfg` - config file
|
|
pub fn do_backup(cfg: AlbatrossConfig, output: Option<PathBuf>) -> Result<()> {
|
|
let server_base_dir = cfg.backup.minecraft_dir.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 = match output {
|
|
Some(out_path) => out_path,
|
|
None => cfg.backup.output_dir.clone(),
|
|
};
|
|
output_archive.push(backup_name);
|
|
let mut tmp_dir = cfg.backup.output_dir.clone();
|
|
tmp_dir.push("tmp");
|
|
remove_dir_all(&tmp_dir).ok();
|
|
|
|
create_dir_all(tmp_dir.clone())?;
|
|
|
|
let timer = Instant::now();
|
|
|
|
send_webhook("**Albatross is swooping in to backup your worlds!**", &cfg);
|
|
|
|
backup_worlds(&cfg, server_base_dir, worlds, &mut tmp_dir).map_err(|e| {
|
|
send_webhook("Failed to copy worlds to backup location", &cfg);
|
|
println!("Failed to copy worlds: {}", e);
|
|
e
|
|
})?;
|
|
|
|
backup::compress_backup(&tmp_dir, &output_archive).map_err(|e| {
|
|
send_webhook("Failed to compress backup", &cfg);
|
|
println!("Failed to compress backup: {}", e);
|
|
e
|
|
})?;
|
|
|
|
remove_dir_all(&tmp_dir)?;
|
|
|
|
let mut local_backup = FileBackup::new(&cfg.backup).unwrap();
|
|
|
|
match local_backup.cleanup() {
|
|
Ok(backups_removed) => {
|
|
if backups_removed > 0 {
|
|
let msg = format!(
|
|
"Albatross mistook **{}** of your old backups for some french fries and ate them!! SKRAWWWW",
|
|
backups_removed
|
|
);
|
|
send_webhook(msg.as_str(), &cfg);
|
|
}
|
|
}
|
|
Err(e) => {
|
|
send_webhook("Failed to remove old backups!", &cfg);
|
|
println!("Failed to remove old backups: {}", e)
|
|
}
|
|
}
|
|
|
|
if let Some(remote_backup_config) = &cfg.remote {
|
|
match do_remote_backup(remote_backup_config, output_archive) {
|
|
Ok(_) => {
|
|
send_webhook("Remote backup completed!", &cfg);
|
|
}
|
|
Err(e) => {
|
|
send_webhook("Remote backup failed!", &cfg);
|
|
println!("Remote backup failed with error: {}", e);
|
|
}
|
|
}
|
|
}
|
|
|
|
let secs = timer.elapsed().as_secs();
|
|
send_webhook(
|
|
format!("**Full backup completed in {}s**! *SKREEEEEEEEEE*", secs).as_str(),
|
|
&cfg,
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
fn backup_worlds(
|
|
cfg: &AlbatrossConfig,
|
|
server_base_dir: PathBuf,
|
|
worlds: Vec<WorldConfig>,
|
|
tmp_dir: &mut PathBuf,
|
|
) -> Result<()> {
|
|
for world in worlds {
|
|
let mut world_dir = server_base_dir.clone();
|
|
let world_name = world.world_name.clone();
|
|
let world_type = match world.world_type.clone() {
|
|
Some(world_type) => world_type,
|
|
None => WorldType::OVERWORLD,
|
|
};
|
|
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,
|
|
);
|
|
let webhook_msg = match world_type {
|
|
WorldType::OVERWORLD => {
|
|
let (region_count, player_count) =
|
|
backup_overworld(world_dir.clone(), tmp_dir.clone(), &world)?;
|
|
format!(
|
|
"{} regions and {} player files backed up.",
|
|
region_count, player_count
|
|
)
|
|
}
|
|
WorldType::NETHER => {
|
|
let region_count = backup_nether(world_dir, tmp_dir.clone(), &world)?;
|
|
format!("{} regions backed up.", region_count)
|
|
}
|
|
WorldType::END => {
|
|
let region_count = backup_end(world_dir, tmp_dir.clone(), &world)?;
|
|
format!("{} regions backed up.", region_count)
|
|
}
|
|
};
|
|
|
|
send_webhook(&webhook_msg, &cfg);
|
|
} else {
|
|
send_webhook(format!("Error: {} not found.", world_name).as_str(), &cfg);
|
|
println!("World \"{}\" not found", world_name.clone());
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|