Added export subcommand

+ Takes a backup and exports it as a playable single player world
+ Changed file name format
+ Switched to using StructOpt for the argument parsing
+ Updated README.md
backup_error_fix
Joey Hines 2020-10-24 13:41:11 -05:00
parent b54ac0efef
commit b408ed4d2a
5 changed files with 331 additions and 161 deletions

65
Cargo.lock generated
View File

@ -35,7 +35,6 @@ name = "albatross"
version = "0.2.0" version = "0.2.0"
dependencies = [ dependencies = [
"chrono", "chrono",
"clap",
"config", "config",
"discord-hooks-rs", "discord-hooks-rs",
"flate2", "flate2",
@ -43,6 +42,7 @@ dependencies = [
"regex", "regex",
"reqwest", "reqwest",
"serde 1.0.117", "serde 1.0.117",
"structopt",
"tar", "tar",
] ]
@ -390,6 +390,15 @@ dependencies = [
"tokio-util", "tokio-util",
] ]
[[package]]
name = "heck"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205"
dependencies = [
"unicode-segmentation",
]
[[package]] [[package]]
name = "hermit-abi" name = "hermit-abi"
version = "0.1.13" version = "0.1.13"
@ -810,6 +819,30 @@ version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "237a5ed80e274dbc66f86bd59c1e25edc039660be53194b5fe0a482e0f2612ea" checksum = "237a5ed80e274dbc66f86bd59c1e25edc039660be53194b5fe0a482e0f2612ea"
[[package]]
name = "proc-macro-error"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
dependencies = [
"proc-macro-error-attr",
"proc-macro2",
"quote",
"syn",
"version_check 0.9.2",
]
[[package]]
name = "proc-macro-error-attr"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
dependencies = [
"proc-macro2",
"quote",
"version_check 0.9.2",
]
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.24" version = "1.0.24"
@ -1089,6 +1122,30 @@ version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
[[package]]
name = "structopt"
version = "0.3.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "126d630294ec449fae0b16f964e35bf3c74f940da9dca17ee9b905f7b3112eb8"
dependencies = [
"clap",
"lazy_static 1.4.0",
"structopt-derive",
]
[[package]]
name = "structopt-derive"
version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65e51c492f9e23a220534971ff5afc14037289de430e3c83f9daf6a1b6ae91e8"
dependencies = [
"heck",
"proc-macro-error",
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "syn" name = "syn"
version = "1.0.46" version = "1.0.46"
@ -1256,6 +1313,12 @@ dependencies = [
"smallvec", "smallvec",
] ]
[[package]]
name = "unicode-segmentation"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e83e153d1053cbb5a118eeff7fd5be06ed99153f00dbcd8ae310c5fb2b22edc0"
[[package]] [[package]]
name = "unicode-width" name = "unicode-width"
version = "0.1.7" version = "0.1.7"

View File

@ -7,7 +7,7 @@ edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
clap = "2.33.0" structopt = "0.3.20"
serde = { version="1.0.116", features=["derive"] } serde = { version="1.0.116", features=["derive"] }
config = "0.9" config = "0.9"
log = "0.4.8" log = "0.4.8"

View File

@ -4,6 +4,30 @@ 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 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. webhooks. Backups are compressed and stored as `tar.gz` archives.
## Help
```
albatross 0.2.0
Backup your Minecraft Server!
USAGE:
albatross --config-path <config-path> <SUBCOMMAND>
FLAGS:
-h, --help Prints help information
-V, --version Prints version information
OPTIONS:
-c, --config-path <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
```
## Config ## Config
```toml ```toml
[backup] [backup]

View File

@ -1,10 +1,18 @@
use crate::config::WorldConfig; use crate::backup;
use crate::config::{AlbatrossConfig, WorldConfig, WorldType};
use crate::discord::send_webhook;
use crate::region::Region; use crate::region::Region;
use chrono::{NaiveDateTime, Utc};
use flate2::read::GzDecoder;
use flate2::write::GzEncoder; use flate2::write::GzEncoder;
use flate2::Compression; use flate2::Compression;
use std::convert::TryFrom; use std::convert::TryFrom;
use std::fs::{copy, create_dir, File}; use std::fs::{
copy, create_dir, create_dir_all, remove_dir_all, remove_file, rename, DirEntry, File,
};
use std::path::PathBuf; use std::path::PathBuf;
use std::time::Instant;
use tar::Archive;
/// Backup a file /// Backup a file
/// ///
@ -189,3 +197,184 @@ pub fn compress_backup(tmp_dir: &PathBuf, output_file: &PathBuf) -> Result<(), s
tar_builder.append_dir_all(".", tmp_dir)?; tar_builder.append_dir_all(".", tmp_dir)?;
Ok(()) Ok(())
} }
/// 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<(), 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)?;
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(())
}
/// 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<Option<NaiveDateTime>, 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<usize, std::io::Error> {
let mut backups = vec![];
let mut num_of_removed_backups: usize = 0;
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)
});
num_of_removed_backups = backups.len() - keep as usize;
for _i in 0..num_of_removed_backups {
let oldest = backups.pop().unwrap();
remove_file(oldest.path())?;
}
}
Ok(num_of_removed_backups)
}
/// Backup the configured worlds from a minecraft server
///
/// # Params
/// * `cfg` - config file
pub fn do_backup(cfg: AlbatrossConfig, output: Option<PathBuf>) -> Result<(), std::io::Error> {
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()).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();
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());
}
}
backup::compress_backup(&tmp_dir, &output_archive)?;
remove_dir_all(&tmp_dir)?;
let backups_removed = remove_old_backups(&cfg.backup.output_dir, cfg.backup.backups_to_keep)?;
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);
}
let secs = timer.elapsed().as_secs();
send_webhook(
format!("**Full backup completed in {}s**! *SKREEEEEEEEEE*", secs).as_str(),
&cfg,
);
Ok(())
}

