feat: store server info in metadata file; change cli flags
ci/woodpecker/push/lint Pipeline was successful Details
ci/woodpecker/push/clippy Pipeline was successful Details
ci/woodpecker/push/build Pipeline was successful Details

incremental-backups
Jef Roosens 2023-06-20 19:31:50 +02:00
parent ef631fab1d
commit 53dc3783ca
Signed by: Jef Roosens
GPG Key ID: B75D4F293C7052DB
9 changed files with 97 additions and 72 deletions

View File

@ -14,10 +14,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* Chain length descibres how many incremental backups to create from the * Chain length descibres how many incremental backups to create from the
same full backup same full backup
* "backups to keep" has been replaced by "chains to keep" * "backups to keep" has been replaced by "chains to keep"
* Server type & version is now stored as metadata in the metadata file
### Changed ### Changed
* Running the server now uses the `run` CLI subcommand * Running the server now uses the `run` CLI subcommand
* `server_type` and `server_version` arguments are now optional flags
### Removed ### Removed

View File

@ -24,6 +24,7 @@ impl Delta {
/// Update the current state so that its result becomes the merge of itself and the other /// Update the current state so that its result becomes the merge of itself and the other
/// state. /// state.
#[allow(dead_code)]
pub fn merge(&mut self, delta: &Self) { pub fn merge(&mut self, delta: &Self) {
for (dir, added) in delta.added.iter() { for (dir, added) in delta.added.iter() {
// Files that were removed in the current state, but added in the new state, are no // Files that were removed in the current state, but added in the new state, are no

View File

@ -1,18 +1,27 @@
use super::Backup; use super::Backup;
use serde::de::DeserializeOwned;
use serde::Serialize;
use std::fs::File; use std::fs::File;
use std::io; use std::io;
use std::path::PathBuf; use std::path::PathBuf;
pub struct Manager { pub struct Manager<T>
where
T: Clone + Serialize + DeserializeOwned,
{
backup_dir: PathBuf, backup_dir: PathBuf,
config_dir: PathBuf, config_dir: PathBuf,
world_dir: PathBuf, world_dir: PathBuf,
default_metadata: T,
chain_len: u64, chain_len: u64,
chains_to_keep: u64, chains_to_keep: u64,
chains: Vec<Vec<Backup>>, chains: Vec<Vec<Backup<T>>>,
} }
impl Manager { impl<T> Manager<T>
where
T: Clone + Serialize + DeserializeOwned,
{
const METADATA_FILE: &str = "alex.json"; const METADATA_FILE: &str = "alex.json";
/// Initialize a new instance of a `BackupManager`. /// Initialize a new instance of a `BackupManager`.
@ -20,6 +29,7 @@ impl Manager {
backup_dir: PathBuf, backup_dir: PathBuf,
config_dir: PathBuf, config_dir: PathBuf,
world_dir: PathBuf, world_dir: PathBuf,
metadata: T,
chain_len: u64, chain_len: u64,
chains_to_keep: u64, chains_to_keep: u64,
) -> Self { ) -> Self {
@ -27,6 +37,7 @@ impl Manager {
backup_dir, backup_dir,
config_dir, config_dir,
world_dir, world_dir,
default_metadata: metadata,
chain_len, chain_len,
chains_to_keep, chains_to_keep,
chains: Vec::new(), chains: Vec::new(),
@ -40,32 +51,32 @@ impl Manager {
(PathBuf::from("worlds"), self.world_dir.clone()), (PathBuf::from("worlds"), self.world_dir.clone()),
]; ];
// I kinda hate this statement, please just let me combine let statements in if statements // We start a new chain if the current chain is complete, or if there isn't a first chain
// already // yet
let backup = if let Some(current_chain) = self.chains.last() { if let Some(current_chain) = self.chains.last() {
let current_chain_len: u64 = current_chain.len().try_into().unwrap(); let current_chain_len: u64 = current_chain.len().try_into().unwrap();
if current_chain_len < self.chain_len { if current_chain_len >= self.chain_len {
if let Some(previous_backup) = current_chain.last() {
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)?
}
} else {
self.chains.push(Vec::new()); self.chains.push(Vec::new());
Backup::create(&self.backup_dir, dirs)?
} }
} else { } else {
self.chains.push(Vec::new()); 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::create(&self.backup_dir, dirs)?
}; };
// The above statement always creates this element, so this unwrap is safe backup.set_metadata(self.default_metadata.clone());
self.chains.last_mut().unwrap().push(backup);
current_chain.push(backup);
self.save()?; self.save()?;
@ -73,7 +84,7 @@ impl Manager {
} }
/// Delete all backups associated with outdated chains, and forget those chains. /// Delete all backups associated with outdated chains, and forget those chains.
pub fn remove_old_backups(&mut self) -> std::io::Result<()> { pub fn remove_old_backups(&mut self) -> io::Result<()> {
let chains_to_store: usize = self.chains_to_keep.try_into().unwrap(); let chains_to_store: usize = self.chains_to_keep.try_into().unwrap();
if chains_to_store < self.chains.len() { if chains_to_store < self.chains.len() {
@ -91,15 +102,15 @@ impl Manager {
std::fs::remove_file(path)?; std::fs::remove_file(path)?;
} }
} }
}
self.save()?; self.save()?;
}
Ok(()) Ok(())
} }
/// Write the in-memory state to disk. /// Write the in-memory state to disk.
pub fn save(&self) -> std::io::Result<()> { pub fn save(&self) -> io::Result<()> {
let json_file = File::create(self.backup_dir.join(Self::METADATA_FILE))?; let json_file = File::create(self.backup_dir.join(Self::METADATA_FILE))?;
serde_json::to_writer(json_file, &self.chains)?; serde_json::to_writer(json_file, &self.chains)?;
@ -107,7 +118,7 @@ impl Manager {
} }
/// Overwrite the in-memory state with the on-disk state. /// Overwrite the in-memory state with the on-disk state.
pub fn load(&mut self) -> std::io::Result<()> { pub fn load(&mut self) -> io::Result<()> {
let json_file = match File::open(self.backup_dir.join(Self::METADATA_FILE)) { let json_file = match File::open(self.backup_dir.join(Self::METADATA_FILE)) {
Ok(f) => f, Ok(f) => f,
Err(e) => { Err(e) => {
@ -121,6 +132,7 @@ impl Manager {
} }
} }
}; };
self.chains = serde_json::from_reader(json_file)?; self.chains = serde_json::from_reader(json_file)?;
Ok(()) Ok(())

View File

@ -22,21 +22,37 @@ pub enum BackupType {
} }
/// Represents a successful backup /// Represents a successful backup
#[derive(Debug, Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
pub struct Backup { pub struct Backup<T: Clone> {
/// When the backup was started (also corresponds to the name) /// When the backup was started (also corresponds to the name)
pub start_time: chrono::DateTime<Utc>, pub start_time: chrono::DateTime<Utc>,
/// Type of the backup /// Type of the backup
pub type_: BackupType, pub type_: BackupType,
pub delta: Delta, pub delta: Delta,
/// Additional metadata that can be associated with a given backup
pub metadata: Option<T>,
} }
impl Backup { impl Backup<()> {
/// Return the path to a backup file by properly formatting the data.
pub fn path<P: AsRef<Path>>(backup_dir: P, start_time: chrono::DateTime<Utc>) -> PathBuf {
let backup_dir = backup_dir.as_ref();
let filename = format!("{}", start_time.format(Self::FILENAME_FORMAT));
backup_dir.join(filename)
}
}
impl<T: Clone> Backup<T> {
const FILENAME_FORMAT: &str = "%Y-%m-%d_%H-%M-%S.tar.gz"; 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 /// Resolve the state of the list of backups by applying their deltas in-order to an initially
/// empty state. /// empty state.
pub fn state(backups: &Vec<Backup>) -> HashMap<PathBuf, HashSet<PathBuf>> { pub fn state(backups: &Vec<Self>) -> HashMap<PathBuf, HashSet<PathBuf>> {
let mut state: HashMap<PathBuf, HashSet<PathBuf>> = HashMap::new(); let mut state: HashMap<PathBuf, HashSet<PathBuf>> = HashMap::new();
for backup in backups { for backup in backups {
@ -46,14 +62,6 @@ impl Backup {
state state
} }
/// Return the path to a backup file by properly formatting the data.
pub fn path<P: AsRef<Path>>(backup_dir: P, start_time: chrono::DateTime<Utc>) -> PathBuf {
let backup_dir = backup_dir.as_ref();
let filename = format!("{}", start_time.format(Self::FILENAME_FORMAT));
backup_dir.join(filename)
}
/// Create a new Full backup, populated with the given directories. /// Create a new Full backup, populated with the given directories.
/// ///
/// # Arguments /// # Arguments
@ -71,7 +79,7 @@ impl Backup {
) -> io::Result<Self> { ) -> io::Result<Self> {
let start_time = chrono::offset::Utc::now(); let start_time = chrono::offset::Utc::now();
let path = Self::path(backup_dir, start_time); let path = Backup::path(backup_dir, start_time);
let tar_gz = File::create(path)?; let tar_gz = File::create(path)?;
let enc = GzEncoder::new(tar_gz, Compression::default()); let enc = GzEncoder::new(tar_gz, Compression::default());
let mut ar = tar::Builder::new(enc); let mut ar = tar::Builder::new(enc);
@ -96,6 +104,7 @@ impl Backup {
type_: BackupType::Full, type_: BackupType::Full,
start_time, start_time,
delta, delta,
metadata: None,
}) })
} }
@ -108,7 +117,7 @@ impl Backup {
) -> io::Result<Self> { ) -> io::Result<Self> {
let start_time = chrono::offset::Utc::now(); let start_time = chrono::offset::Utc::now();
let path = Self::path(backup_dir, start_time); let path = Backup::path(backup_dir, start_time);
let tar_gz = File::create(path)?; let tar_gz = File::create(path)?;
let enc = GzEncoder::new(tar_gz, Compression::default()); let enc = GzEncoder::new(tar_gz, Compression::default());
let mut ar = tar::Builder::new(enc); let mut ar = tar::Builder::new(enc);
@ -145,6 +154,7 @@ impl Backup {
type_: BackupType::Incremental, type_: BackupType::Incremental,
start_time, start_time,
delta, delta,
metadata: None,
}) })
} }
} }

