283 lines
8.1 KiB
Rust
283 lines
8.1 KiB
Rust
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::{Path, PathBuf};
|
|
|
|
#[derive(Subcommand)]
|
|
pub enum BackupCommands {
|
|
/// List all tracked backups
|
|
List(BackupListArgs),
|
|
/// Manually create a new backup
|
|
Create(BackupCreateArgs),
|
|
/// Restore a backup including all of its required predecessors
|
|
Restore(BackupRestoreArgs),
|
|
/// Export a backup into a full archive
|
|
Export(BackupExportArgs),
|
|
/// Extract a single backup; meant as a convenience method for working with the output of
|
|
/// `export`
|
|
Extract(BackupExtractArgs),
|
|
}
|
|
|
|
#[derive(Args)]
|
|
pub struct BackupArgs {
|
|
#[command(subcommand)]
|
|
pub command: BackupCommands,
|
|
}
|
|
|
|
#[derive(Args)]
|
|
pub struct BackupCreateArgs {
|
|
/// What layer to create a backup in
|
|
layer: String,
|
|
}
|
|
|
|
#[derive(Args)]
|
|
pub struct BackupListArgs {
|
|
/// What layer to list
|
|
layer: Option<String>,
|
|
}
|
|
|
|
#[derive(Args)]
|
|
pub struct BackupRestoreArgs {
|
|
/// Path to the backup inside the backup directory to restore
|
|
path: PathBuf,
|
|
/// Directory to store config in
|
|
output_config: PathBuf,
|
|
/// Directory to store worlds in
|
|
output_worlds: PathBuf,
|
|
/// Whether to overwrite the contents of the output directories
|
|
#[arg(short, long, default_value_t = false)]
|
|
force: bool,
|
|
/// Create output directories if they don't exist
|
|
#[arg(short, long, default_value_t = false)]
|
|
make: bool,
|
|
}
|
|
|
|
#[derive(Args)]
|
|
pub struct BackupExportArgs {
|
|
/// Path to the backup inside the backup directory to export
|
|
path: PathBuf,
|
|
/// Path to store the exported archive
|
|
output: PathBuf,
|
|
/// Create output directories if they don't exist
|
|
#[arg(short, long, default_value_t = false)]
|
|
make: bool,
|
|
}
|
|
|
|
#[derive(Args)]
|
|
pub struct BackupExtractArgs {
|
|
/// Path to the backup to extract
|
|
path: PathBuf,
|
|
/// Directory to store config in
|
|
output_config: PathBuf,
|
|
/// Directory to store worlds in
|
|
output_worlds: PathBuf,
|
|
/// Whether to overwrite the contents of the output directories
|
|
#[arg(short, long, default_value_t = false)]
|
|
force: bool,
|
|
/// Create output directories if they don't exist
|
|
#[arg(short, long, default_value_t = false)]
|
|
make: bool,
|
|
}
|
|
|
|
impl BackupArgs {
|
|
pub fn run(&self, cli: &Cli) -> io::Result<()> {
|
|
match &self.command {
|
|
BackupCommands::Create(args) => args.run(cli),
|
|
BackupCommands::List(args) => args.run(cli),
|
|
BackupCommands::Restore(args) => args.run(cli),
|
|
BackupCommands::Export(args) => args.run(cli),
|
|
BackupCommands::Extract(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 BackupListArgs {
|
|
pub fn run(&self, cli: &Cli) -> io::Result<()> {
|
|
let meta = cli.meta()?;
|
|
|
|
// A bit scuffed? Sure
|
|
for (name, manager) in meta
|
|
.managers()
|
|
.iter()
|
|
.filter(|(name, _)| self.layer.is_none() || &self.layer.as_ref().unwrap() == name)
|
|
{
|
|
println!("{}", name);
|
|
|
|
for chain in manager.chains().iter().filter(|c| !c.is_empty()) {
|
|
let mut iter = chain.iter();
|
|
println!(" {}", iter.next().unwrap());
|
|
|
|
for backup in iter {
|
|
println!(" {}", backup);
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// Tries to parse the given path as the path to a backup inside the backup directory with a
|
|
/// formatted timestamp.
|
|
fn parse_backup_path(
|
|
backup_dir: &Path,
|
|
backup_path: &Path,
|
|
) -> io::Result<(String, chrono::DateTime<Utc>)> {
|
|
if !backup_path.starts_with(backup_dir) {
|
|
return Err(other("Provided file is not inside the backup directory."));
|
|
}
|
|
|
|
let layer = if let Some(parent) = backup_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) = backup_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."));
|
|
};
|
|
|
|
Ok((layer.to_string(), timestamp))
|
|
}
|
|
|
|
impl BackupRestoreArgs {
|
|
pub fn run(&self, cli: &Cli) -> io::Result<()> {
|
|
let backup_dir = cli.backup.canonicalize()?;
|
|
|
|
// Create directories if needed
|
|
if self.make {
|
|
std::fs::create_dir_all(&self.output_config)?;
|
|
std::fs::create_dir_all(&self.output_worlds)?;
|
|
}
|
|
|
|
let output_config = self.output_config.canonicalize()?;
|
|
let output_worlds = self.output_worlds.canonicalize()?;
|
|
|
|
// Parse input path
|
|
let backup_path = self.path.canonicalize()?;
|
|
let (layer, timestamp) = parse_backup_path(&backup_dir, &backup_path)?;
|
|
|
|
let meta = cli.meta()?;
|
|
|
|
// Clear previous contents of directories
|
|
let mut entries = output_config
|
|
.read_dir()?
|
|
.chain(output_worlds.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)?;
|
|
}
|
|
}
|
|
|
|
let dirs = vec![
|
|
(PathBuf::from("config"), output_config),
|
|
(PathBuf::from("worlds"), output_worlds),
|
|
];
|
|
|
|
// Restore the backup
|
|
if let Some(res) = meta.restore_backup(&layer, timestamp, &dirs) {
|
|
res
|
|
} else {
|
|
Err(other("Unknown layer"))
|
|
}
|
|
}
|
|
}
|
|
|
|
impl BackupExportArgs {
|
|
pub fn run(&self, cli: &Cli) -> io::Result<()> {
|
|
let backup_dir = cli.backup.canonicalize()?;
|
|
|
|
if self.make {
|
|
if let Some(parent) = &self.output.parent() {
|
|
std::fs::create_dir_all(parent)?;
|
|
}
|
|
}
|
|
|
|
// Parse input path
|
|
let backup_path = self.path.canonicalize()?;
|
|
let (layer, timestamp) = parse_backup_path(&backup_dir, &backup_path)?;
|
|
|
|
let meta = cli.meta()?;
|
|
|
|
if let Some(res) = meta.export_backup(&layer, timestamp, &self.output) {
|
|
res
|
|
} else {
|
|
Err(other("Unknown layer"))
|
|
}
|
|
}
|
|
}
|
|
|
|
impl BackupExtractArgs {
|
|
pub fn run(&self, _cli: &Cli) -> io::Result<()> {
|
|
// Create directories if needed
|
|
if self.make {
|
|
std::fs::create_dir_all(&self.output_config)?;
|
|
std::fs::create_dir_all(&self.output_worlds)?;
|
|
}
|
|
|
|
let output_config = self.output_config.canonicalize()?;
|
|
let output_worlds = self.output_worlds.canonicalize()?;
|
|
let backup_path = self.path.canonicalize()?;
|
|
|
|
// Clear previous contents of directories
|
|
let mut entries = output_config
|
|
.read_dir()?
|
|
.chain(output_worlds.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)?;
|
|
}
|
|
}
|
|
|
|
let dirs = vec![
|
|
(PathBuf::from("config"), output_config),
|
|
(PathBuf::from("worlds"), output_worlds),
|
|
];
|
|
|
|
Backup::extract_archive(backup_path, &dirs)
|
|
}
|
|
}
|