From c2cdbd56ac710b64a04f5dcb69dc3463ea027e84 Mon Sep 17 00:00:00 2001 From: Joey Hines Date: Sat, 6 Jun 2020 19:59:02 -0500 Subject: [PATCH] Added compressed backups and removing old backups + Added docs + Backups are compressed as `tar.gz` files + a configurable amount of backups are kept --- Cargo.lock | 107 ++++++++++++++++++-------- Cargo.toml | 3 +- src/albatross_config.rs | 9 +++ src/main.rs | 164 ++++++++++++++++++++++++++++++++++++++-- 4 files changed, 245 insertions(+), 38 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cb8f0d2..6d67fb9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,5 +1,11 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. +[[package]] +name = "adler32" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d2e7343e7fc9de883d1b0341e0b13970f764c14101234857d2ddafa1cb1cac2" + [[package]] name = "aho-corasick" version = "0.7.10" @@ -16,11 +22,12 @@ dependencies = [ "chrono", "clap", "config", + "flate2", "log", "regex", "serde 1.0.111", "serde_derive", - "walkdir", + "tar", ] [[package]] @@ -103,6 +110,39 @@ dependencies = [ "yaml-rust", ] +[[package]] +name = "crc32fast" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba125de2af0df55319f41944744ad91c71113bf74a4646efff39afe1f6842db1" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "filetime" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "affc17579b132fc2461adf7c575cc6e8b134ebca52c51f5411388965227dc695" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "winapi", +] + +[[package]] +name = "flate2" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cfff41391129e0a856d6d822600b8d71179d46879e310417eb9c762eb178b42" +dependencies = [ + "cfg-if", + "crc32fast", + "libc", + "miniz_oxide", +] + [[package]] name = "hermit-abi" version = "0.1.13" @@ -167,6 +207,15 @@ version = "2.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400" +[[package]] +name = "miniz_oxide" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa679ff6578b1cddee93d7e82e263b94a575e0bfced07284eb0c037c1d2416a5" +dependencies = [ + "adler32", +] + [[package]] name = "nom" version = "4.2.3" @@ -223,6 +272,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "redox_syscall" +version = "0.1.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2439c63f3f6139d1b57529d16bc3b8bb855230c8efcc5d3a896c8bea7c3b1e84" + [[package]] name = "regex" version = "1.3.9" @@ -253,15 +308,6 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - [[package]] name = "serde" version = "0.8.23" @@ -335,6 +381,18 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "tar" +version = "0.4.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c058ad0bd6ccb84faa24cc44d4fc99bee8a5d7ba9ff33aa4d993122d1aeeac2" +dependencies = [ + "filetime", + "libc", + "redox_syscall", + "xattr", +] + [[package]] name = "textwrap" version = "0.11.0" @@ -396,17 +454,6 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd" -[[package]] -name = "walkdir" -version = "2.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "777182bc735b6424e1a57516d35ed72cb8019d85c8c9bf536dccb3445c1a2f7d" -dependencies = [ - "same-file", - "winapi", - "winapi-util", -] - [[package]] name = "winapi" version = "0.3.8" @@ -423,21 +470,21 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" -[[package]] -name = "winapi-util" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" -dependencies = [ - "winapi", -] - [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "xattr" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "244c3741f4240ef46274860397c7c74e50eb23624996930e484c16679633a54c" +dependencies = [ + "libc", +] + [[package]] name = "yaml-rust" version = "0.4.4" diff --git a/Cargo.toml b/Cargo.toml index b511bbb..5dbe8ca 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,5 +13,6 @@ serde_derive = "1.0.104" config = "0.9" log = "0.4.8" chrono = "0.4" -walkdir = "2.3.1" regex = "1.3.9" +flate2 = "1.0.14" +tar = "0.4.28" diff --git a/src/albatross_config.rs b/src/albatross_config.rs index ce569c0..af5001f 100644 --- a/src/albatross_config.rs +++ b/src/albatross_config.rs @@ -1,14 +1,19 @@ use config::{Config, ConfigError, File}; use std::path::PathBuf; +/// World types supported #[derive(Debug, Deserialize, Clone)] pub enum WorldType { + /// The End (DIM1) END, + /// Nether (DIM-1) NETHER, + /// Overworld OVERWORLD, } impl From for WorldType { + /// Convert config strings to WorldType fn from(string: String) -> Self { match string.as_str() { "END" => WorldType::END, @@ -18,6 +23,7 @@ impl From for WorldType { } } +/// Config for individual WorldConfig #[derive(Debug, Deserialize, Clone)] pub struct WorldConfig { pub world_name: String, @@ -25,6 +31,7 @@ pub struct WorldConfig { pub world_type: Option, } +/// Config for doing backups #[derive(Debug, Deserialize, Clone)] pub struct BackupConfig { pub minecraft_dir: PathBuf, @@ -32,6 +39,7 @@ pub struct BackupConfig { pub backups_to_keep: u64, } +/// Configs #[derive(Debug, Deserialize, Clone)] pub struct AlbatrossConfig { pub backup: BackupConfig, @@ -39,6 +47,7 @@ pub struct AlbatrossConfig { } impl AlbatrossConfig { + /// Create new backup from file pub fn new(config_path: &str) -> Result { let mut cfg = Config::new(); cfg.merge(File::with_name(config_path))?; diff --git a/src/main.rs b/src/main.rs index 23c09a6..a9d668b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,18 +3,23 @@ extern crate serde; #[macro_use] extern crate serde_derive; -use chrono::Utc; +use chrono::{NaiveDateTime, Utc}; use clap::{App, Arg}; +use flate2::write::GzEncoder; +use flate2::Compression; use regex::Regex; -use std::fs::{copy, create_dir, create_dir_all}; +use std::fs::{copy, create_dir, create_dir_all, remove_dir_all, remove_file, DirEntry, File}; use std::path::PathBuf; mod albatross_config; use albatross_config::{AlbatrossConfig, WorldConfig, WorldType}; +/// Struct to store information about the region struct Region { + /// x position of the region x: i64, + /// y position of the region y: i64, } @@ -34,6 +39,11 @@ impl Region { } } +/// Backup a directory +/// +/// # Param +/// * `world_path` - path to the world folder +/// * `backup_path` - path to the backup folder fn backup_dir( dir_name: &str, world_path: &PathBuf, @@ -56,6 +66,13 @@ fn backup_dir( Ok(()) } +/// 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 fn backup_region( dir_name: &str, save_radius: u64, @@ -91,6 +108,12 @@ fn backup_region( Ok(()) } +/// Backup a world +/// +/// # Param +/// * `world_path` - path to the world folder +/// * `backup_path` - path to the backup folder +/// * `world_config` - world config options fn backup_world( world_path: PathBuf, backup_path: PathBuf, @@ -112,6 +135,12 @@ fn backup_world( Ok(()) } +/// Backup the overworld +/// +/// # Param +/// * `world_path` - path to the world folder +/// * `backup_path` - path to the backup folder +/// * `world_config` - world config options fn backup_overworld( world_path: PathBuf, backup_path: PathBuf, @@ -121,13 +150,119 @@ fn backup_overworld( Ok(()) } +/// Backup the nether +/// +/// # Param +/// * `world_path` - path to the world folder +/// * `backup_path` - path to the backup folder +/// * `world_config` - world config options +fn backup_nether( + world_path: PathBuf, + backup_path: PathBuf, + world_config: &WorldConfig, +) -> Result<(), 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 the end +/// +/// # Param +/// * `world_path` - path to the world folder +/// * `backup_path` - path to the backup folder +/// * `world_config` - world config options +fn backup_end( + world_path: PathBuf, + backup_path: PathBuf, + world_config: &WorldConfig, +) -> Result<(), std::io::Error> { + let mut end_path = world_path.clone(); + end_path.push("DIM1"); + + backup_world(end_path, backup_path, world_config)?; + Ok(()) +} + +/// Compress the backup after the files have been copied +/// +/// # Param +/// * `tmp_dir`: tmp directory with the backed up files +/// * `output_file`: output archive +fn compress_backup(tmp_dir: &PathBuf, output_file: &PathBuf) -> Result<(), std::io::Error> { + 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(()) +} + +/// Get the time of the backup from a file name +/// +/// # Param +/// * `archive_entry`: archive entry +fn get_time_from_file_name( + archive_entry: &DirEntry, +) -> Result, std::io::Error> { + let file_name = archive_entry.file_name().to_str().unwrap().to_string(); + let name: Vec<&str> = file_name.split("_").collect(); + + Ok(chrono::NaiveDateTime::parse_from_str(name[0], "%d-%m-%y-%H:%M:%S").ok()) +} + +/// Removes the old backups from the ouput directory +/// +/// # Params +/// * `output_dir` - output directory containing +/// * `keep` - number of backups to keep +fn remove_old_backups(output_dir: &PathBuf, keep: u64) -> Result<(), std::io::Error> { + let mut backups = vec![]; + + for entry in output_dir.read_dir()? { + let entry = entry?; + + if let Some(ext) = entry.path().extension() { + if ext == "gz" { + backups.push(entry); + } + } + } + + if backups.len() > keep as usize { + backups.sort_by(|a, b| { + let a_time = get_time_from_file_name(a).unwrap().unwrap(); + let b_time = get_time_from_file_name(b).unwrap().unwrap(); + + b_time.cmp(&a_time) + }); + + let number_to_remove = backups.len() - keep as usize; + + for _i in 0..number_to_remove { + let oldest = backups.pop().unwrap(); + remove_file(oldest.path())?; + } + } + + 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 time_str = Utc::now().format("%d-%m-%y_%H:%M%S").to_string(); - let backup_name = format!("{}-backup", time_str); + 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(); + output_archive.push(backup_name); let mut tmp_dir = cfg.backup.output_dir.clone(); - tmp_dir.push(backup_name); + tmp_dir.push("tmp"); + remove_dir_all(&tmp_dir).ok(); create_dir_all(tmp_dir.clone()).unwrap(); @@ -143,18 +278,33 @@ fn do_backup(cfg: AlbatrossConfig) -> Result<(), std::io::Error> { if world_dir.exists() && world_dir.is_dir() { match world_type { WorldType::OVERWORLD => { - backup_overworld(world_dir, tmp_dir.clone(), &world)?; + backup_overworld(world_dir.clone(), tmp_dir.clone(), &world)?; + backup_dir("playerdata", &world_dir, &tmp_dir)?; + } + WorldType::NETHER => { + backup_nether(world_dir, tmp_dir.clone(), &world)?; + } + WorldType::END => { + backup_end(world_dir, tmp_dir.clone(), &world)?; } - _ => {} }; } else { println!("World \"{}\" not found", world_name.clone()); } } + compress_backup(&tmp_dir, &output_archive)?; + + remove_dir_all(&tmp_dir)?; + + remove_old_backups(&cfg.backup.output_dir, cfg.backup.backups_to_keep)?; + Ok(()) } +/// Albatross +/// +/// Run backups of a Minecraft world fn main() { let mut app = App::new("Albatross").about("Backup your worlds").arg( Arg::with_name("config")