feat: add periodic backups thread
parent
7248ea8b90
commit
49546a449e
|
@ -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"
|
69
src/main.rs
69
src/main.rs
|
@ -1,9 +1,10 @@
|
||||||
mod server;
|
mod server;
|
||||||
|
|
||||||
use clap::{Parser, Subcommand};
|
use clap::Parser;
|
||||||
use server::{ServerCommand, ServerType};
|
use server::ServerType;
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
#[command(author, version, about, long_about = None)]
|
#[command(author, version, about, long_about = None)]
|
||||||
|
@ -15,43 +16,61 @@ struct Cli {
|
||||||
/// Server jar to execute
|
/// Server jar to execute
|
||||||
jar: PathBuf,
|
jar: PathBuf,
|
||||||
|
|
||||||
/// Directory where configs are stored, and where the server will run; defaults to the current
|
/// Directory where configs are stored, and where the server will run [default: .]
|
||||||
/// directory.
|
|
||||||
#[arg(long, value_name = "CONFIG_DIR")]
|
#[arg(long, value_name = "CONFIG_DIR")]
|
||||||
config: Option<PathBuf>,
|
config: Option<PathBuf>,
|
||||||
/// 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")]
|
#[arg(long, value_name = "WORLD_DIR")]
|
||||||
world: Option<PathBuf>,
|
world: Option<PathBuf>,
|
||||||
/// Directory where backups will be stored; defaults to ../backups
|
/// Directory where backups will be stored [default: ../backups]
|
||||||
#[arg(long, value_name = "BACKUP_DIR")]
|
#[arg(long, value_name = "BACKUP_DIR")]
|
||||||
backup: Option<PathBuf>,
|
backup: Option<PathBuf>,
|
||||||
|
/// 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
|
/// XMS value in megabytes for the server instance
|
||||||
#[arg(long)]
|
#[arg(long, default_value_t = 1024)]
|
||||||
xms: Option<u32>,
|
xms: u64,
|
||||||
/// XMX value for the server instance
|
/// XMX value in megabytes for the server instance
|
||||||
#[arg(long)]
|
#[arg(long, default_value_t = 2048)]
|
||||||
xmx: Option<u32>,
|
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<Mutex<server::ServerProcess>>, frequency: u64) {
|
||||||
|
loop {
|
||||||
|
std::thread::sleep(std::time::Duration::from_secs(frequency * 60));
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut server = counter.lock().unwrap();
|
||||||
|
server.backup();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let cli = Cli::parse();
|
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)
|
.jar(cli.jar)
|
||||||
.config(cli.config.unwrap_or(".".into()))
|
.config(cli.config.unwrap_or(".".into()))
|
||||||
.world(cli.world.unwrap_or("../worlds".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 {
|
let clone = Arc::clone(&counter);
|
||||||
cmd = cmd.xms(xms);
|
std::thread::spawn(move || backups_thread(clone, cli.frequency));
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(xmx) = cli.xmx {
|
|
||||||
cmd = cmd.xmx(xmx);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut server = cmd.spawn().expect("Failed to start server.");
|
|
||||||
|
|
||||||
let stdin = io::stdin();
|
let stdin = io::stdin();
|
||||||
let input = &mut String::new();
|
let input = &mut String::new();
|
||||||
|
@ -59,10 +78,12 @@ fn main() {
|
||||||
loop {
|
loop {
|
||||||
input.clear();
|
input.clear();
|
||||||
stdin.read_line(input);
|
stdin.read_line(input);
|
||||||
println!("input: {}", input.trim());
|
{
|
||||||
|
let mut server = counter.lock().unwrap();
|
||||||
if let Err(e) = server.send_command(input) {
|
if let Err(e) = server.send_command(input) {
|
||||||
println!("{}", e);
|
println!("{}", e);
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if input.trim() == "stop" {
|
if input.trim() == "stop" {
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -4,7 +4,7 @@ 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::{Child, Command, Stdio};
|
use std::process::{Command, Stdio};
|
||||||
|
|
||||||
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
|
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
|
||||||
pub enum ServerType {
|
pub enum ServerType {
|
||||||
|
@ -33,8 +33,9 @@ pub struct ServerCommand {
|
||||||
config_dir: PathBuf,
|
config_dir: PathBuf,
|
||||||
world_dir: PathBuf,
|
world_dir: PathBuf,
|
||||||
backup_dir: PathBuf,
|
backup_dir: PathBuf,
|
||||||
xms: u32,
|
xms: u64,
|
||||||
xmx: u32,
|
xmx: u64,
|
||||||
|
max_backups: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ServerCommand {
|
impl ServerCommand {
|
||||||
|
@ -49,6 +50,7 @@ impl ServerCommand {
|
||||||
backup_dir: PathBuf::from("backups"),
|
backup_dir: PathBuf::from("backups"),
|
||||||
xms: 1024,
|
xms: 1024,
|
||||||
xmx: 2048,
|
xmx: 2048,
|
||||||
|
max_backups: 7,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -79,21 +81,26 @@ impl ServerCommand {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn xms(mut self, v: u32) -> Self {
|
pub fn xms(mut self, v: u64) -> Self {
|
||||||
self.xms = v;
|
self.xms = v;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn xmx(mut self, v: u32) -> Self {
|
pub fn xmx(mut self, v: u64) -> Self {
|
||||||
self.xmx = v;
|
self.xmx = v;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn max_backups(mut self, v: u64) -> Self {
|
||||||
|
self.max_backups = v;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
fn accept_eula(&self) -> std::io::Result<()> {
|
fn accept_eula(&self) -> std::io::Result<()> {
|
||||||
let mut eula_path = self.config_dir.clone();
|
let mut eula_path = self.config_dir.clone();
|
||||||
eula_path.push("eula.txt");
|
eula_path.push("eula.txt");
|
||||||
let mut eula_file = File::create(eula_path)?;
|
let mut eula_file = File::create(eula_path)?;
|
||||||
eula_file.write(b"eula=true")?;
|
eula_file.write_all(b"eula=true")?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -123,6 +130,7 @@ impl ServerCommand {
|
||||||
config_dir,
|
config_dir,
|
||||||
world_dir,
|
world_dir,
|
||||||
backup_dir,
|
backup_dir,
|
||||||
|
self.max_backups,
|
||||||
child,
|
child,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,15 @@
|
||||||
use crate::server::ServerType;
|
use crate::server::ServerType;
|
||||||
use flate2::write::GzEncoder;
|
use flate2::write::GzEncoder;
|
||||||
use flate2::Compression;
|
use flate2::Compression;
|
||||||
use std::fs::File;
|
|
||||||
use std::io;
|
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::PathBuf;
|
||||||
use std::process::Child;
|
use std::process::Child;
|
||||||
use std::process::{Command, Stdio};
|
|
||||||
|
#[link(name = "c")]
|
||||||
|
extern "C" {
|
||||||
|
fn geteuid() -> u32;
|
||||||
|
fn getegid() -> u32;
|
||||||
|
}
|
||||||
|
|
||||||
pub struct ServerProcess {
|
pub struct ServerProcess {
|
||||||
type_: ServerType,
|
type_: ServerType,
|
||||||
|
@ -14,6 +17,7 @@ pub struct ServerProcess {
|
||||||
config_dir: PathBuf,
|
config_dir: PathBuf,
|
||||||
world_dir: PathBuf,
|
world_dir: PathBuf,
|
||||||
backup_dir: PathBuf,
|
backup_dir: PathBuf,
|
||||||
|
max_backups: u64,
|
||||||
child: Child,
|
child: Child,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,6 +28,7 @@ impl ServerProcess {
|
||||||
config_dir: PathBuf,
|
config_dir: PathBuf,
|
||||||
world_dir: PathBuf,
|
world_dir: PathBuf,
|
||||||
backup_dir: PathBuf,
|
backup_dir: PathBuf,
|
||||||
|
max_backups: u64,
|
||||||
child: Child,
|
child: Child,
|
||||||
) -> ServerProcess {
|
) -> ServerProcess {
|
||||||
ServerProcess {
|
ServerProcess {
|
||||||
|
@ -32,6 +37,7 @@ impl ServerProcess {
|
||||||
config_dir,
|
config_dir,
|
||||||
world_dir,
|
world_dir,
|
||||||
backup_dir,
|
backup_dir,
|
||||||
|
max_backups,
|
||||||
child,
|
child,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -61,17 +67,41 @@ impl ServerProcess {
|
||||||
Ok(())
|
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<()> {
|
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
|
// Make sure the server isn't modifying the files during the backup
|
||||||
self.custom("save-off")?;
|
self.custom("save-off")?;
|
||||||
self.custom("save-all")?;
|
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
|
// Create a gzip-compressed tarball of the worlds folder
|
||||||
let filename = format!(
|
let filename = format!(
|
||||||
"{}",
|
"{}",
|
||||||
chrono::offset::Local::now().format("%Y-%m-%d_%H-%M-%S.tar.gz")
|
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 tar_gz = std::fs::File::create(path)?;
|
||||||
let enc = GzEncoder::new(tar_gz, Compression::default());
|
let enc = GzEncoder::new(tar_gz, Compression::default());
|
||||||
let mut tar = tar::Builder::new(enc);
|
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
|
// We add a file to the backup describing for what version it was made
|
||||||
let info = format!("{} {}", self.type_, self.version);
|
let info = format!("{} {}", self.type_, self.version);
|
||||||
let info_bytes = info.as_bytes();
|
let info_bytes = info.as_bytes();
|
||||||
|
|
||||||
let mut header = tar::Header::new_gnu();
|
let mut header = tar::Header::new_gnu();
|
||||||
header.set_size(info_bytes.len().try_into().unwrap());
|
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_data(&mut header, "info.txt", info_bytes)?;
|
||||||
|
|
||||||
// tar.append_dir_all("config", &self.config_dir)?;
|
// tar.append_dir_all("config", &self.config_dir)?;
|
||||||
//
|
|
||||||
// Backup file gets finalized in the drop
|
// 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::<Vec<PathBuf>>();
|
||||||
|
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(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue