diff --git a/Cargo.lock b/Cargo.lock index 4aeecd7..ee59b5e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -43,6 +43,7 @@ dependencies = [ "regex", "reqwest", "serde 1.0.117", + "ssh2", "structopt", "tar", ] @@ -183,6 +184,15 @@ dependencies = [ "vec_map", ] +[[package]] +name = "cloudabi" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" +dependencies = [ + "bitflags", +] + [[package]] name = "config" version = "0.9.3" @@ -579,6 +589,32 @@ version = "0.2.71" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9457b06509d27052635f90d6466700c65095fdf75409b3fbdd903e988b886f49" +[[package]] +name = "libssh2-sys" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0186af0d8f171ae6b9c4c90ec51898bad5d08a2d5e470903a50d9ad8959cbee" +dependencies = [ + "cc", + "libc", + "libz-sys", + "openssl-sys", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "libz-sys" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "602113192b08db8f38796c4e85c39e960c145965140e918018bcde1952429655" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "linked-hash-map" version = "0.3.0" @@ -595,6 +631,15 @@ version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8dd5a6d5999d9907cda8ed67bbd137d3af8085216c2ac62de5be860bd41f304a" +[[package]] +name = "lock_api" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4da24a77a3d8a6d4862d95f72e6fdb9c09a643ecdb402d754004a557f2bec75" +dependencies = [ + "scopeguard", +] + [[package]] name = "log" version = "0.4.11" @@ -815,6 +860,30 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "parking_lot" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3a704eb390aafdc107b0e392f56a82b668e3a71366993b5340f5833fd62505e" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d58c7c768d4ba344e3e8d72518ac13e259d7c7ade24167003b8488e10b6740a3" +dependencies = [ + "cfg-if 0.1.10", + "cloudabi", + "libc", + "redox_syscall", + "smallvec", + "winapi 0.3.8", +] + [[package]] name = "percent-encoding" version = "2.1.0" @@ -1050,6 +1119,12 @@ dependencies = [ "winapi 0.3.8", ] +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + [[package]] name = "security-framework" version = "0.4.4" @@ -1168,6 +1243,18 @@ dependencies = [ "winapi 0.3.8", ] +[[package]] +name = "ssh2" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d876d4d57f6bbf2245d43f7ec53759461f801a446d3693704aa6d27b257844d7" +dependencies = [ + "bitflags", + "libc", + "libssh2-sys", + "parking_lot", +] + [[package]] name = "strsim" version = "0.8.0" diff --git a/Cargo.toml b/Cargo.toml index c04c8e2..b784767 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,3 +18,4 @@ tar = "0.4.28" reqwest = { version = "0.10", features = ["blocking", "json"] } discord-hooks-rs = { git = "https://github.com/joeyahines/discord-hooks-rs" } anvil-region = "0.4.0" +ssh2 = "0.9.1" diff --git a/src/backup.rs b/src/backup.rs index 1bc4a46..df325d4 100644 --- a/src/backup.rs +++ b/src/backup.rs @@ -2,6 +2,7 @@ use crate::backup; use crate::config::{AlbatrossConfig, WorldConfig, WorldType}; use crate::discord::send_webhook; use crate::region::Region; +use crate::remote_backup::remote_backup; use chrono::{NaiveDateTime, Utc}; use flate2::read::GzDecoder; use flate2::write::GzEncoder; @@ -321,8 +322,42 @@ pub fn do_backup(cfg: AlbatrossConfig, output: Option) -> Result<(), st create_dir_all(tmp_dir.clone()).unwrap(); - send_webhook("**Albatross is swooping in to backup your worlds!**", &cfg); let timer = Instant::now(); + backup_worlds(&cfg, server_base_dir, worlds, &mut tmp_dir)?; + + backup::compress_backup(&tmp_dir, &output_archive)?; + + remove_dir_all(&tmp_dir)?; + + let backups_removed = remove_old_backups(&cfg.backup.output_dir, cfg.backup.backups_to_keep)?; + + if backups_removed > 0 { + let msg = format!( + "Albatross mistook **{}** of your old backups for some french fries and ate them!! SKRAWWWW", + backups_removed + ); + send_webhook(msg.as_str(), &cfg); + } + + if let Some(remote_backup_cfg) = &cfg.remote { + remote_backup(output_archive, remote_backup_cfg); + } + + let secs = timer.elapsed().as_secs(); + send_webhook( + format!("**Full backup completed in {}s**! *SKREEEEEEEEEE*", secs).as_str(), + &cfg, + ); + Ok(()) +} + +fn backup_worlds( + cfg: &AlbatrossConfig, + server_base_dir: PathBuf, + worlds: Vec, + tmp_dir: &mut PathBuf, +) -> Result<(), std::io::Error> { + send_webhook("**Albatross is swooping in to backup your worlds!**", &cfg); for world in worlds { let mut world_dir = server_base_dir.clone(); let world_name = world.world_name.clone(); @@ -363,24 +398,5 @@ pub fn do_backup(cfg: AlbatrossConfig, output: Option) -> Result<(), st } } - backup::compress_backup(&tmp_dir, &output_archive)?; - - remove_dir_all(&tmp_dir)?; - - let backups_removed = remove_old_backups(&cfg.backup.output_dir, cfg.backup.backups_to_keep)?; - - if backups_removed > 0 { - let msg = format!( - "Albatross mistook **{}** of your old backups for some french fries and ate them!! SKRAWWWW", - backups_removed - ); - send_webhook(msg.as_str(), &cfg); - } - - let secs = timer.elapsed().as_secs(); - send_webhook( - format!("**Full backup completed in {}s**! *SKREEEEEEEEEE*", secs).as_str(), - &cfg, - ); Ok(()) } diff --git a/src/config.rs b/src/config.rs index fe078a0..17c638d 100644 --- a/src/config.rs +++ b/src/config.rs @@ -24,7 +24,7 @@ impl From for WorldType { } } -/// Config for individual WorldConfig +/// Config for individual world configuration #[derive(Debug, Deserialize, Clone)] pub struct WorldConfig { pub world_name: String, @@ -41,11 +41,31 @@ pub struct BackupConfig { pub discord_webhook: Option, } +/// Config for remote backups +#[derive(Debug, Deserialize, Clone)] +pub struct RemoteBackupConfig { + /// Remote server address + pub sftp_server_addr: String, + /// Remote output directory + pub remote_dir: PathBuf, + /// Remote server username + pub username: String, + /// Public key for key auth + pub public_key: Option, + /// Private key for key auth + pub private_key: Option, + /// Password if using password auth + pub password: Option, + /// Remote backups to keep + pub backups_to_keep: u64, +} + /// Configs #[derive(Debug, Deserialize, Clone)] pub struct AlbatrossConfig { pub backup: BackupConfig, pub world_config: Option>, + pub remote: Option, } impl AlbatrossConfig { diff --git a/src/main.rs b/src/main.rs index c3d5a0e..3f85c76 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,6 +6,7 @@ mod chunk_coordinate; mod config; mod discord; mod region; +mod remote_backup; mod restore; use crate::backup::{convert_backup_to_sp, do_backup}; diff --git a/src/remote_backup.rs b/src/remote_backup.rs new file mode 100644 index 0000000..43e9f91 --- /dev/null +++ b/src/remote_backup.rs @@ -0,0 +1,47 @@ +use crate::config::RemoteBackupConfig; +use ssh2::Session; +use std::net::TcpStream; +use std::path::PathBuf; + +fn open_ssh_session(remote_config: &RemoteBackupConfig) -> Session { + let tcp = TcpStream::connect(&remote_config.sftp_server_addr).unwrap(); + let mut sess = Session::new().unwrap(); + sess.set_tcp_stream(tcp); + sess.handshake().unwrap(); + + if let Some(password) = &remote_config.password { + sess.userauth_password(&remote_config.username, password) + .unwrap(); + } else if let Some(key) = &remote_config.private_key { + let public_key = + remote_config + .public_key + .as_ref() + .map(|pub_key| pub_key.as_path().clone()); + sess.userauth_pubkey_file( + &remote_config.username, + public_key, + key, + None, + ) + .unwrap(); + } else { + panic!("No key provided") + } + + sess +} + +pub fn remote_backup(file: PathBuf, remote_config: &RemoteBackupConfig) { + let sess = open_ssh_session(remote_config); + + let remote_path = remote_config.remote_dir.join(file.file_name().unwrap()); + + let mut local_file = std::fs::File::open(&file).unwrap(); + + let sftp = sess.sftp().unwrap(); + + let mut remote_file = sftp.create(&remote_path).unwrap(); + + std::io::copy(&mut local_file, &mut remote_file).unwrap(); +}