Added 26.1 world format

This commit is contained in:
Joey Hines 2026-04-12 11:39:45 -06:00
parent 38630a4d57
commit a22c781c52
No known key found for this signature in database
GPG Key ID: E99D8FB14855100E
7 changed files with 732 additions and 246 deletions

661
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
[package]
name = "albatross"
version = "0.6.1"
version = "0.7.0"
authors = ["Joey Hines <joey@ahines.net>"]
edition = "2024"
@ -15,7 +15,7 @@ chrono = "0.4"
regex = "1.3.9"
flate2 = "1.0.14"
tar = "0.4.28"
reqwest = { version = "0.12.22", features = ["blocking", "json"] }
reqwest = { version = "0.13.2", features = ["blocking", "json"] }
discord-hooks-rs = { git = "https://github.com/joeyahines/discord-hooks-rs" }
anvil-region = "0.8.1"
ssh2 = "0.9.1"

View File

@ -8,7 +8,7 @@ Backups can also be transferred to a remote server using SFTP.
## Help
```
albatross 0.4.0
albatross 0.7.0
Backup your Minecraft Server!
USAGE:
@ -25,10 +25,7 @@ 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)
restore Restore certain chunks from a backup
Process finished with exit code 1
restore Restore certain chunks from a backup```
```
## Examples
@ -49,6 +46,44 @@ 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) -u (2,2)
## Config
### For Minecraft Versions After 26.1
```toml
[backup]
# Minecraft sever directory
minecraft_dir = "/home/mc/server"
# Optional Discord webhook
discord_webhook = "https://discordapp.com/api/webhooks/"
# Number of backups to keep
backups_to_keep = 10
[backup.output_config]
# Directory to place backups
path = "./backups"
[world_26_plus_config]
world_name = "world"
[[world_26_plus_config.dimensions]]
# world name
world_name = "minecraft/overworld"
# world save radius (in blocks)
save_radius = 1000
[[world_26_plus_config.dimensions]]
# world name
world_name = "minecraft/the_end"
# world save radius (in blocks)
save_radius = 1000
[[world_26_plus_config.dimensions]]
# world name
world_name = "minecraft/the_nether"
# world save radius (in blocks)
save_radius = 1000
```
### For Minecraft Versions Before 26.1
```toml
# Local Backup Config
[backup]

View File

