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, } #[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)> { 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) } }