feat: restore backup chains using cli commands
ci/woodpecker/push/lint Pipeline was successful Details
ci/woodpecker/push/clippy Pipeline was successful Details
ci/woodpecker/push/build Pipeline was successful Details

export-backup
Jef Roosens 2023-06-23 22:47:38 +02:00
parent e373fc85f1
commit a4e2a1276f
Signed by: Jef Roosens
GPG Key ID: B75D4F293C7052DB
7 changed files with 178 additions and 13 deletions

View File

@ -9,7 +9,7 @@ use std::path::PathBuf;
/// Manages a collection of backup layers, allowing them to be utilized as a single object.
pub struct MetaManager<T>
where
T: Clone + Serialize + for<'de> Deserialize<'de>,
T: Clone + Serialize + for<'de> Deserialize<'de> + std::fmt::Debug,
{
backup_dir: PathBuf,
dirs: Vec<(PathBuf, PathBuf)>,
@ -19,7 +19,7 @@ where
impl<T> MetaManager<T>
where
T: Clone + Serialize + for<'de> Deserialize<'de>,
T: Clone + Serialize + for<'de> Deserialize<'de> + std::fmt::Debug,
{
pub fn new<P: Into<PathBuf>>(
backup_dir: P,
@ -116,4 +116,15 @@ where
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))
}
}

View File

@ -5,6 +5,8 @@ pub use config::ManagerConfig;
pub use meta::MetaManager;
use super::Backup;
use crate::other;
use chrono::SubsecRound;
use chrono::Utc;
use serde::Deserialize;
use serde::Serialize;
@ -15,7 +17,7 @@ use std::path::PathBuf;
/// Manages a single backup layer consisting of one or more chains of backups.
pub struct Manager<T>
where
T: Clone + Serialize + for<'de> Deserialize<'de>,
T: Clone + Serialize + for<'de> Deserialize<'de> + std::fmt::Debug,
{
backup_dir: PathBuf,
dirs: Vec<(PathBuf, PathBuf)>,
@ -28,7 +30,7 @@ where
impl<T> Manager<T>
where
T: Clone + Serialize + for<'de> Deserialize<'de>,
T: Clone + Serialize + for<'de> Deserialize<'de> + std::fmt::Debug,
{
const METADATA_FILE: &str = "alex.json";
@ -156,4 +158,27 @@ where
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."))
}
}

View File

@ -8,6 +8,7 @@ pub use manager::ManagerConfig;
pub use manager::MetaManager;
use chrono::Utc;
use flate2::read::GzDecoder;
use flate2::write::GzEncoder;
use flate2::Compression;
use path::PathExt;
@ -24,7 +25,7 @@ pub enum BackupType {
}
/// Represents a successful backup
#[derive(Serialize, Deserialize)]
#[derive(Serialize, Deserialize, Debug)]
pub struct Backup<T: Clone> {
/// When the backup was started (also corresponds to the name)
pub start_time: chrono::DateTime<Utc>,
@ -36,6 +37,8 @@ pub struct Backup<T: Clone> {
}
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.
pub fn path<P: AsRef<Path>>(backup_dir: P, start_time: chrono::DateTime<Utc>) -> PathBuf {
let backup_dir = backup_dir.as_ref();
@ -46,8 +49,6 @@ impl Backup<()> {
}
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) {
self.metadata = Some(metadata);
}
@ -159,4 +160,48 @@ impl<T: Clone> Backup<T> {
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(())
}
}

View File

@ -1,11 +1,19 @@
use crate::backup::Backup;
use crate::cli::Cli;
use crate::other;
use chrono::{TimeZone, Utc};
use clap::{Args, Subcommand};
use std::io;
use std::path::PathBuf;
#[derive(Subcommand)]
pub enum BackupCommands {
Create(BackupCreateArgs),
/// List all tracked backups
List,
/// Manually create a new backup
Create(BackupCreateArgs),
/// Restore a backup
Restore(BackupRestoreArgs),
}
#[derive(Args)]
@ -20,11 +28,21 @@ pub struct BackupCreateArgs {
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),
}
}
}
@ -40,3 +58,65 @@ impl BackupCreateArgs {
}
}
}
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"))
}
}
}

View File

@ -81,10 +81,10 @@ impl Cli {
server_version: self.server_version.clone(),
};
let dirs = vec![
(PathBuf::from("config"), self.config.clone()),
(PathBuf::from("worlds"), self.world.clone()),
(PathBuf::from("config"), self.config.canonicalize()?),
(PathBuf::from("worlds"), self.world.canonicalize()?),
];
let mut meta = MetaManager::new(self.backup.clone(), dirs, metadata);
let mut meta = MetaManager::new(self.backup.canonicalize()?, dirs, metadata);
meta.add_all(&self.layers)?;
Ok(meta)

View File

@ -8,6 +8,10 @@ use crate::cli::Cli;
use clap::Parser;
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<()> {
// let metadata = server::Metadata {
// server_type: cli.server,

View File

@ -8,7 +8,7 @@ use clap::ValueEnum;
use serde::{Deserialize, Serialize};
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 {
Unknown,
Paper,
@ -29,7 +29,7 @@ impl fmt::Display for ServerType {
}
}
#[derive(Clone, Serialize, Deserialize)]
#[derive(Clone, Serialize, Deserialize, Debug)]
pub struct Metadata {
pub server_type: ServerType,
pub server_version: String,