@ -1,6 +1,8 @@
use crate::config::{AlbatrossConfig, RemoteBackupConfig, WorldConfig, WorldType};
use crate::config::{
AlbatrossConfig, RemoteBackupConfig, World26PlusConfig, WorldConfig, WorldType,
};
use crate::discord::send_webhook;
use crate::error::Result;
use crate::error::{AlbatrossError, Result};
use crate::region::Region;
use crate::remote::RemoteBackupSite;
use crate::remote::file::FileBackup;
@ -43,23 +45,36 @@ pub fn backup_file(file_name: &str, world_path: &Path, backup_path: &Path) -> Re
pub fn backup_dir(dir_name: &str, world_path: &Path, backup_path: &Path) -> Result<u64> {
let src_dir = world_path.join(dir_name);
if !src_dir.exists() {
if !src_dir.exists() || !src_dir.is_dir() {
warn!("Directory '{dir_name}' does not exist in '{world_path:?}'");
return Ok(0);
}
let backup_dir = backup_path.join(dir_name);
create_dir(&backup_dir)?;
let backup_dir_path = backup_path.join(dir_name);
create_dir(&backup_dir_path)?;
let mut file_count = 0;
for entry in src_dir.read_dir()? {
let entry = entry?;
let mut target = backup_dir.clone();
let mut target = backup_dir_path.clone();
if entry.path().is_dir() {
let sub_dir = entry.file_name();
let world_path = world_path.join(dir_name);
let backup_path = backup_path.join(dir_name);
if !backup_path.exists() {
create_dir(&backup_path)?;
}
file_count += backup_dir(sub_dir.to_str().unwrap(), &world_path, &backup_path)?;
} else {
target.push(entry.file_name());
copy(entry.path(), target)?;
file_count += 1;
}
}
Ok(file_count)
}
@ -94,8 +109,10 @@ pub fn backup_region(
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 {
if let Ok(region) = Region::try_from(file_name)
&& region.x.abs() <= save_radius
&& region.y.abs() <= save_radius
{
let mut target = backup_dir.clone();
target.push(entry.file_name());
@ -103,7 +120,6 @@ pub fn backup_region(
count += 1;
}
}
}
Ok(count)
}
@ -270,7 +286,7 @@ pub fn do_remote_backup(
/// * `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 timer = Instant::now();
let time_str = Utc::now().format("%d-%m-%y_%H.%M.%S").to_string();
let backup_name = format!("{time_str}_backup.tar.gz");
let mut output_archive = match output {
@ -284,15 +300,21 @@ pub fn do_backup(cfg: AlbatrossConfig, output: Option<PathBuf>) -> Result<()> {
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, &tmp_dir).map_err(|e| {
send_webhook("Failed to copy worlds to backup location", &cfg);
error!("Failed to copy worlds: {e}");
e
})?;
let backup_res = if let Some(worlds) = &cfg.world_config {
backup_worlds(&cfg, server_base_dir, worlds, &tmp_dir)
} else if let Some(world_config) = &cfg.world_26_plus_config {
backup_v26_plus_world_format(&cfg, world_config, &tmp_dir)
} else {
Err(AlbatrossError::MissingConfig)
};
if let Err(err) = backup_res {
send_webhook("Failed to backup worlds", &cfg);
error!("Failed to backup worlds: {err}");
return Err(err);
}
compress_backup(&tmp_dir, &output_archive).map_err(|e| {
send_webhook("Failed to compress backup", &cfg);
@ -302,8 +324,7 @@ pub fn do_backup(cfg: AlbatrossConfig, output: Option<PathBuf>) -> Result<()> {
remove_dir_all(&tmp_dir)?;
let mut local_backup =
FileBackup::new(&cfg.backup.output_config, cfg.backup.backups_to_keep).unwrap();
let mut local_backup = FileBackup::new(&cfg.backup.output_config, cfg.backup.backups_to_keep)?;
match local_backup.cleanup() {
Ok(backups_removed) => {
@ -342,10 +363,81 @@ pub fn do_backup(cfg: AlbatrossConfig, output: Option<PathBuf>) -> Result<()> {
Ok(())
}
/// Backup a dimension
///
/// # Param
/// * `world_path` - path to the world folder
/// * `backup_path` - path to the backup folder
/// * `world_config` - world config options
pub fn backup_dimension(
world_path: &Path,
backup_path: &Path,
world_config: &WorldConfig,
) -> Result<u64> {
let dimension_path = Path::new("dimensions").join(&world_config.world_name);
let backup_path = backup_path.join(&dimension_path);
let src_path = world_path.join(dimension_path);
create_dir_all(backup_path.as_path())?;
backup_dir("data", &src_path, &backup_path)?;
let region_count = backup_region("region", world_config.save_radius, &src_path, &backup_path)?;
Ok(region_count)
}
/// Backup v26.1 + version
///
/// # Param
/// * `world_path` - path to the world folder
/// * `backup_path` - path to the backup folder
/// * `world_config` - world config options
pub fn backup_v26_plus_world_format(
cfg: &AlbatrossConfig,
world_config: &World26PlusConfig,
tmp_dir: &Path,
) -> Result<()> {
let world_path = cfg.backup.minecraft_dir.join(&world_config.world_name);
let backup_path = tmp_dir.join(&world_config.world_name);
if !backup_path.exists() {
create_dir(&backup_path)?;
}
send_webhook(
format!("Starting backup **{}**", world_config.world_name).as_str(),
cfg,
);
// Backup common files
backup_dir("data", &world_path, &backup_path)?;
backup_dir("datapacks", &world_path, &backup_path)?;
backup_file("level.dat", &world_path, &backup_path)?;
backup_file("level.dat_old", &world_path, &backup_path).ok();
backup_file("session.lock", &world_path, &backup_path).ok();
let player_count = backup_dir("players", &world_path, &backup_path)?;
send_webhook(format!("Backed up {player_count} players").as_str(), cfg);
info!("Backed up {player_count} players");
for world_config in &world_config.dimensions {
send_webhook(
format!("Starting backup of **{}**", world_config.world_name).as_str(),
cfg,
);
info!(
"Starting backup of dimension **{}**",
world_config.world_name
);
let region_count = backup_dimension(&world_path, &backup_path, world_config)?;
send_webhook(format!("{region_count} regions backed up.").as_str(), cfg);
info!("{region_count} regions backed up.")
}
Ok(())
}
fn backup_worlds(
cfg: &AlbatrossConfig,
server_base_dir: PathBuf,
worlds: Vec<WorldConfig>,
worlds: &[WorldConfig],
tmp_dir: &Path,
) -> Result<()> {
for world in worlds {
@ -360,15 +452,15 @@ fn backup_worlds(
let webhook_msg = match world_type {
WorldType::Overworld => {
let (region_count, player_count) =
backup_overworld(&world_dir.clone(), tmp_dir, &world)?;
backup_overworld(&world_dir.clone(), tmp_dir, world)?;
format!("{region_count} regions and {player_count} player files backed up.")
}
WorldType::Nether => {
let region_count = backup_nether(&world_dir, tmp_dir, &world)?;
let region_count = backup_nether(&world_dir, tmp_dir, world)?;
format!("{region_count} regions backed up.")
}
WorldType::End => {
let region_count = backup_end(&world_dir, tmp_dir, &world)?;
let region_count = backup_end(&world_dir, tmp_dir, world)?;
format!("{region_count} regions backed up.")
}
};

View File

@ -64,11 +64,19 @@ pub struct RemoteBackupConfig {
pub file: Option<FileConfig>,
}
/// Config for individual world configuration
#[derive(Debug, Deserialize, Clone)]
pub struct World26PlusConfig {
pub world_name: String,
pub dimensions: Vec<WorldConfig>,
}
/// Configs
#[derive(Debug, Deserialize, Clone)]
pub struct AlbatrossConfig {
pub backup: BackupConfig,
pub world_config: Option<Vec<WorldConfig>>,
pub world_26_plus_config: Option<World26PlusConfig>,
pub remote: Option<RemoteBackupConfig>,
}

View File

@ -11,6 +11,7 @@ pub enum AlbatrossError {
ChronoParseError(chrono::ParseError),
NoSSHAuth,
FTPError(ftp::FtpError),
MissingConfig,
}
impl std::error::Error for AlbatrossError {}
@ -27,6 +28,7 @@ impl std::fmt::Display for AlbatrossError {
AlbatrossError::ChronoParseError(e) => write!(f, "Unable to parse time: {e}"),
AlbatrossError::NoSSHAuth => write!(f, "No SSH auth methods provided in the config"),
AlbatrossError::FTPError(e) => write!(f, "FTP error: {e}"),
AlbatrossError::MissingConfig => write!(f, "Missing world config"),
}
}
}

View File

@ -74,7 +74,6 @@ fn main() {
let cfg = AlbatrossConfig::new(opt.config_path.into_os_string().to_str().unwrap())
.expect("Config error");
if cfg.world_config.is_some() {
match opt.sub_command {
SubCommand::Backup { output } => {
info!("Starting backup");
@ -131,7 +130,4 @@ fn main() {
}
}
}
} else {
info!("No worlds specified in config file!")
}
}