View File

@ -1,178 +1,72 @@
use chrono::{NaiveDateTime, Utc};
use clap::{App, Arg};
use std::fs::{create_dir_all, remove_dir_all, remove_file, DirEntry};
use std::path::PathBuf; use std::path::PathBuf;
use std::time::Instant; use structopt::StructOpt;
mod backup; mod backup;
mod config; mod config;
mod discord; mod discord;
mod region; mod region;
use crate::config::{AlbatrossConfig, WorldType}; use crate::backup::{convert_backup_to_sp, do_backup};
use backup::{backup_end, backup_nether, backup_overworld}; use crate::config::AlbatrossConfig;
use discord::send_webhook;
/// Get the time of the backup from a file name #[derive(Debug, StructOpt)]
/// #[structopt(about = "Backup your Minecraft Server!")]
/// # Param struct Albatross {
/// * `archive_entry`: archive entry /// Path to the Albatross config
fn get_time_from_file_name( #[structopt(short, long, env = "ALBATROSS_CONFIG", parse(from_os_str))]
archive_entry: &DirEntry, config_path: PathBuf,
) -> Result<Option<NaiveDateTime>, std::io::Error> { /// Subcommand
let file_name = archive_entry.file_name().to_str().unwrap().to_string(); #[structopt(subcommand)]
let name: Vec<&str> = file_name.split('_').collect(); sub_command: SubCommand,
Ok(chrono::NaiveDateTime::parse_from_str(name[0], "%d-%m-%y-%H.%M.%S").ok())
} }
/// Removes the old backups from the ouput directory #[derive(Debug, StructOpt)]
/// enum SubCommand {
/// # Params /// Backup a server
/// * `output_dir` - output directory containing Backup {
/// * `keep` - number of backups to keep /// Output location override
fn remove_old_backups(output_dir: &PathBuf, keep: u64) -> Result<usize, std::io::Error> { #[structopt(short = "o", long = "ouptut", parse(from_os_str))]
let mut backups = vec![]; output: Option<PathBuf>,
let mut num_of_removed_backups: usize = 0; },
/// Export a backup as a single player world
Export {
/// Convert backup to singleplayer world
#[structopt(parse(from_os_str))]
input_backup: PathBuf,
for entry in output_dir.read_dir()? { /// Output location override
let entry = entry?; #[structopt(parse(from_os_str))]
output: PathBuf,
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)
});
num_of_removed_backups = backups.len() - keep as usize;
for _i in 0..num_of_removed_backups {
let oldest = backups.pop().unwrap();
remove_file(oldest.path())?;
}
}
Ok(num_of_removed_backups)
} }
/// 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.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();
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()).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();
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());
}
}
backup::compress_backup(&tmp_dir, &output_archive)?;
remove_dir_all(&tmp_dir)?;
let backups_removed = remove_old_backups(&cfg.backup.output_dir, cfg.backup.backups_to_keep)?;
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);
}
let secs = timer.elapsed().as_secs();
send_webhook(
format!("**Full backup completed in {}s**! *SKREEEEEEEEEE*", secs).as_str(),
&cfg,
);
Ok(())
}
/// Albatross
///
/// Run backups of a Minecraft world
fn main() { fn main() {
let mut app = App::new("Albatross").about("Backup your worlds").arg( let opt = Albatross::from_args();
Arg::with_name("config")
.index(1)
.short("c")
.long("config")
.value_name("CONFIG_PATH")
.help("Config file path"),
);
// Get arg parser
let matches = app.clone().get_matches();
if let Some(cfg_path) = matches.value_of("config") { let cfg = AlbatrossConfig::new(opt.config_path.into_os_string().to_str().unwrap())
let cfg = AlbatrossConfig::new(cfg_path).expect("Config not found"); .expect("Config not found");
if cfg.world_config.is_some() { if cfg.world_config.is_some() {
match opt.sub_command {
SubCommand::Backup { output } => {
println!("Starting backup"); println!("Starting backup");
match do_backup(cfg) { match do_backup(cfg, output) {
Ok(_) => println!("Backup complete"), Ok(_) => println!("Backup complete!"),
Err(e) => println!("Error doing backup: {:?}", e), Err(e) => println!("Error doing backup: {:?}", e),
};
}
SubCommand::Export {
input_backup,
output,
} => {
println!("Starting export");
match convert_backup_to_sp(&cfg, &input_backup, &output) {
Ok(_) => println!("Export complete!"),
Err(e) => println!("Error exporting backup: {:?}", e),
};
}
} }
} else { } else {
println!("No worlds specified to backed up!") println!("No worlds specified in config file!")
}
} else {
app.print_help().expect("Unable to print help");
} }
} }