diff --git a/.cargo/config.toml b/.cargo/config.toml index 09b7896..d1675c8 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,3 +1,3 @@ [alias] -runs = "run -- run paper 1.19.4-550 --config data/config --backup data/backups --world data/worlds --jar paper-1.19.4-550.jar" -runrs = "run --release -- paper 1.19.4-545 --config data/config --backup data/backups --world data/worlds --jar data/paper-1.19.4-525.jar" +runs = "run -- paper 1.19.4-545 --config data/config --backup data/backups --world data/worlds --jar data/paper.jar" +runrs = "run --release -- paper 1.19.4-545 --config data/config --backup data/backups --world data/worlds --jar data/paper.jar" diff --git a/.gitignore b/.gitignore index 3695da7..4259b1b 100644 --- a/.gitignore +++ b/.gitignore @@ -19,4 +19,4 @@ target/ # testing files *.jar -data*/ +data/ diff --git a/CHANGELOG.md b/CHANGELOG.md index c6009a6..7b9570a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,24 +7,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://git.rustybever.be/Chewing_Bever/alex/src/branch/dev) -### Added - -* `backup` CLI command -* Incremental backups - * Chain length describes how many incremental backups to create from the - same full backup - * "backups to keep" has been replaced by "chains to keep" -* Server type & version is now stored as metadata in the metadata file - -### Changed - -* Running the server now uses the `run` CLI subcommand -* `server_type` and `server_version` arguments are now optional flags - -### Removed - -* `max_backups` setting - ## [0.2.2](https://git.rustybever.be/Chewing_Bever/alex/src/tag/0.2.2) ### Fixed diff --git a/Cargo.lock b/Cargo.lock index 5ba48c9..3e798dd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -15,8 +15,6 @@ dependencies = [ "chrono", "clap", "flate2", - "serde", - "serde_json", "signal-hook", "tar", ] @@ -125,7 +123,6 @@ dependencies = [ "iana-time-zone", "js-sys", "num-traits", - "serde", "time", "wasm-bindgen", "winapi", @@ -295,12 +292,6 @@ dependencies = [ "windows-sys", ] -[[package]] -name = "itoa" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" - [[package]] name = "js-sys" version = "0.3.63" @@ -393,43 +384,6 @@ dependencies = [ "windows-sys", ] -[[package]] -name = "ryu" -version = "1.0.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" - -[[package]] -name = "serde" -version = "1.0.164" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e8c8cf938e98f769bc164923b06dce91cea1751522f46f8466461af04c9027d" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.164" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9735b638ccc51c28bf6914d90a2e9725b377144fc612c49a611fddd1b631d68" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "serde_json" -version = "1.0.96" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1" -dependencies = [ - "itoa", - "ryu", - "serde", -] - [[package]] name = "signal-hook" version = "0.3.15" diff --git a/Cargo.toml b/Cargo.toml index 2ac6b39..2c4045b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,11 +12,10 @@ edition = "2021" tar = "0.4.38" # Used to compress said tarballs using gzip flate2 = "1.0.26" -chrono = { version = "0.4.26", features = ["serde"] } +# Used for backup filenames +chrono = "0.4.26" clap = { version = "4.3.1", features = ["derive", "env"] } signal-hook = "0.3.15" -serde = { version = "1.0.164", features = ["derive", "rc"] } -serde_json = "1.0.96" [profile.release] lto = "fat" diff --git a/src/backup/delta.rs b/src/backup/delta.rs deleted file mode 100644 index 34971f0..0000000 --- a/src/backup/delta.rs +++ /dev/null @@ -1,76 +0,0 @@ -use serde::{Deserialize, Serialize}; -use std::collections::{HashMap, HashSet}; -use std::path::PathBuf; - -/// Represents the changes relative to the previous backup -#[derive(Debug, Serialize, Deserialize)] -pub struct Delta { - /// What files were added/modified in each part of the tarball. - pub added: HashMap>, - /// What files were removed in this backup, in comparison to the previous backup. For full - /// backups, this will always be empty, as they do not consider previous backups. - /// The map stores a separate list for each top-level directory, as the contents of these - /// directories can come for different source directories. - pub removed: HashMap>, -} - -impl Delta { - pub fn new() -> Self { - Self { - added: HashMap::new(), - removed: HashMap::new(), - } - } - - /// Update the current state so that its result becomes the merge of itself and the other - /// state. - #[allow(dead_code)] - pub fn merge(&mut self, delta: &Self) { - for (dir, added) in delta.added.iter() { - // Files that were removed in the current state, but added in the new state, are no - // longer removed - if let Some(orig_removed) = self.removed.get_mut(dir) { - orig_removed.retain(|k| !added.contains(k)); - } - - // Newly added files are added to the state as well - if let Some(orig_added) = self.added.get_mut(dir) { - orig_added.extend(added.iter().cloned()); - } else { - self.added.insert(dir.clone(), added.clone()); - } - } - - for (dir, removed) in delta.removed.iter() { - // Files that were originally added, but now deleted are removed from the added list - if let Some(orig_added) = self.added.get_mut(dir) { - orig_added.retain(|k| !removed.contains(k)); - } - - // Newly removed files are added to the state as well - if let Some(orig_removed) = self.removed.get_mut(dir) { - orig_removed.extend(removed.iter().cloned()); - } else { - self.removed.insert(dir.clone(), removed.clone()); - } - } - } - - /// Modify the given state by applying this delta's changes to it - pub fn apply(&self, state: &mut HashMap>) { - // First we add new files, then we remove the old ones - for (dir, added) in self.added.iter() { - if let Some(current) = state.get_mut(dir) { - current.extend(added.iter().cloned()); - } else { - state.insert(dir.clone(), added.clone()); - } - } - - for (dir, removed) in self.removed.iter() { - if let Some(current) = state.get_mut(dir) { - current.retain(|k| !removed.contains(k)); - } - } - } -} diff --git a/src/backup/manager.rs b/src/backup/manager.rs deleted file mode 100644 index 1dfec16..0000000 --- a/src/backup/manager.rs +++ /dev/null @@ -1,140 +0,0 @@ -use super::Backup; -use serde::Deserialize; -use serde::Serialize; -use std::fs::File; -use std::io; -use std::path::PathBuf; - -pub struct Manager -where - T: Clone + Serialize + for<'de> Deserialize<'de>, -{ - backup_dir: PathBuf, - config_dir: PathBuf, - world_dir: PathBuf, - default_metadata: T, - chain_len: u64, - chains_to_keep: u64, - chains: Vec>>, -} - -impl Manager -where - T: Clone + Serialize + for<'de> Deserialize<'de>, -{ - const METADATA_FILE: &str = "alex.json"; - - /// Initialize a new instance of a `BackupManager`. - pub fn new( - backup_dir: PathBuf, - config_dir: PathBuf, - world_dir: PathBuf, - metadata: T, - chain_len: u64, - chains_to_keep: u64, - ) -> Self { - Self { - backup_dir, - config_dir, - world_dir, - default_metadata: metadata, - chain_len, - chains_to_keep, - chains: Vec::new(), - } - } - - /// Create a new backup with the expected type. - pub fn create_backup(&mut self) -> io::Result<()> { - let dirs = vec![ - (PathBuf::from("config"), self.config_dir.clone()), - (PathBuf::from("worlds"), self.world_dir.clone()), - ]; - - // We start a new chain if the current chain is complete, or if there isn't a first chain - // yet - if let Some(current_chain) = self.chains.last() { - let current_chain_len: u64 = current_chain.len().try_into().unwrap(); - - if current_chain_len >= self.chain_len { - self.chains.push(Vec::new()); - } - } else { - self.chains.push(Vec::new()); - } - - let current_chain = self.chains.last_mut().unwrap(); - - let mut backup = if !current_chain.is_empty() { - let previous_backup = current_chain.last().unwrap(); - let state = Backup::state(current_chain); - - Backup::create_from(state, previous_backup.start_time, &self.backup_dir, dirs)? - } else { - Backup::create(&self.backup_dir, dirs)? - }; - - backup.set_metadata(self.default_metadata.clone()); - - current_chain.push(backup); - - self.save()?; - - Ok(()) - } - - /// Delete all backups associated with outdated chains, and forget those chains. - pub fn remove_old_backups(&mut self) -> io::Result<()> { - let chains_to_store: usize = self.chains_to_keep.try_into().unwrap(); - - if chains_to_store < self.chains.len() { - let mut remove_count: usize = self.chains.len() - chains_to_store; - - // We only count finished chains towards the list of stored chains - let chain_len: usize = self.chain_len.try_into().unwrap(); - if self.chains.last().unwrap().len() < chain_len { - remove_count -= 1; - } - - for chain in self.chains.drain(..remove_count) { - for backup in chain { - let path = Backup::path(&self.backup_dir, backup.start_time); - std::fs::remove_file(path)?; - } - } - - self.save()?; - } - - Ok(()) - } - - /// Write the in-memory state to disk. - pub fn save(&self) -> io::Result<()> { - let json_file = File::create(self.backup_dir.join(Self::METADATA_FILE))?; - serde_json::to_writer(json_file, &self.chains)?; - - Ok(()) - } - - /// Overwrite the in-memory state with the on-disk state. - pub fn load(&mut self) -> io::Result<()> { - let json_file = match File::open(self.backup_dir.join(Self::METADATA_FILE)) { - Ok(f) => f, - Err(e) => { - // Don't error out if the file isn't there, it will be created when necessary - if e.kind() == io::ErrorKind::NotFound { - self.chains = Vec::new(); - - return Ok(()); - } else { - return Err(e); - } - } - }; - - self.chains = serde_json::from_reader(json_file)?; - - Ok(()) - } -} diff --git a/src/backup/mod.rs b/src/backup/mod.rs deleted file mode 100644 index 5b12b8c..0000000 --- a/src/backup/mod.rs +++ /dev/null @@ -1,160 +0,0 @@ -mod delta; -mod manager; -mod path; - -use delta::Delta; -pub use manager::Manager; - -use chrono::Utc; -use flate2::write::GzEncoder; -use flate2::Compression; -use path::PathExt; -use serde::{Deserialize, Serialize}; -use std::collections::{HashMap, HashSet}; -use std::fs::File; -use std::io; -use std::path::{Path, PathBuf}; - -#[derive(Debug, PartialEq, Serialize, Deserialize)] -pub enum BackupType { - Full, - Incremental, -} - -/// Represents a successful backup -#[derive(Serialize, Deserialize)] -pub struct Backup { - /// When the backup was started (also corresponds to the name) - pub start_time: chrono::DateTime, - /// Type of the backup - pub type_: BackupType, - pub delta: Delta, - /// Additional metadata that can be associated with a given backup - pub metadata: Option, -} - -impl Backup<()> { - /// Return the path to a backup file by properly formatting the data. - pub fn path>(backup_dir: P, start_time: chrono::DateTime) -> PathBuf { - let backup_dir = backup_dir.as_ref(); - - let filename = format!("{}", start_time.format(Self::FILENAME_FORMAT)); - backup_dir.join(filename) - } -} - -impl Backup { - const FILENAME_FORMAT: &str = "%Y-%m-%d_%H-%M-%S.tar.gz"; - - pub fn set_metadata(&mut self, metadata: T) { - self.metadata = Some(metadata); - } - - /// Resolve the state of the list of backups by applying their deltas in-order to an initially - /// empty state. - pub fn state(backups: &Vec) -> HashMap> { - let mut state: HashMap> = HashMap::new(); - - for backup in backups { - backup.delta.apply(&mut state); - } - - state - } - - /// Create a new Full backup, populated with the given directories. - /// - /// # Arguments - /// - /// * `backup_dir` - Directory to store archive in - /// * `dirs` - list of tuples `(path_in_tar, src_dir)` with `path_in_tar` the directory name - /// under which `src_dir`'s contents should be stored in the archive - /// - /// # Returns - /// - /// The `Backup` instance describing this new backup. - pub fn create>( - backup_dir: P, - dirs: Vec<(PathBuf, PathBuf)>, - ) -> io::Result { - let start_time = chrono::offset::Utc::now(); - - let path = Backup::path(backup_dir, start_time); - let tar_gz = File::create(path)?; - let enc = GzEncoder::new(tar_gz, Compression::default()); - let mut ar = tar::Builder::new(enc); - - let mut delta = Delta::new(); - - for (dir_in_tar, src_dir) in dirs { - let mut added_files: HashSet = HashSet::new(); - - for entry in src_dir.read_dir_recursive()?.ignored("cache").files() { - let path = entry?.path(); - let stripped = path.strip_prefix(&src_dir).unwrap(); - - ar.append_path_with_name(&path, dir_in_tar.join(stripped))?; - added_files.insert(stripped.to_path_buf()); - } - - delta.added.insert(dir_in_tar, added_files); - } - - Ok(Backup { - type_: BackupType::Full, - start_time, - delta, - metadata: None, - }) - } - - /// Create a new incremental backup from a given previous backup - pub fn create_from>( - previous_state: HashMap>, - previous_start_time: chrono::DateTime, - backup_dir: P, - dirs: Vec<(PathBuf, PathBuf)>, - ) -> io::Result { - let start_time = chrono::offset::Utc::now(); - - let path = Backup::path(backup_dir, start_time); - let tar_gz = File::create(path)?; - let enc = GzEncoder::new(tar_gz, Compression::default()); - let mut ar = tar::Builder::new(enc); - - let mut delta = Delta::new(); - - for (dir_in_tar, src_dir) in dirs { - let mut all_files: HashSet = HashSet::new(); - let mut added_files: HashSet = HashSet::new(); - - for entry in src_dir.read_dir_recursive()?.ignored("cache").files() { - let path = entry?.path(); - let stripped = path.strip_prefix(&src_dir).unwrap(); - - if !path.not_modified_since(previous_start_time) { - ar.append_path_with_name(&path, dir_in_tar.join(stripped))?; - added_files.insert(stripped.to_path_buf()); - } - - all_files.insert(stripped.to_path_buf()); - } - - delta.added.insert(dir_in_tar.clone(), added_files); - - if let Some(previous_files) = previous_state.get(&dir_in_tar) { - delta.removed.insert( - dir_in_tar, - previous_files.difference(&all_files).cloned().collect(), - ); - } - } - - Ok(Backup { - type_: BackupType::Incremental, - start_time, - delta, - metadata: None, - }) - } -} diff --git a/src/backup/path.rs b/src/backup/path.rs deleted file mode 100644 index b8b5ae9..0000000 --- a/src/backup/path.rs +++ /dev/null @@ -1,147 +0,0 @@ -use chrono::{Local, Utc}; -use std::collections::HashSet; -use std::ffi::OsString; -use std::fs::DirEntry; -use std::path::{Path, PathBuf}; -use std::{fs, io}; - -pub struct ReadDirRecursive { - ignored: HashSet, - read_dir: fs::ReadDir, - dir_stack: Vec, - files_only: bool, -} - -impl ReadDirRecursive { - /// Start the iterator for a new directory - pub fn start>(path: P) -> io::Result { - let path = path.as_ref(); - let read_dir = path.read_dir()?; - - Ok(ReadDirRecursive { - ignored: HashSet::new(), - read_dir, - dir_stack: Vec::new(), - files_only: false, - }) - } - - pub fn ignored>(mut self, s: S) -> Self { - self.ignored.insert(s.into()); - - self - } - - pub fn files(mut self) -> Self { - self.files_only = true; - - self - } - - /// Tries to populate the `read_dir` field with a new `ReadDir` instance to consume. - fn next_read_dir(&mut self) -> io::Result { - if let Some(path) = self.dir_stack.pop() { - self.read_dir = path.read_dir()?; - - Ok(true) - } else { - Ok(false) - } - } - - /// Convenience method to add a new directory to the stack. - fn push_entry(&mut self, entry: &io::Result) { - if let Ok(entry) = entry { - if entry.path().is_dir() { - self.dir_stack.push(entry.path()); - } - } - } - - /// Determine whether an entry should be returned by the iterator. - fn should_return(&self, entry: &io::Result) -> bool { - if let Ok(entry) = entry { - let mut res = !self.ignored.contains(&entry.file_name()); - - // Please just let me combine these already - if self.files_only { - if let Ok(file_type) = entry.file_type() { - res = res && file_type.is_file(); - } - // We couldn't determine if it's a file, so we don't return it - else { - res = false; - } - } - - res - } else { - true - } - } -} - -impl Iterator for ReadDirRecursive { - type Item = io::Result; - - fn next(&mut self) -> Option { - loop { - // First, we try to consume the current directory's items - while let Some(entry) = self.read_dir.next() { - self.push_entry(&entry); - - if self.should_return(&entry) { - return Some(entry); - } - } - - // If we get an error while setting up a new directory, we return this, otherwise we - // keep trying to consume the directories - match self.next_read_dir() { - Ok(true) => (), - // There's no more directories to traverse, so the iterator is done - Ok(false) => return None, - Err(e) => return Some(Err(e)), - } - } - } -} - -pub trait PathExt { - /// Confirm whether the file has not been modified since the given timestamp. - /// - /// This function will only return true if it can determine with certainty that the file hasn't - /// been modified. - /// - /// # Args - /// - /// * `timestamp` - Timestamp to compare modified time with - /// - /// # Returns - /// - /// True if the file has not been modified for sure, false otherwise. - fn not_modified_since(&self, timestamp: chrono::DateTime) -> bool; - - /// An extension of the `read_dir` command that runs through the entire underlying directory - /// structure using breadth-first search - fn read_dir_recursive(&self) -> io::Result; -} - -impl PathExt for Path { - fn not_modified_since(&self, timestamp: chrono::DateTime) -> bool { - if let Ok(metadata) = self.metadata() { - if let Ok(last_modified) = metadata.modified() { - let t: chrono::DateTime = last_modified.into(); - let t = t.with_timezone(&Local); - - return t < timestamp; - } - } - - false - } - - fn read_dir_recursive(&self) -> io::Result { - ReadDirRecursive::start(self) - } -} diff --git a/src/cli.rs b/src/cli.rs deleted file mode 100644 index 166fa82..0000000 --- a/src/cli.rs +++ /dev/null @@ -1,106 +0,0 @@ -use crate::server::ServerType; -use clap::{Args, Parser, Subcommand}; -use std::path::PathBuf; - -#[derive(Parser)] -#[command(author, version, about, long_about = None)] -pub struct Cli { - #[command(subcommand)] - pub command: Commands, - /// Directory where configs are stored, and where the server will run - #[arg( - long, - value_name = "CONFIG_DIR", - default_value = ".", - env = "ALEX_CONFIG_DIR", - global = true - )] - pub config: PathBuf, - /// Directory where world files will be saved - #[arg( - long, - value_name = "WORLD_DIR", - default_value = "../worlds", - env = "ALEX_WORLD_DIR", - global = true - )] - pub world: PathBuf, - /// Directory where backups will be stored - #[arg( - long, - value_name = "BACKUP_DIR", - default_value = "../backups", - env = "ALEX_BACKUP_DIR", - global = true - )] - pub backup: PathBuf, - - /// Length of a backup chain - #[arg( - short = 'l', - long, - default_value_t = 4, - env = "ALEX_CHAIN_LEN", - global = true - )] - pub chain_len: u64, - /// How many backup chains to keep - #[arg( - short = 'n', - long, - default_value_t = 7, - env = "ALEX_CHAINS", - global = true - )] - pub chains: u64, - /// Type of server - #[arg(long, default_value = "unknown", env = "ALEX_SERVER")] - pub server: ServerType, - /// Version string for the server, e.g. 1.19.4-545 - #[arg(long, default_value = "", env = "ALEX_SERVER_VERSION")] - pub server_version: String, -} - -#[derive(Subcommand)] -pub enum Commands { - /// Run the server - Run(RunArgs), - /// Create a new backup of the server. This command should only be used when the server is not - /// running. - Backup(BackupArgs), -} - -#[derive(Args)] -pub struct RunArgs { - /// Server jar to execute - #[arg( - long, - value_name = "JAR_PATH", - default_value = "server.jar", - env = "ALEX_JAR" - )] - pub jar: PathBuf, - - /// Java command to run the server jar with - #[arg(long, value_name = "JAVA_CMD", default_value_t = String::from("java"), env = "ALEX_JAVA")] - pub java: String, - - /// XMS value in megabytes for the server instance - #[arg(long, default_value_t = 1024, env = "ALEX_XMS")] - pub xms: u64, - /// XMX value in megabytes for the server instance - #[arg(long, default_value_t = 2048, env = "ALEX_XMX")] - pub xmx: u64, - - /// How frequently to perform a backup, in minutes; 0 to disable. - #[arg(short = 't', long, default_value_t = 0, env = "ALEX_FREQUENCY")] - pub frequency: u64, - - /// Don't actually run the server, but simply output the server configuration that would have - /// been ran - #[arg(short, long, default_value_t = false)] - pub dry: bool, -} - -#[derive(Args)] -pub struct BackupArgs {} diff --git a/src/main.rs b/src/main.rs index ffb0c93..2cd84ff 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,14 +1,78 @@ -mod backup; -mod cli; mod server; mod signals; mod stdin; use clap::Parser; -use cli::{BackupArgs, Cli, Commands, RunArgs}; +use server::ServerType; use std::io; +use std::path::PathBuf; use std::sync::{Arc, Mutex}; +#[derive(Parser)] +#[command(author, version, about, long_about = None)] +struct Cli { + /// Type of server + type_: ServerType, + /// Version string for the server, e.g. 1.19.4-545 + #[arg(env = "ALEX_SERVER_VERSION")] + server_version: String, + + /// Server jar to execute + #[arg( + long, + value_name = "JAR_PATH", + default_value = "server.jar", + env = "ALEX_JAR" + )] + jar: PathBuf, + /// Directory where configs are stored, and where the server will run + #[arg( + long, + value_name = "CONFIG_DIR", + default_value = ".", + env = "ALEX_CONFIG_DIR" + )] + config: PathBuf, + /// Directory where world files will be saved + #[arg( + long, + value_name = "WORLD_DIR", + default_value = "../worlds", + env = "ALEX_WORLD_DIR" + )] + world: PathBuf, + /// Directory where backups will be stored + #[arg( + long, + value_name = "BACKUP_DIR", + default_value = "../backups", + env = "ALEX_BACKUP_DIR" + )] + backup: PathBuf, + /// Java command to run the server jar with + #[arg(long, value_name = "JAVA_CMD", default_value_t = String::from("java"), env = "ALEX_JAVA")] + java: String, + + /// XMS value in megabytes for the server instance + #[arg(long, default_value_t = 1024, env = "ALEX_XMS")] + xms: u64, + /// XMX value in megabytes for the server instance + #[arg(long, default_value_t = 2048, env = "ALEX_XMX")] + xmx: u64, + + /// How many backups to keep + #[arg(short = 'n', long, default_value_t = 7, env = "ALEX_MAX_BACKUPS")] + max_backups: u64, + /// How frequently to perform a backup, in minutes; 0 to disable. + #[arg(short = 't', long, default_value_t = 0, env = "ALEX_FREQUENCY")] + frequency: u64, + + /// Don't actually run the server, but simply output the server configuration that would have + /// been ran + #[arg(short, long, default_value_t = false)] + dry: bool, +} + fn backups_thread(counter: Arc>, frequency: u64) { loop { std::thread::sleep(std::time::Duration::from_secs(frequency * 60)); @@ -22,22 +86,22 @@ fn backups_thread(counter: Arc>, frequency: u64) { } } -fn command_run(cli: &Cli, args: &RunArgs) -> io::Result<()> { +fn main() -> io::Result<()> { let (_, mut signals) = signals::install_signal_handlers()?; + let cli = Cli::parse(); - let mut cmd = server::ServerCommand::new(cli.server, &cli.server_version) - .java(&args.java) - .jar(args.jar.clone()) - .config(cli.config.clone()) - .world(cli.world.clone()) - .backup(cli.backup.clone()) - .xms(args.xms) - .xmx(args.xmx) - .chain_len(cli.chain_len) - .chains_to_keep(cli.chains); + let mut cmd = server::ServerCommand::new(cli.type_, &cli.server_version) + .java(&cli.java) + .jar(cli.jar) + .config(cli.config) + .world(cli.world) + .backup(cli.backup) + .xms(cli.xms) + .xmx(cli.xmx) + .max_backups(cli.max_backups); cmd.canonicalize()?; - if args.dry { + if cli.dry { print!("{}", cmd); return Ok(()); @@ -45,10 +109,9 @@ fn command_run(cli: &Cli, args: &RunArgs) -> io::Result<()> { let counter = Arc::new(Mutex::new(cmd.spawn()?)); - if args.frequency > 0 { + if cli.frequency > 0 { let clone = Arc::clone(&counter); - let frequency = args.frequency; - std::thread::spawn(move || backups_thread(clone, frequency)); + std::thread::spawn(move || backups_thread(clone, cli.frequency)); } // Spawn thread that handles the main stdin loop @@ -58,32 +121,3 @@ fn command_run(cli: &Cli, args: &RunArgs) -> io::Result<()> { // Signal handler loop exits the process when necessary signals::handle_signals(&mut signals, counter) } - -fn commands_backup(cli: &Cli, _args: &BackupArgs) -> io::Result<()> { - let metadata = server::Metadata { - server_type: cli.server, - server_version: cli.server_version.clone(), - }; - - let mut manager = backup::Manager::new( - cli.backup.clone(), - cli.config.clone(), - cli.world.clone(), - metadata, - cli.chain_len, - cli.chains, - ); - manager.load()?; - - manager.create_backup()?; - manager.remove_old_backups() -} - -fn main() -> io::Result<()> { - let cli = Cli::parse(); - - match &cli.command { - Commands::Run(args) => command_run(&cli, args), - Commands::Backup(args) => commands_backup(&cli, args), - } -} diff --git a/src/server/command.rs b/src/server/command.rs index d06f8bc..641c6b8 100644 --- a/src/server/command.rs +++ b/src/server/command.rs @@ -1,16 +1,13 @@ -use crate::backup::Manager as BackupManager; use crate::server::ServerProcess; use clap::ValueEnum; -use serde::{Deserialize, Serialize}; use std::fmt; use std::fs::File; use std::io::Write; use std::path::{Path, PathBuf}; use std::process::{Command, Stdio}; -#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Serialize, Deserialize)] +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)] pub enum ServerType { - Unknown, Paper, Forge, Vanilla, @@ -19,7 +16,6 @@ pub enum ServerType { impl fmt::Display for ServerType { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let s = match self { - ServerType::Unknown => "Unknown", ServerType::Paper => "PaperMC", ServerType::Forge => "Forge", ServerType::Vanilla => "Vanilla", @@ -29,12 +25,6 @@ impl fmt::Display for ServerType { } } -#[derive(Clone, Serialize, Deserialize)] -pub struct Metadata { - pub server_type: ServerType, - pub server_version: String, -} - pub struct ServerCommand { type_: ServerType, version: String, @@ -45,8 +35,7 @@ pub struct ServerCommand { backup_dir: PathBuf, xms: u64, xmx: u64, - chain_len: u64, - chains_to_keep: u64, + max_backups: u64, } impl ServerCommand { @@ -61,8 +50,7 @@ impl ServerCommand { backup_dir: PathBuf::from("backups"), xms: 1024, xmx: 2048, - chain_len: 4, - chains_to_keep: 7, + max_backups: 7, } } @@ -103,13 +91,8 @@ impl ServerCommand { self } - pub fn chain_len(mut self, v: u64) -> Self { - self.chain_len = v; - self - } - - pub fn chains_to_keep(mut self, v: u64) -> Self { - self.chains_to_keep = v; + pub fn max_backups(mut self, v: u64) -> Self { + self.max_backups = v; self } @@ -196,25 +179,19 @@ impl ServerCommand { } pub fn spawn(&mut self) -> std::io::Result { - let metadata = Metadata { - server_type: self.type_, - server_version: self.version.clone(), - }; - let mut manager = BackupManager::new( - self.backup_dir.clone(), - self.config_dir.clone(), - self.world_dir.clone(), - metadata, - self.chain_len, - self.chains_to_keep, - ); - manager.load()?; - let mut cmd = self.create_cmd(); self.accept_eula()?; let child = cmd.spawn()?; - Ok(ServerProcess::new(manager, child)) + Ok(ServerProcess::new( + self.type_, + self.version.clone(), + self.config_dir.clone(), + self.world_dir.clone(), + self.backup_dir.clone(), + self.max_backups, + child, + )) } } diff --git a/src/server/mod.rs b/src/server/mod.rs index 0f01c9a..e3e3131 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -1,5 +1,5 @@ mod command; mod process; -pub use command::{Metadata, ServerCommand, ServerType}; +pub use command::{ServerCommand, ServerType}; pub use process::ServerProcess; diff --git a/src/server/process.rs b/src/server/process.rs index 0a09ab0..a9a9f45 100644 --- a/src/server/process.rs +++ b/src/server/process.rs @@ -1,18 +1,44 @@ -use crate::backup::Manager as BackupManager; -use crate::server::Metadata; +use crate::server::ServerType; +use flate2::write::GzEncoder; +use flate2::Compression; 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, + config_dir: PathBuf, + world_dir: PathBuf, + backup_dir: PathBuf, + max_backups: u64, child: Child, - backups: BackupManager, } impl ServerProcess { - pub fn new(manager: BackupManager, child: Child) -> 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, - backups: manager, } } @@ -58,33 +84,94 @@ 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::Utc::now(); - let res = self.backups.create_backup(); + 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("save-all")?; if res.is_ok() { - self.backups.remove_old_backups()?; - } - - let duration = chrono::offset::Utc::now() - start_time; - let duration_str = format!( - "{}m{}s", - duration.num_seconds() / 60, - duration.num_seconds() % 60 - ); - - if res.is_ok() { - self.custom(&format!("say server backed up in {}", duration_str))?; + self.custom("say server backed up successfully")?; } else { - self.custom(&format!( - "an error occured after {} while backing up the server", - duration_str - ))?; + self.custom("an error occured while backing up the server")?; } 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(()) + } }