From bfa6a9859ac66caf3fb3d666d439506a7b20f375 Mon Sep 17 00:00:00 2001 From: Anthony Tornetta Date: Fri, 20 Dec 2024 17:38:29 -0500 Subject: [PATCH 1/4] Added world backups --- .gitignore | 2 + Cargo.lock | 331 +++++++++++++++++++++- cosmos_server/Cargo.toml | 2 + cosmos_server/src/persistence/autosave.rs | 64 +++++ cosmos_server/src/persistence/backup.rs | 76 +++++ cosmos_server/src/persistence/mod.rs | 4 + 6 files changed, 478 insertions(+), 1 deletion(-) create mode 100644 cosmos_server/src/persistence/autosave.rs create mode 100644 cosmos_server/src/persistence/backup.rs diff --git a/.gitignore b/.gitignore index 86a0690f..11eaeae2 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,7 @@ cosmos_server/blueprints/ cosmos_server/world/* # This directory is for run-time generated assets cosmos_server/assets/temp/ +cosmos_server/backups/* trace-*.json @@ -36,3 +37,4 @@ debug.svg # Local to this player name.env + diff --git a/Cargo.lock b/Cargo.lock index 083971a7..e8256528 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -99,6 +99,17 @@ dependencies = [ "generic-array", ] +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + [[package]] name = "ahash" version = "0.8.11" @@ -177,6 +188,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + [[package]] name = "android_log-sys" version = "0.3.1" @@ -256,6 +273,15 @@ dependencies = [ "num-traits", ] +[[package]] +name = "arbitrary" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" +dependencies = [ + "derive_arbitrary", +] + [[package]] name = "arboard" version = "3.4.0" @@ -1669,6 +1695,15 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "block2" version = "0.5.1" @@ -1735,6 +1770,27 @@ version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" +[[package]] +name = "bzip2" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8" +dependencies = [ + "bzip2-sys", + "libc", +] + +[[package]] +name = "bzip2-sys" +version = "0.1.11+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + [[package]] name = "calloop" version = "0.13.0" @@ -1829,6 +1885,20 @@ dependencies = [ "zeroize", ] +[[package]] +name = "chrono" +version = "0.4.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-targets 0.52.6", +] + [[package]] name = "cipher" version = "0.4.4" @@ -2175,6 +2245,7 @@ dependencies = [ "bincode", "bitflags 2.6.0", "bytemuck", + "chrono", "clap", "cosmos_core", "crossterm", @@ -2192,6 +2263,7 @@ dependencies = [ "thiserror 2.0.4", "thread-priority", "walkdir", + "zip", ] [[package]] @@ -2226,6 +2298,21 @@ dependencies = [ "libc", ] +[[package]] +name = "crc" +version = "3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + [[package]] name = "crc32fast" version = "1.4.2" @@ -2362,6 +2449,32 @@ version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" +[[package]] +name = "deflate64" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da692b8d1080ea3045efaab14434d40468c3d8657e42abddfffca87b428f4c1b" + +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "derive_arbitrary" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", +] + [[package]] name = "derive_more" version = "1.0.0" @@ -2383,12 +2496,34 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + [[package]] name = "dispatch" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", +] + [[package]] name = "disqualified" version = "1.0.0" @@ -3057,6 +3192,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + [[package]] name = "hmac-sha256" version = "1.1.7" @@ -3072,6 +3216,29 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "iana-time-zone" +version = "0.1.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core 0.52.0", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "idna" version = "0.5.0" @@ -3378,6 +3545,12 @@ dependencies = [ "scopeguard", ] +[[package]] +name = "lockfree-object-pool" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9374ef4228402d4b7e403e5838cb880d9ee663314b0a900d5a6aabf0c213552e" + [[package]] name = "log" version = "0.4.22" @@ -3393,6 +3566,16 @@ dependencies = [ "twox-hash", ] +[[package]] +name = "lzma-rs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "297e814c836ae64db86b36cf2a557ba54368d03f6afcd7d947c266692f71115e" +dependencies = [ + "byteorder", + "crc", +] + [[package]] name = "mach2" version = "0.4.2" @@ -3736,6 +3919,12 @@ dependencies = [ "serde", ] +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + [[package]] name = "num-derive" version = "0.4.2" @@ -4156,6 +4345,16 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest", + "hmac", +] + [[package]] name = "percent-encoding" version = "2.3.1" @@ -4256,6 +4455,12 @@ dependencies = [ "universal-hash", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "pp-rs" version = "0.2.1" @@ -4803,6 +5008,17 @@ dependencies = [ "serde", ] +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -5224,6 +5440,25 @@ dependencies = [ "weezl", ] +[[package]] +name = "time" +version = "0.3.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde", + "time-core", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + [[package]] name = "tiny-keccak" version = "2.0.2" @@ -5968,7 +6203,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.59.0", ] [[package]] @@ -5997,6 +6232,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-core" version = "0.54.0" @@ -6446,3 +6690,88 @@ name = "zeroize" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", +] + +[[package]] +name = "zip" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae9c1ea7b3a5e1f4b922ff856a129881167511563dc219869afe3787fc0c1a45" +dependencies = [ + "aes", + "arbitrary", + "bzip2", + "constant_time_eq", + "crc32fast", + "crossbeam-utils", + "deflate64", + "displaydoc", + "flate2", + "hmac", + "indexmap", + "lzma-rs", + "memchr", + "pbkdf2", + "rand", + "sha1", + "thiserror 2.0.4", + "time", + "zeroize", + "zopfli", + "zstd", +] + +[[package]] +name = "zopfli" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5019f391bac5cf252e93bbcc53d039ffd62c7bfb7c150414d61369afe57e946" +dependencies = [ + "bumpalo", + "crc32fast", + "lockfree-object-pool", + "log", + "once_cell", + "simd-adler32", +] + +[[package]] +name = "zstd" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcf2b778a664581e31e389454a7072dab1647606d44f7feea22cd5abb9c9f3f9" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54a3ab4db68cea366acc5c897c7b4d4d1b8994a9cd6e6f841f8964566a419059" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.13+zstd.1.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38ff0f21cfee8f97d94cef41359e0c89aa6113028ab0291aa8ca0038995a95aa" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/cosmos_server/Cargo.toml b/cosmos_server/Cargo.toml index 9982c716..2c146243 100644 --- a/cosmos_server/Cargo.toml +++ b/cosmos_server/Cargo.toml @@ -78,4 +78,6 @@ derive_more = { workspace = true } bevy_easy_compute = { workspace = true } bytemuck = { workspace = true } +zip = "2.2.2" +chrono = "0.4.39" # iyes_perf_ui = { workspace = true } diff --git a/cosmos_server/src/persistence/autosave.rs b/cosmos_server/src/persistence/autosave.rs new file mode 100644 index 00000000..fcb76da6 --- /dev/null +++ b/cosmos_server/src/persistence/autosave.rs @@ -0,0 +1,64 @@ +//! Performs regular autosaves of the world + +use std::time::Duration; + +use bevy::{prelude::*, time::common_conditions::on_timer}; +use cosmos_core::{ + ecs::NeedsDespawned, entities::player::Player, netty::system_sets::NetworkingSystemsSet, persistence::LoadingDistance, + physics::location::Location, +}; + +use super::{backup::CreateWorldBackup, saving::NeedsSaved}; + +const AUTOSAVE_INTERVAL: Duration = Duration::from_mins(5); + +#[derive(Event, Default)] +/// Send this event to save every savable entity in the game +pub struct SaveEverything; + +fn backup_before_saving(mut evw_create_backup: EventWriter, mut evr_save_everything: EventReader) { + if evr_save_everything.is_empty() { + return; + } + evr_save_everything.clear(); + evw_create_backup.send_default(); +} + +fn save_everything( + mut commands: Commands, + q_needs_saved: Query, With, With)>, + mut evr_save_everything: EventReader, +) { + if evr_save_everything.is_empty() { + return; + }; + evr_save_everything.clear(); + + info!("Saving all entities! Expect some lag."); + for entity in q_needs_saved.iter() { + commands.entity(entity).insert(NeedsSaved); + } +} + +fn trigger_autosave(mut evw_create_backup: EventWriter, q_players: Query<(), With>) { + if q_players.is_empty() { + return; + } + + info!("Triggering autosave"); + evw_create_backup.send_default(); +} + +pub(super) fn register(app: &mut App) { + app.add_systems( + Last, + ( + trigger_autosave.run_if(on_timer(AUTOSAVE_INTERVAL)), + backup_before_saving, + save_everything, + ) + .in_set(NetworkingSystemsSet::SyncComponents) + .chain(), + ) + .add_event::(); +} diff --git a/cosmos_server/src/persistence/backup.rs b/cosmos_server/src/persistence/backup.rs new file mode 100644 index 00000000..3f6e37cb --- /dev/null +++ b/cosmos_server/src/persistence/backup.rs @@ -0,0 +1,76 @@ +//! Used to backup the current world's save file. This does NOT save any new items, only creates a +//! backup of all currently saved data. + +use bevy::prelude::*; +use chrono::Utc; +use std::{ + fs::File, + io::{self, Read, Write}, + path::Path, +}; +use zip::write::SimpleFileOptions; + +use super::saving::SavingSystemSet; + +#[derive(Event, Default)] +/// Send this event to trigger a world backup +pub struct CreateWorldBackup; + +fn backup_world(mut evr_create_backup: EventReader) { + if evr_create_backup.is_empty() { + return; + } + + evr_create_backup.clear(); + + info!("Backing up existing save data"); + let date_time = Utc::now(); + + let formatted = format!("{}", date_time.format("%Y_%m_%d_%H_%M_%S")); + let _ = std::fs::create_dir("./backups"); + if let Err(e) = zip_directory(Path::new("./world"), Path::new(&format!("./backups/{formatted}_world_backup.zip"))) { + error!("Error backing up world!!!\n{e:?}"); + } +} + +/// Zips the contents of a directory into a zip file. +/// +/// # Arguments +/// +/// * `src_dir` - The source directory to zip. +/// * `dest_file` - The path to the output zip file. +/// +/// # Errors +/// +/// Returns an error if the directory traversal or file writing fails. +pub fn zip_directory(src_dir: &Path, dest_file: &Path) -> io::Result<()> { + let file = File::create(dest_file)?; + let mut zip = zip::ZipWriter::new(file); + let mut buffer = Vec::new(); + + let options = SimpleFileOptions::default(); + + for entry in walkdir::WalkDir::new(src_dir) { + let entry = entry?; + let path = entry.path(); + let name = path.strip_prefix(src_dir).unwrap().to_str().unwrap(); + + if path.is_file() { + zip.start_file(name, options)?; + let mut f = File::open(path)?; + f.read_to_end(&mut buffer)?; + zip.write_all(&buffer)?; + buffer.clear(); + } else if path.is_dir() { + zip.add_directory(name, options)?; + } + } + + zip.finish()?; + Ok(()) +} + +pub(super) fn register(app: &mut App) { + app.add_systems(First, backup_world.before(SavingSystemSet::BeginSaving)) + .add_event::(); +} diff --git a/cosmos_server/src/persistence/mod.rs b/cosmos_server/src/persistence/mod.rs index f4cba136..14f98089 100644 --- a/cosmos_server/src/persistence/mod.rs +++ b/cosmos_server/src/persistence/mod.rs @@ -19,6 +19,8 @@ use cosmos_core::{ structure::chunk::netty::SaveData, }; +pub mod autosave; +pub mod backup; pub mod loading; pub mod make_persistent; pub mod player_loading; @@ -313,6 +315,8 @@ pub(super) fn register(app: &mut App) { saving::register(app); loading::register(app); player_loading::register(app); + autosave::register(app); + backup::register(app); app.register_type::().register_type::(); } From db7a0c2081893c0febead8a605eb9de92a11e5a5 Mon Sep 17 00:00:00 2001 From: Anthony Tornetta Date: Fri, 20 Dec 2024 19:38:44 -0500 Subject: [PATCH 2/4] Old backups will now be pruned Untested --- cosmos_server/src/persistence/backup.rs | 76 ++++++++++++++++++++++++- 1 file changed, 73 insertions(+), 3 deletions(-) diff --git a/cosmos_server/src/persistence/backup.rs b/cosmos_server/src/persistence/backup.rs index 3f6e37cb..36b09694 100644 --- a/cosmos_server/src/persistence/backup.rs +++ b/cosmos_server/src/persistence/backup.rs @@ -1,13 +1,15 @@ //! Used to backup the current world's save file. This does NOT save any new items, only creates a //! backup of all currently saved data. -use bevy::prelude::*; -use chrono::Utc; +use bevy::{prelude::*, time::common_conditions::on_timer}; +use chrono::{DateTime, Duration, NaiveDateTime, Utc}; use std::{ + ffi::OsStr, fs::File, io::{self, Read, Write}, path::Path, }; +use walkdir::WalkDir; use zip::write::SimpleFileOptions; use super::saving::SavingSystemSet; @@ -16,6 +18,8 @@ use super::saving::SavingSystemSet; /// Send this event to trigger a world backup pub struct CreateWorldBackup; +const DATE_FORMAT: &str = "%Y_%m_%d_%H_%M_%S"; + fn backup_world(mut evr_create_backup: EventReader) { if evr_create_backup.is_empty() { return; @@ -26,13 +30,78 @@ fn backup_world(mut evr_create_backup: EventReader) { info!("Backing up existing save data"); let date_time = Utc::now(); - let formatted = format!("{}", date_time.format("%Y_%m_%d_%H_%M_%S")); + let formatted = format!("{}", date_time.format(DATE_FORMAT)); let _ = std::fs::create_dir("./backups"); if let Err(e) = zip_directory(Path::new("./world"), Path::new(&format!("./backups/{formatted}_world_backup.zip"))) { error!("Error backing up world!!!\n{e:?}"); } } +fn cleanup_backups() { + let mut backups = vec![]; + for backup in WalkDir::new("backups").max_depth(1) { + let Ok(backup) = backup else { + continue; + }; + + let path = backup.path(); + if path.extension() != Some(OsStr::new("zip")) { + continue; + } + + let Some(file_name) = path.file_name().and_then(|x| x.to_str()) else { + continue; + }; + + let Ok(date_time_parsed) = + NaiveDateTime::parse_from_str(&file_name[0..file_name.len() - ".zip".len()], DATE_FORMAT).map(|x| x.and_utc()) + else { + continue; + }; + + info!("{date_time_parsed:?}"); + + backups.push((date_time_parsed, path.to_string_lossy().to_string())); + } + + backups.sort_by_key(|x| x.0); + backups.reverse(); + + let now = Utc::now(); + + // Keep one backup every 5 minutes for the last hour + prune_by_interval(&mut backups, now, Duration::minutes(5), Duration::hours(1)); + + // Keep one backup every hour for the last 24 hours + prune_by_interval(&mut backups, now, Duration::hours(1), Duration::hours(24)); + + // Keep one backup per day for the last 7 days + prune_by_interval(&mut backups, now, Duration::days(1), Duration::days(7)); + + // Keep one backup per week for the last 4 weeks + prune_by_interval(&mut backups, now, Duration::weeks(1), Duration::weeks(4)); + + // Remove all other backups + for (_, path) in backups { + info!("Pruning old backup {path}"); + std::fs::remove_file(&path).unwrap_or_else(|e| panic!("failed to remove file @ {path}!\n{e:?}")); + } +} + +fn prune_by_interval(backups: &mut Vec<(DateTime, String)>, now: DateTime, interval: Duration, max_age: Duration) { + let mut next_cutoff = now - interval; + let max_cutoff = now - max_age; + + backups.retain(|(timestamp, _)| { + if *timestamp > max_cutoff && *timestamp <= next_cutoff { + next_cutoff -= interval; + false + } else { + true + } + }); +} + /// Zips the contents of a directory into a zip file. /// /// # Arguments @@ -72,5 +141,6 @@ pub fn zip_directory(src_dir: &Path, dest_file: &Path) -> io::Result<()> { pub(super) fn register(app: &mut App) { app.add_systems(First, backup_world.before(SavingSystemSet::BeginSaving)) + .add_systems(Update, cleanup_backups.run_if(on_timer(std::time::Duration::from_mins(5)))) .add_event::(); } From 3626b508513ee3cf6adeadc0ca0c031ce52a4567 Mon Sep 17 00:00:00 2001 From: Anthony Tornetta Date: Fri, 20 Dec 2024 20:02:15 -0500 Subject: [PATCH 3/4] Better backup pruning logic --- cosmos_server/src/persistence/backup.rs | 80 +++++++++++++++---------- 1 file changed, 50 insertions(+), 30 deletions(-) diff --git a/cosmos_server/src/persistence/backup.rs b/cosmos_server/src/persistence/backup.rs index 36b09694..b647c49a 100644 --- a/cosmos_server/src/persistence/backup.rs +++ b/cosmos_server/src/persistence/backup.rs @@ -18,7 +18,10 @@ use super::saving::SavingSystemSet; /// Send this event to trigger a world backup pub struct CreateWorldBackup; +const MAX_BACKUPS: usize = 25; + const DATE_FORMAT: &str = "%Y_%m_%d_%H_%M_%S"; +const BACKUP_ENDING: &str = "_world_backup.zip"; fn backup_world(mut evr_create_backup: EventReader) { if evr_create_backup.is_empty() { @@ -32,13 +35,18 @@ fn backup_world(mut evr_create_backup: EventReader) { let formatted = format!("{}", date_time.format(DATE_FORMAT)); let _ = std::fs::create_dir("./backups"); - if let Err(e) = zip_directory(Path::new("./world"), Path::new(&format!("./backups/{formatted}_world_backup.zip"))) { + if let Err(e) = zip_directory(Path::new("./world"), Path::new(&format!("./backups/{formatted}{BACKUP_ENDING}"))) { error!("Error backing up world!!!\n{e:?}"); } } fn cleanup_backups() { + info!("Initiating backup prune!"); + + let now = Utc::now(); + let mut backups = vec![]; + for backup in WalkDir::new("backups").max_depth(1) { let Ok(backup) = backup else { continue; @@ -53,53 +61,65 @@ fn cleanup_backups() { continue; }; + if !file_name.ends_with(BACKUP_ENDING) { + continue; + } + let Ok(date_time_parsed) = - NaiveDateTime::parse_from_str(&file_name[0..file_name.len() - ".zip".len()], DATE_FORMAT).map(|x| x.and_utc()) + NaiveDateTime::parse_from_str(&file_name[0..file_name.len() - BACKUP_ENDING.len()], DATE_FORMAT).map(|x| x.and_utc()) else { continue; }; - info!("{date_time_parsed:?}"); - + if now.signed_duration_since(date_time_parsed).num_milliseconds() < 0 { + // Don't delete backups marked as being taken in the future, the system clock is + // probably wrong in that case. + continue; + } backups.push((date_time_parsed, path.to_string_lossy().to_string())); } backups.sort_by_key(|x| x.0); - backups.reverse(); - - let now = Utc::now(); - // Keep one backup every 5 minutes for the last hour - prune_by_interval(&mut backups, now, Duration::minutes(5), Duration::hours(1)); + let n_backups = backups.len(); - // Keep one backup every hour for the last 24 hours - prune_by_interval(&mut backups, now, Duration::hours(1), Duration::hours(24)); + if n_backups > MAX_BACKUPS { + backups.reverse(); - // Keep one backup per day for the last 7 days - prune_by_interval(&mut backups, now, Duration::days(1), Duration::days(7)); + prune_by_interval(&mut backups, now, Duration::minutes(5), Duration::hours(1)); + prune_by_interval(&mut backups, now, Duration::hours(1), Duration::hours(24)); + prune_by_interval(&mut backups, now, Duration::days(1), Duration::days(7)); + prune_by_interval(&mut backups, now, Duration::weeks(1), Duration::weeks(4)); - // Keep one backup per week for the last 4 weeks - prune_by_interval(&mut backups, now, Duration::weeks(1), Duration::weeks(4)); + if backups.is_empty() { + info!("No backups to prune."); + } - // Remove all other backups - for (_, path) in backups { - info!("Pruning old backup {path}"); - std::fs::remove_file(&path).unwrap_or_else(|e| panic!("failed to remove file @ {path}!\n{e:?}")); + // If any backups remain in this list, they don't meet our time-span criteria. + // Also never remove backups if that would put us under the max allowed backups. + for (_, path) in backups.into_iter().take(n_backups - MAX_BACKUPS) { + info!("Pruning old backup {path}"); + std::fs::remove_file(&path).unwrap_or_else(|e| panic!("failed to remove file @ {path}!\n{e:?}")); + } } } +/// Keep one backup ever `interval` timespan for the last `max_age`. fn prune_by_interval(backups: &mut Vec<(DateTime, String)>, now: DateTime, interval: Duration, max_age: Duration) { - let mut next_cutoff = now - interval; - let max_cutoff = now - max_age; - - backups.retain(|(timestamp, _)| { - if *timestamp > max_cutoff && *timestamp <= next_cutoff { - next_cutoff -= interval; - false - } else { - true + let mut range_end = now; + let range_start = now - max_age; + + while range_end > range_start { + let range_begin = range_end - interval; + // Find the most recent backup within the range + if let Some(pos) = backups + .iter() + .position(|(timestamp, _)| *timestamp <= range_end && *timestamp > range_begin) + { + backups.remove(pos); } - }); + range_end = range_begin; + } } /// Zips the contents of a directory into a zip file. @@ -141,6 +161,6 @@ pub fn zip_directory(src_dir: &Path, dest_file: &Path) -> io::Result<()> { pub(super) fn register(app: &mut App) { app.add_systems(First, backup_world.before(SavingSystemSet::BeginSaving)) - .add_systems(Update, cleanup_backups.run_if(on_timer(std::time::Duration::from_mins(5)))) + .add_systems(Update, cleanup_backups.run_if(on_timer(std::time::Duration::from_secs(10)))) .add_event::(); } From 90fa9604083d05de245018cdff7cd00cef66b4e1 Mon Sep 17 00:00:00 2001 From: Anthony Tornetta Date: Fri, 20 Dec 2024 20:21:59 -0500 Subject: [PATCH 4/4] Finishing backup logic --- cosmos_server/src/persistence/autosave.rs | 2 +- cosmos_server/src/persistence/backup.rs | 33 +++++++++-------------- 2 files changed, 14 insertions(+), 21 deletions(-) diff --git a/cosmos_server/src/persistence/autosave.rs b/cosmos_server/src/persistence/autosave.rs index fcb76da6..3ac3e94b 100644 --- a/cosmos_server/src/persistence/autosave.rs +++ b/cosmos_server/src/persistence/autosave.rs @@ -10,7 +10,7 @@ use cosmos_core::{ use super::{backup::CreateWorldBackup, saving::NeedsSaved}; -const AUTOSAVE_INTERVAL: Duration = Duration::from_mins(5); +const AUTOSAVE_INTERVAL: Duration = Duration::from_mins(10); #[derive(Event, Default)] /// Send this event to save every savable entity in the game diff --git a/cosmos_server/src/persistence/backup.rs b/cosmos_server/src/persistence/backup.rs index b647c49a..412c8676 100644 --- a/cosmos_server/src/persistence/backup.rs +++ b/cosmos_server/src/persistence/backup.rs @@ -18,8 +18,6 @@ use super::saving::SavingSystemSet; /// Send this event to trigger a world backup pub struct CreateWorldBackup; -const MAX_BACKUPS: usize = 25; - const DATE_FORMAT: &str = "%Y_%m_%d_%H_%M_%S"; const BACKUP_ENDING: &str = "_world_backup.zip"; @@ -41,7 +39,7 @@ fn backup_world(mut evr_create_backup: EventReader) { } fn cleanup_backups() { - info!("Initiating backup prune!"); + info!("Initiating backup prune."); let now = Utc::now(); @@ -81,26 +79,21 @@ fn cleanup_backups() { backups.sort_by_key(|x| x.0); - let n_backups = backups.len(); - - if n_backups > MAX_BACKUPS { - backups.reverse(); + backups.reverse(); - prune_by_interval(&mut backups, now, Duration::minutes(5), Duration::hours(1)); - prune_by_interval(&mut backups, now, Duration::hours(1), Duration::hours(24)); - prune_by_interval(&mut backups, now, Duration::days(1), Duration::days(7)); - prune_by_interval(&mut backups, now, Duration::weeks(1), Duration::weeks(4)); + prune_by_interval(&mut backups, now, Duration::minutes(10), Duration::hours(1)); + prune_by_interval(&mut backups, now, Duration::hours(1), Duration::hours(24)); + prune_by_interval(&mut backups, now, Duration::days(1), Duration::days(7)); + prune_by_interval(&mut backups, now, Duration::weeks(1), Duration::weeks(4)); - if backups.is_empty() { - info!("No backups to prune."); - } + if backups.is_empty() { + info!("No backups to prune."); + } - // If any backups remain in this list, they don't meet our time-span criteria. - // Also never remove backups if that would put us under the max allowed backups. - for (_, path) in backups.into_iter().take(n_backups - MAX_BACKUPS) { - info!("Pruning old backup {path}"); - std::fs::remove_file(&path).unwrap_or_else(|e| panic!("failed to remove file @ {path}!\n{e:?}")); - } + // If any backups remain in this list, they don't meet our time-span criteria. + for (_, path) in backups.into_iter() { + info!("Pruning old backup {path}"); + std::fs::remove_file(&path).unwrap_or_else(|e| panic!("failed to remove file @ {path}!\n{e:?}")); } }