From 19d255b98c76c0bfed2cc347302b3c3d865ee45e Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Wed, 7 Jun 2023 21:15:15 +0200 Subject: [PATCH 1/3] feat: show backup time in message --- Cargo.lock | 2 +- Cargo.toml | 2 +- src/server/process.rs | 15 +++++++++++++-- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5124e00..0991d89 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10,7 +10,7 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "alex" -version = "0.1.0" +version = "0.2.0" dependencies = [ "chrono", "clap", diff --git a/Cargo.toml b/Cargo.toml index 4c0ba69..1de2d06 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "alex" -version = "0.1.0" +version = "0.2.0" description = "Wrapper around Minecraft server processes, designed to complement Docker image installations." authors = ["Jef Roosens"] edition = "2021" diff --git a/src/server/process.rs b/src/server/process.rs index a9a9f45..233a678 100644 --- a/src/server/process.rs +++ b/src/server/process.rs @@ -84,6 +84,7 @@ impl ServerProcess { // We wait some time to (hopefully) ensure the save-all call has completed std::thread::sleep(std::time::Duration::from_secs(10)); + let start_time = chrono::offset::Local::now(); let res = self.create_backup_archive(); if res.is_ok() { @@ -93,10 +94,20 @@ impl ServerProcess { // The server's save feature needs to be enabled again even if the archive failed to create self.custom("save-on")?; + let duration = chrono::offset::Local::now() - start_time; + let duration_str = format!( + "{}m{}s", + duration.num_seconds() / 60, + duration.num_seconds() % 60 + ); + if res.is_ok() { - self.custom("say server backed up successfully")?; + self.custom(&format!("say server backed up in {}", duration_str))?; } else { - self.custom("an error occured while backing up the server")?; + self.custom(&format!( + "an error occured after {} while backing up the server", + duration_str + ))?; } res From 90033aa91ecd39476601a9cfcf046fd08e664804 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Fri, 9 Jun 2023 09:25:51 +0200 Subject: [PATCH 2/3] refactor: move backup logic to separate module --- .gitignore | 2 +- src/server/backups.rs | 108 ++++++++++++++++++++++++++++++++++++++++++ src/server/mod.rs | 2 + src/server/process.rs | 93 +++++------------------------------- 4 files changed, 123 insertions(+), 82 deletions(-) create mode 100644 src/server/backups.rs diff --git a/.gitignore b/.gitignore index 4259b1b..3695da7 100644 --- a/.gitignore +++ b/.gitignore @@ -19,4 +19,4 @@ target/ # testing files *.jar -data/ +data*/ diff --git a/src/server/backups.rs b/src/server/backups.rs new file mode 100644 index 0000000..37e6021 --- /dev/null +++ b/src/server/backups.rs @@ -0,0 +1,108 @@ +use flate2::write::GzEncoder; +use flate2::Compression; +use std::fs::File; +use std::io; +use std::path::{Path, PathBuf}; + +#[link(name = "c")] +extern "C" { + fn geteuid() -> u32; + fn getegid() -> u32; +} + +static FILENAME_FORMAT: &str = "%Y-%m-%d_%H-%M-%S.tar.gz"; + +pub struct BackupManager { + backup_dir: PathBuf, + config_dir: PathBuf, + world_dir: PathBuf, + max_backups: u64, + start_time: Option>, +} + +impl BackupManager { + pub fn open( + backup_dir: PathBuf, + config_dir: PathBuf, + world_dir: PathBuf, + max_backups: u64, + ) -> Self { + BackupManager { + backup_dir, + config_dir, + world_dir, + max_backups, + start_time: None, + } + } + + pub fn create_archive(&mut self) -> io::Result<()> { + let start_time = chrono::offset::Local::now(); + self.start_time = Some(start_time); + + let filename = format!("{}", start_time.format(FILENAME_FORMAT)); + let path = self.backup_dir.join(filename); + let tar_gz = 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)?; + + // Add all files from the config directory that aren't the cache + for entry in self + .config_dir + .read_dir()? + .filter_map(|e| e.ok()) + .filter(|e| e.file_name() != "cache") + { + let tar_path = Path::new("config").join(entry.file_name()); + + if entry.file_type()?.is_dir() { + tar.append_dir_all(tar_path, entry.path())?; + } else { + tar.append_path_with_name(entry.path(), tar_path)?; + } + } + + // TODO re-add this info file in some way + // 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)?; + + Ok(()) + } + + /// Remove the oldest backups + pub 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 = self + .backup_dir + .read_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(()) + } +} diff --git a/src/server/mod.rs b/src/server/mod.rs index e3e3131..ed5cb21 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -1,5 +1,7 @@ +mod backups; mod command; mod process; +pub use backups::BackupManager; pub use command::{ServerCommand, ServerType}; pub use process::ServerProcess; diff --git a/src/server/process.rs b/src/server/process.rs index 233a678..f503c84 100644 --- a/src/server/process.rs +++ b/src/server/process.rs @@ -1,3 +1,4 @@ +use crate::server::BackupManager; use crate::server::ServerType; use flate2::write::GzEncoder; use flate2::Compression; @@ -5,12 +6,6 @@ use std::io::Write; use std::path::{Path, PathBuf}; use std::process::Child; -#[link(name = "c")] -extern "C" { - fn geteuid() -> u32; - fn getegid() -> u32; -} - pub struct ServerProcess { type_: ServerType, version: String, @@ -19,6 +14,7 @@ pub struct ServerProcess { backup_dir: PathBuf, max_backups: u64, child: Child, + backups: BackupManager, } impl ServerProcess { @@ -31,6 +27,13 @@ impl ServerProcess { max_backups: u64, child: Child, ) -> ServerProcess { + let backup_manager = BackupManager::open( + backup_dir.clone(), + config_dir.clone(), + world_dir.clone(), + max_backups, + ); + ServerProcess { type_, version, @@ -39,6 +42,7 @@ impl ServerProcess { backup_dir, max_backups, child, + backups: backup_manager, } } @@ -85,10 +89,10 @@ impl ServerProcess { std::thread::sleep(std::time::Duration::from_secs(10)); let start_time = chrono::offset::Local::now(); - let res = self.create_backup_archive(); + let res = self.backups.create_archive(); if res.is_ok() { - self.remove_old_backups()?; + self.backups.remove_old_backups()?; } // The server's save feature needs to be enabled again even if the archive failed to create @@ -112,77 +116,4 @@ impl ServerProcess { 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)?; - - // Add all files from the config directory that aren't the cache - for entry in self - .config_dir - .read_dir()? - .filter_map(|e| e.ok()) - .filter(|e| e.file_name() != "cache") - { - let tar_path = Path::new("config").join(entry.file_name()); - - if entry.file_type()?.is_dir() { - tar.append_dir_all(tar_path, entry.path())?; - } else { - tar.append_path_with_name(entry.path(), tar_path)?; - } - } - - // 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)?; - - // 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 = self - .backup_dir - .read_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(()) - } } From 3e0324703d5b4d666ff1ca2a3b03d250191b6de0 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Fri, 9 Jun 2023 10:11:02 +0200 Subject: [PATCH 3/3] feat: implement own listing of files --- src/server/backups.rs | 50 ++++++++++++++++++++++++++++++------------- 1 file changed, 35 insertions(+), 15 deletions(-) diff --git a/src/server/backups.rs b/src/server/backups.rs index 37e6021..e5fd4de 100644 --- a/src/server/backups.rs +++ b/src/server/backups.rs @@ -18,6 +18,7 @@ pub struct BackupManager { world_dir: PathBuf, max_backups: u64, start_time: Option>, + files: Vec<(PathBuf, PathBuf)> } impl BackupManager { @@ -33,9 +34,40 @@ impl BackupManager { world_dir, max_backups, start_time: None, + files: Vec::new() } } + fn set_files_to_backup(&mut self) -> io::Result<()> { + let mut dirs = vec![ + (PathBuf::from("worlds"), self.world_dir.clone()), + (PathBuf::from("config"), self.config_dir.clone()), + ]; + self.files.clear(); + + while let Some((path_in_tar, path)) = dirs.pop() { + for res in path.read_dir()? { + let entry = res?; + + if entry.file_name() == "cache" { + continue; + } + + let new_path_in_tar = path_in_tar.join(entry.file_name()); + + // All dirs get expanded recursively, while all files get returned as output + // NOTE: does this remove empty directories from backups? Is this a problem? + if entry.file_type()?.is_dir() { + dirs.push((new_path_in_tar, entry.path())); + } else { + self.files.push((new_path_in_tar, entry.path())); + } + } + } + + Ok(()) + } + pub fn create_archive(&mut self) -> io::Result<()> { let start_time = chrono::offset::Local::now(); self.start_time = Some(start_time); @@ -46,22 +78,10 @@ impl BackupManager { let enc = GzEncoder::new(tar_gz, Compression::default()); let mut tar = tar::Builder::new(enc); - tar.append_dir_all("worlds", &self.world_dir)?; + self.set_files_to_backup()?; - // Add all files from the config directory that aren't the cache - for entry in self - .config_dir - .read_dir()? - .filter_map(|e| e.ok()) - .filter(|e| e.file_name() != "cache") - { - let tar_path = Path::new("config").join(entry.file_name()); - - if entry.file_type()?.is_dir() { - tar.append_dir_all(tar_path, entry.path())?; - } else { - tar.append_path_with_name(entry.path(), tar_path)?; - } + for (path_in_tar, path) in &self.files { + tar.append_path_with_name(path, path_in_tar)?; } // TODO re-add this info file in some way