use crate::server::ServerType; use flate2::write::GzEncoder; use flate2::Compression; use std::io::Write; use std::path::PathBuf; use std::process::Child; #[link(name = "c")] extern "C" { fn geteuid() -> u32; fn getegid() -> u32; } pub struct ServerProcess { type_: ServerType, version: String, config_dir: PathBuf, world_dir: PathBuf, backup_dir: PathBuf, max_backups: u64, child: Child, } impl ServerProcess { pub fn new( type_: ServerType, version: String, config_dir: PathBuf, world_dir: PathBuf, backup_dir: PathBuf, max_backups: u64, child: Child, ) -> ServerProcess { ServerProcess { type_, version, config_dir, world_dir, backup_dir, max_backups, child, } } pub fn send_command(&mut self, cmd: &str) -> std::io::Result<()> { match cmd.trim() { "stop" | "exit" => self.stop()?, "backup" => self.backup()?, s => self.custom(s)?, } Ok(()) } fn custom(&mut self, cmd: &str) -> std::io::Result<()> { let mut stdin = self.child.stdin.as_ref().unwrap(); stdin.write_all(format!("{}\n", cmd.trim()).as_bytes())?; stdin.flush()?; Ok(()) } pub fn stop(&mut self) -> std::io::Result<()> { self.custom("stop")?; self.child.wait()?; 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 tar_gz = std::fs::File::create(path)?; let enc = GzEncoder::new(tar_gz, Compression::default()); let mut tar = tar::Builder::new(enc); tar.append_dir_all("worlds", &self.world_dir)?; // We don't store all files in the config, as this would include caches tar.append_path_with_name( self.config_dir.join("server.properties"), "config/server.properties", )?; // 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 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(()) } }