Compare commits
2 Commits
1cfe13674d
...
a4e2a1276f
| Author | SHA1 | Date |
|---|---|---|
|
|
a4e2a1276f | |
|
|
e373fc85f1 |
|
|
@ -1,3 +1,3 @@
|
||||||
[alias]
|
[alias]
|
||||||
runs = "run -- run --config data/config --backup data/backups --world data/worlds --jar paper-1.19.4-550.jar --layers 2min,2,4,4;3min,3,2,2"
|
runs = "run -- --config data/config --backup data/backups --world data/worlds --layers 2min,2,4,4;3min,3,2,2"
|
||||||
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"
|
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"
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ use std::path::PathBuf;
|
||||||
/// Manages a collection of backup layers, allowing them to be utilized as a single object.
|
/// Manages a collection of backup layers, allowing them to be utilized as a single object.
|
||||||
pub struct MetaManager<T>
|
pub struct MetaManager<T>
|
||||||
where
|
where
|
||||||
T: Clone + Serialize + for<'de> Deserialize<'de>,
|
T: Clone + Serialize + for<'de> Deserialize<'de> + std::fmt::Debug,
|
||||||
{
|
{
|
||||||
backup_dir: PathBuf,
|
backup_dir: PathBuf,
|
||||||
dirs: Vec<(PathBuf, PathBuf)>,
|
dirs: Vec<(PathBuf, PathBuf)>,
|
||||||
|
|
@ -19,7 +19,7 @@ where
|
||||||
|
|
||||||
impl<T> MetaManager<T>
|
impl<T> MetaManager<T>
|
||||||
where
|
where
|
||||||
T: Clone + Serialize + for<'de> Deserialize<'de>,
|
T: Clone + Serialize + for<'de> Deserialize<'de> + std::fmt::Debug,
|
||||||
{
|
{
|
||||||
pub fn new<P: Into<PathBuf>>(
|
pub fn new<P: Into<PathBuf>>(
|
||||||
backup_dir: P,
|
backup_dir: P,
|
||||||
|
|
@ -101,4 +101,30 @@ where
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create a manual backup for a specific layer
|
||||||
|
pub fn create_backup(&mut self, layer: &str) -> Option<io::Result<()>> {
|
||||||
|
if let Some(manager) = self.managers.get_mut(layer) {
|
||||||
|
let mut res = manager.create_backup();
|
||||||
|
|
||||||
|
if res.is_ok() {
|
||||||
|
res = manager.remove_old_backups();
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(res)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Restore a backup for a specific layer
|
||||||
|
pub fn restore_backup(
|
||||||
|
&self,
|
||||||
|
layer: &str,
|
||||||
|
start_time: chrono::DateTime<Utc>,
|
||||||
|
) -> Option<io::Result<()>> {
|
||||||
|
self.managers
|
||||||
|
.get(layer)
|
||||||
|
.map(|manager| manager.restore_backup(start_time))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@ pub use config::ManagerConfig;
|
||||||
pub use meta::MetaManager;
|
pub use meta::MetaManager;
|
||||||
|
|
||||||
use super::Backup;
|
use super::Backup;
|
||||||
|
use crate::other;
|
||||||
|
use chrono::SubsecRound;
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
@ -15,7 +17,7 @@ use std::path::PathBuf;
|
||||||
/// Manages a single backup layer consisting of one or more chains of backups.
|
/// Manages a single backup layer consisting of one or more chains of backups.
|
||||||
pub struct Manager<T>
|
pub struct Manager<T>
|
||||||
where
|
where
|
||||||
T: Clone + Serialize + for<'de> Deserialize<'de>,
|
T: Clone + Serialize + for<'de> Deserialize<'de> + std::fmt::Debug,
|
||||||
{
|
{
|
||||||
backup_dir: PathBuf,
|
backup_dir: PathBuf,
|
||||||
dirs: Vec<(PathBuf, PathBuf)>,
|
dirs: Vec<(PathBuf, PathBuf)>,
|
||||||
|
|
@ -28,7 +30,7 @@ where
|
||||||
|
|
||||||
impl<T> Manager<T>
|
impl<T> Manager<T>
|
||||||
where
|
where
|
||||||
T: Clone + Serialize + for<'de> Deserialize<'de>,
|
T: Clone + Serialize + for<'de> Deserialize<'de> + std::fmt::Debug,
|
||||||
{
|
{
|
||||||
const METADATA_FILE: &str = "alex.json";
|
const METADATA_FILE: &str = "alex.json";
|
||||||
|
|
||||||
|
|
@ -156,4 +158,27 @@ where
|
||||||
|
|
||||||
chrono::offset::Utc::now()
|
chrono::offset::Utc::now()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Restore the backup with the given start time by restoring its chain up to and including the
|
||||||
|
/// backup, in order.
|
||||||
|
pub fn restore_backup(&self, start_time: chrono::DateTime<Utc>) -> io::Result<()> {
|
||||||
|
// Iterate over each chain, skipping elements until the element with the given start time
|
||||||
|
// is possibly found.
|
||||||
|
for chain in &self.chains {
|
||||||
|
// If we find the element in the chain, restore the entire chain up to and including
|
||||||
|
// the element
|
||||||
|
if let Some(index) = chain
|
||||||
|
.iter()
|
||||||
|
.position(|b| b.start_time.trunc_subsecs(0) == start_time)
|
||||||
|
{
|
||||||
|
for backup in chain.iter().take(index + 1) {
|
||||||
|
backup.restore(&self.backup_dir, &self.dirs)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(other("Unknown backup."))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ pub use manager::ManagerConfig;
|
||||||
pub use manager::MetaManager;
|
pub use manager::MetaManager;
|
||||||
|
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
|
use flate2::read::GzDecoder;
|
||||||
use flate2::write::GzEncoder;
|
use flate2::write::GzEncoder;
|
||||||
use flate2::Compression;
|
use flate2::Compression;
|
||||||
use path::PathExt;
|
use path::PathExt;
|
||||||
|
|
@ -24,7 +25,7 @@ pub enum BackupType {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Represents a successful backup
|
/// Represents a successful backup
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
pub struct Backup<T: Clone> {
|
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>,
|
||||||
|
|
@ -36,6 +37,8 @@ pub struct Backup<T: Clone> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Backup<()> {
|
impl Backup<()> {
|
||||||
|
pub const FILENAME_FORMAT: &str = "%Y-%m-%d_%H-%M-%S.tar.gz";
|
||||||
|
|
||||||
/// Return the path to a backup file by properly formatting the data.
|
/// 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 {
|
pub fn path<P: AsRef<Path>>(backup_dir: P, start_time: chrono::DateTime<Utc>) -> PathBuf {
|
||||||
let backup_dir = backup_dir.as_ref();
|
let backup_dir = backup_dir.as_ref();
|
||||||
|
|
@ -46,8 +49,6 @@ impl Backup<()> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Clone> Backup<T> {
|
impl<T: Clone> Backup<T> {
|
||||||
const FILENAME_FORMAT: &str = "%Y-%m-%d_%H-%M-%S.tar.gz";
|
|
||||||
|
|
||||||
pub fn set_metadata(&mut self, metadata: T) {
|
pub fn set_metadata(&mut self, metadata: T) {
|
||||||
self.metadata = Some(metadata);
|
self.metadata = Some(metadata);
|
||||||
}
|
}
|
||||||
|
|
@ -159,4 +160,48 @@ impl<T: Clone> Backup<T> {
|
||||||
metadata: None,
|
metadata: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn restore<P: AsRef<Path>>(
|
||||||
|
&self,
|
||||||
|
backup_dir: P,
|
||||||
|
dirs: &Vec<(PathBuf, PathBuf)>,
|
||||||
|
) -> io::Result<()> {
|
||||||
|
let path = Backup::path(backup_dir, self.start_time);
|
||||||
|
let tar_gz = File::open(path)?;
|
||||||
|
let enc = GzDecoder::new(tar_gz);
|
||||||
|
let mut ar = tar::Archive::new(enc);
|
||||||
|
|
||||||
|
// Unpack each file by matching it with one of the destination directories and extracting
|
||||||
|
// it to the right path
|
||||||
|
for entry in ar.entries()? {
|
||||||
|
let mut entry = entry?;
|
||||||
|
let entry_path_in_tar = entry.path()?.to_path_buf();
|
||||||
|
|
||||||
|
for (path_in_tar, dst_dir) in dirs {
|
||||||
|
if entry_path_in_tar.starts_with(path_in_tar) {
|
||||||
|
let dst_path =
|
||||||
|
dst_dir.join(entry_path_in_tar.strip_prefix(path_in_tar).unwrap());
|
||||||
|
|
||||||
|
// Ensure all parent directories are present
|
||||||
|
std::fs::create_dir_all(dst_path.parent().unwrap())?;
|
||||||
|
|
||||||
|
entry.unpack(dst_path)?;
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove any files
|
||||||
|
for (path_in_tar, dst_dir) in dirs {
|
||||||
|
if let Some(removed) = self.delta.removed.get(path_in_tar) {
|
||||||
|
for path in removed {
|
||||||
|
let dst_path = dst_dir.join(path);
|
||||||
|
std::fs::remove_file(dst_path)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,19 @@
|
||||||
|
use crate::backup::Backup;
|
||||||
|
use crate::cli::Cli;
|
||||||
|
use crate::other;
|
||||||
|
use chrono::{TimeZone, Utc};
|
||||||
use clap::{Args, Subcommand};
|
use clap::{Args, Subcommand};
|
||||||
|
use std::io;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
#[derive(Subcommand)]
|
#[derive(Subcommand)]
|
||||||
pub enum BackupCommands {
|
pub enum BackupCommands {
|
||||||
|
/// List all tracked backups
|
||||||
List,
|
List,
|
||||||
|
/// Manually create a new backup
|
||||||
|
Create(BackupCreateArgs),
|
||||||
|
/// Restore a backup
|
||||||
|
Restore(BackupRestoreArgs),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Args)]
|
#[derive(Args)]
|
||||||
|
|
@ -10,3 +21,102 @@ pub struct BackupArgs {
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
pub command: BackupCommands,
|
pub command: BackupCommands,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Args)]
|
||||||
|
pub struct BackupCreateArgs {
|
||||||
|
/// What layer to create a backup in
|
||||||
|
layer: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Args)]
|
||||||
|
pub struct BackupRestoreArgs {
|
||||||
|
/// Path to the backup inside the backup directory
|
||||||
|
path: PathBuf,
|
||||||
|
/// Whether to overwrite the contents of the existing directories
|
||||||
|
#[arg(short, long, default_value_t = false)]
|
||||||
|
force: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BackupArgs {
|
||||||
|
pub fn run(&self, cli: &Cli) -> io::Result<()> {
|
||||||
|
match &self.command {
|
||||||
|
BackupCommands::Create(args) => args.run(cli),
|
||||||
|
BackupCommands::List => Ok(()),
|
||||||
|
BackupCommands::Restore(args) => args.run(cli),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BackupCreateArgs {
|
||||||
|
pub fn run(&self, cli: &Cli) -> io::Result<()> {
|
||||||
|
let mut meta = cli.meta()?;
|
||||||
|
|
||||||
|
if let Some(res) = meta.create_backup(&self.layer) {
|
||||||
|
res
|
||||||
|
} else {
|
||||||
|
Err(io::Error::new(io::ErrorKind::Other, "Unknown layer"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BackupRestoreArgs {
|
||||||
|
pub fn run(&self, cli: &Cli) -> io::Result<()> {
|
||||||
|
let backup_dir = cli.backup.canonicalize()?;
|
||||||
|
|
||||||
|
// Parse input path
|
||||||
|
let path = self.path.canonicalize()?;
|
||||||
|
|
||||||
|
if !path.starts_with(&backup_dir) {
|
||||||
|
return Err(other("Provided file is not inside the backup directory."));
|
||||||
|
}
|
||||||
|
|
||||||
|
let layer = if let Some(parent) = path.parent() {
|
||||||
|
// Backup files should be stored nested inside a layer's folder
|
||||||
|
if parent != backup_dir {
|
||||||
|
parent.file_name().unwrap().to_string_lossy()
|
||||||
|
} else {
|
||||||
|
return Err(other("Invalid path."));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Err(other("Invalid path."));
|
||||||
|
};
|
||||||
|
|
||||||
|
let timestamp = if let Some(filename) = path.file_name() {
|
||||||
|
Utc.datetime_from_str(&filename.to_string_lossy(), Backup::FILENAME_FORMAT)
|
||||||
|
.map_err(|_| other("Invalid filename."))?
|
||||||
|
} else {
|
||||||
|
return Err(other("Invalid filename."));
|
||||||
|
};
|
||||||
|
|
||||||
|
let meta = cli.meta()?;
|
||||||
|
|
||||||
|
// Clear previous contents of directories
|
||||||
|
let mut entries = cli
|
||||||
|
.config
|
||||||
|
.canonicalize()?
|
||||||
|
.read_dir()?
|
||||||
|
.chain(cli.world.canonicalize()?.read_dir()?)
|
||||||
|
.peekable();
|
||||||
|
|
||||||
|
if entries.peek().is_some() && !self.force {
|
||||||
|
return Err(other("Output directories are not empty. If you wish to overwrite these contents, use the force flag."));
|
||||||
|
}
|
||||||
|
|
||||||
|
for entry in entries {
|
||||||
|
let path = entry?.path();
|
||||||
|
|
||||||
|
if path.is_dir() {
|
||||||
|
std::fs::remove_dir_all(path)?;
|
||||||
|
} else {
|
||||||
|
std::fs::remove_file(path)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore the backup
|
||||||
|
if let Some(res) = meta.restore_backup(&layer, timestamp) {
|
||||||
|
res
|
||||||
|
} else {
|
||||||
|
Err(other("Unknown layer"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
mod backup;
|
mod backup;
|
||||||
mod run;
|
mod run;
|
||||||
|
|
||||||
|
pub use crate::backup::MetaManager;
|
||||||
|
pub use crate::server::Metadata;
|
||||||
pub use backup::{BackupArgs, BackupCommands};
|
pub use backup::{BackupArgs, BackupCommands};
|
||||||
pub use run::RunArgs;
|
pub use run::RunArgs;
|
||||||
|
|
||||||
|
|
@ -68,7 +70,23 @@ impl Cli {
|
||||||
pub fn run(&self) -> io::Result<()> {
|
pub fn run(&self) -> io::Result<()> {
|
||||||
match &self.command {
|
match &self.command {
|
||||||
Commands::Run(args) => args.run(self),
|
Commands::Run(args) => args.run(self),
|
||||||
Commands::Backup(_) => Ok(()),
|
Commands::Backup(args) => args.run(self),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Convenience method to initialize backup manager from the cli arguments
|
||||||
|
pub fn meta(&self) -> io::Result<MetaManager<Metadata>> {
|
||||||
|
let metadata = Metadata {
|
||||||
|
server_type: self.server,
|
||||||
|
server_version: self.server_version.clone(),
|
||||||
|
};
|
||||||
|
let dirs = vec![
|
||||||
|
(PathBuf::from("config"), self.config.canonicalize()?),
|
||||||
|
(PathBuf::from("worlds"), self.world.canonicalize()?),
|
||||||
|
];
|
||||||
|
let mut meta = MetaManager::new(self.backup.canonicalize()?, dirs, metadata);
|
||||||
|
meta.add_all(&self.layers)?;
|
||||||
|
|
||||||
|
Ok(meta)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,10 @@ use crate::cli::Cli;
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use std::io;
|
use std::io;
|
||||||
|
|
||||||
|
pub fn other(msg: &str) -> io::Error {
|
||||||
|
io::Error::new(io::ErrorKind::Other, msg)
|
||||||
|
}
|
||||||
|
|
||||||
// fn commands_backup(cli: &Cli, args: &BackupArgs) -> io::Result<()> {
|
// fn commands_backup(cli: &Cli, args: &BackupArgs) -> io::Result<()> {
|
||||||
// let metadata = server::Metadata {
|
// let metadata = server::Metadata {
|
||||||
// server_type: cli.server,
|
// server_type: cli.server,
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ use clap::ValueEnum;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
||||||
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Serialize, Deserialize)]
|
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Serialize, Deserialize, Debug)]
|
||||||
pub enum ServerType {
|
pub enum ServerType {
|
||||||
Unknown,
|
Unknown,
|
||||||
Paper,
|
Paper,
|
||||||
|
|
@ -29,7 +29,7 @@ impl fmt::Display for ServerType {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Serialize, Deserialize)]
|
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||||
pub struct Metadata {
|
pub struct Metadata {
|
||||||
pub server_type: ServerType,
|
pub server_type: ServerType,
|
||||||
pub server_version: String,
|
pub server_version: String,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue