From 82de40903b14f972df51cb189f413d4b7f370ad5 Mon Sep 17 00:00:00 2001 From: lifegpc Date: Fri, 19 Jul 2024 05:07:31 +0000 Subject: [PATCH] Add files --- .gitignore | 1 - Cargo.lock | 150 +++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 10 +++ src/cfg.rs | 107 +++++++++++++++++++++++++++++++ src/main.rs | 177 +++++++++++++++++++++++++++++++++++++++++++++++++++ src/utils.rs | 38 +++++++++++ 6 files changed, 482 insertions(+), 1 deletion(-) create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 src/cfg.rs create mode 100644 src/main.rs create mode 100644 src/utils.rs diff --git a/.gitignore b/.gitignore index 6985cf1..c4379b8 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,6 @@ target/ # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html -Cargo.lock # These are backup files generated by rustfmt **/*.rs.bk diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..ed676d4 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,150 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "derive_more" +version = "0.99.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f33878137e4dafd7fa914ad4e259e18a4e8e532b9617a2d0150262bf53abfce" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn", +] + +[[package]] +name = "game-auto-sync" +version = "0.1.0" +dependencies = [ + "derive_more", + "getopts", + "subprocess", + "yaml-rust", +] + +[[package]] +name = "getopts" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "libc" +version = "0.2.155" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" + +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + +[[package]] +name = "proc-macro2" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + +[[package]] +name = "semver" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" + +[[package]] +name = "subprocess" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c2e86926081dda636c546d8c5e641661049d7562a68f5488be4a1f7f66f6086" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "syn" +version = "2.0.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b146dcf730474b4bcd16c311627b31ede9ab149045db4d6088b3becaea046462" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unicode-width" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..089a6d4 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "game-auto-sync" +version = "0.1.0" +edition = "2021" + +[dependencies] +derive_more = "0.99.18" +getopts = "0.2.21" +subprocess = "0.2.9" +yaml-rust = "0.4.5" diff --git a/src/cfg.rs b/src/cfg.rs new file mode 100644 index 0000000..b8a2c85 --- /dev/null +++ b/src/cfg.rs @@ -0,0 +1,107 @@ +use std::path::Path; +use std::{fs::File, io::Read}; +use yaml_rust::{yaml::Hash, ScanError, Yaml, YamlLoader}; + +#[derive(Debug, derive_more::Display, derive_more::From)] +pub enum ConfigError { + YAML(ScanError), + IO(std::io::Error), + Invalid, +} + +#[derive(Debug)] +pub struct Config { + obj: Hash, +} + +impl Config { + pub fn from_file_path + ?Sized>(path: &P) -> Result { + let mut f = File::open(path.as_ref())?; + let mut s = String::new(); + f.read_to_string(&mut s)?; + Self::from_str(&s) + } + + pub fn from_str + ?Sized>(s: &S) -> Result { + let re = YamlLoader::load_from_str(s.as_ref())?; + if re.is_empty() { + return Err(ConfigError::Invalid); + } + let re = re[0].clone(); + let obj = match re { + Yaml::Hash(h) => h, + _ => { + return Err(ConfigError::Invalid); + } + }; + Ok(Self { obj }) + } + + pub fn get + ?Sized>(&self, s: &S) -> Option<&Yaml> { + let k = Yaml::from_str(s.as_ref()); + self.obj.get(&k) + } + + pub fn get_str + ?Sized>(&self, s: &S) -> Option<&str> { + match self.get(s) { + Some(y) => match y { + Yaml::String(s) => Some(s), + _ => None, + }, + None => None, + } + } + + pub fn get_str_vec + ?Sized>(&self, s: &S) -> Option> { + match self.get(s) { + Some(e) => match e { + Yaml::String(s) => Some(vec![s.to_owned()]), + Yaml::Array(s) => Some(s + .iter() + .filter_map(|s| match s { + Yaml::String(s) => Some(s.to_owned()), + Yaml::Integer(i) => Some(i.to_string()), + Yaml::Real(i) => Some(i.to_owned()), + _ => None, + }) + .collect()), + _ => None, + }, + None => None, + } + } + + pub fn game_backuper_cfg(&self) -> String { + match self.get_str("game_backuper_cfg") { + Some(s) => { + let mut pb = crate::utils::get_exe_path_else_current(); + pb.push(s); + pb.to_string_lossy().to_string() + }, + None => { + let mut pb = crate::utils::get_exe_path_else_current(); + pb.push("game_backuper.yml"); + pb.to_string_lossy().to_string() + }, + } + } + + pub fn game_backuper_exe(&self) -> String { + match self.get_str("game_backuper_exe") { + Some(s) => s.to_owned(), + None => String::from("game-backuper"), + } + } + + pub fn game_exe(&self) -> Option> { + self.get_str_vec("game_exe") + } + + pub fn backup_command(&self) -> Option> { + self.get_str_vec("backup_command") + } + + pub fn restore_command(&self) -> Option> { + self.get_str_vec("restore_command") + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..d89e3db --- /dev/null +++ b/src/main.rs @@ -0,0 +1,177 @@ +mod cfg; +mod utils; + +use getopts::Options; +use std::process::ExitCode; +use subprocess::ExitStatus; + +pub fn print_usage(prog: &str, opts: &Options) { + let brief = format!( + "{} +{} [options]", + "Usage:", prog, + ); + println!("{}", opts.usage(brief.as_str())); +} + +#[derive(Debug, derive_more::Display, derive_more::From)] +enum Error { + Popen(subprocess::PopenError), + Exited, +} + +struct Main { + _cfg: cfg::Config, + _dryrun: bool, +} + +impl Main { + fn new(cfg: cfg::Config, dryrun: bool) -> Self { + Self { + _cfg: cfg, + _dryrun: dryrun, + } + } + + fn backup(&self) -> Result<(), Error> { + let cml = match self._cfg.backup_command() { + Some(cml) => cml, + None => { + let mut def = vec![self._cfg.game_backuper_exe()]; + let cfg_path = self._cfg.game_backuper_cfg(); + if std::fs::exists(&cfg_path).unwrap_or(false) { + def.push(String::from("-c")); + def.push(cfg_path); + } + def.push(String::from("backup")); + def + } + }; + if self._dryrun { + println!("Backup command line: {:?}", cml); + Ok(()) + } else { + let e = Self::call(cml)?; + let ok = match &e { + ExitStatus::Exited(c) => *c != 0, + _ => false, + }; + if !ok { + println!("Backup failed: {:?}.", e); + return Err(Error::Exited); + } + Ok(()) + } + } + + fn call(cml: Vec) -> Result { + let mut p = subprocess::Popen::create(&cml, subprocess::PopenConfig::default())?; + p.wait() + } + + fn restore(&self) -> Result<(), Error> { + let cml = match self._cfg.restore_command() { + Some(cml) => cml, + None => { + let mut def = vec![self._cfg.game_backuper_exe()]; + let cfg_path = self._cfg.game_backuper_cfg(); + if std::fs::exists(&cfg_path).unwrap_or(false) { + def.push(String::from("-c")); + def.push(cfg_path); + } + def.push(String::from("restore")); + def + } + }; + if self._dryrun { + println!("Restore command line: {:?}", cml); + Ok(()) + } else { + let e = Self::call(cml)?; + let ok = match &e { + ExitStatus::Exited(c) => *c != 0, + _ => false, + }; + if !ok { + println!("Restore failed: {:?}.", e); + if !utils::ask_continue() { + return Err(Error::Exited); + } + } + Ok(()) + } + } + + fn run(&self) -> Result<(), Error> { + self.restore()?; + self.run_exe()?; + self.backup()?; + Ok(()) + } + + fn run_exe(&self) -> Result<(), Error> { + let cml = self._cfg.game_exe().unwrap(); + if self._dryrun { + println!("Run command line: {:?}", cml); + Ok(()) + } else { + let e = Self::call(cml)?; + let ok = match &e { + ExitStatus::Exited(c) => *c != 0, + _ => false, + }; + if !ok { + println!("Run failed: {:?}.", e); + if !utils::ask_continue() { + return Err(Error::Exited); + } + } + Ok(()) + } + } +} + +fn main() -> ExitCode { + println!("Hello, world!"); + let argv: Vec = std::env::args().collect(); + let mut opts = Options::new(); + opts.optflag("h", "help", "Print help message."); + opts.optopt("c", "config", "The location of config file.", "FILE"); + opts.optflag("d", "dryrun", "Run without calling any process."); + let result = match opts.parse(&argv[1..]) { + Ok(m) => m, + Err(err) => { + println!("{}", err.to_string()); + return ExitCode::from(1); + } + }; + if result.opt_present("h") { + print_usage(&argv[0], &opts); + return ExitCode::from(0); + } + let cfg_path = result.opt_str("c").unwrap_or_else(|| { + let mut pb = utils::get_exe_path_else_current(); + pb.push("game-auto-sync.yml"); + pb.to_string_lossy().to_string() + }); + let cfg = match cfg::Config::from_file_path(&cfg_path) { + Ok(cfg) => cfg, + Err(e) => { + println!("{}", e); + return ExitCode::from(1); + } + }; + if cfg.game_exe().unwrap_or(vec![]).is_empty() { + println!("game_exe need be set."); + return ExitCode::from(1); + } + let m = Main::new(cfg, result.opt_present("d")); + match m.run() { + Ok(_) => {} + Err(e) => { + println!("{}", e); + return ExitCode::from(1); + } + } + return ExitCode::from(0); +} diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..ac540da --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,38 @@ +use std::env; +use std::io::Write; +use std::path::Path; +use std::path::PathBuf; + +/// Get executable location, if not found, return current directory (./) +pub fn get_exe_path_else_current() -> PathBuf { + let re = env::current_exe(); + match re { + Ok(pa) => { + let mut p = pa.clone(); + p.pop(); + p + } + Err(_) => { + let p = Path::new("./"); + p.to_path_buf() + } + } +} + +pub fn ask_continue() -> bool { + print!("Do you want to continue?(y/n)"); + std::io::stdout().flush().unwrap(); + let mut d = String::from(""); + loop { + let re = std::io::stdin().read_line(&mut d); + if re.is_err() { + continue; + } + let d = d.trim().to_lowercase(); + if d == "y" { + return true; + } else { + return false; + } + } +}