feat: persistently store backup state
ci/woodpecker/push/lint Pipeline was successful Details
ci/woodpecker/push/clippy Pipeline failed Details
ci/woodpecker/push/build Pipeline was successful Details

incremental-backups
Jef Roosens 2023-06-15 20:36:46 +02:00
parent d204c68400
commit 8add96b39b
Signed by: Jef Roosens
GPG Key ID: B75D4F293C7052DB
5 changed files with 121 additions and 34 deletions

46
Cargo.lock generated
View File

@ -15,6 +15,8 @@ dependencies = [
"chrono", "chrono",
"clap", "clap",
"flate2", "flate2",
"serde",
"serde_json",
"signal-hook", "signal-hook",
"tar", "tar",
] ]
@ -123,6 +125,7 @@ dependencies = [
"iana-time-zone", "iana-time-zone",
"js-sys", "js-sys",
"num-traits", "num-traits",
"serde",
"time", "time",
"wasm-bindgen", "wasm-bindgen",
"winapi", "winapi",
@ -292,6 +295,12 @@ dependencies = [
"windows-sys", "windows-sys",
] ]
[[package]]
name = "itoa"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6"
[[package]] [[package]]
name = "js-sys" name = "js-sys"
version = "0.3.63" version = "0.3.63"
@ -384,6 +393,43 @@ dependencies = [
"windows-sys", "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]] [[package]]
name = "signal-hook" name = "signal-hook"
version = "0.3.15" version = "0.3.15"

View File

@ -12,10 +12,11 @@ edition = "2021"
tar = "0.4.38" tar = "0.4.38"
# Used to compress said tarballs using gzip # Used to compress said tarballs using gzip
flate2 = "1.0.26" flate2 = "1.0.26"
# Used for backup filenames chrono = { version = "0.4.26", features = ["serde"] }
chrono = "0.4.26"
clap = { version = "4.3.1", features = ["derive", "env"] } clap = { version = "4.3.1", features = ["derive", "env"] }
signal-hook = "0.3.15" signal-hook = "0.3.15"
serde = { version = "1.0.164", features = ["derive", "rc"] }
serde_json = "1.0.96"
[profile.release] [profile.release]
lto = "fat" lto = "fat"

View File

@ -1,6 +1,7 @@
use chrono::{Local, Utc}; use chrono::{Local, Utc};
use flate2::write::GzEncoder; use flate2::write::GzEncoder;
use flate2::Compression; use flate2::Compression;
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::fs::File; use std::fs::File;
use std::io; use std::io;
@ -60,7 +61,7 @@ fn not_modified_since<T: AsRef<Path>>(time: chrono::DateTime<Utc>, path: T) -> b
false false
} }
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq, Serialize, Deserialize)]
pub enum BackupType { pub enum BackupType {
Full, Full,
Incremental, Incremental,
@ -74,7 +75,7 @@ pub enum BackupError {
type BackupResult<T> = Result<T, BackupError>; type BackupResult<T> = Result<T, BackupError>;
/// Represents the changes relative to the previous backup /// Represents the changes relative to the previous backup
#[derive(Debug)] #[derive(Debug, Serialize, Deserialize)]
pub struct BackupDelta { pub struct BackupDelta {
/// What files were added/modified in each part of the tarball. /// What files were added/modified in each part of the tarball.
pub added: HashMap<PathBuf, HashSet<PathBuf>>, pub added: HashMap<PathBuf, HashSet<PathBuf>>,
@ -146,8 +147,9 @@ impl BackupDelta {
} }
/// Represents a successful backup /// Represents a successful backup
#[derive(Debug)] #[derive(Debug, Serialize, Deserialize)]
pub struct Backup { pub struct Backup {
#[serde(skip)]
previous: Option<Arc<Backup>>, previous: Option<Arc<Backup>>,
/// When the backup was started (also corresponds to the name) /// When the backup was started (also corresponds to the name)
start_time: chrono::DateTime<Utc>, start_time: chrono::DateTime<Utc>,
@ -174,6 +176,11 @@ impl Backup {
return Err(BackupError::NoFullAncestor); return Err(BackupError::NoFullAncestor);
} }
} }
pub fn set_previous(&mut self, previous: Arc<Self>) {
self.previous = Some(previous);
}
/// Create a new Full backup, populated with the given directories. /// Create a new Full backup, populated with the given directories.
/// ///
/// # Arguments /// # Arguments
@ -282,7 +289,9 @@ pub struct BackupManager {
} }
impl BackupManager { impl BackupManager {
pub fn open( const METADATA_FILE: &str = "alex.json";
pub fn new(
backup_dir: PathBuf, backup_dir: PathBuf,
config_dir: PathBuf, config_dir: PathBuf,
world_dir: PathBuf, world_dir: PathBuf,
@ -297,6 +306,18 @@ impl BackupManager {
} }
} }
pub fn open(
backup_dir: PathBuf,
config_dir: PathBuf,
world_dir: PathBuf,
max_backups: u64,
) -> std::io::Result<Self> {
let mut manager = Self::new(backup_dir, config_dir, world_dir, max_backups);
manager.load_json()?;
Ok(manager)
}
pub fn create_backup(&mut self) -> io::Result<()> { pub fn create_backup(&mut self) -> io::Result<()> {
let dirs = vec![ let dirs = vec![
(PathBuf::from("config"), self.config_dir.clone()), (PathBuf::from("config"), self.config_dir.clone()),
@ -310,6 +331,7 @@ impl BackupManager {
}; };
self.last_backup = Some(Arc::new(backup)); self.last_backup = Some(Arc::new(backup));
self.write_json()?;
Ok(()) Ok(())
} }
@ -337,4 +359,40 @@ impl BackupManager {
Ok(()) Ok(())
} }
pub fn write_json(&self) -> std::io::Result<()> {
// Put the backup chain into a list that can be serialized
let mut backups: Vec<Arc<Backup>> = Vec::new();
let mut backup_opt = &self.last_backup;
while let Some(backup) = backup_opt {
backups.insert(0, Arc::clone(backup));
backup_opt = &backup.previous;
}
let json_file = File::create(self.backup_dir.join(Self::METADATA_FILE))?;
serde_json::to_writer(json_file, &backups)?;
Ok(())
}
pub fn load_json(&mut self) -> std::io::Result<()> {
let json_file = File::open(self.backup_dir.join(Self::METADATA_FILE))?;
let mut backups: Vec<Arc<Backup>> = serde_json::from_reader(json_file)?;
if !backups.is_empty() {
for i in 1..backups.len() {
let previous = Arc::clone(&backups[i - 1]);
// We can unwrap here, as this function creates the first instance of each Arc,
// meaning we're definitely the only pointer.
Arc::get_mut(&mut backups[i])
.unwrap()
.set_previous(previous);
}
self.last_backup = Some(Arc::clone(backups.last().unwrap()));
}
Ok(())
}
} }

View File

@ -1,4 +1,4 @@
use crate::server::ServerProcess; use crate::server::{BackupManager, ServerProcess};
use clap::ValueEnum; use clap::ValueEnum;
use std::fmt; use std::fmt;
use std::fs::File; use std::fs::File;
@ -179,6 +179,12 @@ impl ServerCommand {
} }
pub fn spawn(&mut self) -> std::io::Result<ServerProcess> { pub fn spawn(&mut self) -> std::io::Result<ServerProcess> {
let manager = BackupManager::open(
self.backup_dir.clone(),
self.config_dir.clone(),
self.world_dir.clone(),
self.max_backups,
)?;
let mut cmd = self.create_cmd(); let mut cmd = self.create_cmd();
self.accept_eula()?; self.accept_eula()?;
let child = cmd.spawn()?; let child = cmd.spawn()?;
@ -186,10 +192,7 @@ impl ServerCommand {
Ok(ServerProcess::new( Ok(ServerProcess::new(
self.type_, self.type_,
self.version.clone(), self.version.clone(),
self.config_dir.clone(), manager,
self.world_dir.clone(),
self.backup_dir.clone(),
self.max_backups,
child, child,
)) ))
} }

View File

@ -1,18 +1,11 @@
use crate::server::BackupManager; use crate::server::BackupManager;
use crate::server::ServerType; use crate::server::ServerType;
use flate2::write::GzEncoder;
use flate2::Compression;
use std::io::Write; use std::io::Write;
use std::path::{Path, PathBuf};
use std::process::Child; use std::process::Child;
pub struct ServerProcess { pub struct ServerProcess {
type_: ServerType, type_: ServerType,
version: String, version: String,
config_dir: PathBuf,
world_dir: PathBuf,
backup_dir: PathBuf,
max_backups: u64,
child: Child, child: Child,
backups: BackupManager, backups: BackupManager,
} }
@ -21,28 +14,14 @@ impl ServerProcess {
pub fn new( pub fn new(
type_: ServerType, type_: ServerType,
version: String, version: String,
config_dir: PathBuf, manager: BackupManager,
world_dir: PathBuf,
backup_dir: PathBuf,
max_backups: u64,
child: Child, child: Child,
) -> ServerProcess { ) -> ServerProcess {
let backup_manager = BackupManager::open(
backup_dir.clone(),
config_dir.clone(),
world_dir.clone(),
max_backups,
);
ServerProcess { ServerProcess {
type_, type_,
version, version,
config_dir,
world_dir,
backup_dir,
max_backups,
child, child,
backups: backup_manager, backups: manager,
} }
} }