diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..52f99fb --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,2 @@ +[alias] +runs = "run -- paper 1.19.4-545 --config data/config --backup data/backups --world data/worlds data/paper-1.19.4-545.jar" diff --git a/src/main.rs b/src/main.rs index 658dc57..35a5b9d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,9 +1,10 @@ mod server; -use clap::{Parser, Subcommand}; -use server::{ServerCommand, ServerType}; +use clap::Parser; +use server::ServerType; use std::io; use std::path::PathBuf; +use std::sync::{Arc, Mutex}; #[derive(Parser)] #[command(author, version, about, long_about = None)] @@ -15,43 +16,61 @@ struct Cli { /// Server jar to execute jar: PathBuf, - /// Directory where configs are stored, and where the server will run; defaults to the current - /// directory. + /// Directory where configs are stored, and where the server will run [default: .] #[arg(long, value_name = "CONFIG_DIR")] config: Option, - /// Directory where world files will be saved; defaults to ../worlds + /// Directory where world files will be saved [default: ../worlds] #[arg(long, value_name = "WORLD_DIR")] world: Option, - /// Directory where backups will be stored; defaults to ../backups + /// Directory where backups will be stored [default: ../backups] #[arg(long, value_name = "BACKUP_DIR")] backup: Option, + /// Java command to run the server jar with + #[arg(long, value_name = "JAVA_CMD", default_value_t = String::from("java"))] + java: String, - /// XMS value for the server instance - #[arg(long)] - xms: Option, - /// XMX value for the server instance - #[arg(long)] - xmx: Option, + /// XMS value in megabytes for the server instance + #[arg(long, default_value_t = 1024)] + xms: u64, + /// XMX value in megabytes for the server instance + #[arg(long, default_value_t = 2048)] + xmx: u64, + + /// How many backups to keep + #[arg(short = 'n', long, default_value_t = 7)] + max_backups: u64, + /// How frequently to perform a backup, in minutes + #[arg(short = 't', long, default_value_t = 720)] + frequency: u64, +} + +fn backups_thread(counter: Arc>, frequency: u64) { + loop { + std::thread::sleep(std::time::Duration::from_secs(frequency * 60)); + + { + let mut server = counter.lock().unwrap(); + server.backup(); + } + } } fn main() { let cli = Cli::parse(); - let mut cmd = server::ServerCommand::new(cli.type_, &cli.server_version) + let cmd = server::ServerCommand::new(cli.type_, &cli.server_version) + .java(&cli.java) .jar(cli.jar) .config(cli.config.unwrap_or(".".into())) .world(cli.world.unwrap_or("../worlds".into())) - .backup(cli.backup.unwrap_or("../backups".into())); + .backup(cli.backup.unwrap_or("../backups".into())) + .xms(cli.xms) + .xmx(cli.xmx) + .max_backups(cli.max_backups); + let counter = Arc::new(Mutex::new(cmd.spawn().expect("Failed to start server."))); - if let Some(xms) = cli.xms { - cmd = cmd.xms(xms); - } - - if let Some(xmx) = cli.xmx { - cmd = cmd.xmx(xmx); - } - - let mut server = cmd.spawn().expect("Failed to start server."); + let clone = Arc::clone(&counter); + std::thread::spawn(move || backups_thread(clone, cli.frequency)); let stdin = io::stdin(); let input = &mut String::new(); @@ -59,10 +78,12 @@ fn main() { loop { input.clear(); stdin.read_line(input); - println!("input: {}", input.trim()); - if let Err(e) = server.send_command(input) { - println!("{}", e); - }; + { + let mut server = counter.lock().unwrap(); + if let Err(e) = server.send_command(input) { + println!("{}", e); + }; + } if input.trim() == "stop" { break; diff --git a/src/server/command.rs b/src/server/command.rs index 50075de..e774bb5 100644 --- a/src/server/command.rs +++ b/src/server/command.rs @@ -4,7 +4,7 @@ use std::fmt; use std::fs::File; use std::io::Write; use std::path::{Path, PathBuf}; -use std::process::{Child, Command, Stdio}; +use std::process::{Command, Stdio}; #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)] pub enum ServerType { @@ -33,8 +33,9 @@ pub struct ServerCommand { config_dir: PathBuf, world_dir: PathBuf, backup_dir: PathBuf, - xms: u32, - xmx: u32, + xms: u64, + xmx: u64, + max_backups: u64, } impl ServerCommand { @@ -49,6 +50,7 @@ impl ServerCommand { backup_dir: PathBuf::from("backups"), xms: 1024, xmx: 2048, + max_backups: 7, } } @@ -79,21 +81,26 @@ impl ServerCommand { self } - pub fn xms(mut self, v: u32) -> Self { + pub fn xms(mut self, v: u64) -> Self { self.xms = v; self } - pub fn xmx(mut self, v: u32) -> Self { + pub fn xmx(mut self, v: u64) -> Self { self.xmx = v; self } + pub fn max_backups(mut self, v: u64) -> Self { + self.max_backups = v; + self + } + fn accept_eula(&self) -> std::io::Result<()> { let mut eula_path = self.config_dir.clone(); eula_path.push("eula.txt"); let mut eula_file = File::create(eula_path)?; - eula_file.write(b"eula=true")?; + eula_file.write_all(b"eula=true")?; Ok(()) } @@ -123,6 +130,7 @@ impl ServerCommand { config_dir, world_dir, backup_dir, + self.max_backups, child, )) } diff --git a/src/server/process.rs b/src/server/process.rs index ffc8982..2b6f6fa 100644 --- a/src/server/process.rs +++ b/src/server/process.rs @@ -1,12 +1,15 @@ use crate::server::ServerType; use flate2::write::GzEncoder; use flate2::Compression; -use std::fs::File; -use std::io; use std::io::Write; -use std::path::{Path, PathBuf}; +use std::path::PathBuf; use std::process::Child; -use std::process::{Command, Stdio}; + +#[link(name = "c")] +extern "C" { + fn geteuid() -> u32; + fn getegid() -> u32; +} pub struct ServerProcess { type_: ServerType, @@ -14,6 +17,7 @@ pub struct ServerProcess { config_dir: PathBuf, world_dir: PathBuf, backup_dir: PathBuf, + max_backups: u64, child: Child, } @@ -24,6 +28,7 @@ impl ServerProcess { config_dir: PathBuf, world_dir: PathBuf, backup_dir: PathBuf, + max_backups: u64, child: Child, ) -> ServerProcess { ServerProcess { @@ -32,6 +37,7 @@ impl ServerProcess { config_dir, world_dir, backup_dir, + max_backups, child, } } @@ -61,17 +67,41 @@ impl ServerProcess { Ok(()) } + /// Perform a backup by disabling the server's save feature and flushing its data, before + /// creating an archive file. pub fn backup(&mut self) -> std::io::Result<()> { + self.custom("say backing up server")?; + // Make sure the server isn't modifying the files during the backup self.custom("save-off")?; self.custom("save-all")?; + // TODO implement a better mechanism + // We wait some time to (hopefully) ensure the save-all call has completed + std::thread::sleep(std::time::Duration::from_secs(10)); + + let res = self.create_backup_archive(); + + if res.is_ok() { + self.remove_old_backups()?; + } + + // The server's save feature needs to be enabled again even if the archive failed to create + self.custom("save-on")?; + + self.custom("say server backed up successfully")?; + + res + } + + /// Create a new compressed backup archive of the server's data. + fn create_backup_archive(&mut self) -> std::io::Result<()> { // Create a gzip-compressed tarball of the worlds folder let filename = format!( "{}", chrono::offset::Local::now().format("%Y-%m-%d_%H-%M-%S.tar.gz") ); - let path = self.backup_dir.join(&filename); + let path = self.backup_dir.join(filename); let tar_gz = std::fs::File::create(path)?; let enc = GzEncoder::new(tar_gz, Compression::default()); let mut tar = tar::Builder::new(enc); @@ -87,15 +117,43 @@ impl ServerProcess { // We add a file to the backup describing for what version it was made let info = format!("{} {}", self.type_, self.version); let info_bytes = info.as_bytes(); + let mut header = tar::Header::new_gnu(); header.set_size(info_bytes.len().try_into().unwrap()); + header.set_mode(0o100644); + unsafe { + header.set_gid(getegid().into()); + header.set_uid(geteuid().into()); + } tar.append_data(&mut header, "info.txt", info_bytes)?; // tar.append_dir_all("config", &self.config_dir)?; - // + // Backup file gets finalized in the drop - self.custom("save-on") + Ok(()) + } + + /// Remove the oldest backups + fn remove_old_backups(&mut self) -> std::io::Result<()> { + // The naming format used allows us to sort the backups by name and still get a sorting by + // creation time + let mut backups = std::fs::read_dir(&self.backup_dir)? + .filter_map(|res| res.map(|e| e.path()).ok()) + .collect::>(); + backups.sort(); + + let max_backups: usize = self.max_backups.try_into().unwrap(); + + if backups.len() > max_backups { + let excess_backups = backups.len() - max_backups; + + for backup in &backups[0..excess_backups] { + std::fs::remove_file(backup)?; + } + } + + Ok(()) } }