From a94d0221c5f7bc240c4981a81d59df4aa2cb8822 Mon Sep 17 00:00:00 2001 From: Joey Hines Date: Tue, 3 Nov 2020 21:46:16 -0600 Subject: [PATCH 1/3] Added the ability to restore chunks from a backup + Added `restore` subcommand + Needs more testing, but appears to be working --- Cargo.lock | 58 +++++++++++++++++++++++++++++++++++++++++++++++--- Cargo.toml | 3 ++- src/backup.rs | 17 ++++++++++----- src/main.rs | 51 ++++++++++++++++++++++++++++++++++++++++++-- src/restore.rs | 58 ++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 176 insertions(+), 11 deletions(-) create mode 100644 src/restore.rs diff --git a/Cargo.lock b/Cargo.lock index c28b380..4aeecd7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -32,8 +32,9 @@ dependencies = [ [[package]] name = "albatross" -version = "0.2.0" +version = "0.3.0" dependencies = [ + "anvil-region", "chrono", "config", "discord-hooks-rs", @@ -55,6 +56,18 @@ dependencies = [ "winapi 0.3.8", ] +[[package]] +name = "anvil-region" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8e1613ef793128ec9fb0bacd879a09bc5590e9757d81eb3104b2fd08fb5c59f" +dependencies = [ + "bitvec", + "byteorder", + "log", + "named-binary-tag", +] + [[package]] name = "atty" version = "0.2.14" @@ -98,12 +111,28 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" +[[package]] +name = "bitvec" +version = "0.17.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41262f11d771fd4a61aa3ce019fca363b4b6c282fca9da2a31186d3965a47a5c" +dependencies = [ + "either", + "radium", +] + [[package]] name = "bumpalo" version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e8c087f005730276d1096a652e92a8bacee2e2472bcc9715a74d2bec38b5820" +[[package]] +name = "byteorder" +version = "1.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de" + [[package]] name = "bytes" version = "0.5.4" @@ -211,6 +240,12 @@ version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4358a9e11b9a09cf52383b451b49a169e8d797b68aa02301ff586d70d9661ea3" +[[package]] +name = "either" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" + [[package]] name = "encoding_rs" version = "0.8.23" @@ -562,9 +597,9 @@ checksum = "8dd5a6d5999d9907cda8ed67bbd137d3af8085216c2ac62de5be860bd41f304a" [[package]] name = "log" -version = "0.4.8" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14b6052be84e6b71ab17edffc2eeabf5c2c3ae1fdb464aae35ac50c67a44e1f7" +checksum = "4fabed175da42fed1fa0746b0ea71f412aa9d35e76e95e59b192c64b9dc2bf8b" dependencies = [ "cfg-if 0.1.10", ] @@ -647,6 +682,17 @@ dependencies = [ "ws2_32-sys", ] +[[package]] +name = "named-binary-tag" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ae8ccc227f1ca068299ad32bcac0da2d2cf90b1b51c4679d0cc30900703ba60" +dependencies = [ + "byteorder", + "flate2", + "linked-hash-map 0.5.3", +] + [[package]] name = "native-tls" version = "0.2.4" @@ -861,6 +907,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "radium" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "def50a86306165861203e7f84ecffbbdfdea79f0e51039b33de1e952358c47ac" + [[package]] name = "rand" version = "0.7.3" diff --git a/Cargo.toml b/Cargo.toml index a2142a5..c04c8e2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "albatross" -version = "0.2.0" +version = "0.3.0" authors = ["Joey Hines "] edition = "2018" @@ -17,3 +17,4 @@ flate2 = "1.0.14" tar = "0.4.28" reqwest = { version = "0.10", features = ["blocking", "json"] } discord-hooks-rs = { git = "https://github.com/joeyahines/discord-hooks-rs" } +anvil-region = "0.4.0" diff --git a/src/backup.rs b/src/backup.rs index c7e8591..d09bca8 100644 --- a/src/backup.rs +++ b/src/backup.rs @@ -198,6 +198,16 @@ pub fn compress_backup(tmp_dir: &PathBuf, output_file: &PathBuf) -> Result<(), s Ok(()) } +pub fn uncompress_backup(backup: &PathBuf) -> Result { + 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 @@ -209,11 +219,8 @@ pub fn convert_backup_to_sp( backup: &PathBuf, output: &PathBuf, ) -> Result<(), std::io::Error> { - 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)?; + + let extract_path = uncompress_backup(backup)?; if let Some(worlds) = &config.world_config { for world in worlds { diff --git a/src/main.rs b/src/main.rs index a9dc8b3..b2893dc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,9 +5,11 @@ mod backup; mod config; mod discord; mod region; +mod restore; use crate::backup::{convert_backup_to_sp, do_backup}; use crate::config::AlbatrossConfig; +use crate::restore::{restore_range_from_backup, restore_chunk_from_backup}; #[derive(Debug, StructOpt)] #[structopt(about = "Backup your Minecraft Server!")] @@ -24,13 +26,13 @@ struct Albatross { enum SubCommand { /// Backup a server Backup { - /// Output location override + /// Output path #[structopt(short = "o", long = "ouptut", parse(from_os_str))] output: Option, }, /// Export a backup as a single player world Export { - /// Convert backup to singleplayer world + /// Backup to convert #[structopt(parse(from_os_str))] input_backup: PathBuf, @@ -38,6 +40,19 @@ enum SubCommand { #[structopt(parse(from_os_str))] output: PathBuf, }, + /// Restore certain chunks from a backup + Restore { + /// Output server directory + #[structopt(short, long, parse(from_os_str))] + server_directory: Option, + /// World to restore + world_name: String, + /// Backup to restore from + #[structopt(parse(from_os_str))] + backup_path: PathBuf, + /// Backup range can be a single chunk coordinate pair or a chunk range + chunk_range: Vec, + }, } fn main() { @@ -65,6 +80,38 @@ fn main() { Err(e) => println!("Error exporting backup: {:?}", e), }; } + SubCommand::Restore { + server_directory, + world_name, + backup_path, + chunk_range + } => { + println!("Starting restore"); + + let server_directory = match server_directory { + Some(dir) => dir, + None => cfg.backup.minecraft_dir + }; + + if chunk_range.len() > 2 { + let lower_x = chunk_range[0]; + let lower_z = chunk_range[1]; + let upper_x = chunk_range[2]; + let upper_z = chunk_range[3]; + match restore_range_from_backup(world_name.as_str(), lower_x, upper_x, lower_z, upper_z, &backup_path, &server_directory) { + Ok(count) => println!("Restored {} chunks!", count), + Err(e) => println!("Error restoring backup: {:?}", e), + }; + } + else if chunk_range.len() == 2 { + let x = chunk_range[0]; + let z = chunk_range[1]; + match restore_chunk_from_backup(world_name.as_str(), x, z, &backup_path, &server_directory) { + Ok(_) => println!("Restored chunk!"), + Err(e) => println!("Error restoring backup: {:?}", e), + }; + } + } } } else { println!("No worlds specified in config file!") diff --git a/src/restore.rs b/src/restore.rs new file mode 100644 index 0000000..73ee8c0 --- /dev/null +++ b/src/restore.rs @@ -0,0 +1,58 @@ +use anvil_region::AnvilChunkProvider; +use std::path::PathBuf; +use crate::backup::uncompress_backup; +use std::error; +use std::fs::remove_dir_all; + +struct ChunkAccess { + src_path: PathBuf, + dest_path: PathBuf, +} + +impl ChunkAccess { + pub fn new(world_name: &str, src_path: &PathBuf, dest_path: &PathBuf) -> Result { + let src_path = uncompress_backup(src_path)?.join(world_name).join("region"); + let dest_path= dest_path.join("region"); + + Ok(ChunkAccess { + src_path, + dest_path, + }) + } + + pub fn copy_chunk(&self, x:i32, z:i32) { + let src_provider = AnvilChunkProvider::new(self.src_path.to_str().unwrap()); + let dest_provider = AnvilChunkProvider::new(self.dest_path.to_str().unwrap()); + + let chunk = src_provider.load_chunk(x, z).expect("Unable to load chunk"); + dest_provider.save_chunk(x, z, chunk).expect("Unable to save chunk"); + } + + pub fn cleanup(self) -> Result<(), std::io::Error> { + remove_dir_all("tmp") + } + +} + +pub fn restore_range_from_backup(world_name: &str, lower_x: i32, upper_x: i32, lower_z: i32, upper_z: i32, backup_path: &PathBuf, minecraft_dir: &PathBuf) -> Result> { + let chunk_access = ChunkAccess::new(world_name, backup_path, minecraft_dir)?; + let mut count = 0; + + for x in lower_x..upper_x { + for z in lower_z..upper_z { + chunk_access.copy_chunk(x, z); + count += 1; + } + } + + chunk_access.cleanup()?; + Ok(count) +} + +pub fn restore_chunk_from_backup(world_name: &str, x: i32, z: i32, backup_path: &PathBuf, minecraft_dir: &PathBuf) -> Result<(), Box> { + let chunk_access = ChunkAccess::new(world_name, backup_path, minecraft_dir)?; + chunk_access.copy_chunk(x, z); + + chunk_access.cleanup()?; + Ok(()) +} From 5a403b4b648819cc4faa19bd05d2e7d1b8f33d75 Mon Sep 17 00:00:00 2001 From: Joey Hines Date: Tue, 3 Nov 2020 21:54:15 -0600 Subject: [PATCH 2/3] Updated readme --- README.md | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index e3fc79b..640a98e 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ webhooks. Backups are compressed and stored as `tar.gz` archives. ## Help ``` -albatross 0.2.0 +albatross 0.3.0 Backup your Minecraft Server! USAGE: @@ -20,14 +20,25 @@ OPTIONS: -c, --config-path Path to the Albatross config [env: ALBATROSS_CONFIG=] SUBCOMMANDS: - backup Backup a server - export Export a backup as a single player world - help Prints this message or the help of the given subcommand(s) - -Process finished with exit code 1 - + backup Backup a server + export Export a backup as a single player world + help Prints this message or the help of the given subcommand(s) + restore Restore certain chunks from a backup ``` +## Examples +Running a backup: + +`albatorss -c test.toml backup` + +Exporting a backup to a single player world: + +`albatorss -c test.toml export backups/04-11-20_01.51.27_backup.tar.gz sp.tar.gz` + +Restoring a range of chunks (from -2,-2 to 2,2): + +`albatorss -c test.toml restore world backups/04-11-20_01.51.27_backup.tar.gz sp.tar.gz` -2 -2 2 2 + ## Config ```toml [backup] From a7e2b260bc7498fe9b44caa36182a3548e2808d6 Mon Sep 17 00:00:00 2001 From: Joey Hines Date: Thu, 5 Nov 2020 20:35:34 -0600 Subject: [PATCH 3/3] Fixed chunk range for restore + A single chunk can now also be restored + clippy+fmt --- README.md | 9 +++++- src/backup.rs | 1 - src/chunk_coordinate.rs | 62 +++++++++++++++++++++++++++++++++++++++++ src/main.rs | 42 +++++++++++++++++----------- src/restore.rs | 59 ++++++++++++++++++++++++++++----------- 5 files changed, 138 insertions(+), 35 deletions(-) create mode 100644 src/chunk_coordinate.rs diff --git a/README.md b/README.md index 640a98e..43c924f 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,9 @@ SUBCOMMANDS: export Export a backup as a single player world help Prints this message or the help of the given subcommand(s) restore Restore certain chunks from a backup + +Process finished with exit code 1 + ``` ## Examples @@ -35,9 +38,13 @@ Exporting a backup to a single player world: `albatorss -c test.toml export backups/04-11-20_01.51.27_backup.tar.gz sp.tar.gz` +Restoring a single chunk (from -2,-2 to 2,2): + +`albatorss -c test.toml restore world backups/04-11-20_01.51.27_backup.tar.gz sp.tar.gz` (0,0) + Restoring a range of chunks (from -2,-2 to 2,2): -`albatorss -c test.toml restore world backups/04-11-20_01.51.27_backup.tar.gz sp.tar.gz` -2 -2 2 2 +`albatorss -c test.toml restore world backups/04-11-20_01.51.27_backup.tar.gz sp.tar.gz` (-2,-2) -u (2,2) ## Config ```toml diff --git a/src/backup.rs b/src/backup.rs index d09bca8..1bc4a46 100644 --- a/src/backup.rs +++ b/src/backup.rs @@ -219,7 +219,6 @@ pub fn convert_backup_to_sp( backup: &PathBuf, output: &PathBuf, ) -> Result<(), std::io::Error> { - let extract_path = uncompress_backup(backup)?; if let Some(worlds) = &config.world_config { diff --git a/src/chunk_coordinate.rs b/src/chunk_coordinate.rs new file mode 100644 index 0000000..a5ca72c --- /dev/null +++ b/src/chunk_coordinate.rs @@ -0,0 +1,62 @@ +use regex::Regex; +use std::error::Error; +use std::fmt; +use std::num::ParseIntError; +use std::str::FromStr; + +/// Chunk error +#[derive(Debug)] +pub enum ChunkCoordinateErr { + /// Error parsing integer + ParseIntError(ParseIntError), + /// Regex error + RegexError(regex::Error), + /// Invalid chunk coordinate given + InvalidChunk, +} + +impl From for ChunkCoordinateErr { + fn from(e: ParseIntError) -> Self { + Self::ParseIntError(e) + } +} + +impl From for ChunkCoordinateErr { + fn from(e: regex::Error) -> Self { + Self::RegexError(e) + } +} + +impl fmt::Display for ChunkCoordinateErr { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Unable to parse chunk range: {:?}", self) + } +} + +impl Error for ChunkCoordinateErr {} + +/// Chunk Coordinate paiir +#[derive(Debug)] +pub struct ChunkCoordinate { + /// X Coordinate + pub x: i32, + /// Z Coordinate + pub z: i32, +} + +impl FromStr for ChunkCoordinate { + type Err = ChunkCoordinateErr; + + fn from_str(s: &str) -> Result { + let re = Regex::new(r"\((?P-?[0-9]*),(?P-?[0-9]*)\)").unwrap(); + + if let Some(cap) = re.captures(s) { + let x = cap["x"].parse::()?; + let z = cap["z"].parse::()?; + + Ok(Self { x, z }) + } else { + Err(ChunkCoordinateErr::InvalidChunk) + } + } +} diff --git a/src/main.rs b/src/main.rs index b2893dc..c3d5a0e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,14 +2,16 @@ use std::path::PathBuf; use structopt::StructOpt; mod backup; +mod chunk_coordinate; mod config; mod discord; mod region; mod restore; use crate::backup::{convert_backup_to_sp, do_backup}; +use crate::chunk_coordinate::ChunkCoordinate; use crate::config::AlbatrossConfig; -use crate::restore::{restore_range_from_backup, restore_chunk_from_backup}; +use crate::restore::{restore_chunk_from_backup, restore_range_from_backup}; #[derive(Debug, StructOpt)] #[structopt(about = "Backup your Minecraft Server!")] @@ -50,8 +52,11 @@ enum SubCommand { /// Backup to restore from #[structopt(parse(from_os_str))] backup_path: PathBuf, - /// Backup range can be a single chunk coordinate pair or a chunk range - chunk_range: Vec, + /// Chunk to backup + chunk: ChunkCoordinate, + /// Upper chunk range bound + #[structopt(short, long)] + upper_bound: Option, }, } @@ -84,29 +89,34 @@ fn main() { server_directory, world_name, backup_path, - chunk_range + chunk, + upper_bound, } => { println!("Starting restore"); let server_directory = match server_directory { Some(dir) => dir, - None => cfg.backup.minecraft_dir + None => cfg.backup.minecraft_dir, }; - if chunk_range.len() > 2 { - let lower_x = chunk_range[0]; - let lower_z = chunk_range[1]; - let upper_x = chunk_range[2]; - let upper_z = chunk_range[3]; - match restore_range_from_backup(world_name.as_str(), lower_x, upper_x, lower_z, upper_z, &backup_path, &server_directory) { + if let Some(upper_bound) = upper_bound { + match restore_range_from_backup( + world_name.as_str(), + chunk, + upper_bound, + &backup_path, + &server_directory, + ) { Ok(count) => println!("Restored {} chunks!", count), Err(e) => println!("Error restoring backup: {:?}", e), }; - } - else if chunk_range.len() == 2 { - let x = chunk_range[0]; - let z = chunk_range[1]; - match restore_chunk_from_backup(world_name.as_str(), x, z, &backup_path, &server_directory) { + } else { + match restore_chunk_from_backup( + world_name.as_str(), + chunk, + &backup_path, + &server_directory, + ) { Ok(_) => println!("Restored chunk!"), Err(e) => println!("Error restoring backup: {:?}", e), }; diff --git a/src/restore.rs b/src/restore.rs index 73ee8c0..28d4581 100644 --- a/src/restore.rs +++ b/src/restore.rs @@ -1,45 +1,64 @@ -use anvil_region::AnvilChunkProvider; -use std::path::PathBuf; use crate::backup::uncompress_backup; +use crate::chunk_coordinate::ChunkCoordinate; +use anvil_region::AnvilChunkProvider; use std::error; use std::fs::remove_dir_all; +use std::path::PathBuf; -struct ChunkAccess { +/// Struct for manipulating a world from a backup +struct RestoreAccess { + /// Chunk source src_path: PathBuf, + /// Chunk destination dest_path: PathBuf, } -impl ChunkAccess { - pub fn new(world_name: &str, src_path: &PathBuf, dest_path: &PathBuf) -> Result { +impl RestoreAccess { + /// Create new RestoreAccess + pub fn new( + world_name: &str, + src_path: &PathBuf, + dest_path: &PathBuf, + ) -> Result { let src_path = uncompress_backup(src_path)?.join(world_name).join("region"); - let dest_path= dest_path.join("region"); + let dest_path = dest_path.join(world_name).join("region"); - Ok(ChunkAccess { + Ok(RestoreAccess { src_path, dest_path, }) } - pub fn copy_chunk(&self, x:i32, z:i32) { + /// Copy chunk from source to desination + pub fn copy_chunk(&self, x: i32, z: i32) { let src_provider = AnvilChunkProvider::new(self.src_path.to_str().unwrap()); let dest_provider = AnvilChunkProvider::new(self.dest_path.to_str().unwrap()); let chunk = src_provider.load_chunk(x, z).expect("Unable to load chunk"); - dest_provider.save_chunk(x, z, chunk).expect("Unable to save chunk"); + dest_provider + .save_chunk(x, z, chunk) + .expect("Unable to save chunk"); } + /// Cleanup process pub fn cleanup(self) -> Result<(), std::io::Error> { remove_dir_all("tmp") } - } -pub fn restore_range_from_backup(world_name: &str, lower_x: i32, upper_x: i32, lower_z: i32, upper_z: i32, backup_path: &PathBuf, minecraft_dir: &PathBuf) -> Result> { - let chunk_access = ChunkAccess::new(world_name, backup_path, minecraft_dir)?; +/// Restore a range of chunks from a backup +pub fn restore_range_from_backup( + world_name: &str, + lower: ChunkCoordinate, + upper: ChunkCoordinate, + backup_path: &PathBuf, + minecraft_dir: &PathBuf, +) -> Result> { + let chunk_access = RestoreAccess::new(world_name, backup_path, minecraft_dir)?; let mut count = 0; - for x in lower_x..upper_x { - for z in lower_z..upper_z { + for x in lower.x..=upper.x { + for z in lower.z..=upper.z { chunk_access.copy_chunk(x, z); count += 1; } @@ -49,9 +68,15 @@ pub fn restore_range_from_backup(world_name: &str, lower_x: i32, upper_x: i32, l Ok(count) } -pub fn restore_chunk_from_backup(world_name: &str, x: i32, z: i32, backup_path: &PathBuf, minecraft_dir: &PathBuf) -> Result<(), Box> { - let chunk_access = ChunkAccess::new(world_name, backup_path, minecraft_dir)?; - chunk_access.copy_chunk(x, z); +/// Restore a single chunk from a backup +pub fn restore_chunk_from_backup( + world_name: &str, + chunk: ChunkCoordinate, + backup_path: &PathBuf, + minecraft_dir: &PathBuf, +) -> Result<(), Box> { + let chunk_access = RestoreAccess::new(world_name, backup_path, minecraft_dir)?; + chunk_access.copy_chunk(chunk.x, chunk.z); chunk_access.cleanup()?; Ok(())