View File

@ -53,6 +53,12 @@ pub struct Cli {
global = true global = true
)] )]
pub chains: u64, 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)] #[derive(Subcommand)]
@ -66,12 +72,6 @@ pub enum Commands {
#[derive(Args)] #[derive(Args)]
pub struct RunArgs { pub struct RunArgs {
/// Type of server
pub type_: ServerType,
/// Version string for the server, e.g. 1.19.4-545
#[arg(env = "ALEX_SERVER_VERSION")]
pub server_version: String,
/// Server jar to execute /// Server jar to execute
#[arg( #[arg(
long, long,
@ -103,10 +103,4 @@ pub struct RunArgs {
} }
#[derive(Args)] #[derive(Args)]
pub struct BackupArgs { pub struct BackupArgs {}
/// Type of server
pub type_: ServerType,
/// Version string for the server, e.g. 1.19.4-545
#[arg(env = "ALEX_SERVER_VERSION")]
pub server_version: String,
}

View File

@ -25,7 +25,7 @@ fn backups_thread(counter: Arc<Mutex<server::ServerProcess>>, frequency: u64) {
fn command_run(cli: &Cli, args: &RunArgs) -> io::Result<()> { fn command_run(cli: &Cli, args: &RunArgs) -> io::Result<()> {
let (_, mut signals) = signals::install_signal_handlers()?; let (_, mut signals) = signals::install_signal_handlers()?;
let mut cmd = server::ServerCommand::new(args.type_, &args.server_version) let mut cmd = server::ServerCommand::new(cli.server, &cli.server_version)
.java(&args.java) .java(&args.java)
.jar(args.jar.clone()) .jar(args.jar.clone())
.config(cli.config.clone()) .config(cli.config.clone())
@ -60,10 +60,16 @@ fn command_run(cli: &Cli, args: &RunArgs) -> io::Result<()> {
} }
fn commands_backup(cli: &Cli, _args: &BackupArgs) -> io::Result<()> { 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( let mut manager = backup::Manager::new(
cli.backup.clone(), cli.backup.clone(),
cli.config.clone(), cli.config.clone(),
cli.world.clone(), cli.world.clone(),
metadata,
cli.chain_len, cli.chain_len,
cli.chains, cli.chains,
); );

View File

@ -1,14 +1,16 @@
use crate::backup::Manager as BackupManager; use crate::backup::Manager as BackupManager;
use crate::server::ServerProcess; use crate::server::ServerProcess;
use clap::ValueEnum; use clap::ValueEnum;
use serde::{Deserialize, Serialize};
use std::fmt; use std::fmt;
use std::fs::File; use std::fs::File;
use std::io::Write; use std::io::Write;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::process::{Command, Stdio}; use std::process::{Command, Stdio};
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)] #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Serialize, Deserialize)]
pub enum ServerType { pub enum ServerType {
Unknown,
Paper, Paper,
Forge, Forge,
Vanilla, Vanilla,
@ -17,6 +19,7 @@ pub enum ServerType {
impl fmt::Display for ServerType { impl fmt::Display for ServerType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = match self { let s = match self {
ServerType::Unknown => "Unknown",
ServerType::Paper => "PaperMC", ServerType::Paper => "PaperMC",
ServerType::Forge => "Forge", ServerType::Forge => "Forge",
ServerType::Vanilla => "Vanilla", ServerType::Vanilla => "Vanilla",
@ -26,6 +29,12 @@ impl fmt::Display for ServerType {
} }
} }
#[derive(Clone, Serialize, Deserialize)]
pub struct Metadata {
pub server_type: ServerType,
pub server_version: String,
}
pub struct ServerCommand { pub struct ServerCommand {
type_: ServerType, type_: ServerType,
version: String, version: String,
@ -187,10 +196,15 @@ impl ServerCommand {
} }
pub fn spawn(&mut self) -> std::io::Result<ServerProcess> { pub fn spawn(&mut self) -> std::io::Result<ServerProcess> {
let metadata = Metadata {
server_type: self.type_,
server_version: self.version.clone(),
};
let mut manager = BackupManager::new( let mut manager = BackupManager::new(
self.backup_dir.clone(), self.backup_dir.clone(),
self.config_dir.clone(), self.config_dir.clone(),
self.world_dir.clone(), self.world_dir.clone(),
metadata,
self.chain_len, self.chain_len,
self.chains_to_keep, self.chains_to_keep,
); );
@ -200,12 +214,7 @@ impl ServerCommand {
self.accept_eula()?; self.accept_eula()?;
let child = cmd.spawn()?; let child = cmd.spawn()?;
Ok(ServerProcess::new( Ok(ServerProcess::new(manager, child))
self.type_,
self.version.clone(),
manager,
child,
))
} }
} }

View File

@ -1,5 +1,5 @@
mod command; mod command;
mod process; mod process;
pub use command::{ServerCommand, ServerType}; pub use command::{Metadata, ServerCommand, ServerType};
pub use process::ServerProcess; pub use process::ServerProcess;

View File

@ -1,25 +1,16 @@
use crate::backup::Manager as BackupManager; use crate::backup::Manager as BackupManager;
use crate::server::ServerType; use crate::server::Metadata;
use std::io::Write; use std::io::Write;
use std::process::Child; use std::process::Child;
pub struct ServerProcess { pub struct ServerProcess {
type_: ServerType,
version: String,
child: Child, child: Child,
backups: BackupManager, backups: BackupManager<Metadata>,
} }
impl ServerProcess { impl ServerProcess {
pub fn new( pub fn new(manager: BackupManager<Metadata>, child: Child) -> ServerProcess {
type_: ServerType,
version: String,
manager: BackupManager,
child: Child,
) -> ServerProcess {
ServerProcess { ServerProcess {
type_,
version,
child, child,
backups: manager, backups: manager,
} }