From 640364405f3c34e2d098514391a641d3824e6fb6 Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Tue, 6 Jun 2023 19:27:35 +0200 Subject: [PATCH 01/93] feat: add java optimisation flags --- .cargo/config.toml | 2 +- src/server/command.rs | 49 +++++++++++++++++++++++++++++++++++++++---- 2 files changed, 46 insertions(+), 5 deletions(-) diff --git a/.cargo/config.toml b/.cargo/config.toml index 3056b64..711c41b 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,2 +1,2 @@ [alias] -runs = "run -- paper --config data/config --backup data/backups --world data/worlds --jar data/paper.jar" +runs = "run -- paper 1.19.4-545 --config data/config --backup data/backups --world data/worlds --jar data/paper.jar" diff --git a/src/server/command.rs b/src/server/command.rs index e774bb5..7b6d948 100644 --- a/src/server/command.rs +++ b/src/server/command.rs @@ -113,16 +113,57 @@ impl ServerCommand { let backup_dir = self.backup_dir.canonicalize()?; self.accept_eula()?; + let mut cmd = Command::new(&self.java); - let child = Command::new(&self.java) - .current_dir(&config_dir) + // Apply JVM optimisation flags + // https://aikar.co/2018/07/02/tuning-the-jvm-g1gc-garbage-collector-flags-for-minecraft/ + cmd.arg(format!("-Xms{}M", self.xms)) + .arg(format!("-Xmx{}M", self.xmx)) + .args([ + "-XX:+UseG1GC", + "-XX:+ParallelRefProcEnabled", + "-XX:MaxGCPauseMillis=200", + "-XX:+UnlockExperimentalVMOptions", + "-XX:+DisableExplicitGC", + "-XX:+AlwaysPreTouch", + "-XX:G1HeapWastePercent=5", + "-XX:G1MixedGCCountTarget=4", + "-XX:G1MixedGCLiveThresholdPercent=90", + "-XX:G1RSetUpdatingPauseTimePercent=5", + "-XX:SurvivorRatio=32", + "-XX:+PerfDisableSharedMem", + "-XX:MaxTenuringThreshold=1", + "-Dusing.aikars.flags=https://mcflags.emc.gs", + "-Daikars.new.flags=true", + ]); + + if self.xms > 12 * 1024 { + cmd.args([ + "-XX:G1NewSizePercent=40", + "-XX:G1MaxNewSizePercent=50", + "-XX:G1HeapRegionSize=16M", + "-XX:G1ReservePercent=15", + "-XX:InitiatingHeapOccupancyPercent=20", + ]); + } else { + cmd.args([ + "-XX:G1NewSizePercent=30", + "-XX:G1MaxNewSizePercent=40", + "-XX:G1HeapRegionSize=8M", + "-XX:G1ReservePercent=15", + "-XX:InitiatingHeapOccupancyPercent=15", + ]); + } + + cmd.current_dir(&config_dir) .arg("-jar") .arg(&jar) .arg("--universe") .arg(&world_dir) .arg("--nogui") - .stdin(Stdio::piped()) - .spawn()?; + .stdin(Stdio::piped()); + + let child = cmd.spawn()?; Ok(ServerProcess::new( self.type_, From f5fc8b588f6f030dc3e1973021456d15ce4a2bcf Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Tue, 6 Jun 2023 20:12:42 +0200 Subject: [PATCH 02/93] feat: properly backup config directory --- .cargo/config.toml | 1 + src/server/process.rs | 28 +++++++++++++++++++--------- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/.cargo/config.toml b/.cargo/config.toml index 711c41b..d1675c8 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,2 +1,3 @@ [alias] runs = "run -- paper 1.19.4-545 --config data/config --backup data/backups --world data/worlds --jar data/paper.jar" +runrs = "run --release -- paper 1.19.4-545 --config data/config --backup data/backups --world data/worlds --jar data/paper.jar" diff --git a/src/server/process.rs b/src/server/process.rs index 2b6f6fa..5640b18 100644 --- a/src/server/process.rs +++ b/src/server/process.rs @@ -2,7 +2,7 @@ use crate::server::ServerType; use flate2::write::GzEncoder; use flate2::Compression; use std::io::Write; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::process::Child; #[link(name = "c")] @@ -108,11 +108,21 @@ impl ServerProcess { tar.append_dir_all("worlds", &self.world_dir)?; - // We don't store all files in the config, as this would include caches - tar.append_path_with_name( - self.config_dir.join("server.properties"), - "config/server.properties", - )?; + // Add all files from the config directory that aren't the cache + for entry in self + .config_dir + .read_dir()? + .filter_map(|e| e.ok()) + .filter(|e| e.file_name() != "cache") + { + let tar_path = Path::new("config").join(entry.file_name()); + + if entry.file_type()?.is_dir() { + tar.append_dir_all(tar_path, entry.path())?; + } else { + tar.append_path_with_name(entry.path(), tar_path)?; + } + } // We add a file to the backup describing for what version it was made let info = format!("{} {}", self.type_, self.version); @@ -128,8 +138,6 @@ impl ServerProcess { tar.append_data(&mut header, "info.txt", info_bytes)?; - // tar.append_dir_all("config", &self.config_dir)?; - // Backup file gets finalized in the drop Ok(()) @@ -139,7 +147,9 @@ impl ServerProcess { fn remove_old_backups(&mut self) -> std::io::Result<()> { // The naming format used allows us to sort the backups by name and still get a sorting by // creation time - let mut backups = std::fs::read_dir(&self.backup_dir)? + let mut backups = self + .backup_dir + .read_dir()? .filter_map(|res| res.map(|e| e.path()).ok()) .collect::>(); backups.sort(); From 0faa6a85780b43214ad34ef8cc314b2026977e41 Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Tue, 6 Jun 2023 16:57:59 +0200 Subject: [PATCH 03/93] feat: add basis for signal handling --- Cargo.lock | 20 +++++++++++++++ Cargo.toml | 1 + src/main.rs | 37 +++++++++------------------- src/server/process.rs | 4 +++ src/signals.rs | 57 +++++++++++++++++++++++++++++++++++++++++++ src/stdin.rs | 29 ++++++++++++++++++++++ 6 files changed, 123 insertions(+), 25 deletions(-) create mode 100644 src/signals.rs create mode 100644 src/stdin.rs diff --git a/Cargo.lock b/Cargo.lock index 5953204..5124e00 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -15,6 +15,7 @@ dependencies = [ "chrono", "clap", "flate2", + "signal-hook", "tar", ] @@ -383,6 +384,25 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "signal-hook" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "732768f1176d21d09e076c23a93123d40bba92d50c4058da34d45c8de8e682b9" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +dependencies = [ + "libc", +] + [[package]] name = "strsim" version = "0.10.0" diff --git a/Cargo.toml b/Cargo.toml index dd2f79b..4c0ba69 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ flate2 = "1.0.26" # Used for backup filenames chrono = "0.4.26" clap = { version = "4.3.1", features = ["derive", "env"] } +signal-hook = "0.3.15" [profile.release] lto = "fat" diff --git a/src/main.rs b/src/main.rs index 7837887..1c083fa 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,6 @@ mod server; +mod signals; +mod stdin; use clap::Parser; use server::ServerType; @@ -79,7 +81,8 @@ fn backups_thread(counter: Arc>, frequency: u64) { } } -fn main() { +fn main() -> io::Result<()> { + let (term, mut signals) = signals::install_signal_handlers()?; let cli = Cli::parse(); let cmd = server::ServerCommand::new(cli.type_, &cli.server_version) @@ -91,33 +94,17 @@ fn main() { .xms(cli.xms) .xmx(cli.xmx) .max_backups(cli.max_backups); - let counter = Arc::new(Mutex::new(cmd.spawn().expect("Failed to start server."))); + let server = Arc::new(cmd.spawn()?); if cli.frequency > 0 { - let clone = Arc::clone(&counter); - std::thread::spawn(move || backups_thread(clone, cli.frequency)); + let clone = Arc::clone(&server); + std::thread::spawn(move || backups_thread(server, cli.frequency)); } - let stdin = io::stdin(); - let input = &mut String::new(); + // Spawn thread that handles the main stdin loop + let clone = Arc::clone(&server); + std::thread::spawn(move || stdin::handle_stdin(server)); - loop { - input.clear(); - - if stdin.read_line(input).is_err() { - continue; - }; - - { - let mut server = counter.lock().unwrap(); - - if let Err(e) = server.send_command(input) { - println!("{}", e); - }; - } - - if input.trim() == "stop" { - break; - } - } + // Signal handler loop exits the process when necessary + signals::handle_signals(&mut signals, term, server) } diff --git a/src/server/process.rs b/src/server/process.rs index 5640b18..b5baae9 100644 --- a/src/server/process.rs +++ b/src/server/process.rs @@ -67,6 +67,10 @@ impl ServerProcess { Ok(()) } + pub fn kill(&mut self) -> std::io::Result<()> { + self.child.kill() + } + /// Perform a backup by disabling the server's save feature and flushing its data, before /// creating an archive file. pub fn backup(&mut self) -> std::io::Result<()> { diff --git a/src/signals.rs b/src/signals.rs new file mode 100644 index 0000000..a173047 --- /dev/null +++ b/src/signals.rs @@ -0,0 +1,57 @@ +use std::io; +use std::sync::{Arc, Mutex}; +use std::sync::atomic::{AtomicBool, Ordering}; + +use signal_hook::consts::TERM_SIGNALS; +use signal_hook::flag; +use signal_hook::iterator::{Signals, SignalsInfo}; + +use crate::server; + +/// Install the required signal handlers for terminating signals. +pub fn install_signal_handlers() -> io::Result<(Arc, SignalsInfo)> { + let term = Arc::new(AtomicBool::new(false)); + + // For each terminating signal, we register both a shutdown handler and a handler that sets an + // atomic bool. With this, the process will get killed immediately once it receives a second + // termination signal (e.g. a double ctrl-c). + // https://docs.rs/signal-hook/0.3.15/signal_hook/#a-complex-signal-handling-with-a-background-thread + // for sig in TERM_SIGNALS { + // // But this will "arm" the above for the second time, by setting it to true. + // // The order of registering these is important, if you put this one first, it will + // // first arm and then terminate ‒ all in the first round. + // flag::register(*sig, Arc::clone(&term))?; + // } + + let signals = TERM_SIGNALS; + + Ok((term, Signals::new(signals)?)) +} + +/// Loop that handles terminating signals as they come in. +pub fn handle_signals(signals: &mut SignalsInfo, term: Arc, server: Arc) -> io::Result<()> { + let mut force = false; + + // We only register terminating signals, so we don't need to differentiate between what kind of + // signal came in + for _ in signals { + // If term is already true, this is the second signal, meaning we kill the process + // immediately. + if force { + return server.kill() + } + // The stop command runs in a separate thread to avoid blocking the signal handling loop. + // After stopping the server, the thread terminates the process. + else { + let clone = Arc::clone(&server); + std::thread::spawn(move || { + let _ = clone.stop(); + std::process::exit(0); + }); + } + + force = true; + } + + Ok(()) +} diff --git a/src/stdin.rs b/src/stdin.rs new file mode 100644 index 0000000..98d8463 --- /dev/null +++ b/src/stdin.rs @@ -0,0 +1,29 @@ +use std::sync::{Arc, Mutex}; +use std::io; + +use crate::server; + +pub fn handle_stdin(counter: Arc>) { + let stdin = io::stdin(); + let input = &mut String::new(); + + loop { + input.clear(); + + if stdin.read_line(input).is_err() { + continue; + }; + + { + let mut server = counter.lock().unwrap(); + + if let Err(e) = server.send_command(input) { + println!("{}", e); + }; + } + + if input.trim() == "stop" { + std::process::exit(0); + } + } +} From 50cdd3115f37a0170f2ff1c26b7b3f2468b62a7c Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Tue, 6 Jun 2023 20:49:00 +0200 Subject: [PATCH 04/93] feat: solely handle single terminating signal for now --- .dockerignore | 5 ++++ CHANGELOG.md | 8 +++++++ Dockerfile | 64 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 14 +++++------ src/signals.rs | 33 +++++++++++++++++--------- src/stdin.rs | 2 +- 6 files changed, 107 insertions(+), 19 deletions(-) create mode 100644 .dockerignore create mode 100644 Dockerfile diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..1917669 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,5 @@ +* + +!Cargo.toml +!Cargo.lock +!src/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b969cb..4339669 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://git.rustybever.be/Chewing_Bever/alex/src/branch/dev) +### Added + +* Rudimentary signal handling for gently stopping server + * A single stop signal will trigger the Java process to shut down, but Alex + still expects to be run from a utility such as dumb-init +* Properly back up entire config directory +* Inject Java optimisation flags + ## [0.1.1](https://git.rustybever.be/Chewing_Bever/alex/src/tag/0.1.1) ### Changed diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..cb3b9e0 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,64 @@ +FROM rust:1.70-alpine3.18 AS builder + +ARG DI_VER=1.2.5 + +WORKDIR /app + +COPY . ./ + +RUN apk add --no-cache build-base unzip curl && \ + curl -Lo - "https://github.com/Yelp/dumb-init/archive/refs/tags/v${DI_VER}.tar.gz" | tar -xzf - && \ + cd "dumb-init-${DI_VER}" && \ + make SHELL=/bin/sh && \ + mv dumb-init .. + +RUN cargo build && \ + [ "$(readelf -d target/debug/alex | grep NEEDED | wc -l)" = 0 ] + + +# We use ${:-} instead of a default value because the argument is always passed +# to the build, it'll just be blank most likely +FROM eclipse-temurin:17-jre-alpine + +# Build arguments +ARG MC_VERSION=1.19.4 +ARG PAPERMC_VERSION=545 + +RUN addgroup -Sg 1000 paper && \ + adduser -SHG paper -u 1000 paper + +# Create worlds and config directory +WORKDIR /app +RUN mkdir -p worlds config/cache backups + +# Download server file +ADD "https://papermc.io/api/v2/projects/paper/versions/$MC_VERSION/builds/$PAPERMC_VERSION/downloads/paper-$MC_VERSION-$PAPERMC_VERSION.jar" server.jar + +# Make sure the server user can access all necessary folders +RUN chown -R paper:paper /app + +# Store the cache in an anonymous volume, which means it won't get stored in the other volumes +VOLUME /app/config/cache +VOLUME /app/backups + +COPY --from=builder /app/dumb-init /bin/dumb-init +COPY --from=builder /app/target/debug/alex /bin/alex + +RUN chmod +x /bin/alex + +# Default value to keep users from eating up all ram accidentally +ENV ALEX_XMS=1024 \ + ALEX_XMX=2048 \ + ALEX_JAR=/app/server.jar \ + ALEX_CONFIG_DIR=/app/config \ + ALEX_WORLD_DIR=/app/worlds \ + ALEX_BACKUPS_DIR=/app/backups \ + ALEX_SERVER_VERSION="${MC_VERSION}-${PAPERMC_VERSION}" + +# Document exposed ports +EXPOSE 25565 + +# Switch to non-root user +USER paper:paper + +ENTRYPOINT ["/bin/alex", "paper"] diff --git a/src/main.rs b/src/main.rs index 1c083fa..9f993a9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -82,7 +82,7 @@ fn backups_thread(counter: Arc>, frequency: u64) { } fn main() -> io::Result<()> { - let (term, mut signals) = signals::install_signal_handlers()?; + let (_, mut signals) = signals::install_signal_handlers()?; let cli = Cli::parse(); let cmd = server::ServerCommand::new(cli.type_, &cli.server_version) @@ -94,17 +94,17 @@ fn main() -> io::Result<()> { .xms(cli.xms) .xmx(cli.xmx) .max_backups(cli.max_backups); - let server = Arc::new(cmd.spawn()?); + let counter = Arc::new(Mutex::new(cmd.spawn()?)); if cli.frequency > 0 { - let clone = Arc::clone(&server); - std::thread::spawn(move || backups_thread(server, cli.frequency)); + let clone = Arc::clone(&counter); + std::thread::spawn(move || backups_thread(clone, cli.frequency)); } // Spawn thread that handles the main stdin loop - let clone = Arc::clone(&server); - std::thread::spawn(move || stdin::handle_stdin(server)); + let clone = Arc::clone(&counter); + std::thread::spawn(move || stdin::handle_stdin(clone)); // Signal handler loop exits the process when necessary - signals::handle_signals(&mut signals, term, server) + signals::handle_signals(&mut signals, counter) } diff --git a/src/signals.rs b/src/signals.rs index a173047..61c38f9 100644 --- a/src/signals.rs +++ b/src/signals.rs @@ -1,6 +1,6 @@ use std::io; +use std::sync::atomic::AtomicBool; use std::sync::{Arc, Mutex}; -use std::sync::atomic::{AtomicBool, Ordering}; use signal_hook::consts::TERM_SIGNALS; use signal_hook::flag; @@ -16,12 +16,15 @@ pub fn install_signal_handlers() -> io::Result<(Arc, SignalsInfo)> { // atomic bool. With this, the process will get killed immediately once it receives a second // termination signal (e.g. a double ctrl-c). // https://docs.rs/signal-hook/0.3.15/signal_hook/#a-complex-signal-handling-with-a-background-thread - // for sig in TERM_SIGNALS { - // // But this will "arm" the above for the second time, by setting it to true. - // // The order of registering these is important, if you put this one first, it will - // // first arm and then terminate ‒ all in the first round. - // flag::register(*sig, Arc::clone(&term))?; - // } + for sig in TERM_SIGNALS { + // When terminated by a second term signal, exit with exit code 1. + // This will do nothing the first time (because term_now is false). + flag::register_conditional_shutdown(*sig, 1, Arc::clone(&term))?; + // But this will "arm" the above for the second time, by setting it to true. + // The order of registering these is important, if you put this one first, it will + // first arm and then terminate ‒ all in the first round. + flag::register(*sig, Arc::clone(&term))?; + } let signals = TERM_SIGNALS; @@ -29,7 +32,10 @@ pub fn install_signal_handlers() -> io::Result<(Arc, SignalsInfo)> { } /// Loop that handles terminating signals as they come in. -pub fn handle_signals(signals: &mut SignalsInfo, term: Arc, server: Arc) -> io::Result<()> { +pub fn handle_signals( + signals: &mut SignalsInfo, + counter: Arc>, +) -> io::Result<()> { let mut force = false; // We only register terminating signals, so we don't need to differentiate between what kind of @@ -37,15 +43,20 @@ pub fn handle_signals(signals: &mut SignalsInfo, term: Arc, server: for _ in signals { // If term is already true, this is the second signal, meaning we kill the process // immediately. + // This will currently not work, as the initial stop command will block the kill from + // happening. if force { - return server.kill() + let mut server = counter.lock().unwrap(); + return server.kill(); } // The stop command runs in a separate thread to avoid blocking the signal handling loop. // After stopping the server, the thread terminates the process. else { - let clone = Arc::clone(&server); + let clone = Arc::clone(&counter); + std::thread::spawn(move || { - let _ = clone.stop(); + let mut server = clone.lock().unwrap(); + let _ = server.stop(); std::process::exit(0); }); } diff --git a/src/stdin.rs b/src/stdin.rs index 98d8463..f9a22e8 100644 --- a/src/stdin.rs +++ b/src/stdin.rs @@ -1,5 +1,5 @@ -use std::sync::{Arc, Mutex}; use std::io; +use std::sync::{Arc, Mutex}; use crate::server; From 69ce8616d5232a08ebb4ceac3fa3e9062d7245ff Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Tue, 6 Jun 2023 20:51:58 +0200 Subject: [PATCH 05/93] feat: custom message if backups failed --- src/server/process.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/server/process.rs b/src/server/process.rs index b5baae9..a9a9f45 100644 --- a/src/server/process.rs +++ b/src/server/process.rs @@ -93,7 +93,11 @@ impl ServerProcess { // The server's save feature needs to be enabled again even if the archive failed to create self.custom("save-on")?; - self.custom("say server backed up successfully")?; + if res.is_ok() { + self.custom("say server backed up successfully")?; + } else { + self.custom("an error occured while backing up the server")?; + } res } From 45d736d1bb515879ef7049d4bc51541775300d14 Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Tue, 13 Jun 2023 11:40:18 +0200 Subject: [PATCH 06/93] chore: bump version --- CHANGELOG.md | 2 ++ Cargo.lock | 2 +- Cargo.toml | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4339669..3160ee1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://git.rustybever.be/Chewing_Bever/alex/src/branch/dev) +## [0.2.0](https://git.rustybever.be/Chewing_Bever/alex/src/tag/0.2.0) + ### Added * Rudimentary signal handling for gently stopping server diff --git a/Cargo.lock b/Cargo.lock index 5124e00..0991d89 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10,7 +10,7 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "alex" -version = "0.1.0" +version = "0.2.0" dependencies = [ "chrono", "clap", diff --git a/Cargo.toml b/Cargo.toml index 4c0ba69..1de2d06 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "alex" -version = "0.1.0" +version = "0.2.0" description = "Wrapper around Minecraft server processes, designed to complement Docker image installations." authors = ["Jef Roosens"] edition = "2021" From acb3cfd8e6c172a2f4201d051bd222e99e4c8130 Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Tue, 13 Jun 2023 11:51:18 +0200 Subject: [PATCH 07/93] chore: update readme --- README.md | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f6215c3..4e00b78 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,26 @@ -# mc-wrapper +# Alex -A wrapper around a standard Minecraft server, written in Rust. \ No newline at end of file +Alex is a wrapper around a typical Minecraft server process. It acts as the +parent process, and sits in between the user's input and the server's stdin. +This allows Alex to support additional commands that execute Rust code. + +## Why + +The primary usecase for this is backups. A common problem I've had with +Minecraft backups is that they fail, because the server is writing to one of +the region files as the backup is being created. Alex solves this be sending +`save-off` and `save-all` to the server, before creating the tarball. +Afterwards, saving is enabled again with `save-on`. + +## Features + +* Create safe backups as gzip-compressed tarballs using the `backup` command +* Automatically create backups periodically +* Properly configures the process (working directory, optimisation flags) +* Configure everything as CLI arguments or environment variables + +## Installation + +Alex is distributed as statically compiled binaries for Linux amd64 and arm64. +These can be found +[here](https://git.rustybever.be/Chewing_Bever/alex/packages). From b08ba3853fa3ad1bc98e197e269599e8f2861e76 Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Tue, 13 Jun 2023 12:49:31 +0200 Subject: [PATCH 08/93] feat: add --dry flag --- CHANGELOG.md | 4 +++ Dockerfile | 2 +- src/main.rs | 15 ++++++++++- src/server/command.rs | 63 ++++++++++++++++++++++++++++++++++--------- 4 files changed, 69 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3160ee1..9124e7c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://git.rustybever.be/Chewing_Bever/alex/src/branch/dev) +### Added + +* `--dry` flag to inspect command that will be run + ## [0.2.0](https://git.rustybever.be/Chewing_Bever/alex/src/tag/0.2.0) ### Added diff --git a/Dockerfile b/Dockerfile index cb3b9e0..d754728 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,7 +18,7 @@ RUN cargo build && \ # We use ${:-} instead of a default value because the argument is always passed # to the build, it'll just be blank most likely -FROM eclipse-temurin:17-jre-alpine +FROM eclipse-temurin:18-jre-alpine # Build arguments ARG MC_VERSION=1.19.4 diff --git a/src/main.rs b/src/main.rs index 9f993a9..10500b3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -66,6 +66,11 @@ struct Cli { /// How frequently to perform a backup, in minutes; 0 to disable. #[arg(short = 't', long, default_value_t = 0, env = "ALEX_FREQUENCY")] frequency: u64, + + /// Don't actually run the server, but simply output the server configuration that would have + /// been ran + #[arg(short, long, default_value_t = false)] + dry: bool, } fn backups_thread(counter: Arc>, frequency: u64) { @@ -85,7 +90,7 @@ fn main() -> io::Result<()> { let (_, mut signals) = signals::install_signal_handlers()?; let cli = Cli::parse(); - let cmd = server::ServerCommand::new(cli.type_, &cli.server_version) + let mut cmd = server::ServerCommand::new(cli.type_, &cli.server_version) .java(&cli.java) .jar(cli.jar) .config(cli.config) @@ -94,6 +99,14 @@ fn main() -> io::Result<()> { .xms(cli.xms) .xmx(cli.xmx) .max_backups(cli.max_backups); + cmd.canonicalize()?; + + if cli.dry { + print!("{}", cmd); + + return Ok(()); + } + let counter = Arc::new(Mutex::new(cmd.spawn()?)); if cli.frequency > 0 { diff --git a/src/server/command.rs b/src/server/command.rs index 7b6d948..f410c88 100644 --- a/src/server/command.rs +++ b/src/server/command.rs @@ -105,14 +105,19 @@ impl ServerCommand { Ok(()) } - pub fn spawn(self) -> std::io::Result { + /// Canonicalize all paths to absolute paths. Without this command, all paths will be + /// interpreted relatively from the config directory. + pub fn canonicalize(&mut self) -> std::io::Result<()> { // To avoid any issues, we use absolute paths for everything when spawning the process - let jar = self.jar.canonicalize()?; - let config_dir = self.config_dir.canonicalize()?; - let world_dir = self.world_dir.canonicalize()?; - let backup_dir = self.backup_dir.canonicalize()?; + self.jar = self.jar.canonicalize()?; + self.config_dir = self.config_dir.canonicalize()?; + self.world_dir = self.world_dir.canonicalize()?; + self.backup_dir = self.backup_dir.canonicalize()?; - self.accept_eula()?; + Ok(()) + } + + fn create_cmd(&self) -> std::process::Command { let mut cmd = Command::new(&self.java); // Apply JVM optimisation flags @@ -155,24 +160,56 @@ impl ServerCommand { ]); } - cmd.current_dir(&config_dir) + cmd.current_dir(&self.config_dir) .arg("-jar") - .arg(&jar) + .arg(&self.jar) .arg("--universe") - .arg(&world_dir) + .arg(&self.world_dir) .arg("--nogui") .stdin(Stdio::piped()); + cmd + } + + pub fn spawn(&mut self) -> std::io::Result { + let mut cmd = self.create_cmd(); + self.accept_eula()?; let child = cmd.spawn()?; Ok(ServerProcess::new( self.type_, - self.version, - config_dir, - world_dir, - backup_dir, + self.version.clone(), + self.config_dir.clone(), + self.world_dir.clone(), + self.backup_dir.clone(), self.max_backups, child, )) } } + +impl fmt::Display for ServerCommand { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let cmd = self.create_cmd(); + + write!(f, "Command: {}\n", self.java)?; + write!(f, "Working dir: {}\n", self.config_dir.as_path().display())?; + + // Print command env vars + write!(f, "Environment:\n")?; + + for (key, val) in cmd.get_envs().filter(|(_, v)| v.is_some()) { + let val = val.unwrap(); + write!(f, " {}={}\n", key.to_string_lossy(), val.to_string_lossy())?; + } + + // Print command arguments + write!(f, "Arguments:\n")?; + + for arg in cmd.get_args() { + write!(f, " {}\n", arg.to_string_lossy())?; + } + + Ok(()) + } +} From 5ae23c931a607e4f48f9eb1230656e6ad9b98a85 Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Tue, 13 Jun 2023 13:00:42 +0200 Subject: [PATCH 09/93] feat: change jvm flags order --- CHANGELOG.md | 4 ++++ src/server/command.rs | 31 +++++++++++++++++++------------ 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9124e7c..2c02234 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * `--dry` flag to inspect command that will be run +### Changed + +* JVM flags now narrowely follow Aikar's specifications + ## [0.2.0](https://git.rustybever.be/Chewing_Bever/alex/src/tag/0.2.0) ### Added diff --git a/src/server/command.rs b/src/server/command.rs index f410c88..e725a38 100644 --- a/src/server/command.rs +++ b/src/server/command.rs @@ -131,15 +131,6 @@ impl ServerCommand { "-XX:+UnlockExperimentalVMOptions", "-XX:+DisableExplicitGC", "-XX:+AlwaysPreTouch", - "-XX:G1HeapWastePercent=5", - "-XX:G1MixedGCCountTarget=4", - "-XX:G1MixedGCLiveThresholdPercent=90", - "-XX:G1RSetUpdatingPauseTimePercent=5", - "-XX:SurvivorRatio=32", - "-XX:+PerfDisableSharedMem", - "-XX:MaxTenuringThreshold=1", - "-Dusing.aikars.flags=https://mcflags.emc.gs", - "-Daikars.new.flags=true", ]); if self.xms > 12 * 1024 { @@ -148,18 +139,34 @@ impl ServerCommand { "-XX:G1MaxNewSizePercent=50", "-XX:G1HeapRegionSize=16M", "-XX:G1ReservePercent=15", - "-XX:InitiatingHeapOccupancyPercent=20", ]); } else { cmd.args([ "-XX:G1NewSizePercent=30", "-XX:G1MaxNewSizePercent=40", "-XX:G1HeapRegionSize=8M", - "-XX:G1ReservePercent=15", - "-XX:InitiatingHeapOccupancyPercent=15", + "-XX:G1ReservePercent=20", ]); } + cmd.args(["-XX:G1HeapWastePercent=5", "-XX:G1MixedGCCountTarget=4"]); + + if self.xms > 12 * 1024 { + cmd.args(["-XX:InitiatingHeapOccupancyPercent=20"]); + } else { + cmd.args(["-XX:InitiatingHeapOccupancyPercent=15"]); + } + + cmd.args([ + "-XX:G1MixedGCLiveThresholdPercent=90", + "-XX:G1RSetUpdatingPauseTimePercent=5", + "-XX:SurvivorRatio=32", + "-XX:+PerfDisableSharedMem", + "-XX:MaxTenuringThreshold=1", + "-Dusing.aikars.flags=https://mcflags.emc.gs", + "-Daikars.new.flags=true", + ]); + cmd.current_dir(&self.config_dir) .arg("-jar") .arg(&self.jar) From ce3dcdd4b1e49d64938e9154457cc8a8f96845e7 Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Tue, 13 Jun 2023 13:01:47 +0200 Subject: [PATCH 10/93] chore: please clippy --- src/server/command.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/server/command.rs b/src/server/command.rs index e725a38..641c6b8 100644 --- a/src/server/command.rs +++ b/src/server/command.rs @@ -199,22 +199,22 @@ impl fmt::Display for ServerCommand { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let cmd = self.create_cmd(); - write!(f, "Command: {}\n", self.java)?; - write!(f, "Working dir: {}\n", self.config_dir.as_path().display())?; + writeln!(f, "Command: {}", self.java)?; + writeln!(f, "Working dir: {}", self.config_dir.as_path().display())?; // Print command env vars - write!(f, "Environment:\n")?; + writeln!(f, "Environment:")?; for (key, val) in cmd.get_envs().filter(|(_, v)| v.is_some()) { let val = val.unwrap(); - write!(f, " {}={}\n", key.to_string_lossy(), val.to_string_lossy())?; + writeln!(f, " {}={}", key.to_string_lossy(), val.to_string_lossy())?; } // Print command arguments - write!(f, "Arguments:\n")?; + writeln!(f, "Arguments:")?; for arg in cmd.get_args() { - write!(f, " {}\n", arg.to_string_lossy())?; + writeln!(f, " {}", arg.to_string_lossy())?; } Ok(()) From 375a68fbd6890cce15e94965944f655fd1bb878b Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Tue, 13 Jun 2023 13:02:27 +0200 Subject: [PATCH 11/93] chore: bump versions --- CHANGELOG.md | 2 ++ Cargo.lock | 2 +- Cargo.toml | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c02234..75eed18 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://git.rustybever.be/Chewing_Bever/alex/src/branch/dev) +## [0.2.1](https://git.rustybever.be/Chewing_Bever/alex/src/tag/0.2.1) + ### Added * `--dry` flag to inspect command that will be run diff --git a/Cargo.lock b/Cargo.lock index 0991d89..e802625 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10,7 +10,7 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "alex" -version = "0.2.0" +version = "0.2.1" dependencies = [ "chrono", "clap", diff --git a/Cargo.toml b/Cargo.toml index 1de2d06..43e1246 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "alex" -version = "0.2.0" +version = "0.2.1" description = "Wrapper around Minecraft server processes, designed to complement Docker image installations." authors = ["Jef Roosens"] edition = "2021" From 9ce8199d5fabffc4b652b4f62ee8d753738631d9 Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Tue, 13 Jun 2023 13:44:08 +0200 Subject: [PATCH 12/93] fix: use correct env var for backup dir --- CHANGELOG.md | 6 ++++++ Cargo.lock | 2 +- Cargo.toml | 2 +- Dockerfile | 5 +++-- src/main.rs | 2 +- 5 files changed, 12 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 75eed18..7b9570a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://git.rustybever.be/Chewing_Bever/alex/src/branch/dev) +## [0.2.2](https://git.rustybever.be/Chewing_Bever/alex/src/tag/0.2.2) + +### Fixed + +* Use correct env var for backup directory + ## [0.2.1](https://git.rustybever.be/Chewing_Bever/alex/src/tag/0.2.1) ### Added diff --git a/Cargo.lock b/Cargo.lock index e802625..3e798dd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10,7 +10,7 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "alex" -version = "0.2.1" +version = "0.2.2" dependencies = [ "chrono", "clap", diff --git a/Cargo.toml b/Cargo.toml index 43e1246..2c4045b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "alex" -version = "0.2.1" +version = "0.2.2" description = "Wrapper around Minecraft server processes, designed to complement Docker image installations." authors = ["Jef Roosens"] edition = "2021" diff --git a/Dockerfile b/Dockerfile index d754728..dba2674 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,7 +22,7 @@ FROM eclipse-temurin:18-jre-alpine # Build arguments ARG MC_VERSION=1.19.4 -ARG PAPERMC_VERSION=545 +ARG PAPERMC_VERSION=525 RUN addgroup -Sg 1000 paper && \ adduser -SHG paper -u 1000 paper @@ -61,4 +61,5 @@ EXPOSE 25565 # Switch to non-root user USER paper:paper -ENTRYPOINT ["/bin/alex", "paper"] +ENTRYPOINT ["/bin/dumb-init", "--"] +CMD ["/bin/alex", "paper"] diff --git a/src/main.rs b/src/main.rs index 10500b3..2cd84ff 100644 --- a/src/main.rs +++ b/src/main.rs @@ -46,7 +46,7 @@ struct Cli { long, value_name = "BACKUP_DIR", default_value = "../backups", - env = "ALEX_WORLD_DIR" + env = "ALEX_BACKUP_DIR" )] backup: PathBuf, /// Java command to run the server jar with From 90aa929b738b462aa2ebbb6b47da02830b7d58f6 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Wed, 7 Jun 2023 21:15:15 +0200 Subject: [PATCH 13/93] feat: show backup time in message --- src/server/process.rs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/server/process.rs b/src/server/process.rs index a9a9f45..233a678 100644 --- a/src/server/process.rs +++ b/src/server/process.rs @@ -84,6 +84,7 @@ impl ServerProcess { // We wait some time to (hopefully) ensure the save-all call has completed std::thread::sleep(std::time::Duration::from_secs(10)); + let start_time = chrono::offset::Local::now(); let res = self.create_backup_archive(); if res.is_ok() { @@ -93,10 +94,20 @@ impl ServerProcess { // The server's save feature needs to be enabled again even if the archive failed to create self.custom("save-on")?; + let duration = chrono::offset::Local::now() - start_time; + let duration_str = format!( + "{}m{}s", + duration.num_seconds() / 60, + duration.num_seconds() % 60 + ); + if res.is_ok() { - self.custom("say server backed up successfully")?; + self.custom(&format!("say server backed up in {}", duration_str))?; } else { - self.custom("an error occured while backing up the server")?; + self.custom(&format!( + "an error occured after {} while backing up the server", + duration_str + ))?; } res From 4958257f6e28c179d4ac81e1c23d037093438fda Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Fri, 9 Jun 2023 09:25:51 +0200 Subject: [PATCH 14/93] refactor: move backup logic to separate module --- .gitignore | 2 +- src/server/backups.rs | 108 ++++++++++++++++++++++++++++++++++++++++++ src/server/mod.rs | 2 + src/server/process.rs | 93 +++++------------------------------- 4 files changed, 123 insertions(+), 82 deletions(-) create mode 100644 src/server/backups.rs diff --git a/.gitignore b/.gitignore index 4259b1b..3695da7 100644 --- a/.gitignore +++ b/.gitignore @@ -19,4 +19,4 @@ target/ # testing files *.jar -data/ +data*/ diff --git a/src/server/backups.rs b/src/server/backups.rs new file mode 100644 index 0000000..37e6021 --- /dev/null +++ b/src/server/backups.rs @@ -0,0 +1,108 @@ +use flate2::write::GzEncoder; +use flate2::Compression; +use std::fs::File; +use std::io; +use std::path::{Path, PathBuf}; + +#[link(name = "c")] +extern "C" { + fn geteuid() -> u32; + fn getegid() -> u32; +} + +static FILENAME_FORMAT: &str = "%Y-%m-%d_%H-%M-%S.tar.gz"; + +pub struct BackupManager { + backup_dir: PathBuf, + config_dir: PathBuf, + world_dir: PathBuf, + max_backups: u64, + start_time: Option>, +} + +impl BackupManager { + pub fn open( + backup_dir: PathBuf, + config_dir: PathBuf, + world_dir: PathBuf, + max_backups: u64, + ) -> Self { + BackupManager { + backup_dir, + config_dir, + world_dir, + max_backups, + start_time: None, + } + } + + pub fn create_archive(&mut self) -> io::Result<()> { + let start_time = chrono::offset::Local::now(); + self.start_time = Some(start_time); + + let filename = format!("{}", start_time.format(FILENAME_FORMAT)); + let path = self.backup_dir.join(filename); + let tar_gz = File::create(path)?; + let enc = GzEncoder::new(tar_gz, Compression::default()); + let mut tar = tar::Builder::new(enc); + + tar.append_dir_all("worlds", &self.world_dir)?; + + // Add all files from the config directory that aren't the cache + for entry in self + .config_dir + .read_dir()? + .filter_map(|e| e.ok()) + .filter(|e| e.file_name() != "cache") + { + let tar_path = Path::new("config").join(entry.file_name()); + + if entry.file_type()?.is_dir() { + tar.append_dir_all(tar_path, entry.path())?; + } else { + tar.append_path_with_name(entry.path(), tar_path)?; + } + } + + // TODO re-add this info file in some way + // We add a file to the backup describing for what version it was made + // let info = format!("{} {}", self.type_, self.version); + // let info_bytes = info.as_bytes(); + + // let mut header = tar::Header::new_gnu(); + // header.set_size(info_bytes.len().try_into().unwrap()); + // header.set_mode(0o100644); + // unsafe { + // header.set_gid(getegid().into()); + // header.set_uid(geteuid().into()); + // } + + // tar.append_data(&mut header, "info.txt", info_bytes)?; + + Ok(()) + } + + /// Remove the oldest backups + pub fn remove_old_backups(&mut self) -> std::io::Result<()> { + // The naming format used allows us to sort the backups by name and still get a sorting by + // creation time + let mut backups = self + .backup_dir + .read_dir()? + .filter_map(|res| res.map(|e| e.path()).ok()) + .collect::>(); + backups.sort(); + + let max_backups: usize = self.max_backups.try_into().unwrap(); + + if backups.len() > max_backups { + let excess_backups = backups.len() - max_backups; + + for backup in &backups[0..excess_backups] { + std::fs::remove_file(backup)?; + } + } + + Ok(()) + } +} diff --git a/src/server/mod.rs b/src/server/mod.rs index e3e3131..ed5cb21 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -1,5 +1,7 @@ +mod backups; mod command; mod process; +pub use backups::BackupManager; pub use command::{ServerCommand, ServerType}; pub use process::ServerProcess; diff --git a/src/server/process.rs b/src/server/process.rs index 233a678..f503c84 100644 --- a/src/server/process.rs +++ b/src/server/process.rs @@ -1,3 +1,4 @@ +use crate::server::BackupManager; use crate::server::ServerType; use flate2::write::GzEncoder; use flate2::Compression; @@ -5,12 +6,6 @@ use std::io::Write; use std::path::{Path, PathBuf}; use std::process::Child; -#[link(name = "c")] -extern "C" { - fn geteuid() -> u32; - fn getegid() -> u32; -} - pub struct ServerProcess { type_: ServerType, version: String, @@ -19,6 +14,7 @@ pub struct ServerProcess { backup_dir: PathBuf, max_backups: u64, child: Child, + backups: BackupManager, } impl ServerProcess { @@ -31,6 +27,13 @@ impl ServerProcess { max_backups: u64, child: Child, ) -> ServerProcess { + let backup_manager = BackupManager::open( + backup_dir.clone(), + config_dir.clone(), + world_dir.clone(), + max_backups, + ); + ServerProcess { type_, version, @@ -39,6 +42,7 @@ impl ServerProcess { backup_dir, max_backups, child, + backups: backup_manager, } } @@ -85,10 +89,10 @@ impl ServerProcess { std::thread::sleep(std::time::Duration::from_secs(10)); let start_time = chrono::offset::Local::now(); - let res = self.create_backup_archive(); + let res = self.backups.create_archive(); if res.is_ok() { - self.remove_old_backups()?; + self.backups.remove_old_backups()?; } // The server's save feature needs to be enabled again even if the archive failed to create @@ -112,77 +116,4 @@ impl ServerProcess { res } - - /// Create a new compressed backup archive of the server's data. - fn create_backup_archive(&mut self) -> std::io::Result<()> { - // Create a gzip-compressed tarball of the worlds folder - let filename = format!( - "{}", - chrono::offset::Local::now().format("%Y-%m-%d_%H-%M-%S.tar.gz") - ); - let path = self.backup_dir.join(filename); - let tar_gz = std::fs::File::create(path)?; - let enc = GzEncoder::new(tar_gz, Compression::default()); - let mut tar = tar::Builder::new(enc); - - tar.append_dir_all("worlds", &self.world_dir)?; - - // Add all files from the config directory that aren't the cache - for entry in self - .config_dir - .read_dir()? - .filter_map(|e| e.ok()) - .filter(|e| e.file_name() != "cache") - { - let tar_path = Path::new("config").join(entry.file_name()); - - if entry.file_type()?.is_dir() { - tar.append_dir_all(tar_path, entry.path())?; - } else { - tar.append_path_with_name(entry.path(), tar_path)?; - } - } - - // We add a file to the backup describing for what version it was made - let info = format!("{} {}", self.type_, self.version); - let info_bytes = info.as_bytes(); - - let mut header = tar::Header::new_gnu(); - header.set_size(info_bytes.len().try_into().unwrap()); - header.set_mode(0o100644); - unsafe { - header.set_gid(getegid().into()); - header.set_uid(geteuid().into()); - } - - tar.append_data(&mut header, "info.txt", info_bytes)?; - - // Backup file gets finalized in the drop - - Ok(()) - } - - /// Remove the oldest backups - fn remove_old_backups(&mut self) -> std::io::Result<()> { - // The naming format used allows us to sort the backups by name and still get a sorting by - // creation time - let mut backups = self - .backup_dir - .read_dir()? - .filter_map(|res| res.map(|e| e.path()).ok()) - .collect::>(); - backups.sort(); - - let max_backups: usize = self.max_backups.try_into().unwrap(); - - if backups.len() > max_backups { - let excess_backups = backups.len() - max_backups; - - for backup in &backups[0..excess_backups] { - std::fs::remove_file(backup)?; - } - } - - Ok(()) - } } From 29d6713486bae0bd38d8bbebe533c02fa93f8fbd Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Fri, 9 Jun 2023 10:11:02 +0200 Subject: [PATCH 15/93] feat: implement own listing of files --- src/server/backups.rs | 50 ++++++++++++++++++++++++++++++------------- 1 file changed, 35 insertions(+), 15 deletions(-) diff --git a/src/server/backups.rs b/src/server/backups.rs index 37e6021..e5fd4de 100644 --- a/src/server/backups.rs +++ b/src/server/backups.rs @@ -18,6 +18,7 @@ pub struct BackupManager { world_dir: PathBuf, max_backups: u64, start_time: Option>, + files: Vec<(PathBuf, PathBuf)> } impl BackupManager { @@ -33,9 +34,40 @@ impl BackupManager { world_dir, max_backups, start_time: None, + files: Vec::new() } } + fn set_files_to_backup(&mut self) -> io::Result<()> { + let mut dirs = vec![ + (PathBuf::from("worlds"), self.world_dir.clone()), + (PathBuf::from("config"), self.config_dir.clone()), + ]; + self.files.clear(); + + while let Some((path_in_tar, path)) = dirs.pop() { + for res in path.read_dir()? { + let entry = res?; + + if entry.file_name() == "cache" { + continue; + } + + let new_path_in_tar = path_in_tar.join(entry.file_name()); + + // All dirs get expanded recursively, while all files get returned as output + // NOTE: does this remove empty directories from backups? Is this a problem? + if entry.file_type()?.is_dir() { + dirs.push((new_path_in_tar, entry.path())); + } else { + self.files.push((new_path_in_tar, entry.path())); + } + } + } + + Ok(()) + } + pub fn create_archive(&mut self) -> io::Result<()> { let start_time = chrono::offset::Local::now(); self.start_time = Some(start_time); @@ -46,22 +78,10 @@ impl BackupManager { let enc = GzEncoder::new(tar_gz, Compression::default()); let mut tar = tar::Builder::new(enc); - tar.append_dir_all("worlds", &self.world_dir)?; + self.set_files_to_backup()?; - // Add all files from the config directory that aren't the cache - for entry in self - .config_dir - .read_dir()? - .filter_map(|e| e.ok()) - .filter(|e| e.file_name() != "cache") - { - let tar_path = Path::new("config").join(entry.file_name()); - - if entry.file_type()?.is_dir() { - tar.append_dir_all(tar_path, entry.path())?; - } else { - tar.append_path_with_name(entry.path(), tar_path)?; - } + for (path_in_tar, path) in &self.files { + tar.append_path_with_name(path, path_in_tar)?; } // TODO re-add this info file in some way From 703a25e8beb36e4546d588808a3f11d59f7c80bd Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Fri, 9 Jun 2023 10:42:17 +0200 Subject: [PATCH 16/93] refactor: use utc time --- src/server/backups.rs | 4 ++-- src/server/process.rs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/server/backups.rs b/src/server/backups.rs index e5fd4de..d567f42 100644 --- a/src/server/backups.rs +++ b/src/server/backups.rs @@ -17,7 +17,7 @@ pub struct BackupManager { config_dir: PathBuf, world_dir: PathBuf, max_backups: u64, - start_time: Option>, + start_time: Option>, files: Vec<(PathBuf, PathBuf)> } @@ -69,7 +69,7 @@ impl BackupManager { } pub fn create_archive(&mut self) -> io::Result<()> { - let start_time = chrono::offset::Local::now(); + let start_time = chrono::offset::Utc::now(); self.start_time = Some(start_time); let filename = format!("{}", start_time.format(FILENAME_FORMAT)); diff --git a/src/server/process.rs b/src/server/process.rs index f503c84..7555aa3 100644 --- a/src/server/process.rs +++ b/src/server/process.rs @@ -88,7 +88,7 @@ impl ServerProcess { // We wait some time to (hopefully) ensure the save-all call has completed std::thread::sleep(std::time::Duration::from_secs(10)); - let start_time = chrono::offset::Local::now(); + let start_time = chrono::offset::Utc::now(); let res = self.backups.create_archive(); if res.is_ok() { @@ -98,7 +98,7 @@ impl ServerProcess { // The server's save feature needs to be enabled again even if the archive failed to create self.custom("save-on")?; - let duration = chrono::offset::Local::now() - start_time; + let duration = chrono::offset::Utc::now() - start_time; let duration_str = format!( "{}m{}s", duration.num_seconds() / 60, From b7a678e32f5af5bdcb10f430771767db22a51431 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Tue, 13 Jun 2023 15:09:07 +0200 Subject: [PATCH 17/93] feat: lots of backup stuff --- .cargo/config.toml | 4 +- src/server/backups.rs | 155 +++++++++++++++++++++++++----------------- src/server/mod.rs | 1 + src/server/path.rs | 19 ++++++ src/server/process.rs | 2 +- 5 files changed, 116 insertions(+), 65 deletions(-) create mode 100644 src/server/path.rs diff --git a/.cargo/config.toml b/.cargo/config.toml index d1675c8..37bb90a 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,3 +1,3 @@ [alias] -runs = "run -- paper 1.19.4-545 --config data/config --backup data/backups --world data/worlds --jar data/paper.jar" -runrs = "run --release -- paper 1.19.4-545 --config data/config --backup data/backups --world data/worlds --jar data/paper.jar" +runs = "run -- paper 1.19.4-545 --config data/config --backup data/backups --world data/worlds --jar data/paper-1.19.4-525.jar" +runrs = "run --release -- paper 1.19.4-545 --config data/config --backup data/backups --world data/worlds --jar data/paper-1.19.4-525.jar" diff --git a/src/server/backups.rs b/src/server/backups.rs index d567f42..e0937f1 100644 --- a/src/server/backups.rs +++ b/src/server/backups.rs @@ -1,5 +1,7 @@ +use chrono::{Local, Utc}; use flate2::write::GzEncoder; use flate2::Compression; +use std::collections::{HashMap, HashSet}; use std::fs::File; use std::io; use std::path::{Path, PathBuf}; @@ -10,15 +12,95 @@ extern "C" { fn getegid() -> u32; } -static FILENAME_FORMAT: &str = "%Y-%m-%d_%H-%M-%S.tar.gz"; +const FILENAME_FORMAT: &str = "%Y-%m-%d_%H-%M-%S.tar.gz"; + +pub enum BackupType { + Full, + Incremental, +} + +/// Represents a successful backup +pub struct Backup { + previous: Option>, + /// When the backup was started (also corresponds to the name) + start_time: chrono::DateTime, + /// Type of the backup + type_: BackupType, + /// What files were added/modified in each part of the tarball. + pub added: HashMap>, + /// What files were removed in this backup, in comparison to the previous backup. For full + /// backups, this will always be empty, as they do not consider previous backups. + /// The map stores a separate list for each top-level directory, as the contents of these + /// directories can come for different source directories. + pub removed: HashMap>, +} + +fn files(src_dir: PathBuf) -> io::Result> { + let mut dirs = vec![src_dir.clone()]; + let mut files: HashSet = HashSet::new(); + + while let Some(dir) = dirs.pop() { + for res in dir.read_dir()? { + let entry = res?; + + if entry.file_name() == "cache" { + continue; + } + + if entry.file_type()?.is_dir() { + dirs.push(entry.path()); + } else { + files.insert(entry.path().strip_prefix(&src_dir).unwrap().to_path_buf()); + } + } + } + + Ok(files) +} + +impl Backup { + /// Create a new full backup + pub fn create>( + backup_dir: P, + dirs: Vec<(PathBuf, PathBuf)>, + ) -> io::Result { + let backup_dir = backup_dir.as_ref(); + let start_time = chrono::offset::Utc::now(); + + let filename = format!("{}", start_time.format(FILENAME_FORMAT)); + let path = backup_dir.join(filename); + let tar_gz = File::create(path)?; + let enc = GzEncoder::new(tar_gz, Compression::default()); + let mut ar = tar::Builder::new(enc); + + let mut added: HashMap> = HashMap::new(); + + for (dir_in_tar, src_dir) in dirs { + let files = files(src_dir.clone())?; + + for path in &files { + ar.append_path_with_name(dir_in_tar.join(&path), src_dir.join(&path))?; + } + + added.insert(dir_in_tar, files); + } + + Ok(Backup { + previous: None, + type_: BackupType::Full, + start_time, + added, + removed: HashMap::new(), + }) + } +} pub struct BackupManager { backup_dir: PathBuf, config_dir: PathBuf, world_dir: PathBuf, max_backups: u64, - start_time: Option>, - files: Vec<(PathBuf, PathBuf)> + last_backup: Option, } impl BackupManager { @@ -33,75 +115,24 @@ impl BackupManager { config_dir, world_dir, max_backups, - start_time: None, - files: Vec::new() + last_backup: None, } } - fn set_files_to_backup(&mut self) -> io::Result<()> { - let mut dirs = vec![ - (PathBuf::from("worlds"), self.world_dir.clone()), + pub fn create_backup(&mut self) -> io::Result<()> { + let dirs = vec![ (PathBuf::from("config"), self.config_dir.clone()), + (PathBuf::from("worlds"), self.world_dir.clone()), ]; - self.files.clear(); - - while let Some((path_in_tar, path)) = dirs.pop() { - for res in path.read_dir()? { - let entry = res?; - - if entry.file_name() == "cache" { - continue; - } - - let new_path_in_tar = path_in_tar.join(entry.file_name()); - - // All dirs get expanded recursively, while all files get returned as output - // NOTE: does this remove empty directories from backups? Is this a problem? - if entry.file_type()?.is_dir() { - dirs.push((new_path_in_tar, entry.path())); - } else { - self.files.push((new_path_in_tar, entry.path())); - } - } + if let Some(last_backup) = &self.last_backup { + todo!(); + } else { + self.last_backup = Some(Backup::create(&self.backup_dir, dirs)?); } Ok(()) } - pub fn create_archive(&mut self) -> io::Result<()> { - let start_time = chrono::offset::Utc::now(); - self.start_time = Some(start_time); - - let filename = format!("{}", start_time.format(FILENAME_FORMAT)); - let path = self.backup_dir.join(filename); - let tar_gz = File::create(path)?; - let enc = GzEncoder::new(tar_gz, Compression::default()); - let mut tar = tar::Builder::new(enc); - - self.set_files_to_backup()?; - - for (path_in_tar, path) in &self.files { - tar.append_path_with_name(path, path_in_tar)?; - } - - // TODO re-add this info file in some way - // We add a file to the backup describing for what version it was made - // let info = format!("{} {}", self.type_, self.version); - // let info_bytes = info.as_bytes(); - - // let mut header = tar::Header::new_gnu(); - // header.set_size(info_bytes.len().try_into().unwrap()); - // header.set_mode(0o100644); - // unsafe { - // header.set_gid(getegid().into()); - // header.set_uid(geteuid().into()); - // } - - // tar.append_data(&mut header, "info.txt", info_bytes)?; - - Ok(()) - } - /// Remove the oldest backups pub fn remove_old_backups(&mut self) -> std::io::Result<()> { // The naming format used allows us to sort the backups by name and still get a sorting by diff --git a/src/server/mod.rs b/src/server/mod.rs index ed5cb21..4c2beb2 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -1,5 +1,6 @@ mod backups; mod command; +mod path; mod process; pub use backups::BackupManager; diff --git a/src/server/path.rs b/src/server/path.rs new file mode 100644 index 0000000..d9df799 --- /dev/null +++ b/src/server/path.rs @@ -0,0 +1,19 @@ +use chrono::Utc; +use std::collections::HashSet; +use std::path::PathBuf; +use std::{fs, io}; + +struct ReadDirRecursive { + ignored_dirs: HashSet, + read_dir: Option, + stack: Vec, +} + +impl ReadDirRecursive { + // pub fn new() +} + +trait PathExt { + fn modified_since(timestamp: chrono::DateTime) -> bool; + fn read_dir_recusive() -> ReadDirRecursive; +} diff --git a/src/server/process.rs b/src/server/process.rs index 7555aa3..6edc484 100644 --- a/src/server/process.rs +++ b/src/server/process.rs @@ -89,7 +89,7 @@ impl ServerProcess { std::thread::sleep(std::time::Duration::from_secs(10)); let start_time = chrono::offset::Utc::now(); - let res = self.backups.create_archive(); + let res = self.backups.create_backup(); if res.is_ok() { self.backups.remove_old_backups()?; From fcc111b4efc75eae67092ec14c61952d2f02322c Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Wed, 14 Jun 2023 21:47:59 +0200 Subject: [PATCH 18/93] feat: possible incremental backup implementation using new abstraction --- src/server/backups.rs | 232 +++++++++++++++++++++++++++++++++++++----- 1 file changed, 204 insertions(+), 28 deletions(-) diff --git a/src/server/backups.rs b/src/server/backups.rs index e0937f1..66c7192 100644 --- a/src/server/backups.rs +++ b/src/server/backups.rs @@ -5,6 +5,7 @@ use std::collections::{HashMap, HashSet}; use std::fs::File; use std::io; use std::path::{Path, PathBuf}; +use std::sync::Arc; #[link(name = "c")] extern "C" { @@ -14,27 +15,6 @@ extern "C" { const FILENAME_FORMAT: &str = "%Y-%m-%d_%H-%M-%S.tar.gz"; -pub enum BackupType { - Full, - Incremental, -} - -/// Represents a successful backup -pub struct Backup { - previous: Option>, - /// When the backup was started (also corresponds to the name) - start_time: chrono::DateTime, - /// Type of the backup - type_: BackupType, - /// What files were added/modified in each part of the tarball. - pub added: HashMap>, - /// What files were removed in this backup, in comparison to the previous backup. For full - /// backups, this will always be empty, as they do not consider previous backups. - /// The map stores a separate list for each top-level directory, as the contents of these - /// directories can come for different source directories. - pub removed: HashMap>, -} - fn files(src_dir: PathBuf) -> io::Result> { let mut dirs = vec![src_dir.clone()]; let mut files: HashSet = HashSet::new(); @@ -58,8 +38,148 @@ fn files(src_dir: PathBuf) -> io::Result> { Ok(files) } +/// Return false only if we can say with certainty that the file wasn't modified since the given +/// timestamp, true otherwise. +fn modified_since>(time: chrono::DateTime, path: T) -> bool { + let path = path.as_ref(); + + if let Ok(metadata) = path.metadata() { + let last_modified = metadata.modified(); + + if let Ok(last_modified) = last_modified { + let t: chrono::DateTime = last_modified.into(); + let t = t.with_timezone(&Local); + + return t >= time; + } + } + + false +} + +#[derive(PartialEq)] +pub enum BackupType { + Full, + Incremental, +} + +#[derive(Debug)] +pub enum BackupError { + NoFullAncestor, +} + +type BackupResult = Result; + +/// Represents the changes relative to the previous backup +pub struct BackupDelta { + /// What files were added/modified in each part of the tarball. + pub added: HashMap>, + /// What files were removed in this backup, in comparison to the previous backup. For full + /// backups, this will always be empty, as they do not consider previous backups. + /// The map stores a separate list for each top-level directory, as the contents of these + /// directories can come for different source directories. + pub removed: HashMap>, +} + +impl BackupDelta { + pub fn new() -> Self { + BackupDelta { + added: HashMap::new(), + removed: HashMap::new(), + } + } + + /// Update the current state so that its result becomes the merge of itself and the other + /// state. + pub fn merge(&mut self, delta: &BackupDelta) { + for (dir, added) in delta.added.iter() { + // Files that were removed in the current state, but added in the new state, are no + // longer removed + if let Some(orig_removed) = self.removed.get_mut(dir) { + orig_removed.retain(|k| !added.contains(k)); + } + + // Newly added files are added to the state as well + if let Some(orig_added) = self.added.get_mut(dir) { + orig_added.extend(added.iter().cloned()); + } else { + self.added.insert(dir.clone(), added.clone()); + } + } + + for (dir, removed) in delta.removed.iter() { + // Files that were originally added, but now deleted are removed from the added list + if let Some(orig_added) = self.added.get_mut(dir) { + orig_added.retain(|k| !removed.contains(k)); + } + + // Newly removed files are added to the state as well + if let Some(orig_removed) = self.removed.get_mut(dir) { + orig_removed.extend(removed.iter().cloned()); + } else { + self.removed.insert(dir.clone(), removed.clone()); + } + } + } + + /// Modify the given state by applying this delta's changes to it + pub fn apply(&self, state: &mut HashMap>) { + // First we add new files, then we remove the old ones + for (dir, added) in self.added.iter() { + if let Some(current) = state.get_mut(dir) { + current.extend(added.iter().cloned()); + } else { + state.insert(dir.clone(), added.clone()); + } + } + + for (dir, removed) in self.removed.iter() { + if let Some(current) = state.get_mut(dir) { + current.retain(|k| !removed.contains(k)); + } + } + } +} + +/// Represents a successful backup +pub struct Backup { + previous: Option>, + /// When the backup was started (also corresponds to the name) + start_time: chrono::DateTime, + /// Type of the backup + type_: BackupType, + delta: BackupDelta, +} + impl Backup { - /// Create a new full backup + /// Calculate the full state of the backup by applying all its ancestors delta's in order, + /// starting from the last full ancestor. + pub fn state(&self) -> BackupResult>> { + if self.type_ == BackupType::Full { + let mut state = HashMap::new(); + self.delta.apply(&mut state); + + Ok(state) + } else if let Some(previous) = &self.previous { + let mut state = previous.state()?; + self.delta.apply(&mut state); + + Ok(state) + } else { + return Err(BackupError::NoFullAncestor); + } + } + /// Create a new Full backup, populated with the given directories. + /// + /// # Arguments + /// + /// * `backup_dir` - Directory to store archive in + /// * `dirs` - list of tuples `(path_in_tar, src_dir)` with `path_in_tar` the directory name + /// under which `src_dir`'s contents should be stored in the archive + /// + /// # Returns + /// + /// The `Backup` instance describing this new backup. pub fn create>( backup_dir: P, dirs: Vec<(PathBuf, PathBuf)>, @@ -79,7 +199,7 @@ impl Backup { let files = files(src_dir.clone())?; for path in &files { - ar.append_path_with_name(dir_in_tar.join(&path), src_dir.join(&path))?; + ar.append_path_with_name(dir_in_tar.join(path), src_dir.join(path))?; } added.insert(dir_in_tar, files); @@ -89,8 +209,58 @@ impl Backup { previous: None, type_: BackupType::Full, start_time, - added, - removed: HashMap::new(), + delta: BackupDelta { + added, + removed: HashMap::new(), + }, + }) + } + + /// Create a new incremental backup from a given previous backup + pub fn create_from>( + previous: Arc, + backup_dir: P, + dirs: Vec<(PathBuf, PathBuf)>, + ) -> io::Result { + let backup_dir = backup_dir.as_ref(); + let start_time = chrono::offset::Utc::now(); + + let filename = format!("{}", start_time.format(FILENAME_FORMAT)); + let path = backup_dir.join(filename); + let tar_gz = File::create(path)?; + let enc = GzEncoder::new(tar_gz, Compression::default()); + let mut ar = tar::Builder::new(enc); + + let previous_state = previous.state().unwrap(); + let mut delta = BackupDelta::new(); + + for (dir_in_tar, src_dir) in dirs { + let files = files(src_dir.clone())?; + let added_files = files + .iter() + .filter(|p| modified_since(previous.start_time, p)) + .cloned() + .collect::>(); + + for path in added_files.iter() { + ar.append_path_with_name(dir_in_tar.join(path), src_dir.join(path))?; + } + + delta.added.insert(dir_in_tar.clone(), added_files); + + if let Some(previous_files) = previous_state.get(&dir_in_tar) { + delta.removed.insert( + dir_in_tar, + previous_files.difference(&files).cloned().collect(), + ); + } + } + + Ok(Backup { + previous: Some(previous), + type_: BackupType::Incremental, + start_time, + delta, }) } } @@ -100,7 +270,7 @@ pub struct BackupManager { config_dir: PathBuf, world_dir: PathBuf, max_backups: u64, - last_backup: Option, + last_backup: Option>, } impl BackupManager { @@ -124,10 +294,16 @@ impl BackupManager { (PathBuf::from("config"), self.config_dir.clone()), (PathBuf::from("worlds"), self.world_dir.clone()), ]; + if let Some(last_backup) = &self.last_backup { - todo!(); + let clone = last_backup.clone(); + self.last_backup = Some(Arc::new(Backup::create_from( + clone, + &self.backup_dir, + dirs, + )?)); } else { - self.last_backup = Some(Backup::create(&self.backup_dir, dirs)?); + self.last_backup = Some(Arc::new(Backup::create(&self.backup_dir, dirs)?)); } Ok(()) From a9e7b215d18f7fe9a9d778d878649ec29df8c09b Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Wed, 14 Jun 2023 22:16:54 +0200 Subject: [PATCH 19/93] feat: move running server to subcommand --- .cargo/config.toml | 2 +- CHANGELOG.md | 4 ++ src/cli.rs | 91 +++++++++++++++++++++++++++++++++++++++ src/main.rs | 103 ++++++++++----------------------------------- 4 files changed, 119 insertions(+), 81 deletions(-) create mode 100644 src/cli.rs diff --git a/.cargo/config.toml b/.cargo/config.toml index 37bb90a..09b7896 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,3 +1,3 @@ [alias] -runs = "run -- paper 1.19.4-545 --config data/config --backup data/backups --world data/worlds --jar data/paper-1.19.4-525.jar" +runs = "run -- run paper 1.19.4-550 --config data/config --backup data/backups --world data/worlds --jar paper-1.19.4-550.jar" runrs = "run --release -- paper 1.19.4-545 --config data/config --backup data/backups --world data/worlds --jar data/paper-1.19.4-525.jar" diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b9570a..e431581 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://git.rustybever.be/Chewing_Bever/alex/src/branch/dev) +### Changed + +* Running the server now uses the `run` CLI subcommand + ## [0.2.2](https://git.rustybever.be/Chewing_Bever/alex/src/tag/0.2.2) ### Fixed diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 0000000..1acbe36 --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,91 @@ +use crate::server::ServerType; +use clap::{Args, Parser, Subcommand}; +use std::path::PathBuf; + +#[derive(Parser)] +#[command(author, version, about, long_about = None)] +pub struct Cli { + #[command(subcommand)] + pub command: Commands, + /// Directory where configs are stored, and where the server will run + #[arg( + long, + value_name = "CONFIG_DIR", + default_value = ".", + env = "ALEX_CONFIG_DIR", + global = true + )] + pub config: PathBuf, + /// Directory where world files will be saved + #[arg( + long, + value_name = "WORLD_DIR", + default_value = "../worlds", + env = "ALEX_WORLD_DIR", + global = true + )] + pub world: PathBuf, + /// Directory where backups will be stored + #[arg( + long, + value_name = "BACKUP_DIR", + default_value = "../backups", + env = "ALEX_BACKUP_DIR", + global = true + )] + pub backup: PathBuf, + + /// How many backups to keep + #[arg( + short = 'n', + long, + default_value_t = 7, + env = "ALEX_MAX_BACKUPS", + global = true + )] + pub max_backups: u64, +} + +#[derive(Subcommand)] +pub enum Commands { + /// Run the server + Run(RunArgs), +} + +#[derive(Args)] +pub struct RunArgs { + /// Type of server + pub type_: ServerType, + /// Version string for the server, e.g. 1.19.4-545 + #[arg(env = "ALEX_SERVER_VERSION")] + pub server_version: String, + + /// Server jar to execute + #[arg( + long, + value_name = "JAR_PATH", + default_value = "server.jar", + env = "ALEX_JAR" + )] + pub jar: PathBuf, + + /// Java command to run the server jar with + #[arg(long, value_name = "JAVA_CMD", default_value_t = String::from("java"), env = "ALEX_JAVA")] + pub java: String, + + /// XMS value in megabytes for the server instance + #[arg(long, default_value_t = 1024, env = "ALEX_XMS")] + pub xms: u64, + /// XMX value in megabytes for the server instance + #[arg(long, default_value_t = 2048, env = "ALEX_XMX")] + pub xmx: u64, + + /// How frequently to perform a backup, in minutes; 0 to disable. + #[arg(short = 't', long, default_value_t = 0, env = "ALEX_FREQUENCY")] + pub frequency: u64, + + /// Don't actually run the server, but simply output the server configuration that would have + /// been ran + #[arg(short, long, default_value_t = false)] + pub dry: bool, +} diff --git a/src/main.rs b/src/main.rs index 2cd84ff..a1ae21c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,78 +1,13 @@ +mod cli; mod server; mod signals; mod stdin; use clap::Parser; -use server::ServerType; +use cli::{Cli, Commands, RunArgs}; use std::io; -use std::path::PathBuf; use std::sync::{Arc, Mutex}; -#[derive(Parser)] -#[command(author, version, about, long_about = None)] -struct Cli { - /// Type of server - type_: ServerType, - /// Version string for the server, e.g. 1.19.4-545 - #[arg(env = "ALEX_SERVER_VERSION")] - server_version: String, - - /// Server jar to execute - #[arg( - long, - value_name = "JAR_PATH", - default_value = "server.jar", - env = "ALEX_JAR" - )] - jar: PathBuf, - /// Directory where configs are stored, and where the server will run - #[arg( - long, - value_name = "CONFIG_DIR", - default_value = ".", - env = "ALEX_CONFIG_DIR" - )] - config: PathBuf, - /// Directory where world files will be saved - #[arg( - long, - value_name = "WORLD_DIR", - default_value = "../worlds", - env = "ALEX_WORLD_DIR" - )] - world: PathBuf, - /// Directory where backups will be stored - #[arg( - long, - value_name = "BACKUP_DIR", - default_value = "../backups", - env = "ALEX_BACKUP_DIR" - )] - backup: PathBuf, - /// Java command to run the server jar with - #[arg(long, value_name = "JAVA_CMD", default_value_t = String::from("java"), env = "ALEX_JAVA")] - java: String, - - /// XMS value in megabytes for the server instance - #[arg(long, default_value_t = 1024, env = "ALEX_XMS")] - xms: u64, - /// XMX value in megabytes for the server instance - #[arg(long, default_value_t = 2048, env = "ALEX_XMX")] - xmx: u64, - - /// How many backups to keep - #[arg(short = 'n', long, default_value_t = 7, env = "ALEX_MAX_BACKUPS")] - max_backups: u64, - /// How frequently to perform a backup, in minutes; 0 to disable. - #[arg(short = 't', long, default_value_t = 0, env = "ALEX_FREQUENCY")] - frequency: u64, - - /// Don't actually run the server, but simply output the server configuration that would have - /// been ran - #[arg(short, long, default_value_t = false)] - dry: bool, -} - fn backups_thread(counter: Arc>, frequency: u64) { loop { std::thread::sleep(std::time::Duration::from_secs(frequency * 60)); @@ -86,22 +21,21 @@ fn backups_thread(counter: Arc>, frequency: u64) { } } -fn main() -> io::Result<()> { +fn command_run(cli: &Cli, args: &RunArgs) -> io::Result<()> { let (_, mut signals) = signals::install_signal_handlers()?; - let cli = Cli::parse(); - let mut cmd = server::ServerCommand::new(cli.type_, &cli.server_version) - .java(&cli.java) - .jar(cli.jar) - .config(cli.config) - .world(cli.world) - .backup(cli.backup) - .xms(cli.xms) - .xmx(cli.xmx) + let mut cmd = server::ServerCommand::new(args.type_, &args.server_version) + .java(&args.java) + .jar(args.jar.clone()) + .config(cli.config.clone()) + .world(cli.world.clone()) + .backup(cli.backup.clone()) + .xms(args.xms) + .xmx(args.xmx) .max_backups(cli.max_backups); cmd.canonicalize()?; - if cli.dry { + if args.dry { print!("{}", cmd); return Ok(()); @@ -109,9 +43,10 @@ fn main() -> io::Result<()> { let counter = Arc::new(Mutex::new(cmd.spawn()?)); - if cli.frequency > 0 { + if args.frequency > 0 { let clone = Arc::clone(&counter); - std::thread::spawn(move || backups_thread(clone, cli.frequency)); + let frequency = args.frequency; + std::thread::spawn(move || backups_thread(clone, frequency)); } // Spawn thread that handles the main stdin loop @@ -121,3 +56,11 @@ fn main() -> io::Result<()> { // Signal handler loop exits the process when necessary signals::handle_signals(&mut signals, counter) } + +fn main() -> io::Result<()> { + let cli = Cli::parse(); + + match &cli.command { + Commands::Run(args) => command_run(&cli, args), + } +} From d204c684008fc35e8e9e6aa619c1aa034328d9f7 Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Thu, 15 Jun 2023 09:56:40 +0200 Subject: [PATCH 20/93] fix: actually working incremental backup --- src/server/backups.rs | 39 ++++++++++++++++++++++----------------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/src/server/backups.rs b/src/server/backups.rs index 66c7192..2620e17 100644 --- a/src/server/backups.rs +++ b/src/server/backups.rs @@ -38,9 +38,12 @@ fn files(src_dir: PathBuf) -> io::Result> { Ok(files) } -/// Return false only if we can say with certainty that the file wasn't modified since the given -/// timestamp, true otherwise. -fn modified_since>(time: chrono::DateTime, path: T) -> bool { +/// Check whether a file has been modified since the given timestamp. +/// +/// Note that this function will *only* return true if it can determine with certainty that the +/// file has not been modified. If any errors occur while obtaining the required metadata (e.g. if +/// the file system does not support this metadata), this function will return false. +fn not_modified_since>(time: chrono::DateTime, path: T) -> bool { let path = path.as_ref(); if let Ok(metadata) = path.metadata() { @@ -50,14 +53,14 @@ fn modified_since>(time: chrono::DateTime, path: T) -> bool let t: chrono::DateTime = last_modified.into(); let t = t.with_timezone(&Local); - return t >= time; + return t < time; } } false } -#[derive(PartialEq)] +#[derive(Debug, PartialEq)] pub enum BackupType { Full, Incremental, @@ -71,6 +74,7 @@ pub enum BackupError { type BackupResult = Result; /// Represents the changes relative to the previous backup +#[derive(Debug)] pub struct BackupDelta { /// What files were added/modified in each part of the tarball. pub added: HashMap>, @@ -142,6 +146,7 @@ impl BackupDelta { } /// Represents a successful backup +#[derive(Debug)] pub struct Backup { previous: Option>, /// When the backup was started (also corresponds to the name) @@ -199,7 +204,7 @@ impl Backup { let files = files(src_dir.clone())?; for path in &files { - ar.append_path_with_name(dir_in_tar.join(path), src_dir.join(path))?; + ar.append_path_with_name(src_dir.join(path), dir_in_tar.join(path))?; } added.insert(dir_in_tar, files); @@ -231,6 +236,7 @@ impl Backup { let enc = GzEncoder::new(tar_gz, Compression::default()); let mut ar = tar::Builder::new(enc); + // TODO remove unwrap let previous_state = previous.state().unwrap(); let mut delta = BackupDelta::new(); @@ -238,12 +244,14 @@ impl Backup { let files = files(src_dir.clone())?; let added_files = files .iter() - .filter(|p| modified_since(previous.start_time, p)) + // This explicit negation is because we wish to also include files for which we + // couldn't determine the last modified time + .filter(|p| !not_modified_since(previous.start_time, src_dir.join(p))) .cloned() .collect::>(); for path in added_files.iter() { - ar.append_path_with_name(dir_in_tar.join(path), src_dir.join(path))?; + ar.append_path_with_name(src_dir.join(path), dir_in_tar.join(path))?; } delta.added.insert(dir_in_tar.clone(), added_files); @@ -295,16 +303,13 @@ impl BackupManager { (PathBuf::from("worlds"), self.world_dir.clone()), ]; - if let Some(last_backup) = &self.last_backup { - let clone = last_backup.clone(); - self.last_backup = Some(Arc::new(Backup::create_from( - clone, - &self.backup_dir, - dirs, - )?)); + let backup = if let Some(last_backup) = &self.last_backup { + Backup::create_from(Arc::clone(last_backup), &self.backup_dir, dirs)? } else { - self.last_backup = Some(Arc::new(Backup::create(&self.backup_dir, dirs)?)); - } + Backup::create(&self.backup_dir, dirs)? + }; + + self.last_backup = Some(Arc::new(backup)); Ok(()) } From 8add96b39bb0050a21e4ed16d66f9781005e9479 Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Thu, 15 Jun 2023 20:36:46 +0200 Subject: [PATCH 21/93] feat: persistently store backup state --- Cargo.lock | 46 ++++++++++++++++++++++++++++++ Cargo.toml | 5 ++-- src/server/backups.rs | 66 ++++++++++++++++++++++++++++++++++++++++--- src/server/command.rs | 13 +++++---- src/server/process.rs | 25 ++-------------- 5 files changed, 121 insertions(+), 34 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3e798dd..5ba48c9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -15,6 +15,8 @@ dependencies = [ "chrono", "clap", "flate2", + "serde", + "serde_json", "signal-hook", "tar", ] @@ -123,6 +125,7 @@ dependencies = [ "iana-time-zone", "js-sys", "num-traits", + "serde", "time", "wasm-bindgen", "winapi", @@ -292,6 +295,12 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "itoa" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" + [[package]] name = "js-sys" version = "0.3.63" @@ -384,6 +393,43 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "ryu" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" + +[[package]] +name = "serde" +version = "1.0.164" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e8c8cf938e98f769bc164923b06dce91cea1751522f46f8466461af04c9027d" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.164" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9735b638ccc51c28bf6914d90a2e9725b377144fc612c49a611fddd1b631d68" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.96" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1" +dependencies = [ + "itoa", + "ryu", + "serde", +] + [[package]] name = "signal-hook" version = "0.3.15" diff --git a/Cargo.toml b/Cargo.toml index 2c4045b..2ac6b39 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,10 +12,11 @@ edition = "2021" tar = "0.4.38" # Used to compress said tarballs using gzip flate2 = "1.0.26" -# Used for backup filenames -chrono = "0.4.26" +chrono = { version = "0.4.26", features = ["serde"] } clap = { version = "4.3.1", features = ["derive", "env"] } signal-hook = "0.3.15" +serde = { version = "1.0.164", features = ["derive", "rc"] } +serde_json = "1.0.96" [profile.release] lto = "fat" diff --git a/src/server/backups.rs b/src/server/backups.rs index 2620e17..8012b5f 100644 --- a/src/server/backups.rs +++ b/src/server/backups.rs @@ -1,6 +1,7 @@ use chrono::{Local, Utc}; use flate2::write::GzEncoder; use flate2::Compression; +use serde::{Deserialize, Serialize}; use std::collections::{HashMap, HashSet}; use std::fs::File; use std::io; @@ -60,7 +61,7 @@ fn not_modified_since>(time: chrono::DateTime, path: T) -> b false } -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Serialize, Deserialize)] pub enum BackupType { Full, Incremental, @@ -74,7 +75,7 @@ pub enum BackupError { type BackupResult = Result; /// Represents the changes relative to the previous backup -#[derive(Debug)] +#[derive(Debug, Serialize, Deserialize)] pub struct BackupDelta { /// What files were added/modified in each part of the tarball. pub added: HashMap>, @@ -146,8 +147,9 @@ impl BackupDelta { } /// Represents a successful backup -#[derive(Debug)] +#[derive(Debug, Serialize, Deserialize)] pub struct Backup { + #[serde(skip)] previous: Option>, /// When the backup was started (also corresponds to the name) start_time: chrono::DateTime, @@ -174,6 +176,11 @@ impl Backup { return Err(BackupError::NoFullAncestor); } } + + pub fn set_previous(&mut self, previous: Arc) { + self.previous = Some(previous); + } + /// Create a new Full backup, populated with the given directories. /// /// # Arguments @@ -282,7 +289,9 @@ pub struct BackupManager { } impl BackupManager { - pub fn open( + const METADATA_FILE: &str = "alex.json"; + + pub fn new( backup_dir: PathBuf, config_dir: PathBuf, world_dir: PathBuf, @@ -297,6 +306,18 @@ impl BackupManager { } } + pub fn open( + backup_dir: PathBuf, + config_dir: PathBuf, + world_dir: PathBuf, + max_backups: u64, + ) -> std::io::Result { + let mut manager = Self::new(backup_dir, config_dir, world_dir, max_backups); + manager.load_json()?; + + Ok(manager) + } + pub fn create_backup(&mut self) -> io::Result<()> { let dirs = vec![ (PathBuf::from("config"), self.config_dir.clone()), @@ -310,6 +331,7 @@ impl BackupManager { }; self.last_backup = Some(Arc::new(backup)); + self.write_json()?; Ok(()) } @@ -337,4 +359,40 @@ impl BackupManager { Ok(()) } + + pub fn write_json(&self) -> std::io::Result<()> { + // Put the backup chain into a list that can be serialized + let mut backups: Vec> = Vec::new(); + let mut backup_opt = &self.last_backup; + + while let Some(backup) = backup_opt { + backups.insert(0, Arc::clone(backup)); + backup_opt = &backup.previous; + } + + let json_file = File::create(self.backup_dir.join(Self::METADATA_FILE))?; + serde_json::to_writer(json_file, &backups)?; + + Ok(()) + } + + pub fn load_json(&mut self) -> std::io::Result<()> { + let json_file = File::open(self.backup_dir.join(Self::METADATA_FILE))?; + let mut backups: Vec> = serde_json::from_reader(json_file)?; + + if !backups.is_empty() { + for i in 1..backups.len() { + let previous = Arc::clone(&backups[i - 1]); + // We can unwrap here, as this function creates the first instance of each Arc, + // meaning we're definitely the only pointer. + Arc::get_mut(&mut backups[i]) + .unwrap() + .set_previous(previous); + } + + self.last_backup = Some(Arc::clone(backups.last().unwrap())); + } + + Ok(()) + } } diff --git a/src/server/command.rs b/src/server/command.rs index 641c6b8..1c44d5b 100644 --- a/src/server/command.rs +++ b/src/server/command.rs @@ -1,4 +1,4 @@ -use crate::server::ServerProcess; +use crate::server::{BackupManager, ServerProcess}; use clap::ValueEnum; use std::fmt; use std::fs::File; @@ -179,6 +179,12 @@ impl ServerCommand { } pub fn spawn(&mut self) -> std::io::Result { + let manager = BackupManager::open( + self.backup_dir.clone(), + self.config_dir.clone(), + self.world_dir.clone(), + self.max_backups, + )?; let mut cmd = self.create_cmd(); self.accept_eula()?; let child = cmd.spawn()?; @@ -186,10 +192,7 @@ impl ServerCommand { Ok(ServerProcess::new( self.type_, self.version.clone(), - self.config_dir.clone(), - self.world_dir.clone(), - self.backup_dir.clone(), - self.max_backups, + manager, child, )) } diff --git a/src/server/process.rs b/src/server/process.rs index 6edc484..fbd806a 100644 --- a/src/server/process.rs +++ b/src/server/process.rs @@ -1,18 +1,11 @@ use crate::server::BackupManager; use crate::server::ServerType; -use flate2::write::GzEncoder; -use flate2::Compression; use std::io::Write; -use std::path::{Path, PathBuf}; use std::process::Child; pub struct ServerProcess { type_: ServerType, version: String, - config_dir: PathBuf, - world_dir: PathBuf, - backup_dir: PathBuf, - max_backups: u64, child: Child, backups: BackupManager, } @@ -21,28 +14,14 @@ impl ServerProcess { pub fn new( type_: ServerType, version: String, - config_dir: PathBuf, - world_dir: PathBuf, - backup_dir: PathBuf, - max_backups: u64, + manager: BackupManager, child: Child, ) -> ServerProcess { - let backup_manager = BackupManager::open( - backup_dir.clone(), - config_dir.clone(), - world_dir.clone(), - max_backups, - ); - ServerProcess { type_, version, - config_dir, - world_dir, - backup_dir, - max_backups, child, - backups: backup_manager, + backups: manager, } } From 27d7e681c3d9d00e90b21bd7c9fd8985183ea54c Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Thu, 15 Jun 2023 22:54:17 +0200 Subject: [PATCH 22/93] feat: temporarily disable "remove old backups" --- src/server/backups.rs | 102 ++++++++++++++++++++++++++++-------------- src/server/process.rs | 6 +-- 2 files changed, 71 insertions(+), 37 deletions(-) diff --git a/src/server/backups.rs b/src/server/backups.rs index 8012b5f..e225aec 100644 --- a/src/server/backups.rs +++ b/src/server/backups.rs @@ -14,8 +14,7 @@ extern "C" { fn getegid() -> u32; } -const FILENAME_FORMAT: &str = "%Y-%m-%d_%H-%M-%S.tar.gz"; - +/// List all files in `src_dir` and all child directories. fn files(src_dir: PathBuf) -> io::Result> { let mut dirs = vec![src_dir.clone()]; let mut files: HashSet = HashSet::new(); @@ -159,7 +158,18 @@ pub struct Backup { } impl Backup { - /// Calculate the full state of the backup by applying all its ancestors delta's in order, + const FILENAME_FORMAT: &str = "%Y-%m-%d_%H-%M-%S.tar.gz"; + + /// Returns a pointer to this backup's previous backup by cloning the Arc pointer. + pub fn previous(&self) -> Option> { + if let Some(previous) = &self.previous { + Some(Arc::clone(&previous)) + } else { + None + } + } + + /// Calculate the full state of the backup by applying all its ancestors' delta's in order, /// starting from the last full ancestor. pub fn state(&self) -> BackupResult>> { if self.type_ == BackupType::Full { @@ -177,8 +187,27 @@ impl Backup { } } - pub fn set_previous(&mut self, previous: Arc) { - self.previous = Some(previous); + /// Returns the n'th ancestor of the given backup, if it exists. + pub fn ancestor(&self, n: u64) -> Option> { + if n == 0 { + None + } else if let Some(previous) = &self.previous { + if n == 1 { + Some(Arc::clone(&previous)) + } else { + previous.ancestor(n - 1) + } + } else { + None + } + } + + /// Return the path to a backup file by properly formatting the data. + pub fn path>(backup_dir: P, start_time: chrono::DateTime) -> PathBuf { + let backup_dir = backup_dir.as_ref(); + + let filename = format!("{}", start_time.format(Self::FILENAME_FORMAT)); + backup_dir.join(filename) } /// Create a new Full backup, populated with the given directories. @@ -196,11 +225,9 @@ impl Backup { backup_dir: P, dirs: Vec<(PathBuf, PathBuf)>, ) -> io::Result { - let backup_dir = backup_dir.as_ref(); let start_time = chrono::offset::Utc::now(); - let filename = format!("{}", start_time.format(FILENAME_FORMAT)); - let path = backup_dir.join(filename); + let path = Self::path(backup_dir, start_time); let tar_gz = File::create(path)?; let enc = GzEncoder::new(tar_gz, Compression::default()); let mut ar = tar::Builder::new(enc); @@ -234,17 +261,16 @@ impl Backup { backup_dir: P, dirs: Vec<(PathBuf, PathBuf)>, ) -> io::Result { - let backup_dir = backup_dir.as_ref(); let start_time = chrono::offset::Utc::now(); - let filename = format!("{}", start_time.format(FILENAME_FORMAT)); - let path = backup_dir.join(filename); + let path = Self::path(backup_dir, start_time); let tar_gz = File::create(path)?; let enc = GzEncoder::new(tar_gz, Compression::default()); let mut ar = tar::Builder::new(enc); - // TODO remove unwrap - let previous_state = previous.state().unwrap(); + let previous_state = previous + .state() + .map_err(|_| io::Error::new(io::ErrorKind::Other, "No Full ancestor"))?; let mut delta = BackupDelta::new(); for (dir_in_tar, src_dir) in dirs { @@ -337,28 +363,38 @@ impl BackupManager { } /// Remove the oldest backups - pub fn remove_old_backups(&mut self) -> std::io::Result<()> { - // The naming format used allows us to sort the backups by name and still get a sorting by - // creation time - let mut backups = self - .backup_dir - .read_dir()? - .filter_map(|res| res.map(|e| e.path()).ok()) - .collect::>(); - backups.sort(); + // pub fn remove_old_backups(&mut self) -> std::io::Result<()> { + // if let Some(last_backup) = &self.last_backup { + // let last_valid_ancestor = last_backup.ancestor(self.max_backups - 1); + // let ancestor = last_valid_ancestor.previous(); - let max_backups: usize = self.max_backups.try_into().unwrap(); + // while let Some(backup) = &ancestor { + // let path = Backup::path(&self.backup_dir, backup.start_time); + // std::fs::remove_file(path)?; + // } + // } - if backups.len() > max_backups { - let excess_backups = backups.len() - max_backups; + // // The naming format used allows us to sort the backups by name and still get a sorting by + // // creation time + // let mut backups = self + // .backup_dir + // .read_dir()? + // .filter_map(|res| res.map(|e| e.path()).ok()) + // .collect::>(); + // backups.sort(); - for backup in &backups[0..excess_backups] { - std::fs::remove_file(backup)?; - } - } + // let max_backups: usize = self.max_backups.try_into().unwrap(); - Ok(()) - } + // if backups.len() > max_backups { + // let excess_backups = backups.len() - max_backups; + + // for backup in &backups[0..excess_backups] { + // std::fs::remove_file(backup)?; + // } + // } + + // Ok(()) + // } pub fn write_json(&self) -> std::io::Result<()> { // Put the backup chain into a list that can be serialized @@ -385,9 +421,7 @@ impl BackupManager { let previous = Arc::clone(&backups[i - 1]); // We can unwrap here, as this function creates the first instance of each Arc, // meaning we're definitely the only pointer. - Arc::get_mut(&mut backups[i]) - .unwrap() - .set_previous(previous); + Arc::get_mut(&mut backups[i]).unwrap().previous = Some(previous); } self.last_backup = Some(Arc::clone(backups.last().unwrap())); diff --git a/src/server/process.rs b/src/server/process.rs index fbd806a..d784061 100644 --- a/src/server/process.rs +++ b/src/server/process.rs @@ -70,9 +70,9 @@ impl ServerProcess { let start_time = chrono::offset::Utc::now(); let res = self.backups.create_backup(); - if res.is_ok() { - self.backups.remove_old_backups()?; - } + // if res.is_ok() { + // self.backups.remove_old_backups()?; + // } // The server's save feature needs to be enabled again even if the archive failed to create self.custom("save-on")?; From 527535635357d574b1d6e042a5a78271b6a9aecf Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Fri, 16 Jun 2023 17:23:36 +0200 Subject: [PATCH 23/93] feat: added backup cli command --- CHANGELOG.md | 4 ++++ src/cli.rs | 12 ++++++++++++ src/main.rs | 14 +++++++++++++- 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e431581..e2ec666 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://git.rustybever.be/Chewing_Bever/alex/src/branch/dev) +### Added + +* Added `backup` CLI command + ### Changed * Running the server now uses the `run` CLI subcommand diff --git a/src/cli.rs b/src/cli.rs index 1acbe36..3c588aa 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -50,6 +50,9 @@ pub struct Cli { pub enum Commands { /// Run the server Run(RunArgs), + /// Create a new backup of the server. This command should only be used when the server is not + /// running. + Backup(BackupArgs), } #[derive(Args)] @@ -89,3 +92,12 @@ pub struct RunArgs { #[arg(short, long, default_value_t = false)] pub dry: bool, } + +#[derive(Args)] +pub struct BackupArgs { + /// Type of server + pub type_: ServerType, + /// Version string for the server, e.g. 1.19.4-545 + #[arg(env = "ALEX_SERVER_VERSION")] + pub server_version: String, +} diff --git a/src/main.rs b/src/main.rs index a1ae21c..3d82d11 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,7 +4,7 @@ mod signals; mod stdin; use clap::Parser; -use cli::{Cli, Commands, RunArgs}; +use cli::{BackupArgs, Cli, Commands, RunArgs}; use std::io; use std::sync::{Arc, Mutex}; @@ -57,10 +57,22 @@ fn command_run(cli: &Cli, args: &RunArgs) -> io::Result<()> { signals::handle_signals(&mut signals, counter) } +fn commands_backup(cli: &Cli, _args: &BackupArgs) -> io::Result<()> { + let mut manager = server::BackupManager::open( + cli.backup.clone(), + cli.config.clone(), + cli.world.clone(), + cli.max_backups, + )?; + + manager.create_backup() +} + fn main() -> io::Result<()> { let cli = Cli::parse(); match &cli.command { Commands::Run(args) => command_run(&cli, args), + Commands::Backup(args) => commands_backup(&cli, args), } } From f7235fb34243767711d42767c912055a47ce7452 Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Sat, 17 Jun 2023 12:08:46 +0200 Subject: [PATCH 24/93] refactor: move iterating over files to Path extension trait --- src/server/backups.rs | 98 ++++++++-------------------- src/server/path.rs | 148 +++++++++++++++++++++++++++++++++++++++--- 2 files changed, 164 insertions(+), 82 deletions(-) diff --git a/src/server/backups.rs b/src/server/backups.rs index e225aec..cdb0409 100644 --- a/src/server/backups.rs +++ b/src/server/backups.rs @@ -1,3 +1,4 @@ +use crate::server::path::PathExt; use chrono::{Local, Utc}; use flate2::write::GzEncoder; use flate2::Compression; @@ -14,52 +15,6 @@ extern "C" { fn getegid() -> u32; } -/// List all files in `src_dir` and all child directories. -fn files(src_dir: PathBuf) -> io::Result> { - let mut dirs = vec![src_dir.clone()]; - let mut files: HashSet = HashSet::new(); - - while let Some(dir) = dirs.pop() { - for res in dir.read_dir()? { - let entry = res?; - - if entry.file_name() == "cache" { - continue; - } - - if entry.file_type()?.is_dir() { - dirs.push(entry.path()); - } else { - files.insert(entry.path().strip_prefix(&src_dir).unwrap().to_path_buf()); - } - } - } - - Ok(files) -} - -/// Check whether a file has been modified since the given timestamp. -/// -/// Note that this function will *only* return true if it can determine with certainty that the -/// file has not been modified. If any errors occur while obtaining the required metadata (e.g. if -/// the file system does not support this metadata), this function will return false. -fn not_modified_since>(time: chrono::DateTime, path: T) -> bool { - let path = path.as_ref(); - - if let Ok(metadata) = path.metadata() { - let last_modified = metadata.modified(); - - if let Ok(last_modified) = last_modified { - let t: chrono::DateTime = last_modified.into(); - let t = t.with_timezone(&Local); - - return t < time; - } - } - - false -} - #[derive(Debug, PartialEq, Serialize, Deserialize)] pub enum BackupType { Full, @@ -162,11 +117,7 @@ impl Backup { /// Returns a pointer to this backup's previous backup by cloning the Arc pointer. pub fn previous(&self) -> Option> { - if let Some(previous) = &self.previous { - Some(Arc::clone(&previous)) - } else { - None - } + self.previous.as_ref().map(Arc::clone) } /// Calculate the full state of the backup by applying all its ancestors' delta's in order, @@ -193,7 +144,7 @@ impl Backup { None } else if let Some(previous) = &self.previous { if n == 1 { - Some(Arc::clone(&previous)) + Some(Arc::clone(previous)) } else { previous.ancestor(n - 1) } @@ -232,26 +183,27 @@ impl Backup { let enc = GzEncoder::new(tar_gz, Compression::default()); let mut ar = tar::Builder::new(enc); - let mut added: HashMap> = HashMap::new(); + let mut delta = BackupDelta::new(); for (dir_in_tar, src_dir) in dirs { - let files = files(src_dir.clone())?; + let mut added_files: HashSet = HashSet::new(); - for path in &files { - ar.append_path_with_name(src_dir.join(path), dir_in_tar.join(path))?; + for entry in src_dir.read_dir_recursive()?.ignored("cache").files() { + let path = entry?.path(); + let stripped = path.strip_prefix(&src_dir).unwrap(); + + ar.append_path_with_name(&path, dir_in_tar.join(stripped))?; + added_files.insert(stripped.to_path_buf()); } - added.insert(dir_in_tar, files); + delta.added.insert(dir_in_tar, added_files); } Ok(Backup { previous: None, type_: BackupType::Full, start_time, - delta: BackupDelta { - added, - removed: HashMap::new(), - }, + delta, }) } @@ -274,17 +226,19 @@ impl Backup { let mut delta = BackupDelta::new(); for (dir_in_tar, src_dir) in dirs { - let files = files(src_dir.clone())?; - let added_files = files - .iter() - // This explicit negation is because we wish to also include files for which we - // couldn't determine the last modified time - .filter(|p| !not_modified_since(previous.start_time, src_dir.join(p))) - .cloned() - .collect::>(); + let mut all_files: HashSet = HashSet::new(); + let mut added_files: HashSet = HashSet::new(); - for path in added_files.iter() { - ar.append_path_with_name(src_dir.join(path), dir_in_tar.join(path))?; + for entry in src_dir.read_dir_recursive()?.ignored("cache").files() { + let path = entry?.path(); + let stripped = path.strip_prefix(&src_dir).unwrap(); + + if !path.not_modified_since(previous.start_time) { + ar.append_path_with_name(&path, dir_in_tar.join(stripped))?; + added_files.insert(stripped.to_path_buf()); + } + + all_files.insert(stripped.to_path_buf()); } delta.added.insert(dir_in_tar.clone(), added_files); @@ -292,7 +246,7 @@ impl Backup { if let Some(previous_files) = previous_state.get(&dir_in_tar) { delta.removed.insert( dir_in_tar, - previous_files.difference(&files).cloned().collect(), + previous_files.difference(&all_files).cloned().collect(), ); } } diff --git a/src/server/path.rs b/src/server/path.rs index d9df799..b8b5ae9 100644 --- a/src/server/path.rs +++ b/src/server/path.rs @@ -1,19 +1,147 @@ -use chrono::Utc; +use chrono::{Local, Utc}; use std::collections::HashSet; -use std::path::PathBuf; +use std::ffi::OsString; +use std::fs::DirEntry; +use std::path::{Path, PathBuf}; use std::{fs, io}; -struct ReadDirRecursive { - ignored_dirs: HashSet, - read_dir: Option, - stack: Vec, +pub struct ReadDirRecursive { + ignored: HashSet, + read_dir: fs::ReadDir, + dir_stack: Vec, + files_only: bool, } impl ReadDirRecursive { - // pub fn new() + /// Start the iterator for a new directory + pub fn start>(path: P) -> io::Result { + let path = path.as_ref(); + let read_dir = path.read_dir()?; + + Ok(ReadDirRecursive { + ignored: HashSet::new(), + read_dir, + dir_stack: Vec::new(), + files_only: false, + }) + } + + pub fn ignored>(mut self, s: S) -> Self { + self.ignored.insert(s.into()); + + self + } + + pub fn files(mut self) -> Self { + self.files_only = true; + + self + } + + /// Tries to populate the `read_dir` field with a new `ReadDir` instance to consume. + fn next_read_dir(&mut self) -> io::Result { + if let Some(path) = self.dir_stack.pop() { + self.read_dir = path.read_dir()?; + + Ok(true) + } else { + Ok(false) + } + } + + /// Convenience method to add a new directory to the stack. + fn push_entry(&mut self, entry: &io::Result) { + if let Ok(entry) = entry { + if entry.path().is_dir() { + self.dir_stack.push(entry.path()); + } + } + } + + /// Determine whether an entry should be returned by the iterator. + fn should_return(&self, entry: &io::Result) -> bool { + if let Ok(entry) = entry { + let mut res = !self.ignored.contains(&entry.file_name()); + + // Please just let me combine these already + if self.files_only { + if let Ok(file_type) = entry.file_type() { + res = res && file_type.is_file(); + } + // We couldn't determine if it's a file, so we don't return it + else { + res = false; + } + } + + res + } else { + true + } + } } -trait PathExt { - fn modified_since(timestamp: chrono::DateTime) -> bool; - fn read_dir_recusive() -> ReadDirRecursive; +impl Iterator for ReadDirRecursive { + type Item = io::Result; + + fn next(&mut self) -> Option { + loop { + // First, we try to consume the current directory's items + while let Some(entry) = self.read_dir.next() { + self.push_entry(&entry); + + if self.should_return(&entry) { + return Some(entry); + } + } + + // If we get an error while setting up a new directory, we return this, otherwise we + // keep trying to consume the directories + match self.next_read_dir() { + Ok(true) => (), + // There's no more directories to traverse, so the iterator is done + Ok(false) => return None, + Err(e) => return Some(Err(e)), + } + } + } +} + +pub trait PathExt { + /// Confirm whether the file has not been modified since the given timestamp. + /// + /// This function will only return true if it can determine with certainty that the file hasn't + /// been modified. + /// + /// # Args + /// + /// * `timestamp` - Timestamp to compare modified time with + /// + /// # Returns + /// + /// True if the file has not been modified for sure, false otherwise. + fn not_modified_since(&self, timestamp: chrono::DateTime) -> bool; + + /// An extension of the `read_dir` command that runs through the entire underlying directory + /// structure using breadth-first search + fn read_dir_recursive(&self) -> io::Result; +} + +impl PathExt for Path { + fn not_modified_since(&self, timestamp: chrono::DateTime) -> bool { + if let Ok(metadata) = self.metadata() { + if let Ok(last_modified) = metadata.modified() { + let t: chrono::DateTime = last_modified.into(); + let t = t.with_timezone(&Local); + + return t < timestamp; + } + } + + false + } + + fn read_dir_recursive(&self) -> io::Result { + ReadDirRecursive::start(self) + } } From bb7b57899b9ab6bcdfa72642c05b7e6a23123c67 Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Sun, 18 Jun 2023 21:15:05 +0200 Subject: [PATCH 25/93] refactor: store backups in nested vecs instead; introduce concept of chains --- src/server/backups.rs | 114 +++++++++++++++++------------------------- 1 file changed, 47 insertions(+), 67 deletions(-) diff --git a/src/server/backups.rs b/src/server/backups.rs index cdb0409..3fcf4b1 100644 --- a/src/server/backups.rs +++ b/src/server/backups.rs @@ -103,8 +103,6 @@ impl BackupDelta { /// Represents a successful backup #[derive(Debug, Serialize, Deserialize)] pub struct Backup { - #[serde(skip)] - previous: Option>, /// When the backup was started (also corresponds to the name) start_time: chrono::DateTime, /// Type of the backup @@ -115,42 +113,14 @@ pub struct Backup { impl Backup { const FILENAME_FORMAT: &str = "%Y-%m-%d_%H-%M-%S.tar.gz"; - /// Returns a pointer to this backup's previous backup by cloning the Arc pointer. - pub fn previous(&self) -> Option> { - self.previous.as_ref().map(Arc::clone) - } + pub fn state(backups: &Vec) -> HashMap> { + let mut state: HashMap> = HashMap::new(); - /// Calculate the full state of the backup by applying all its ancestors' delta's in order, - /// starting from the last full ancestor. - pub fn state(&self) -> BackupResult>> { - if self.type_ == BackupType::Full { - let mut state = HashMap::new(); - self.delta.apply(&mut state); - - Ok(state) - } else if let Some(previous) = &self.previous { - let mut state = previous.state()?; - self.delta.apply(&mut state); - - Ok(state) - } else { - return Err(BackupError::NoFullAncestor); + for backup in backups { + backup.delta.apply(&mut state); } - } - /// Returns the n'th ancestor of the given backup, if it exists. - pub fn ancestor(&self, n: u64) -> Option> { - if n == 0 { - None - } else if let Some(previous) = &self.previous { - if n == 1 { - Some(Arc::clone(previous)) - } else { - previous.ancestor(n - 1) - } - } else { - None - } + state } /// Return the path to a backup file by properly formatting the data. @@ -200,7 +170,6 @@ impl Backup { } Ok(Backup { - previous: None, type_: BackupType::Full, start_time, delta, @@ -209,7 +178,8 @@ impl Backup { /// Create a new incremental backup from a given previous backup pub fn create_from>( - previous: Arc, + previous_state: HashMap>, + previous_start_time: chrono::DateTime, backup_dir: P, dirs: Vec<(PathBuf, PathBuf)>, ) -> io::Result { @@ -220,9 +190,6 @@ impl Backup { let enc = GzEncoder::new(tar_gz, Compression::default()); let mut ar = tar::Builder::new(enc); - let previous_state = previous - .state() - .map_err(|_| io::Error::new(io::ErrorKind::Other, "No Full ancestor"))?; let mut delta = BackupDelta::new(); for (dir_in_tar, src_dir) in dirs { @@ -233,7 +200,7 @@ impl Backup { let path = entry?.path(); let stripped = path.strip_prefix(&src_dir).unwrap(); - if !path.not_modified_since(previous.start_time) { + if !path.not_modified_since(previous_start_time) { ar.append_path_with_name(&path, dir_in_tar.join(stripped))?; added_files.insert(stripped.to_path_buf()); } @@ -252,7 +219,6 @@ impl Backup { } Ok(Backup { - previous: Some(previous), type_: BackupType::Incremental, start_time, delta, @@ -264,8 +230,10 @@ pub struct BackupManager { backup_dir: PathBuf, config_dir: PathBuf, world_dir: PathBuf, + max_chain_length: u64, max_backups: u64, last_backup: Option>, + chains: Vec>, } impl BackupManager { @@ -282,6 +250,8 @@ impl BackupManager { config_dir, world_dir, max_backups, + max_chain_length: 2, + chains: Vec::new(), last_backup: None, } } @@ -304,13 +274,33 @@ impl BackupManager { (PathBuf::from("worlds"), self.world_dir.clone()), ]; - let backup = if let Some(last_backup) = &self.last_backup { - Backup::create_from(Arc::clone(last_backup), &self.backup_dir, dirs)? + // I kinda hate this statement, please just let me combine let statements in if statements + // already + let backup = if let Some(current_chain) = self.chains.last() { + let current_chain_len: u64 = current_chain.len().try_into().unwrap(); + + if current_chain_len < self.max_chain_length { + if let Some(previous_backup) = current_chain.last() { + let state = Backup::state(current_chain); + + Backup::create_from(state, previous_backup.start_time, &self.backup_dir, dirs)? + } else { + Backup::create(&self.backup_dir, dirs)? + } + } else { + self.chains.push(Vec::new()); + + Backup::create(&self.backup_dir, dirs)? + } } else { + self.chains.push(Vec::new()); + Backup::create(&self.backup_dir, dirs)? }; - self.last_backup = Some(Arc::new(backup)); + // The above statement always creates this element, so this unwrap is safe + self.chains.last_mut().unwrap().push(backup); + self.write_json()?; Ok(()) @@ -351,35 +341,25 @@ impl BackupManager { // } pub fn write_json(&self) -> std::io::Result<()> { - // Put the backup chain into a list that can be serialized - let mut backups: Vec> = Vec::new(); - let mut backup_opt = &self.last_backup; - - while let Some(backup) = backup_opt { - backups.insert(0, Arc::clone(backup)); - backup_opt = &backup.previous; - } - let json_file = File::create(self.backup_dir.join(Self::METADATA_FILE))?; - serde_json::to_writer(json_file, &backups)?; + serde_json::to_writer(json_file, &self.chains)?; Ok(()) } pub fn load_json(&mut self) -> std::io::Result<()> { - let json_file = File::open(self.backup_dir.join(Self::METADATA_FILE))?; - let mut backups: Vec> = serde_json::from_reader(json_file)?; - - if !backups.is_empty() { - for i in 1..backups.len() { - let previous = Arc::clone(&backups[i - 1]); - // We can unwrap here, as this function creates the first instance of each Arc, - // meaning we're definitely the only pointer. - Arc::get_mut(&mut backups[i]).unwrap().previous = Some(previous); + let json_file = match File::open(self.backup_dir.join(Self::METADATA_FILE)) { + Ok(f) => f, + Err(e) => { + // Don't error out if the file isn't there, it will be created when necessary + if e.kind() == io::ErrorKind::NotFound { + return Ok(()); + } else { + return Err(e); + } } - - self.last_backup = Some(Arc::clone(backups.last().unwrap())); - } + }; + self.chains = serde_json::from_reader(json_file)?; Ok(()) } From b51d951688684da09b9535b5abb719de019a34f5 Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Sun, 18 Jun 2023 21:56:43 +0200 Subject: [PATCH 26/93] feat: re-implement remove old backups --- src/main.rs | 3 ++- src/server/backups.rs | 54 +++++++++++++++++++------------------------ src/server/process.rs | 9 ++++---- 3 files changed, 31 insertions(+), 35 deletions(-) diff --git a/src/main.rs b/src/main.rs index 3d82d11..c057db2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -65,7 +65,8 @@ fn commands_backup(cli: &Cli, _args: &BackupArgs) -> io::Result<()> { cli.max_backups, )?; - manager.create_backup() + manager.create_backup()?; + manager.remove_old_backups() } fn main() -> io::Result<()> { diff --git a/src/server/backups.rs b/src/server/backups.rs index 3fcf4b1..82d56a7 100644 --- a/src/server/backups.rs +++ b/src/server/backups.rs @@ -230,7 +230,8 @@ pub struct BackupManager { backup_dir: PathBuf, config_dir: PathBuf, world_dir: PathBuf, - max_chain_length: u64, + chain_len: u64, + chains_to_keep: u64, max_backups: u64, last_backup: Option>, chains: Vec>, @@ -250,7 +251,8 @@ impl BackupManager { config_dir, world_dir, max_backups, - max_chain_length: 2, + chain_len: 2, + chains_to_keep: 1, chains: Vec::new(), last_backup: None, } @@ -279,7 +281,7 @@ impl BackupManager { let backup = if let Some(current_chain) = self.chains.last() { let current_chain_len: u64 = current_chain.len().try_into().unwrap(); - if current_chain_len < self.max_chain_length { + if current_chain_len < self.chain_len { if let Some(previous_backup) = current_chain.last() { let state = Backup::state(current_chain); @@ -307,38 +309,30 @@ impl BackupManager { } /// Remove the oldest backups - // pub fn remove_old_backups(&mut self) -> std::io::Result<()> { - // if let Some(last_backup) = &self.last_backup { - // let last_valid_ancestor = last_backup.ancestor(self.max_backups - 1); - // let ancestor = last_valid_ancestor.previous(); + pub fn remove_old_backups(&mut self) -> std::io::Result<()> { + let chains_to_store: usize = self.chains_to_keep.try_into().unwrap(); - // while let Some(backup) = &ancestor { - // let path = Backup::path(&self.backup_dir, backup.start_time); - // std::fs::remove_file(path)?; - // } - // } + if chains_to_store < self.chains.len() { + let mut remove_count: usize = self.chains.len() - chains_to_store; - // // The naming format used allows us to sort the backups by name and still get a sorting by - // // creation time - // let mut backups = self - // .backup_dir - // .read_dir()? - // .filter_map(|res| res.map(|e| e.path()).ok()) - // .collect::>(); - // backups.sort(); + // We only count finished chains towards the list of stored chains + let chain_len: usize = self.chain_len.try_into().unwrap(); + if self.chains.last().unwrap().len() < chain_len { + remove_count -= 1; + } - // let max_backups: usize = self.max_backups.try_into().unwrap(); + for chain in self.chains.drain(..remove_count) { + for backup in chain { + let path = Backup::path(&self.backup_dir, backup.start_time); + std::fs::remove_file(path)?; + } + } + } - // if backups.len() > max_backups { - // let excess_backups = backups.len() - max_backups; + self.write_json()?; - // for backup in &backups[0..excess_backups] { - // std::fs::remove_file(backup)?; - // } - // } - - // Ok(()) - // } + Ok(()) + } pub fn write_json(&self) -> std::io::Result<()> { let json_file = File::create(self.backup_dir.join(Self::METADATA_FILE))?; diff --git a/src/server/process.rs b/src/server/process.rs index d784061..1e24373 100644 --- a/src/server/process.rs +++ b/src/server/process.rs @@ -70,12 +70,13 @@ impl ServerProcess { let start_time = chrono::offset::Utc::now(); let res = self.backups.create_backup(); - // if res.is_ok() { - // self.backups.remove_old_backups()?; - // } - // The server's save feature needs to be enabled again even if the archive failed to create self.custom("save-on")?; + self.custom("save-all")?; + + if res.is_ok() { + self.backups.remove_old_backups()?; + } let duration = chrono::offset::Utc::now() - start_time; let duration_str = format!( From b48c531d80d08b0772e264bf1249fc653fe90d48 Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Sun, 18 Jun 2023 22:45:35 +0200 Subject: [PATCH 27/93] feat: configurable parameters for incremental backups --- CHANGELOG.md | 10 +++++++++- src/cli.rs | 15 ++++++++++++--- src/main.rs | 10 ++++++---- src/server/backups.rs | 29 +++++++++++------------------ src/server/command.rs | 18 +++++++++++++----- 5 files changed, 51 insertions(+), 31 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e2ec666..8157f9e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,12 +9,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -* Added `backup` CLI command +* `backup` CLI command +* Incremental backups + * Chain length descibres how many incremental backups to create from the + same full backup + * "backups to keep" has been replaced by "chains to keep" ### Changed * Running the server now uses the `run` CLI subcommand +### Removed + +* `max_backups` setting + ## [0.2.2](https://git.rustybever.be/Chewing_Bever/alex/src/tag/0.2.2) ### Fixed diff --git a/src/cli.rs b/src/cli.rs index 3c588aa..613dfa1 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -35,15 +35,24 @@ pub struct Cli { )] pub backup: PathBuf, - /// How many backups to keep + /// Length of a backup chain + #[arg( + short = 'l', + long, + default_value_t = 4, + env = "ALEX_CHAIN_LEN", + global = true + )] + pub chain_len: u64, + /// How many backup chains to keep #[arg( short = 'n', long, default_value_t = 7, - env = "ALEX_MAX_BACKUPS", + env = "ALEX_CHAINS", global = true )] - pub max_backups: u64, + pub chains: u64, } #[derive(Subcommand)] diff --git a/src/main.rs b/src/main.rs index c057db2..1898e90 100644 --- a/src/main.rs +++ b/src/main.rs @@ -32,7 +32,8 @@ fn command_run(cli: &Cli, args: &RunArgs) -> io::Result<()> { .backup(cli.backup.clone()) .xms(args.xms) .xmx(args.xmx) - .max_backups(cli.max_backups); + .chain_len(cli.chain_len) + .chains_to_keep(cli.chains); cmd.canonicalize()?; if args.dry { @@ -58,12 +59,13 @@ fn command_run(cli: &Cli, args: &RunArgs) -> io::Result<()> { } fn commands_backup(cli: &Cli, _args: &BackupArgs) -> io::Result<()> { - let mut manager = server::BackupManager::open( + let mut manager = server::BackupManager::new( cli.backup.clone(), cli.config.clone(), cli.world.clone(), - cli.max_backups, - )?; + cli.chain_len, + cli.chains, + ); manager.create_backup()?; manager.remove_old_backups() diff --git a/src/server/backups.rs b/src/server/backups.rs index 82d56a7..c8f235a 100644 --- a/src/server/backups.rs +++ b/src/server/backups.rs @@ -1,5 +1,5 @@ use crate::server::path::PathExt; -use chrono::{Local, Utc}; +use chrono::Utc; use flate2::write::GzEncoder; use flate2::Compression; use serde::{Deserialize, Serialize}; @@ -7,7 +7,6 @@ use std::collections::{HashMap, HashSet}; use std::fs::File; use std::io; use std::path::{Path, PathBuf}; -use std::sync::Arc; #[link(name = "c")] extern "C" { @@ -21,13 +20,6 @@ pub enum BackupType { Incremental, } -#[derive(Debug)] -pub enum BackupError { - NoFullAncestor, -} - -type BackupResult = Result; - /// Represents the changes relative to the previous backup #[derive(Debug, Serialize, Deserialize)] pub struct BackupDelta { @@ -113,6 +105,8 @@ pub struct Backup { impl Backup { const FILENAME_FORMAT: &str = "%Y-%m-%d_%H-%M-%S.tar.gz"; + /// Resolve the state of the list of backups by applying their deltas in-order to an initially + /// empty state. pub fn state(backups: &Vec) -> HashMap> { let mut state: HashMap> = HashMap::new(); @@ -232,29 +226,27 @@ pub struct BackupManager { world_dir: PathBuf, chain_len: u64, chains_to_keep: u64, - max_backups: u64, - last_backup: Option>, chains: Vec>, } impl BackupManager { const METADATA_FILE: &str = "alex.json"; + /// Initialize a new instance of a `BackupManager`. pub fn new( backup_dir: PathBuf, config_dir: PathBuf, world_dir: PathBuf, - max_backups: u64, + chain_len: u64, + chains_to_keep: u64, ) -> Self { BackupManager { backup_dir, config_dir, world_dir, - max_backups, - chain_len: 2, - chains_to_keep: 1, + chain_len, + chains_to_keep, chains: Vec::new(), - last_backup: None, } } @@ -262,9 +254,10 @@ impl BackupManager { backup_dir: PathBuf, config_dir: PathBuf, world_dir: PathBuf, - max_backups: u64, + chain_len: u64, + chains_to_keep: u64, ) -> std::io::Result { - let mut manager = Self::new(backup_dir, config_dir, world_dir, max_backups); + let mut manager = Self::new(backup_dir, config_dir, world_dir, chain_len, chains_to_keep); manager.load_json()?; Ok(manager) diff --git a/src/server/command.rs b/src/server/command.rs index 1c44d5b..fa92804 100644 --- a/src/server/command.rs +++ b/src/server/command.rs @@ -35,7 +35,8 @@ pub struct ServerCommand { backup_dir: PathBuf, xms: u64, xmx: u64, - max_backups: u64, + chain_len: u64, + chains_to_keep: u64, } impl ServerCommand { @@ -50,7 +51,8 @@ impl ServerCommand { backup_dir: PathBuf::from("backups"), xms: 1024, xmx: 2048, - max_backups: 7, + chain_len: 4, + chains_to_keep: 7, } } @@ -91,8 +93,13 @@ impl ServerCommand { self } - pub fn max_backups(mut self, v: u64) -> Self { - self.max_backups = v; + pub fn chain_len(mut self, v: u64) -> Self { + self.chain_len = v; + self + } + + pub fn chains_to_keep(mut self, v: u64) -> Self { + self.chains_to_keep = v; self } @@ -183,7 +190,8 @@ impl ServerCommand { self.backup_dir.clone(), self.config_dir.clone(), self.world_dir.clone(), - self.max_backups, + self.chain_len, + self.chains_to_keep, )?; let mut cmd = self.create_cmd(); self.accept_eula()?; From 74a0b91fd13257e662e680c7047a3e0e3c002c41 Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Sun, 18 Jun 2023 23:33:56 +0200 Subject: [PATCH 28/93] refactor: remove open function --- src/main.rs | 1 + src/server/backups.rs | 28 ++++++++++------------------ src/server/command.rs | 6 ++++-- 3 files changed, 15 insertions(+), 20 deletions(-) diff --git a/src/main.rs b/src/main.rs index 1898e90..0aef676 100644 --- a/src/main.rs +++ b/src/main.rs @@ -66,6 +66,7 @@ fn commands_backup(cli: &Cli, _args: &BackupArgs) -> io::Result<()> { cli.chain_len, cli.chains, ); + manager.load()?; manager.create_backup()?; manager.remove_old_backups() diff --git a/src/server/backups.rs b/src/server/backups.rs index c8f235a..5097746 100644 --- a/src/server/backups.rs +++ b/src/server/backups.rs @@ -250,19 +250,7 @@ impl BackupManager { } } - pub fn open( - backup_dir: PathBuf, - config_dir: PathBuf, - world_dir: PathBuf, - chain_len: u64, - chains_to_keep: u64, - ) -> std::io::Result { - let mut manager = Self::new(backup_dir, config_dir, world_dir, chain_len, chains_to_keep); - manager.load_json()?; - - Ok(manager) - } - + /// Create a new backup with the expected type. pub fn create_backup(&mut self) -> io::Result<()> { let dirs = vec![ (PathBuf::from("config"), self.config_dir.clone()), @@ -296,12 +284,12 @@ impl BackupManager { // The above statement always creates this element, so this unwrap is safe self.chains.last_mut().unwrap().push(backup); - self.write_json()?; + self.save()?; Ok(()) } - /// Remove the oldest backups + /// Delete all backups associated with outdated chains, and forget those chains. pub fn remove_old_backups(&mut self) -> std::io::Result<()> { let chains_to_store: usize = self.chains_to_keep.try_into().unwrap(); @@ -322,24 +310,28 @@ impl BackupManager { } } - self.write_json()?; + self.save()?; Ok(()) } - pub fn write_json(&self) -> std::io::Result<()> { + /// Write the in-memory state to disk. + pub fn save(&self) -> std::io::Result<()> { let json_file = File::create(self.backup_dir.join(Self::METADATA_FILE))?; serde_json::to_writer(json_file, &self.chains)?; Ok(()) } - pub fn load_json(&mut self) -> std::io::Result<()> { + /// Overwrite the in-memory state with the on-disk state. + pub fn load(&mut self) -> std::io::Result<()> { let json_file = match File::open(self.backup_dir.join(Self::METADATA_FILE)) { Ok(f) => f, Err(e) => { // Don't error out if the file isn't there, it will be created when necessary if e.kind() == io::ErrorKind::NotFound { + self.chains = Vec::new(); + return Ok(()); } else { return Err(e); diff --git a/src/server/command.rs b/src/server/command.rs index fa92804..bd1f0e6 100644 --- a/src/server/command.rs +++ b/src/server/command.rs @@ -186,13 +186,15 @@ impl ServerCommand { } pub fn spawn(&mut self) -> std::io::Result { - let manager = BackupManager::open( + let mut manager = BackupManager::new( self.backup_dir.clone(), self.config_dir.clone(), self.world_dir.clone(), self.chain_len, self.chains_to_keep, - )?; + ); + manager.load()?; + let mut cmd = self.create_cmd(); self.accept_eula()?; let child = cmd.spawn()?; From ef631fab1d6c35998cfedbf8c4714bd41deff981 Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Mon, 19 Jun 2023 14:04:38 +0200 Subject: [PATCH 29/93] refactor: separate backup logic into own module --- src/backup/delta.rs | 75 +++++++ src/backup/manager.rs | 128 ++++++++++++ src/backup/mod.rs | 150 ++++++++++++++ src/{server => backup}/path.rs | 0 src/main.rs | 3 +- src/server/backups.rs | 345 --------------------------------- src/server/command.rs | 3 +- src/server/mod.rs | 3 - src/server/process.rs | 2 +- 9 files changed, 358 insertions(+), 351 deletions(-) create mode 100644 src/backup/delta.rs create mode 100644 src/backup/manager.rs create mode 100644 src/backup/mod.rs rename src/{server => backup}/path.rs (100%) delete mode 100644 src/server/backups.rs diff --git a/src/backup/delta.rs b/src/backup/delta.rs new file mode 100644 index 0000000..15f233b --- /dev/null +++ b/src/backup/delta.rs @@ -0,0 +1,75 @@ +use serde::{Deserialize, Serialize}; +use std::collections::{HashMap, HashSet}; +use std::path::PathBuf; + +/// Represents the changes relative to the previous backup +#[derive(Debug, Serialize, Deserialize)] +pub struct Delta { + /// What files were added/modified in each part of the tarball. + pub added: HashMap>, + /// What files were removed in this backup, in comparison to the previous backup. For full + /// backups, this will always be empty, as they do not consider previous backups. + /// The map stores a separate list for each top-level directory, as the contents of these + /// directories can come for different source directories. + pub removed: HashMap>, +} + +impl Delta { + pub fn new() -> Self { + Self { + added: HashMap::new(), + removed: HashMap::new(), + } + } + + /// Update the current state so that its result becomes the merge of itself and the other + /// state. + pub fn merge(&mut self, delta: &Self) { + for (dir, added) in delta.added.iter() { + // Files that were removed in the current state, but added in the new state, are no + // longer removed + if let Some(orig_removed) = self.removed.get_mut(dir) { + orig_removed.retain(|k| !added.contains(k)); + } + + // Newly added files are added to the state as well + if let Some(orig_added) = self.added.get_mut(dir) { + orig_added.extend(added.iter().cloned()); + } else { + self.added.insert(dir.clone(), added.clone()); + } + } + + for (dir, removed) in delta.removed.iter() { + // Files that were originally added, but now deleted are removed from the added list + if let Some(orig_added) = self.added.get_mut(dir) { + orig_added.retain(|k| !removed.contains(k)); + } + + // Newly removed files are added to the state as well + if let Some(orig_removed) = self.removed.get_mut(dir) { + orig_removed.extend(removed.iter().cloned()); + } else { + self.removed.insert(dir.clone(), removed.clone()); + } + } + } + + /// Modify the given state by applying this delta's changes to it + pub fn apply(&self, state: &mut HashMap>) { + // First we add new files, then we remove the old ones + for (dir, added) in self.added.iter() { + if let Some(current) = state.get_mut(dir) { + current.extend(added.iter().cloned()); + } else { + state.insert(dir.clone(), added.clone()); + } + } + + for (dir, removed) in self.removed.iter() { + if let Some(current) = state.get_mut(dir) { + current.retain(|k| !removed.contains(k)); + } + } + } +} diff --git a/src/backup/manager.rs b/src/backup/manager.rs new file mode 100644 index 0000000..71633a3 --- /dev/null +++ b/src/backup/manager.rs @@ -0,0 +1,128 @@ +use super::Backup; +use std::fs::File; +use std::io; +use std::path::PathBuf; + +pub struct Manager { + backup_dir: PathBuf, + config_dir: PathBuf, + world_dir: PathBuf, + chain_len: u64, + chains_to_keep: u64, + chains: Vec>, +} + +impl Manager { + const METADATA_FILE: &str = "alex.json"; + + /// Initialize a new instance of a `BackupManager`. + pub fn new( + backup_dir: PathBuf, + config_dir: PathBuf, + world_dir: PathBuf, + chain_len: u64, + chains_to_keep: u64, + ) -> Self { + Self { + backup_dir, + config_dir, + world_dir, + chain_len, + chains_to_keep, + chains: Vec::new(), + } + } + + /// Create a new backup with the expected type. + pub fn create_backup(&mut self) -> io::Result<()> { + let dirs = vec![ + (PathBuf::from("config"), self.config_dir.clone()), + (PathBuf::from("worlds"), self.world_dir.clone()), + ]; + + // I kinda hate this statement, please just let me combine let statements in if statements + // already + let backup = if let Some(current_chain) = self.chains.last() { + let current_chain_len: u64 = current_chain.len().try_into().unwrap(); + + if current_chain_len < self.chain_len { + if let Some(previous_backup) = current_chain.last() { + let state = Backup::state(current_chain); + + Backup::create_from(state, previous_backup.start_time, &self.backup_dir, dirs)? + } else { + Backup::create(&self.backup_dir, dirs)? + } + } else { + self.chains.push(Vec::new()); + + Backup::create(&self.backup_dir, dirs)? + } + } else { + self.chains.push(Vec::new()); + + Backup::create(&self.backup_dir, dirs)? + }; + + // The above statement always creates this element, so this unwrap is safe + self.chains.last_mut().unwrap().push(backup); + + self.save()?; + + Ok(()) + } + + /// Delete all backups associated with outdated chains, and forget those chains. + pub fn remove_old_backups(&mut self) -> std::io::Result<()> { + let chains_to_store: usize = self.chains_to_keep.try_into().unwrap(); + + if chains_to_store < self.chains.len() { + let mut remove_count: usize = self.chains.len() - chains_to_store; + + // We only count finished chains towards the list of stored chains + let chain_len: usize = self.chain_len.try_into().unwrap(); + if self.chains.last().unwrap().len() < chain_len { + remove_count -= 1; + } + + for chain in self.chains.drain(..remove_count) { + for backup in chain { + let path = Backup::path(&self.backup_dir, backup.start_time); + std::fs::remove_file(path)?; + } + } + } + + self.save()?; + + Ok(()) + } + + /// Write the in-memory state to disk. + pub fn save(&self) -> std::io::Result<()> { + let json_file = File::create(self.backup_dir.join(Self::METADATA_FILE))?; + serde_json::to_writer(json_file, &self.chains)?; + + Ok(()) + } + + /// Overwrite the in-memory state with the on-disk state. + pub fn load(&mut self) -> std::io::Result<()> { + let json_file = match File::open(self.backup_dir.join(Self::METADATA_FILE)) { + Ok(f) => f, + Err(e) => { + // Don't error out if the file isn't there, it will be created when necessary + if e.kind() == io::ErrorKind::NotFound { + self.chains = Vec::new(); + + return Ok(()); + } else { + return Err(e); + } + } + }; + self.chains = serde_json::from_reader(json_file)?; + + Ok(()) + } +} diff --git a/src/backup/mod.rs b/src/backup/mod.rs new file mode 100644 index 0000000..600ef6d --- /dev/null +++ b/src/backup/mod.rs @@ -0,0 +1,150 @@ +mod delta; +mod manager; +mod path; + +use delta::Delta; +pub use manager::Manager; + +use chrono::Utc; +use flate2::write::GzEncoder; +use flate2::Compression; +use path::PathExt; +use serde::{Deserialize, Serialize}; +use std::collections::{HashMap, HashSet}; +use std::fs::File; +use std::io; +use std::path::{Path, PathBuf}; + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +pub enum BackupType { + Full, + Incremental, +} + +/// Represents a successful backup +#[derive(Debug, Serialize, Deserialize)] +pub struct Backup { + /// When the backup was started (also corresponds to the name) + pub start_time: chrono::DateTime, + /// Type of the backup + pub type_: BackupType, + pub delta: Delta, +} + +impl Backup { + const FILENAME_FORMAT: &str = "%Y-%m-%d_%H-%M-%S.tar.gz"; + + /// Resolve the state of the list of backups by applying their deltas in-order to an initially + /// empty state. + pub fn state(backups: &Vec) -> HashMap> { + let mut state: HashMap> = HashMap::new(); + + for backup in backups { + backup.delta.apply(&mut state); + } + + state + } + + /// Return the path to a backup file by properly formatting the data. + pub fn path>(backup_dir: P, start_time: chrono::DateTime) -> PathBuf { + let backup_dir = backup_dir.as_ref(); + + let filename = format!("{}", start_time.format(Self::FILENAME_FORMAT)); + backup_dir.join(filename) + } + + /// Create a new Full backup, populated with the given directories. + /// + /// # Arguments + /// + /// * `backup_dir` - Directory to store archive in + /// * `dirs` - list of tuples `(path_in_tar, src_dir)` with `path_in_tar` the directory name + /// under which `src_dir`'s contents should be stored in the archive + /// + /// # Returns + /// + /// The `Backup` instance describing this new backup. + pub fn create>( + backup_dir: P, + dirs: Vec<(PathBuf, PathBuf)>, + ) -> io::Result { + let start_time = chrono::offset::Utc::now(); + + let path = Self::path(backup_dir, start_time); + let tar_gz = File::create(path)?; + let enc = GzEncoder::new(tar_gz, Compression::default()); + let mut ar = tar::Builder::new(enc); + + let mut delta = Delta::new(); + + for (dir_in_tar, src_dir) in dirs { + let mut added_files: HashSet = HashSet::new(); + + for entry in src_dir.read_dir_recursive()?.ignored("cache").files() { + let path = entry?.path(); + let stripped = path.strip_prefix(&src_dir).unwrap(); + + ar.append_path_with_name(&path, dir_in_tar.join(stripped))?; + added_files.insert(stripped.to_path_buf()); + } + + delta.added.insert(dir_in_tar, added_files); + } + + Ok(Backup { + type_: BackupType::Full, + start_time, + delta, + }) + } + + /// Create a new incremental backup from a given previous backup + pub fn create_from>( + previous_state: HashMap>, + previous_start_time: chrono::DateTime, + backup_dir: P, + dirs: Vec<(PathBuf, PathBuf)>, + ) -> io::Result { + let start_time = chrono::offset::Utc::now(); + + let path = Self::path(backup_dir, start_time); + let tar_gz = File::create(path)?; + let enc = GzEncoder::new(tar_gz, Compression::default()); + let mut ar = tar::Builder::new(enc); + + let mut delta = Delta::new(); + + for (dir_in_tar, src_dir) in dirs { + let mut all_files: HashSet = HashSet::new(); + let mut added_files: HashSet = HashSet::new(); + + for entry in src_dir.read_dir_recursive()?.ignored("cache").files() { + let path = entry?.path(); + let stripped = path.strip_prefix(&src_dir).unwrap(); + + if !path.not_modified_since(previous_start_time) { + ar.append_path_with_name(&path, dir_in_tar.join(stripped))?; + added_files.insert(stripped.to_path_buf()); + } + + all_files.insert(stripped.to_path_buf()); + } + + delta.added.insert(dir_in_tar.clone(), added_files); + + if let Some(previous_files) = previous_state.get(&dir_in_tar) { + delta.removed.insert( + dir_in_tar, + previous_files.difference(&all_files).cloned().collect(), + ); + } + } + + Ok(Backup { + type_: BackupType::Incremental, + start_time, + delta, + }) + } +} diff --git a/src/server/path.rs b/src/backup/path.rs similarity index 100% rename from src/server/path.rs rename to src/backup/path.rs diff --git a/src/main.rs b/src/main.rs index 0aef676..40d112c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,4 @@ +mod backup; mod cli; mod server; mod signals; @@ -59,7 +60,7 @@ fn command_run(cli: &Cli, args: &RunArgs) -> io::Result<()> { } fn commands_backup(cli: &Cli, _args: &BackupArgs) -> io::Result<()> { - let mut manager = server::BackupManager::new( + let mut manager = backup::Manager::new( cli.backup.clone(), cli.config.clone(), cli.world.clone(), diff --git a/src/server/backups.rs b/src/server/backups.rs deleted file mode 100644 index 5097746..0000000 --- a/src/server/backups.rs +++ /dev/null @@ -1,345 +0,0 @@ -use crate::server::path::PathExt; -use chrono::Utc; -use flate2::write::GzEncoder; -use flate2::Compression; -use serde::{Deserialize, Serialize}; -use std::collections::{HashMap, HashSet}; -use std::fs::File; -use std::io; -use std::path::{Path, PathBuf}; - -#[link(name = "c")] -extern "C" { - fn geteuid() -> u32; - fn getegid() -> u32; -} - -#[derive(Debug, PartialEq, Serialize, Deserialize)] -pub enum BackupType { - Full, - Incremental, -} - -/// Represents the changes relative to the previous backup -#[derive(Debug, Serialize, Deserialize)] -pub struct BackupDelta { - /// What files were added/modified in each part of the tarball. - pub added: HashMap>, - /// What files were removed in this backup, in comparison to the previous backup. For full - /// backups, this will always be empty, as they do not consider previous backups. - /// The map stores a separate list for each top-level directory, as the contents of these - /// directories can come for different source directories. - pub removed: HashMap>, -} - -impl BackupDelta { - pub fn new() -> Self { - BackupDelta { - added: HashMap::new(), - removed: HashMap::new(), - } - } - - /// Update the current state so that its result becomes the merge of itself and the other - /// state. - pub fn merge(&mut self, delta: &BackupDelta) { - for (dir, added) in delta.added.iter() { - // Files that were removed in the current state, but added in the new state, are no - // longer removed - if let Some(orig_removed) = self.removed.get_mut(dir) { - orig_removed.retain(|k| !added.contains(k)); - } - - // Newly added files are added to the state as well - if let Some(orig_added) = self.added.get_mut(dir) { - orig_added.extend(added.iter().cloned()); - } else { - self.added.insert(dir.clone(), added.clone()); - } - } - - for (dir, removed) in delta.removed.iter() { - // Files that were originally added, but now deleted are removed from the added list - if let Some(orig_added) = self.added.get_mut(dir) { - orig_added.retain(|k| !removed.contains(k)); - } - - // Newly removed files are added to the state as well - if let Some(orig_removed) = self.removed.get_mut(dir) { - orig_removed.extend(removed.iter().cloned()); - } else { - self.removed.insert(dir.clone(), removed.clone()); - } - } - } - - /// Modify the given state by applying this delta's changes to it - pub fn apply(&self, state: &mut HashMap>) { - // First we add new files, then we remove the old ones - for (dir, added) in self.added.iter() { - if let Some(current) = state.get_mut(dir) { - current.extend(added.iter().cloned()); - } else { - state.insert(dir.clone(), added.clone()); - } - } - - for (dir, removed) in self.removed.iter() { - if let Some(current) = state.get_mut(dir) { - current.retain(|k| !removed.contains(k)); - } - } - } -} - -/// Represents a successful backup -#[derive(Debug, Serialize, Deserialize)] -pub struct Backup { - /// When the backup was started (also corresponds to the name) - start_time: chrono::DateTime, - /// Type of the backup - type_: BackupType, - delta: BackupDelta, -} - -impl Backup { - const FILENAME_FORMAT: &str = "%Y-%m-%d_%H-%M-%S.tar.gz"; - - /// Resolve the state of the list of backups by applying their deltas in-order to an initially - /// empty state. - pub fn state(backups: &Vec) -> HashMap> { - let mut state: HashMap> = HashMap::new(); - - for backup in backups { - backup.delta.apply(&mut state); - } - - state - } - - /// Return the path to a backup file by properly formatting the data. - pub fn path>(backup_dir: P, start_time: chrono::DateTime) -> PathBuf { - let backup_dir = backup_dir.as_ref(); - - let filename = format!("{}", start_time.format(Self::FILENAME_FORMAT)); - backup_dir.join(filename) - } - - /// Create a new Full backup, populated with the given directories. - /// - /// # Arguments - /// - /// * `backup_dir` - Directory to store archive in - /// * `dirs` - list of tuples `(path_in_tar, src_dir)` with `path_in_tar` the directory name - /// under which `src_dir`'s contents should be stored in the archive - /// - /// # Returns - /// - /// The `Backup` instance describing this new backup. - pub fn create>( - backup_dir: P, - dirs: Vec<(PathBuf, PathBuf)>, - ) -> io::Result { - let start_time = chrono::offset::Utc::now(); - - let path = Self::path(backup_dir, start_time); - let tar_gz = File::create(path)?; - let enc = GzEncoder::new(tar_gz, Compression::default()); - let mut ar = tar::Builder::new(enc); - - let mut delta = BackupDelta::new(); - - for (dir_in_tar, src_dir) in dirs { - let mut added_files: HashSet = HashSet::new(); - - for entry in src_dir.read_dir_recursive()?.ignored("cache").files() { - let path = entry?.path(); - let stripped = path.strip_prefix(&src_dir).unwrap(); - - ar.append_path_with_name(&path, dir_in_tar.join(stripped))?; - added_files.insert(stripped.to_path_buf()); - } - - delta.added.insert(dir_in_tar, added_files); - } - - Ok(Backup { - type_: BackupType::Full, - start_time, - delta, - }) - } - - /// Create a new incremental backup from a given previous backup - pub fn create_from>( - previous_state: HashMap>, - previous_start_time: chrono::DateTime, - backup_dir: P, - dirs: Vec<(PathBuf, PathBuf)>, - ) -> io::Result { - let start_time = chrono::offset::Utc::now(); - - let path = Self::path(backup_dir, start_time); - let tar_gz = File::create(path)?; - let enc = GzEncoder::new(tar_gz, Compression::default()); - let mut ar = tar::Builder::new(enc); - - let mut delta = BackupDelta::new(); - - for (dir_in_tar, src_dir) in dirs { - let mut all_files: HashSet = HashSet::new(); - let mut added_files: HashSet = HashSet::new(); - - for entry in src_dir.read_dir_recursive()?.ignored("cache").files() { - let path = entry?.path(); - let stripped = path.strip_prefix(&src_dir).unwrap(); - - if !path.not_modified_since(previous_start_time) { - ar.append_path_with_name(&path, dir_in_tar.join(stripped))?; - added_files.insert(stripped.to_path_buf()); - } - - all_files.insert(stripped.to_path_buf()); - } - - delta.added.insert(dir_in_tar.clone(), added_files); - - if let Some(previous_files) = previous_state.get(&dir_in_tar) { - delta.removed.insert( - dir_in_tar, - previous_files.difference(&all_files).cloned().collect(), - ); - } - } - - Ok(Backup { - type_: BackupType::Incremental, - start_time, - delta, - }) - } -} - -pub struct BackupManager { - backup_dir: PathBuf, - config_dir: PathBuf, - world_dir: PathBuf, - chain_len: u64, - chains_to_keep: u64, - chains: Vec>, -} - -impl BackupManager { - const METADATA_FILE: &str = "alex.json"; - - /// Initialize a new instance of a `BackupManager`. - pub fn new( - backup_dir: PathBuf, - config_dir: PathBuf, - world_dir: PathBuf, - chain_len: u64, - chains_to_keep: u64, - ) -> Self { - BackupManager { - backup_dir, - config_dir, - world_dir, - chain_len, - chains_to_keep, - chains: Vec::new(), - } - } - - /// Create a new backup with the expected type. - pub fn create_backup(&mut self) -> io::Result<()> { - let dirs = vec![ - (PathBuf::from("config"), self.config_dir.clone()), - (PathBuf::from("worlds"), self.world_dir.clone()), - ]; - - // I kinda hate this statement, please just let me combine let statements in if statements - // already - let backup = if let Some(current_chain) = self.chains.last() { - let current_chain_len: u64 = current_chain.len().try_into().unwrap(); - - if current_chain_len < self.chain_len { - if let Some(previous_backup) = current_chain.last() { - let state = Backup::state(current_chain); - - Backup::create_from(state, previous_backup.start_time, &self.backup_dir, dirs)? - } else { - Backup::create(&self.backup_dir, dirs)? - } - } else { - self.chains.push(Vec::new()); - - Backup::create(&self.backup_dir, dirs)? - } - } else { - self.chains.push(Vec::new()); - - Backup::create(&self.backup_dir, dirs)? - }; - - // The above statement always creates this element, so this unwrap is safe - self.chains.last_mut().unwrap().push(backup); - - self.save()?; - - Ok(()) - } - - /// Delete all backups associated with outdated chains, and forget those chains. - pub fn remove_old_backups(&mut self) -> std::io::Result<()> { - let chains_to_store: usize = self.chains_to_keep.try_into().unwrap(); - - if chains_to_store < self.chains.len() { - let mut remove_count: usize = self.chains.len() - chains_to_store; - - // We only count finished chains towards the list of stored chains - let chain_len: usize = self.chain_len.try_into().unwrap(); - if self.chains.last().unwrap().len() < chain_len { - remove_count -= 1; - } - - for chain in self.chains.drain(..remove_count) { - for backup in chain { - let path = Backup::path(&self.backup_dir, backup.start_time); - std::fs::remove_file(path)?; - } - } - } - - self.save()?; - - Ok(()) - } - - /// Write the in-memory state to disk. - pub fn save(&self) -> std::io::Result<()> { - let json_file = File::create(self.backup_dir.join(Self::METADATA_FILE))?; - serde_json::to_writer(json_file, &self.chains)?; - - Ok(()) - } - - /// Overwrite the in-memory state with the on-disk state. - pub fn load(&mut self) -> std::io::Result<()> { - let json_file = match File::open(self.backup_dir.join(Self::METADATA_FILE)) { - Ok(f) => f, - Err(e) => { - // Don't error out if the file isn't there, it will be created when necessary - if e.kind() == io::ErrorKind::NotFound { - self.chains = Vec::new(); - - return Ok(()); - } else { - return Err(e); - } - } - }; - self.chains = serde_json::from_reader(json_file)?; - - Ok(()) - } -} diff --git a/src/server/command.rs b/src/server/command.rs index bd1f0e6..808d6bf 100644 --- a/src/server/command.rs +++ b/src/server/command.rs @@ -1,4 +1,5 @@ -use crate::server::{BackupManager, ServerProcess}; +use crate::backup::Manager as BackupManager; +use crate::server::ServerProcess; use clap::ValueEnum; use std::fmt; use std::fs::File; diff --git a/src/server/mod.rs b/src/server/mod.rs index 4c2beb2..e3e3131 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -1,8 +1,5 @@ -mod backups; mod command; -mod path; mod process; -pub use backups::BackupManager; pub use command::{ServerCommand, ServerType}; pub use process::ServerProcess; diff --git a/src/server/process.rs b/src/server/process.rs index 1e24373..3ac7beb 100644 --- a/src/server/process.rs +++ b/src/server/process.rs @@ -1,4 +1,4 @@ -use crate::server::BackupManager; +use crate::backup::Manager as BackupManager; use crate::server::ServerType; use std::io::Write; use std::process::Child; From 53dc3783ca2696e35f5dbd956c3b9632dd043d7d Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Tue, 20 Jun 2023 19:31:50 +0200 Subject: [PATCH 30/93] feat: store server info in metadata file; change cli flags --- CHANGELOG.md | 2 ++ src/backup/delta.rs | 1 + src/backup/manager.rs | 60 ++++++++++++++++++++++++++----------------- src/backup/mod.rs | 38 +++++++++++++++++---------- src/cli.rs | 20 +++++---------- src/main.rs | 8 +++++- src/server/command.rs | 23 ++++++++++++----- src/server/mod.rs | 2 +- src/server/process.rs | 15 +++-------- 9 files changed, 97 insertions(+), 72 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8157f9e..9d35929 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,10 +14,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Chain length descibres how many incremental backups to create from the same full backup * "backups to keep" has been replaced by "chains to keep" +* Server type & version is now stored as metadata in the metadata file ### Changed * Running the server now uses the `run` CLI subcommand +* `server_type` and `server_version` arguments are now optional flags ### Removed diff --git a/src/backup/delta.rs b/src/backup/delta.rs index 15f233b..34971f0 100644 --- a/src/backup/delta.rs +++ b/src/backup/delta.rs @@ -24,6 +24,7 @@ impl Delta { /// Update the current state so that its result becomes the merge of itself and the other /// state. + #[allow(dead_code)] pub fn merge(&mut self, delta: &Self) { for (dir, added) in delta.added.iter() { // Files that were removed in the current state, but added in the new state, are no diff --git a/src/backup/manager.rs b/src/backup/manager.rs index 71633a3..ce3dbe1 100644 --- a/src/backup/manager.rs +++ b/src/backup/manager.rs @@ -1,18 +1,27 @@ use super::Backup; +use serde::de::DeserializeOwned; +use serde::Serialize; use std::fs::File; use std::io; use std::path::PathBuf; -pub struct Manager { +pub struct Manager +where + T: Clone + Serialize + DeserializeOwned, +{ backup_dir: PathBuf, config_dir: PathBuf, world_dir: PathBuf, + default_metadata: T, chain_len: u64, chains_to_keep: u64, - chains: Vec>, + chains: Vec>>, } -impl Manager { +impl Manager +where + T: Clone + Serialize + DeserializeOwned, +{ const METADATA_FILE: &str = "alex.json"; /// Initialize a new instance of a `BackupManager`. @@ -20,6 +29,7 @@ impl Manager { backup_dir: PathBuf, config_dir: PathBuf, world_dir: PathBuf, + metadata: T, chain_len: u64, chains_to_keep: u64, ) -> Self { @@ -27,6 +37,7 @@ impl Manager { backup_dir, config_dir, world_dir, + default_metadata: metadata, chain_len, chains_to_keep, chains: Vec::new(), @@ -40,32 +51,32 @@ impl Manager { (PathBuf::from("worlds"), self.world_dir.clone()), ]; - // I kinda hate this statement, please just let me combine let statements in if statements - // already - let backup = if let Some(current_chain) = self.chains.last() { + // We start a new chain if the current chain is complete, or if there isn't a first chain + // yet + if let Some(current_chain) = self.chains.last() { let current_chain_len: u64 = current_chain.len().try_into().unwrap(); - if current_chain_len < self.chain_len { - if let Some(previous_backup) = current_chain.last() { - let state = Backup::state(current_chain); - - Backup::create_from(state, previous_backup.start_time, &self.backup_dir, dirs)? - } else { - Backup::create(&self.backup_dir, dirs)? - } - } else { + if current_chain_len >= self.chain_len { self.chains.push(Vec::new()); - - Backup::create(&self.backup_dir, dirs)? } } else { self.chains.push(Vec::new()); + } + let current_chain = self.chains.last_mut().unwrap(); + + let mut backup = if !current_chain.is_empty() { + let previous_backup = current_chain.last().unwrap(); + let state = Backup::state(current_chain); + + Backup::create_from(state, previous_backup.start_time, &self.backup_dir, dirs)? + } else { Backup::create(&self.backup_dir, dirs)? }; - // The above statement always creates this element, so this unwrap is safe - self.chains.last_mut().unwrap().push(backup); + backup.set_metadata(self.default_metadata.clone()); + + current_chain.push(backup); self.save()?; @@ -73,7 +84,7 @@ impl Manager { } /// Delete all backups associated with outdated chains, and forget those chains. - pub fn remove_old_backups(&mut self) -> std::io::Result<()> { + pub fn remove_old_backups(&mut self) -> io::Result<()> { let chains_to_store: usize = self.chains_to_keep.try_into().unwrap(); if chains_to_store < self.chains.len() { @@ -91,15 +102,15 @@ impl Manager { std::fs::remove_file(path)?; } } - } - self.save()?; + self.save()?; + } Ok(()) } /// Write the in-memory state to disk. - pub fn save(&self) -> std::io::Result<()> { + pub fn save(&self) -> io::Result<()> { let json_file = File::create(self.backup_dir.join(Self::METADATA_FILE))?; serde_json::to_writer(json_file, &self.chains)?; @@ -107,7 +118,7 @@ impl Manager { } /// Overwrite the in-memory state with the on-disk state. - pub fn load(&mut self) -> std::io::Result<()> { + pub fn load(&mut self) -> io::Result<()> { let json_file = match File::open(self.backup_dir.join(Self::METADATA_FILE)) { Ok(f) => f, Err(e) => { @@ -121,6 +132,7 @@ impl Manager { } } }; + self.chains = serde_json::from_reader(json_file)?; Ok(()) diff --git a/src/backup/mod.rs b/src/backup/mod.rs index 600ef6d..5b12b8c 100644 --- a/src/backup/mod.rs +++ b/src/backup/mod.rs @@ -22,21 +22,37 @@ pub enum BackupType { } /// Represents a successful backup -#[derive(Debug, Serialize, Deserialize)] -pub struct Backup { +#[derive(Serialize, Deserialize)] +pub struct Backup { /// When the backup was started (also corresponds to the name) pub start_time: chrono::DateTime, /// Type of the backup pub type_: BackupType, pub delta: Delta, + /// Additional metadata that can be associated with a given backup + pub metadata: Option, } -impl Backup { +impl Backup<()> { + /// Return the path to a backup file by properly formatting the data. + pub fn path>(backup_dir: P, start_time: chrono::DateTime) -> PathBuf { + let backup_dir = backup_dir.as_ref(); + + let filename = format!("{}", start_time.format(Self::FILENAME_FORMAT)); + backup_dir.join(filename) + } +} + +impl Backup { const FILENAME_FORMAT: &str = "%Y-%m-%d_%H-%M-%S.tar.gz"; + pub fn set_metadata(&mut self, metadata: T) { + self.metadata = Some(metadata); + } + /// Resolve the state of the list of backups by applying their deltas in-order to an initially /// empty state. - pub fn state(backups: &Vec) -> HashMap> { + pub fn state(backups: &Vec) -> HashMap> { let mut state: HashMap> = HashMap::new(); for backup in backups { @@ -46,14 +62,6 @@ impl Backup { state } - /// Return the path to a backup file by properly formatting the data. - pub fn path>(backup_dir: P, start_time: chrono::DateTime) -> PathBuf { - let backup_dir = backup_dir.as_ref(); - - let filename = format!("{}", start_time.format(Self::FILENAME_FORMAT)); - backup_dir.join(filename) - } - /// Create a new Full backup, populated with the given directories. /// /// # Arguments @@ -71,7 +79,7 @@ impl Backup { ) -> io::Result { let start_time = chrono::offset::Utc::now(); - let path = Self::path(backup_dir, start_time); + let path = Backup::path(backup_dir, start_time); let tar_gz = File::create(path)?; let enc = GzEncoder::new(tar_gz, Compression::default()); let mut ar = tar::Builder::new(enc); @@ -96,6 +104,7 @@ impl Backup { type_: BackupType::Full, start_time, delta, + metadata: None, }) } @@ -108,7 +117,7 @@ impl Backup { ) -> io::Result { let start_time = chrono::offset::Utc::now(); - let path = Self::path(backup_dir, start_time); + let path = Backup::path(backup_dir, start_time); let tar_gz = File::create(path)?; let enc = GzEncoder::new(tar_gz, Compression::default()); let mut ar = tar::Builder::new(enc); @@ -145,6 +154,7 @@ impl Backup { type_: BackupType::Incremental, start_time, delta, + metadata: None, }) } } diff --git a/src/cli.rs b/src/cli.rs index 613dfa1..166fa82 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -53,6 +53,12 @@ pub struct Cli { global = true )] pub chains: u64, + /// Type of server + #[arg(long, default_value = "unknown", env = "ALEX_SERVER")] + pub server: ServerType, + /// Version string for the server, e.g. 1.19.4-545 + #[arg(long, default_value = "", env = "ALEX_SERVER_VERSION")] + pub server_version: String, } #[derive(Subcommand)] @@ -66,12 +72,6 @@ pub enum Commands { #[derive(Args)] pub struct RunArgs { - /// Type of server - pub type_: ServerType, - /// Version string for the server, e.g. 1.19.4-545 - #[arg(env = "ALEX_SERVER_VERSION")] - pub server_version: String, - /// Server jar to execute #[arg( long, @@ -103,10 +103,4 @@ pub struct RunArgs { } #[derive(Args)] -pub struct BackupArgs { - /// Type of server - pub type_: ServerType, - /// Version string for the server, e.g. 1.19.4-545 - #[arg(env = "ALEX_SERVER_VERSION")] - pub server_version: String, -} +pub struct BackupArgs {} diff --git a/src/main.rs b/src/main.rs index 40d112c..ffb0c93 100644 --- a/src/main.rs +++ b/src/main.rs @@ -25,7 +25,7 @@ fn backups_thread(counter: Arc>, frequency: u64) { fn command_run(cli: &Cli, args: &RunArgs) -> io::Result<()> { let (_, mut signals) = signals::install_signal_handlers()?; - let mut cmd = server::ServerCommand::new(args.type_, &args.server_version) + let mut cmd = server::ServerCommand::new(cli.server, &cli.server_version) .java(&args.java) .jar(args.jar.clone()) .config(cli.config.clone()) @@ -60,10 +60,16 @@ fn command_run(cli: &Cli, args: &RunArgs) -> io::Result<()> { } fn commands_backup(cli: &Cli, _args: &BackupArgs) -> io::Result<()> { + let metadata = server::Metadata { + server_type: cli.server, + server_version: cli.server_version.clone(), + }; + let mut manager = backup::Manager::new( cli.backup.clone(), cli.config.clone(), cli.world.clone(), + metadata, cli.chain_len, cli.chains, ); diff --git a/src/server/command.rs b/src/server/command.rs index 808d6bf..d06f8bc 100644 --- a/src/server/command.rs +++ b/src/server/command.rs @@ -1,14 +1,16 @@ use crate::backup::Manager as BackupManager; use crate::server::ServerProcess; use clap::ValueEnum; +use serde::{Deserialize, Serialize}; use std::fmt; use std::fs::File; use std::io::Write; use std::path::{Path, PathBuf}; use std::process::{Command, Stdio}; -#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)] +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Serialize, Deserialize)] pub enum ServerType { + Unknown, Paper, Forge, Vanilla, @@ -17,6 +19,7 @@ pub enum ServerType { impl fmt::Display for ServerType { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let s = match self { + ServerType::Unknown => "Unknown", ServerType::Paper => "PaperMC", ServerType::Forge => "Forge", ServerType::Vanilla => "Vanilla", @@ -26,6 +29,12 @@ impl fmt::Display for ServerType { } } +#[derive(Clone, Serialize, Deserialize)] +pub struct Metadata { + pub server_type: ServerType, + pub server_version: String, +} + pub struct ServerCommand { type_: ServerType, version: String, @@ -187,10 +196,15 @@ impl ServerCommand { } pub fn spawn(&mut self) -> std::io::Result { + let metadata = Metadata { + server_type: self.type_, + server_version: self.version.clone(), + }; let mut manager = BackupManager::new( self.backup_dir.clone(), self.config_dir.clone(), self.world_dir.clone(), + metadata, self.chain_len, self.chains_to_keep, ); @@ -200,12 +214,7 @@ impl ServerCommand { self.accept_eula()?; let child = cmd.spawn()?; - Ok(ServerProcess::new( - self.type_, - self.version.clone(), - manager, - child, - )) + Ok(ServerProcess::new(manager, child)) } } diff --git a/src/server/mod.rs b/src/server/mod.rs index e3e3131..0f01c9a 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -1,5 +1,5 @@ mod command; mod process; -pub use command::{ServerCommand, ServerType}; +pub use command::{Metadata, ServerCommand, ServerType}; pub use process::ServerProcess; diff --git a/src/server/process.rs b/src/server/process.rs index 3ac7beb..0a09ab0 100644 --- a/src/server/process.rs +++ b/src/server/process.rs @@ -1,25 +1,16 @@ use crate::backup::Manager as BackupManager; -use crate::server::ServerType; +use crate::server::Metadata; use std::io::Write; use std::process::Child; pub struct ServerProcess { - type_: ServerType, - version: String, child: Child, - backups: BackupManager, + backups: BackupManager, } impl ServerProcess { - pub fn new( - type_: ServerType, - version: String, - manager: BackupManager, - child: Child, - ) -> ServerProcess { + pub fn new(manager: BackupManager, child: Child) -> ServerProcess { ServerProcess { - type_, - version, child, backups: manager, } From 188fb3034335a4244fd754cc668090de90d8d5f2 Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Thu, 22 Jun 2023 20:10:37 +0200 Subject: [PATCH 31/93] fix: better serde bounds --- CHANGELOG.md | 2 +- src/backup/manager.rs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d35929..c6009a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * `backup` CLI command * Incremental backups - * Chain length descibres how many incremental backups to create from the + * Chain length describes how many incremental backups to create from the same full backup * "backups to keep" has been replaced by "chains to keep" * Server type & version is now stored as metadata in the metadata file diff --git a/src/backup/manager.rs b/src/backup/manager.rs index ce3dbe1..1dfec16 100644 --- a/src/backup/manager.rs +++ b/src/backup/manager.rs @@ -1,5 +1,5 @@ use super::Backup; -use serde::de::DeserializeOwned; +use serde::Deserialize; use serde::Serialize; use std::fs::File; use std::io; @@ -7,7 +7,7 @@ use std::path::PathBuf; pub struct Manager where - T: Clone + Serialize + DeserializeOwned, + T: Clone + Serialize + for<'de> Deserialize<'de>, { backup_dir: PathBuf, config_dir: PathBuf, @@ -20,7 +20,7 @@ where impl Manager where - T: Clone + Serialize + DeserializeOwned, + T: Clone + Serialize + for<'de> Deserialize<'de>, { const METADATA_FILE: &str = "alex.json"; From 4e8d0a8d25e21d642a7a4a378700bd928482fd72 Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Thu, 22 Jun 2023 20:23:13 +0200 Subject: [PATCH 32/93] refactor: we go rusty --- src/backup/manager.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/backup/manager.rs b/src/backup/manager.rs index 1dfec16..5d2f1a3 100644 --- a/src/backup/manager.rs +++ b/src/backup/manager.rs @@ -25,18 +25,18 @@ where const METADATA_FILE: &str = "alex.json"; /// Initialize a new instance of a `BackupManager`. - pub fn new( - backup_dir: PathBuf, - config_dir: PathBuf, - world_dir: PathBuf, + pub fn new, P2: Into, P3: Into>( + backup_dir: P1, + config_dir: P2, + world_dir: P3, metadata: T, chain_len: u64, chains_to_keep: u64, ) -> Self { Self { - backup_dir, - config_dir, - world_dir, + backup_dir: backup_dir.into(), + config_dir: config_dir.into(), + world_dir: world_dir.into(), default_metadata: metadata, chain_len, chains_to_keep, From 0a459ee30b840fbc08ab85c1f04e04c7351c8a27 Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Thu, 22 Jun 2023 21:15:40 +0200 Subject: [PATCH 33/93] refactor: let backup manager calculate next backup time --- .cargo/config.toml | 2 +- src/backup/manager.rs | 16 ++++++++++++++++ src/cli.rs | 19 +++++++++++++------ src/main.rs | 21 +++++++++++++++------ src/server/command.rs | 8 ++++++++ src/server/process.rs | 2 +- 6 files changed, 54 insertions(+), 14 deletions(-) diff --git a/.cargo/config.toml b/.cargo/config.toml index 09b7896..ed8b85c 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,3 +1,3 @@ [alias] -runs = "run -- run paper 1.19.4-550 --config data/config --backup data/backups --world data/worlds --jar paper-1.19.4-550.jar" +runs = "run -- run --config data/config --backup data/backups --world data/worlds --jar paper-1.19.4-550.jar --frequency 2" runrs = "run --release -- paper 1.19.4-545 --config data/config --backup data/backups --world data/worlds --jar data/paper-1.19.4-525.jar" diff --git a/src/backup/manager.rs b/src/backup/manager.rs index 5d2f1a3..cc6dd0c 100644 --- a/src/backup/manager.rs +++ b/src/backup/manager.rs @@ -1,4 +1,5 @@ use super::Backup; +use chrono::Utc; use serde::Deserialize; use serde::Serialize; use std::fs::File; @@ -15,6 +16,7 @@ where default_metadata: T, chain_len: u64, chains_to_keep: u64, + frequency: chrono::Duration, chains: Vec>>, } @@ -32,6 +34,7 @@ where metadata: T, chain_len: u64, chains_to_keep: u64, + frequency: chrono::Duration, ) -> Self { Self { backup_dir: backup_dir.into(), @@ -40,6 +43,7 @@ where default_metadata: metadata, chain_len, chains_to_keep, + frequency, chains: Vec::new(), } } @@ -137,4 +141,16 @@ where Ok(()) } + + /// Calculate the next time a backup should be created. If no backup has been created yet, it + /// will return now. + pub fn next_scheduled_time(&self) -> chrono::DateTime { + if let Some(last_chain) = self.chains.last() { + if let Some(last_backup) = last_chain.last() { + return last_backup.start_time + self.frequency; + } + } + + chrono::offset::Utc::now() + } } diff --git a/src/cli.rs b/src/cli.rs index 166fa82..d9ea9a9 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -53,11 +53,22 @@ pub struct Cli { global = true )] pub chains: u64, + + /// How frequently to perform a backup, in minutes; 0 to disable. + #[arg( + short = 't', + long, + default_value_t = 0, + env = "ALEX_FREQUENCY", + global = true + )] + pub frequency: u32, + /// Type of server - #[arg(long, default_value = "unknown", env = "ALEX_SERVER")] + #[arg(long, default_value = "unknown", env = "ALEX_SERVER", global = true)] pub server: ServerType, /// Version string for the server, e.g. 1.19.4-545 - #[arg(long, default_value = "", env = "ALEX_SERVER_VERSION")] + #[arg(long, default_value = "", env = "ALEX_SERVER_VERSION", global = true)] pub server_version: String, } @@ -92,10 +103,6 @@ pub struct RunArgs { #[arg(long, default_value_t = 2048, env = "ALEX_XMX")] pub xmx: u64, - /// How frequently to perform a backup, in minutes; 0 to disable. - #[arg(short = 't', long, default_value_t = 0, env = "ALEX_FREQUENCY")] - pub frequency: u64, - /// Don't actually run the server, but simply output the server configuration that would have /// been ran #[arg(short, long, default_value_t = false)] diff --git a/src/main.rs b/src/main.rs index ffb0c93..f4b9ef2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,9 +9,17 @@ use cli::{BackupArgs, Cli, Commands, RunArgs}; use std::io; use std::sync::{Arc, Mutex}; -fn backups_thread(counter: Arc>, frequency: u64) { +fn backups_thread(counter: Arc>) { loop { - std::thread::sleep(std::time::Duration::from_secs(frequency * 60)); + let next_scheduled_time = { + let server = counter.lock().unwrap(); + server.backups.next_scheduled_time() + }; + + let now = chrono::offset::Utc::now(); + if next_scheduled_time > now { + std::thread::sleep((next_scheduled_time - now).to_std().unwrap()); + } { let mut server = counter.lock().unwrap(); @@ -34,7 +42,8 @@ fn command_run(cli: &Cli, args: &RunArgs) -> io::Result<()> { .xms(args.xms) .xmx(args.xmx) .chain_len(cli.chain_len) - .chains_to_keep(cli.chains); + .chains_to_keep(cli.chains) + .frequency(cli.frequency); cmd.canonicalize()?; if args.dry { @@ -45,10 +54,9 @@ fn command_run(cli: &Cli, args: &RunArgs) -> io::Result<()> { let counter = Arc::new(Mutex::new(cmd.spawn()?)); - if args.frequency > 0 { + if cli.frequency > 0 { let clone = Arc::clone(&counter); - let frequency = args.frequency; - std::thread::spawn(move || backups_thread(clone, frequency)); + std::thread::spawn(move || backups_thread(clone)); } // Spawn thread that handles the main stdin loop @@ -72,6 +80,7 @@ fn commands_backup(cli: &Cli, _args: &BackupArgs) -> io::Result<()> { metadata, cli.chain_len, cli.chains, + chrono::Duration::minutes(cli.frequency.into()), ); manager.load()?; diff --git a/src/server/command.rs b/src/server/command.rs index d06f8bc..1ce5ce0 100644 --- a/src/server/command.rs +++ b/src/server/command.rs @@ -47,6 +47,7 @@ pub struct ServerCommand { xmx: u64, chain_len: u64, chains_to_keep: u64, + frequency: u32, } impl ServerCommand { @@ -63,6 +64,7 @@ impl ServerCommand { xmx: 2048, chain_len: 4, chains_to_keep: 7, + frequency: 0, } } @@ -113,6 +115,11 @@ impl ServerCommand { self } + pub fn frequency(mut self, v: u32) -> Self { + self.frequency = v; + self + } + fn accept_eula(&self) -> std::io::Result<()> { let mut eula_path = self.config_dir.clone(); eula_path.push("eula.txt"); @@ -207,6 +214,7 @@ impl ServerCommand { metadata, self.chain_len, self.chains_to_keep, + chrono::Duration::minutes(self.frequency.into()), ); manager.load()?; diff --git a/src/server/process.rs b/src/server/process.rs index 0a09ab0..17b86b1 100644 --- a/src/server/process.rs +++ b/src/server/process.rs @@ -5,7 +5,7 @@ use std::process::Child; pub struct ServerProcess { child: Child, - backups: BackupManager, + pub backups: BackupManager, } impl ServerProcess { From a236c36a4f0092ac3c474da3b9940d39de0440a2 Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Fri, 23 Jun 2023 10:53:17 +0200 Subject: [PATCH 34/93] feat: take backup layers as arguments --- src/cli.rs | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/src/cli.rs b/src/cli.rs index d9ea9a9..adf1013 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,6 +1,52 @@ use crate::server::ServerType; use clap::{Args, Parser, Subcommand}; +use std::error::Error; +use std::fmt; use std::path::PathBuf; +use std::str::FromStr; + +#[derive(Clone, Debug)] +pub struct ManagerConfig { + name: String, + frequency: u32, + chain_len: u64, + chains: u64, +} + +#[derive(Debug)] +pub struct ParseManagerConfigErr; + +impl Error for ParseManagerConfigErr {} + +impl fmt::Display for ParseManagerConfigErr { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "parse manager config err") + } +} + +impl FromStr for ManagerConfig { + type Err = ParseManagerConfigErr; + + fn from_str(s: &str) -> Result { + let splits: Vec<&str> = s.split(',').collect(); + + if let [name, frequency, chains, chain_len] = splits[..] { + let name: String = name.parse().map_err(|_| ParseManagerConfigErr)?; + let frequency: u32 = frequency.parse().map_err(|_| ParseManagerConfigErr)?; + let chains: u64 = chains.parse().map_err(|_| ParseManagerConfigErr)?; + let chain_len: u64 = chain_len.parse().map_err(|_| ParseManagerConfigErr)?; + + Ok(ManagerConfig { + name, + chains, + chain_len, + frequency, + }) + } else { + Err(ParseManagerConfigErr) + } + } +} #[derive(Parser)] #[command(author, version, about, long_about = None)] @@ -35,6 +81,11 @@ pub struct Cli { )] pub backup: PathBuf, + /// What backup layers to employ, provided as a list of tuples name,frequency,chain_len,chains + /// delimited by semicolons (;). + #[arg(long, env = "ALEX_LAYERS", global = true, value_delimiter = ';')] + pub layers: Vec, + /// Length of a backup chain #[arg( short = 'l', From 29636ffcdb5344ce36dcc013878bc86045fa3905 Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Fri, 23 Jun 2023 12:30:10 +0200 Subject: [PATCH 35/93] feat: implement backup layers using meta manager --- src/backup/manager/config.rs | 46 +++++++++++++ src/backup/manager/meta.rs | 83 +++++++++++++++++++++++ src/backup/{manager.rs => manager/mod.rs} | 7 +- src/backup/mod.rs | 4 +- src/cli.rs | 76 +-------------------- src/main.rs | 43 +++++------- src/server/command.rs | 64 +++-------------- src/server/mod.rs | 33 ++++++++- src/server/process.rs | 12 ++-- 9 files changed, 201 insertions(+), 167 deletions(-) create mode 100644 src/backup/manager/config.rs create mode 100644 src/backup/manager/meta.rs rename src/backup/{manager.rs => manager/mod.rs} (98%) diff --git a/src/backup/manager/config.rs b/src/backup/manager/config.rs new file mode 100644 index 0000000..c5ba217 --- /dev/null +++ b/src/backup/manager/config.rs @@ -0,0 +1,46 @@ +use std::error::Error; +use std::fmt; +use std::str::FromStr; + +#[derive(Clone, Debug)] +pub struct ManagerConfig { + pub name: String, + pub frequency: u32, + pub chain_len: u64, + pub chains: u64, +} + +#[derive(Debug)] +pub struct ParseManagerConfigErr; + +impl Error for ParseManagerConfigErr {} + +impl fmt::Display for ParseManagerConfigErr { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "parse manager config err") + } +} + +impl FromStr for ManagerConfig { + type Err = ParseManagerConfigErr; + + fn from_str(s: &str) -> Result { + let splits: Vec<&str> = s.split(',').collect(); + + if let [name, frequency, chains, chain_len] = splits[..] { + let name: String = name.parse().map_err(|_| ParseManagerConfigErr)?; + let frequency: u32 = frequency.parse().map_err(|_| ParseManagerConfigErr)?; + let chains: u64 = chains.parse().map_err(|_| ParseManagerConfigErr)?; + let chain_len: u64 = chain_len.parse().map_err(|_| ParseManagerConfigErr)?; + + Ok(ManagerConfig { + name, + chains, + chain_len, + frequency, + }) + } else { + Err(ParseManagerConfigErr) + } + } +} diff --git a/src/backup/manager/meta.rs b/src/backup/manager/meta.rs new file mode 100644 index 0000000..5878f34 --- /dev/null +++ b/src/backup/manager/meta.rs @@ -0,0 +1,83 @@ +use super::{Manager, ManagerConfig}; +use chrono::Utc; +use serde::Deserialize; +use serde::Serialize; +use std::io; +use std::path::PathBuf; + +pub struct MetaManager +where + T: Clone + Serialize + for<'de> Deserialize<'de>, +{ + backup_dir: PathBuf, + config_dir: PathBuf, + world_dir: PathBuf, + default_metadata: T, + managers: Vec>, +} + +impl MetaManager +where + T: Clone + Serialize + for<'de> Deserialize<'de>, +{ + pub fn new, P2: Into, P3: Into>( + backup_dir: P1, + config_dir: P2, + world_dir: P3, + default_metadata: T, + ) -> Self { + MetaManager { + backup_dir: backup_dir.into(), + config_dir: config_dir.into(), + world_dir: world_dir.into(), + default_metadata, + managers: Vec::new(), + } + } + + pub fn add(&mut self, config: &ManagerConfig) -> io::Result<()> { + // Backup dir itself should exist, but we control its contents, so we can create + // separate directories for each layer + let path = self.backup_dir.join(&config.name); + std::fs::create_dir(&path)?; + + let mut manager = Manager::new( + path, + self.config_dir.clone(), + self.world_dir.clone(), + self.default_metadata.clone(), + config.chain_len, + config.chains, + chrono::Duration::minutes(config.frequency.into()), + ); + manager.load()?; + self.managers.push(manager); + + Ok(()) + } + + pub fn add_all(&mut self, configs: &Vec) -> io::Result<()> { + for config in configs { + self.add(config)?; + } + + Ok(()) + } + + pub fn next_scheduled_time(&self) -> Option> { + self.managers.iter().map(|m| m.next_scheduled_time()).min() + } + + pub fn perform_backup_cycle(&mut self) -> io::Result<()> { + if let Some(manager) = self + .managers + .iter_mut() + .min_by_key(|m| m.next_scheduled_time()) + { + manager.create_backup()?; + manager.remove_old_backups() + } else { + Ok(()) + } + } +} diff --git a/src/backup/manager.rs b/src/backup/manager/mod.rs similarity index 98% rename from src/backup/manager.rs rename to src/backup/manager/mod.rs index cc6dd0c..6c991c0 100644 --- a/src/backup/manager.rs +++ b/src/backup/manager/mod.rs @@ -1,3 +1,9 @@ +mod config; +mod meta; + +pub use config::ManagerConfig; +pub use meta::MetaManager; + use super::Backup; use chrono::Utc; use serde::Deserialize; @@ -26,7 +32,6 @@ where { const METADATA_FILE: &str = "alex.json"; - /// Initialize a new instance of a `BackupManager`. pub fn new, P2: Into, P3: Into>( backup_dir: P1, config_dir: P2, diff --git a/src/backup/mod.rs b/src/backup/mod.rs index 5b12b8c..780c557 100644 --- a/src/backup/mod.rs +++ b/src/backup/mod.rs @@ -1,9 +1,11 @@ mod delta; -mod manager; +pub mod manager; mod path; use delta::Delta; pub use manager::Manager; +pub use manager::ManagerConfig; +pub use manager::MetaManager; use chrono::Utc; use flate2::write::GzEncoder; diff --git a/src/cli.rs b/src/cli.rs index adf1013..8d44af3 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,52 +1,7 @@ +use crate::backup::ManagerConfig; use crate::server::ServerType; use clap::{Args, Parser, Subcommand}; -use std::error::Error; -use std::fmt; use std::path::PathBuf; -use std::str::FromStr; - -#[derive(Clone, Debug)] -pub struct ManagerConfig { - name: String, - frequency: u32, - chain_len: u64, - chains: u64, -} - -#[derive(Debug)] -pub struct ParseManagerConfigErr; - -impl Error for ParseManagerConfigErr {} - -impl fmt::Display for ParseManagerConfigErr { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "parse manager config err") - } -} - -impl FromStr for ManagerConfig { - type Err = ParseManagerConfigErr; - - fn from_str(s: &str) -> Result { - let splits: Vec<&str> = s.split(',').collect(); - - if let [name, frequency, chains, chain_len] = splits[..] { - let name: String = name.parse().map_err(|_| ParseManagerConfigErr)?; - let frequency: u32 = frequency.parse().map_err(|_| ParseManagerConfigErr)?; - let chains: u64 = chains.parse().map_err(|_| ParseManagerConfigErr)?; - let chain_len: u64 = chain_len.parse().map_err(|_| ParseManagerConfigErr)?; - - Ok(ManagerConfig { - name, - chains, - chain_len, - frequency, - }) - } else { - Err(ParseManagerConfigErr) - } - } -} #[derive(Parser)] #[command(author, version, about, long_about = None)] @@ -86,35 +41,6 @@ pub struct Cli { #[arg(long, env = "ALEX_LAYERS", global = true, value_delimiter = ';')] pub layers: Vec, - /// Length of a backup chain - #[arg( - short = 'l', - long, - default_value_t = 4, - env = "ALEX_CHAIN_LEN", - global = true - )] - pub chain_len: u64, - /// How many backup chains to keep - #[arg( - short = 'n', - long, - default_value_t = 7, - env = "ALEX_CHAINS", - global = true - )] - pub chains: u64, - - /// How frequently to perform a backup, in minutes; 0 to disable. - #[arg( - short = 't', - long, - default_value_t = 0, - env = "ALEX_FREQUENCY", - global = true - )] - pub frequency: u32, - /// Type of server #[arg(long, default_value = "unknown", env = "ALEX_SERVER", global = true)] pub server: ServerType, diff --git a/src/main.rs b/src/main.rs index f4b9ef2..b7187e4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,7 +5,7 @@ mod signals; mod stdin; use clap::Parser; -use cli::{BackupArgs, Cli, Commands, RunArgs}; +use cli::{Cli, Commands, RunArgs}; use std::io; use std::sync::{Arc, Mutex}; @@ -13,7 +13,7 @@ fn backups_thread(counter: Arc>) { loop { let next_scheduled_time = { let server = counter.lock().unwrap(); - server.backups.next_scheduled_time() + server.backups.next_scheduled_time().unwrap() }; let now = chrono::offset::Utc::now(); @@ -39,11 +39,9 @@ fn command_run(cli: &Cli, args: &RunArgs) -> io::Result<()> { .config(cli.config.clone()) .world(cli.world.clone()) .backup(cli.backup.clone()) + .managers(cli.layers.clone()) .xms(args.xms) - .xmx(args.xmx) - .chain_len(cli.chain_len) - .chains_to_keep(cli.chains) - .frequency(cli.frequency); + .xmx(args.xmx); cmd.canonicalize()?; if args.dry { @@ -54,7 +52,7 @@ fn command_run(cli: &Cli, args: &RunArgs) -> io::Result<()> { let counter = Arc::new(Mutex::new(cmd.spawn()?)); - if cli.frequency > 0 { + if !cli.layers.is_empty() { let clone = Arc::clone(&counter); std::thread::spawn(move || backups_thread(clone)); } @@ -67,32 +65,23 @@ fn command_run(cli: &Cli, args: &RunArgs) -> io::Result<()> { signals::handle_signals(&mut signals, counter) } -fn commands_backup(cli: &Cli, _args: &BackupArgs) -> io::Result<()> { - let metadata = server::Metadata { - server_type: cli.server, - server_version: cli.server_version.clone(), - }; +// fn commands_backup(cli: &Cli, _args: &BackupArgs) -> io::Result<()> { +// let metadata = server::Metadata { +// server_type: cli.server, +// server_version: cli.server_version.clone(), +// }; +// let mut meta = MetaManager::new(cli.backup, cli.config, cli.world, metadata); +// meta.add_all(&cli.layers)?; - let mut manager = backup::Manager::new( - cli.backup.clone(), - cli.config.clone(), - cli.world.clone(), - metadata, - cli.chain_len, - cli.chains, - chrono::Duration::minutes(cli.frequency.into()), - ); - manager.load()?; - - manager.create_backup()?; - manager.remove_old_backups() -} +// manager.create_backup()?; +// manager.remove_old_backups() +// } fn main() -> io::Result<()> { let cli = Cli::parse(); match &cli.command { Commands::Run(args) => command_run(&cli, args), - Commands::Backup(args) => commands_backup(&cli, args), + Commands::Backup(_) => Ok(()), } } diff --git a/src/server/command.rs b/src/server/command.rs index 1ce5ce0..ec1b302 100644 --- a/src/server/command.rs +++ b/src/server/command.rs @@ -1,40 +1,12 @@ -use crate::backup::Manager as BackupManager; -use crate::server::ServerProcess; -use clap::ValueEnum; -use serde::{Deserialize, Serialize}; +use crate::backup::ManagerConfig; +use crate::backup::MetaManager; +use crate::server::{Metadata, ServerProcess, ServerType}; use std::fmt; use std::fs::File; use std::io::Write; use std::path::{Path, PathBuf}; use std::process::{Command, Stdio}; -#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Serialize, Deserialize)] -pub enum ServerType { - Unknown, - Paper, - Forge, - Vanilla, -} - -impl fmt::Display for ServerType { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let s = match self { - ServerType::Unknown => "Unknown", - ServerType::Paper => "PaperMC", - ServerType::Forge => "Forge", - ServerType::Vanilla => "Vanilla", - }; - - write!(f, "{}", s) - } -} - -#[derive(Clone, Serialize, Deserialize)] -pub struct Metadata { - pub server_type: ServerType, - pub server_version: String, -} - pub struct ServerCommand { type_: ServerType, version: String, @@ -45,9 +17,7 @@ pub struct ServerCommand { backup_dir: PathBuf, xms: u64, xmx: u64, - chain_len: u64, - chains_to_keep: u64, - frequency: u32, + managers: Vec, } impl ServerCommand { @@ -62,9 +32,7 @@ impl ServerCommand { backup_dir: PathBuf::from("backups"), xms: 1024, xmx: 2048, - chain_len: 4, - chains_to_keep: 7, - frequency: 0, + managers: Vec::new(), } } @@ -105,18 +73,9 @@ impl ServerCommand { self } - pub fn chain_len(mut self, v: u64) -> Self { - self.chain_len = v; - self - } + pub fn managers(mut self, configs: Vec) -> Self { + self.managers = configs; - pub fn chains_to_keep(mut self, v: u64) -> Self { - self.chains_to_keep = v; - self - } - - pub fn frequency(mut self, v: u32) -> Self { - self.frequency = v; self } @@ -207,22 +166,19 @@ impl ServerCommand { server_type: self.type_, server_version: self.version.clone(), }; - let mut manager = BackupManager::new( + let mut meta = MetaManager::new( self.backup_dir.clone(), self.config_dir.clone(), self.world_dir.clone(), metadata, - self.chain_len, - self.chains_to_keep, - chrono::Duration::minutes(self.frequency.into()), ); - manager.load()?; + meta.add_all(&self.managers)?; let mut cmd = self.create_cmd(); self.accept_eula()?; let child = cmd.spawn()?; - Ok(ServerProcess::new(manager, child)) + Ok(ServerProcess::new(meta, child)) } } diff --git a/src/server/mod.rs b/src/server/mod.rs index 0f01c9a..09f90a5 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -1,5 +1,36 @@ mod command; mod process; -pub use command::{Metadata, ServerCommand, ServerType}; +pub use command::ServerCommand; pub use process::ServerProcess; + +use clap::ValueEnum; +use serde::{Deserialize, Serialize}; +use std::fmt; + +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Serialize, Deserialize)] +pub enum ServerType { + Unknown, + Paper, + Forge, + Vanilla, +} + +impl fmt::Display for ServerType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let s = match self { + ServerType::Unknown => "Unknown", + ServerType::Paper => "PaperMC", + ServerType::Forge => "Forge", + ServerType::Vanilla => "Vanilla", + }; + + write!(f, "{}", s) + } +} + +#[derive(Clone, Serialize, Deserialize)] +pub struct Metadata { + pub server_type: ServerType, + pub server_version: String, +} diff --git a/src/server/process.rs b/src/server/process.rs index 17b86b1..ef6822e 100644 --- a/src/server/process.rs +++ b/src/server/process.rs @@ -1,15 +1,15 @@ -use crate::backup::Manager as BackupManager; +use crate::backup::MetaManager; use crate::server::Metadata; use std::io::Write; use std::process::Child; pub struct ServerProcess { child: Child, - pub backups: BackupManager, + pub backups: MetaManager, } impl ServerProcess { - pub fn new(manager: BackupManager, child: Child) -> ServerProcess { + pub fn new(manager: MetaManager, child: Child) -> ServerProcess { ServerProcess { child, backups: manager, @@ -59,16 +59,12 @@ impl ServerProcess { std::thread::sleep(std::time::Duration::from_secs(10)); let start_time = chrono::offset::Utc::now(); - let res = self.backups.create_backup(); + let res = self.backups.perform_backup_cycle(); // The server's save feature needs to be enabled again even if the archive failed to create self.custom("save-on")?; self.custom("save-all")?; - if res.is_ok() { - self.backups.remove_old_backups()?; - } - let duration = chrono::offset::Utc::now() - start_time; let duration_str = format!( "{}m{}s", From 03e21fda87c9368ed1aaabca93acbeeed65ffb4d Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Fri, 23 Jun 2023 13:01:19 +0200 Subject: [PATCH 36/93] feat: show message describing what layer is backing up --- .cargo/config.toml | 2 +- src/backup/manager/config.rs | 2 +- src/backup/manager/meta.rs | 31 +++++++++++++++++++++++++------ src/server/process.rs | 12 ++++++++---- 4 files changed, 35 insertions(+), 12 deletions(-) diff --git a/.cargo/config.toml b/.cargo/config.toml index ed8b85c..c5834ea 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,3 +1,3 @@ [alias] -runs = "run -- run --config data/config --backup data/backups --world data/worlds --jar paper-1.19.4-550.jar --frequency 2" +runs = "run -- run --config data/config --backup data/backups --world data/worlds --jar paper-1.19.4-550.jar --layers 2min,2,4,4;3min,3,2,2" runrs = "run --release -- paper 1.19.4-545 --config data/config --backup data/backups --world data/worlds --jar data/paper-1.19.4-525.jar" diff --git a/src/backup/manager/config.rs b/src/backup/manager/config.rs index c5ba217..8adbd5a 100644 --- a/src/backup/manager/config.rs +++ b/src/backup/manager/config.rs @@ -6,8 +6,8 @@ use std::str::FromStr; pub struct ManagerConfig { pub name: String, pub frequency: u32, - pub chain_len: u64, pub chains: u64, + pub chain_len: u64, } #[derive(Debug)] diff --git a/src/backup/manager/meta.rs b/src/backup/manager/meta.rs index 5878f34..bf5cc4b 100644 --- a/src/backup/manager/meta.rs +++ b/src/backup/manager/meta.rs @@ -2,6 +2,7 @@ use super::{Manager, ManagerConfig}; use chrono::Utc; use serde::Deserialize; use serde::Serialize; +use std::collections::HashMap; use std::io; use std::path::PathBuf; @@ -13,7 +14,7 @@ where config_dir: PathBuf, world_dir: PathBuf, default_metadata: T, - managers: Vec>, + managers: HashMap>, } impl MetaManager @@ -31,7 +32,7 @@ where config_dir: config_dir.into(), world_dir: world_dir.into(), default_metadata, - managers: Vec::new(), + managers: HashMap::new(), } } @@ -39,7 +40,15 @@ where // Backup dir itself should exist, but we control its contents, so we can create // separate directories for each layer let path = self.backup_dir.join(&config.name); - std::fs::create_dir(&path)?; + + // If the directory already exists, that's okay + match std::fs::create_dir(&path) { + Ok(()) => (), + Err(e) => match e.kind() { + io::ErrorKind::AlreadyExists => (), + _ => return Err(e), + }, + }; let mut manager = Manager::new( path, @@ -51,7 +60,7 @@ where chrono::Duration::minutes(config.frequency.into()), ); manager.load()?; - self.managers.push(manager); + self.managers.insert(config.name.clone(), manager); Ok(()) } @@ -64,14 +73,24 @@ where Ok(()) } + pub fn next_scheduled_layer(&self) -> Option<&str> { + self.managers + .iter() + .min_by_key(|(_, m)| m.next_scheduled_time()) + .map(|(k, _)| k.as_str()) + } + pub fn next_scheduled_time(&self) -> Option> { - self.managers.iter().map(|m| m.next_scheduled_time()).min() + self.managers + .values() + .map(|m| m.next_scheduled_time()) + .min() } pub fn perform_backup_cycle(&mut self) -> io::Result<()> { if let Some(manager) = self .managers - .iter_mut() + .values_mut() .min_by_key(|m| m.next_scheduled_time()) { manager.create_backup()?; diff --git a/src/server/process.rs b/src/server/process.rs index ef6822e..7748c1d 100644 --- a/src/server/process.rs +++ b/src/server/process.rs @@ -48,7 +48,8 @@ impl ServerProcess { /// Perform a backup by disabling the server's save feature and flushing its data, before /// creating an archive file. pub fn backup(&mut self) -> std::io::Result<()> { - self.custom("say backing up server")?; + let layer_name = String::from(self.backups.next_scheduled_layer().unwrap()); + self.custom(&format!("say starting backup for layer '{}'", layer_name))?; // Make sure the server isn't modifying the files during the backup self.custom("save-off")?; @@ -73,11 +74,14 @@ impl ServerProcess { ); if res.is_ok() { - self.custom(&format!("say server backed up in {}", duration_str))?; + self.custom(&format!( + "say backup created for layer '{}' in {}", + layer_name, duration_str + ))?; } else { self.custom(&format!( - "an error occured after {} while backing up the server", - duration_str + "an error occured after {} while creating backup for layer '{}'", + duration_str, layer_name ))?; } From d5cea49c8b3020bef4cb8f19576e2478e92d10ad Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Fri, 23 Jun 2023 14:36:20 +0200 Subject: [PATCH 37/93] feat: further generalize backup code --- src/backup/manager/meta.rs | 22 ++++++++++++---------- src/backup/manager/mod.rs | 28 +++++++++++++--------------- src/backup/mod.rs | 14 +++++++------- src/server/command.rs | 13 +++++++------ 4 files changed, 39 insertions(+), 38 deletions(-) diff --git a/src/backup/manager/meta.rs b/src/backup/manager/meta.rs index bf5cc4b..076bf14 100644 --- a/src/backup/manager/meta.rs +++ b/src/backup/manager/meta.rs @@ -6,13 +6,13 @@ use std::collections::HashMap; use std::io; use std::path::PathBuf; +/// Manages a collection of backup layers, allowing them to be utilized as a single object. pub struct MetaManager where T: Clone + Serialize + for<'de> Deserialize<'de>, { backup_dir: PathBuf, - config_dir: PathBuf, - world_dir: PathBuf, + dirs: Vec<(PathBuf, PathBuf)>, default_metadata: T, managers: HashMap>, } @@ -21,21 +21,20 @@ impl MetaManager where T: Clone + Serialize + for<'de> Deserialize<'de>, { - pub fn new, P2: Into, P3: Into>( - backup_dir: P1, - config_dir: P2, - world_dir: P3, + pub fn new>( + backup_dir: P, + dirs: Vec<(PathBuf, PathBuf)>, default_metadata: T, ) -> Self { MetaManager { backup_dir: backup_dir.into(), - config_dir: config_dir.into(), - world_dir: world_dir.into(), + dirs, default_metadata, managers: HashMap::new(), } } + /// Add a new manager to track, initializing it first. pub fn add(&mut self, config: &ManagerConfig) -> io::Result<()> { // Backup dir itself should exist, but we control its contents, so we can create // separate directories for each layer @@ -52,8 +51,7 @@ where let mut manager = Manager::new( path, - self.config_dir.clone(), - self.world_dir.clone(), + self.dirs.clone(), self.default_metadata.clone(), config.chain_len, config.chains, @@ -65,6 +63,7 @@ where Ok(()) } + /// Convenient wrapper for `add`. pub fn add_all(&mut self, configs: &Vec) -> io::Result<()> { for config in configs { self.add(config)?; @@ -73,6 +72,7 @@ where Ok(()) } + /// Return the name of the next scheduled layer, if one or more managers are present. pub fn next_scheduled_layer(&self) -> Option<&str> { self.managers .iter() @@ -80,6 +80,7 @@ where .map(|(k, _)| k.as_str()) } + /// Return the earliest scheduled time for the underlying managers. pub fn next_scheduled_time(&self) -> Option> { self.managers .values() @@ -87,6 +88,7 @@ where .min() } + /// Perform a backup cycle for the earliest scheduled manager. pub fn perform_backup_cycle(&mut self) -> io::Result<()> { if let Some(manager) = self .managers diff --git a/src/backup/manager/mod.rs b/src/backup/manager/mod.rs index 6c991c0..32c452a 100644 --- a/src/backup/manager/mod.rs +++ b/src/backup/manager/mod.rs @@ -12,13 +12,13 @@ use std::fs::File; use std::io; use std::path::PathBuf; +/// Manages a single backup layer consisting of one or more chains of backups. pub struct Manager where T: Clone + Serialize + for<'de> Deserialize<'de>, { backup_dir: PathBuf, - config_dir: PathBuf, - world_dir: PathBuf, + dirs: Vec<(PathBuf, PathBuf)>, default_metadata: T, chain_len: u64, chains_to_keep: u64, @@ -32,10 +32,9 @@ where { const METADATA_FILE: &str = "alex.json"; - pub fn new, P2: Into, P3: Into>( - backup_dir: P1, - config_dir: P2, - world_dir: P3, + pub fn new>( + backup_dir: P, + dirs: Vec<(PathBuf, PathBuf)>, metadata: T, chain_len: u64, chains_to_keep: u64, @@ -43,8 +42,7 @@ where ) -> Self { Self { backup_dir: backup_dir.into(), - config_dir: config_dir.into(), - world_dir: world_dir.into(), + dirs, default_metadata: metadata, chain_len, chains_to_keep, @@ -55,11 +53,6 @@ where /// Create a new backup with the expected type. pub fn create_backup(&mut self) -> io::Result<()> { - let dirs = vec![ - (PathBuf::from("config"), self.config_dir.clone()), - (PathBuf::from("worlds"), self.world_dir.clone()), - ]; - // We start a new chain if the current chain is complete, or if there isn't a first chain // yet if let Some(current_chain) = self.chains.last() { @@ -78,9 +71,14 @@ where let previous_backup = current_chain.last().unwrap(); let state = Backup::state(current_chain); - Backup::create_from(state, previous_backup.start_time, &self.backup_dir, dirs)? + Backup::create_from( + state, + previous_backup.start_time, + &self.backup_dir, + &self.dirs, + )? } else { - Backup::create(&self.backup_dir, dirs)? + Backup::create(&self.backup_dir, &self.dirs)? }; backup.set_metadata(self.default_metadata.clone()); diff --git a/src/backup/mod.rs b/src/backup/mod.rs index 780c557..0168e67 100644 --- a/src/backup/mod.rs +++ b/src/backup/mod.rs @@ -77,7 +77,7 @@ impl Backup { /// The `Backup` instance describing this new backup. pub fn create>( backup_dir: P, - dirs: Vec<(PathBuf, PathBuf)>, + dirs: &Vec<(PathBuf, PathBuf)>, ) -> io::Result { let start_time = chrono::offset::Utc::now(); @@ -93,13 +93,13 @@ impl Backup { for entry in src_dir.read_dir_recursive()?.ignored("cache").files() { let path = entry?.path(); - let stripped = path.strip_prefix(&src_dir).unwrap(); + let stripped = path.strip_prefix(src_dir).unwrap(); ar.append_path_with_name(&path, dir_in_tar.join(stripped))?; added_files.insert(stripped.to_path_buf()); } - delta.added.insert(dir_in_tar, added_files); + delta.added.insert(dir_in_tar.to_path_buf(), added_files); } Ok(Backup { @@ -115,7 +115,7 @@ impl Backup { previous_state: HashMap>, previous_start_time: chrono::DateTime, backup_dir: P, - dirs: Vec<(PathBuf, PathBuf)>, + dirs: &Vec<(PathBuf, PathBuf)>, ) -> io::Result { let start_time = chrono::offset::Utc::now(); @@ -132,7 +132,7 @@ impl Backup { for entry in src_dir.read_dir_recursive()?.ignored("cache").files() { let path = entry?.path(); - let stripped = path.strip_prefix(&src_dir).unwrap(); + let stripped = path.strip_prefix(src_dir).unwrap(); if !path.not_modified_since(previous_start_time) { ar.append_path_with_name(&path, dir_in_tar.join(stripped))?; @@ -144,9 +144,9 @@ impl Backup { delta.added.insert(dir_in_tar.clone(), added_files); - if let Some(previous_files) = previous_state.get(&dir_in_tar) { + if let Some(previous_files) = previous_state.get(dir_in_tar) { delta.removed.insert( - dir_in_tar, + dir_in_tar.to_path_buf(), previous_files.difference(&all_files).cloned().collect(), ); } diff --git a/src/server/command.rs b/src/server/command.rs index ec1b302..258bb86 100644 --- a/src/server/command.rs +++ b/src/server/command.rs @@ -166,12 +166,13 @@ impl ServerCommand { server_type: self.type_, server_version: self.version.clone(), }; - let mut meta = MetaManager::new( - self.backup_dir.clone(), - self.config_dir.clone(), - self.world_dir.clone(), - metadata, - ); + + let dirs = vec![ + (PathBuf::from("config"), self.config_dir.clone()), + (PathBuf::from("worlds"), self.world_dir.clone()), + ]; + + let mut meta = MetaManager::new(self.backup_dir.clone(), dirs, metadata); meta.add_all(&self.managers)?; let mut cmd = self.create_cmd(); From 1cfe13674dd69b357c1804fa27d0660f85fb83da Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Fri, 23 Jun 2023 15:51:17 +0200 Subject: [PATCH 38/93] refactor: structure code to allow expanding cli functionality --- src/cli/backup.rs | 12 +++++ src/{cli.rs => cli/mod.rs} | 64 ++++++++++---------------- src/cli/run.rs | 94 ++++++++++++++++++++++++++++++++++++++ src/main.rs | 81 ++++++-------------------------- 4 files changed, 144 insertions(+), 107 deletions(-) create mode 100644 src/cli/backup.rs rename src/{cli.rs => cli/mod.rs} (59%) create mode 100644 src/cli/run.rs diff --git a/src/cli/backup.rs b/src/cli/backup.rs new file mode 100644 index 0000000..2eeb4e4 --- /dev/null +++ b/src/cli/backup.rs @@ -0,0 +1,12 @@ +use clap::{Args, Subcommand}; + +#[derive(Subcommand)] +pub enum BackupCommands { + List, +} + +#[derive(Args)] +pub struct BackupArgs { + #[command(subcommand)] + pub command: BackupCommands, +} diff --git a/src/cli.rs b/src/cli/mod.rs similarity index 59% rename from src/cli.rs rename to src/cli/mod.rs index 8d44af3..9ecd9f3 100644 --- a/src/cli.rs +++ b/src/cli/mod.rs @@ -1,8 +1,23 @@ +mod backup; +mod run; + +pub use backup::{BackupArgs, BackupCommands}; +pub use run::RunArgs; + use crate::backup::ManagerConfig; use crate::server::ServerType; -use clap::{Args, Parser, Subcommand}; +use clap::{Parser, Subcommand}; +use std::io; use std::path::PathBuf; +#[derive(Subcommand)] +pub enum Commands { + /// Run the server + Run(RunArgs), + /// Interact with the backup system without starting a server + Backup(BackupArgs), +} + #[derive(Parser)] #[command(author, version, about, long_about = None)] pub struct Cli { @@ -36,7 +51,7 @@ pub struct Cli { )] pub backup: PathBuf, - /// What backup layers to employ, provided as a list of tuples name,frequency,chain_len,chains + /// What backup layers to employ, provided as a list of tuples name,frequency,chains,chain_len /// delimited by semicolons (;). #[arg(long, env = "ALEX_LAYERS", global = true, value_delimiter = ';')] pub layers: Vec, @@ -49,42 +64,11 @@ pub struct Cli { pub server_version: String, } -#[derive(Subcommand)] -pub enum Commands { - /// Run the server - Run(RunArgs), - /// Create a new backup of the server. This command should only be used when the server is not - /// running. - Backup(BackupArgs), +impl Cli { + pub fn run(&self) -> io::Result<()> { + match &self.command { + Commands::Run(args) => args.run(self), + Commands::Backup(_) => Ok(()), + } + } } - -#[derive(Args)] -pub struct RunArgs { - /// Server jar to execute - #[arg( - long, - value_name = "JAR_PATH", - default_value = "server.jar", - env = "ALEX_JAR" - )] - pub jar: PathBuf, - - /// Java command to run the server jar with - #[arg(long, value_name = "JAVA_CMD", default_value_t = String::from("java"), env = "ALEX_JAVA")] - pub java: String, - - /// XMS value in megabytes for the server instance - #[arg(long, default_value_t = 1024, env = "ALEX_XMS")] - pub xms: u64, - /// XMX value in megabytes for the server instance - #[arg(long, default_value_t = 2048, env = "ALEX_XMX")] - pub xmx: u64, - - /// Don't actually run the server, but simply output the server configuration that would have - /// been ran - #[arg(short, long, default_value_t = false)] - pub dry: bool, -} - -#[derive(Args)] -pub struct BackupArgs {} diff --git a/src/cli/run.rs b/src/cli/run.rs new file mode 100644 index 0000000..986fe8a --- /dev/null +++ b/src/cli/run.rs @@ -0,0 +1,94 @@ +use crate::cli::Cli; +use crate::server; +use crate::signals; +use crate::stdin; +use clap::Args; +use std::io; +use std::path::PathBuf; +use std::sync::{Arc, Mutex}; + +#[derive(Args)] +pub struct RunArgs { + /// Server jar to execute + #[arg( + long, + value_name = "JAR_PATH", + default_value = "server.jar", + env = "ALEX_JAR" + )] + pub jar: PathBuf, + + /// Java command to run the server jar with + #[arg(long, value_name = "JAVA_CMD", default_value_t = String::from("java"), env = "ALEX_JAVA")] + pub java: String, + + /// XMS value in megabytes for the server instance + #[arg(long, default_value_t = 1024, env = "ALEX_XMS")] + pub xms: u64, + /// XMX value in megabytes for the server instance + #[arg(long, default_value_t = 2048, env = "ALEX_XMX")] + pub xmx: u64, + + /// Don't actually run the server, but simply output the server configuration that would have + /// been ran + #[arg(short, long, default_value_t = false)] + pub dry: bool, +} + +fn backups_thread(counter: Arc>) { + loop { + let next_scheduled_time = { + let server = counter.lock().unwrap(); + server.backups.next_scheduled_time().unwrap() + }; + + let now = chrono::offset::Utc::now(); + if next_scheduled_time > now { + std::thread::sleep((next_scheduled_time - now).to_std().unwrap()); + } + + { + let mut server = counter.lock().unwrap(); + + // We explicitely ignore the error here, as we don't want the thread to fail + let _ = server.backup(); + } + } +} + +impl RunArgs { + pub fn run(&self, cli: &Cli) -> io::Result<()> { + let (_, mut signals) = signals::install_signal_handlers()?; + + let mut cmd = server::ServerCommand::new(cli.server, &cli.server_version) + .java(&self.java) + .jar(self.jar.clone()) + .config(cli.config.clone()) + .world(cli.world.clone()) + .backup(cli.backup.clone()) + .managers(cli.layers.clone()) + .xms(self.xms) + .xmx(self.xmx); + cmd.canonicalize()?; + + if self.dry { + print!("{}", cmd); + + return Ok(()); + } + + let counter = Arc::new(Mutex::new(cmd.spawn()?)); + + if !cli.layers.is_empty() { + let clone = Arc::clone(&counter); + std::thread::spawn(move || backups_thread(clone)); + } + + // Spawn thread that handles the main stdin loop + let clone = Arc::clone(&counter); + std::thread::spawn(move || stdin::handle_stdin(clone)); + + // Signal handler loop exits the process when necessary + signals::handle_signals(&mut signals, counter) + } +} diff --git a/src/main.rs b/src/main.rs index b7187e4..edad9c3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,84 +4,31 @@ mod server; mod signals; mod stdin; +use crate::cli::Cli; use clap::Parser; -use cli::{Cli, Commands, RunArgs}; use std::io; -use std::sync::{Arc, Mutex}; -fn backups_thread(counter: Arc>) { - loop { - let next_scheduled_time = { - let server = counter.lock().unwrap(); - server.backups.next_scheduled_time().unwrap() - }; - - let now = chrono::offset::Utc::now(); - if next_scheduled_time > now { - std::thread::sleep((next_scheduled_time - now).to_std().unwrap()); - } - - { - let mut server = counter.lock().unwrap(); - - // We explicitely ignore the error here, as we don't want the thread to fail - let _ = server.backup(); - } - } -} - -fn command_run(cli: &Cli, args: &RunArgs) -> io::Result<()> { - let (_, mut signals) = signals::install_signal_handlers()?; - - let mut cmd = server::ServerCommand::new(cli.server, &cli.server_version) - .java(&args.java) - .jar(args.jar.clone()) - .config(cli.config.clone()) - .world(cli.world.clone()) - .backup(cli.backup.clone()) - .managers(cli.layers.clone()) - .xms(args.xms) - .xmx(args.xmx); - cmd.canonicalize()?; - - if args.dry { - print!("{}", cmd); - - return Ok(()); - } - - let counter = Arc::new(Mutex::new(cmd.spawn()?)); - - if !cli.layers.is_empty() { - let clone = Arc::clone(&counter); - std::thread::spawn(move || backups_thread(clone)); - } - - // Spawn thread that handles the main stdin loop - let clone = Arc::clone(&counter); - std::thread::spawn(move || stdin::handle_stdin(clone)); - - // Signal handler loop exits the process when necessary - signals::handle_signals(&mut signals, counter) -} - -// fn commands_backup(cli: &Cli, _args: &BackupArgs) -> io::Result<()> { +// fn commands_backup(cli: &Cli, args: &BackupArgs) -> io::Result<()> { // let metadata = server::Metadata { // server_type: cli.server, // server_version: cli.server_version.clone(), // }; -// let mut meta = MetaManager::new(cli.backup, cli.config, cli.world, metadata); +// let dirs = vec![ +// (PathBuf::from("config"), cli.config.clone()), +// (PathBuf::from("worlds"), cli.world.clone()), +// ]; +// let mut meta = MetaManager::new(cli.backup.clone(), dirs, metadata); // meta.add_all(&cli.layers)?; -// manager.create_backup()?; -// manager.remove_old_backups() +// match &args.command { +// BackupCommands::List => () +// } + +// // manager.create_backup()?; +// // manager.remove_old_backups() // } fn main() -> io::Result<()> { let cli = Cli::parse(); - - match &cli.command { - Commands::Run(args) => command_run(&cli, args), - Commands::Backup(_) => Ok(()), - } + cli.run() } From e373fc85f1b3371ed153f59409d60c753945c565 Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Fri, 23 Jun 2023 18:02:38 +0200 Subject: [PATCH 39/93] feat: create backups from cli for specific layer --- .cargo/config.toml | 2 +- src/backup/manager/meta.rs | 15 +++++++++++++++ src/cli/backup.rs | 30 ++++++++++++++++++++++++++++++ src/cli/mod.rs | 20 +++++++++++++++++++- 4 files changed, 65 insertions(+), 2 deletions(-) diff --git a/.cargo/config.toml b/.cargo/config.toml index c5834ea..37fecab 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,3 +1,3 @@ [alias] -runs = "run -- run --config data/config --backup data/backups --world data/worlds --jar paper-1.19.4-550.jar --layers 2min,2,4,4;3min,3,2,2" +runs = "run -- --config data/config --backup data/backups --world data/worlds --layers 2min,2,4,4;3min,3,2,2" runrs = "run --release -- paper 1.19.4-545 --config data/config --backup data/backups --world data/worlds --jar data/paper-1.19.4-525.jar" diff --git a/src/backup/manager/meta.rs b/src/backup/manager/meta.rs index 076bf14..6ba9c06 100644 --- a/src/backup/manager/meta.rs +++ b/src/backup/manager/meta.rs @@ -101,4 +101,19 @@ where Ok(()) } } + + /// Create a manual backup for a specific layer + pub fn create_backup(&mut self, layer: &str) -> Option> { + if let Some(manager) = self.managers.get_mut(layer) { + let mut res = manager.create_backup(); + + if res.is_ok() { + res = manager.remove_old_backups(); + } + + Some(res) + } else { + None + } + } } diff --git a/src/cli/backup.rs b/src/cli/backup.rs index 2eeb4e4..b71d992 100644 --- a/src/cli/backup.rs +++ b/src/cli/backup.rs @@ -1,7 +1,10 @@ +use crate::cli::Cli; use clap::{Args, Subcommand}; +use std::io; #[derive(Subcommand)] pub enum BackupCommands { + Create(BackupCreateArgs), List, } @@ -10,3 +13,30 @@ pub struct BackupArgs { #[command(subcommand)] pub command: BackupCommands, } + +#[derive(Args)] +pub struct BackupCreateArgs { + /// What layer to create a backup in + layer: String, +} + +impl BackupArgs { + pub fn run(&self, cli: &Cli) -> io::Result<()> { + match &self.command { + BackupCommands::Create(args) => args.run(cli), + BackupCommands::List => Ok(()), + } + } +} + +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")) + } + } +} diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 9ecd9f3..eb4ba63 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -1,6 +1,8 @@ mod backup; mod run; +pub use crate::backup::MetaManager; +pub use crate::server::Metadata; pub use backup::{BackupArgs, BackupCommands}; pub use run::RunArgs; @@ -68,7 +70,23 @@ impl Cli { pub fn run(&self) -> io::Result<()> { match &self.command { Commands::Run(args) => args.run(self), - Commands::Backup(_) => Ok(()), + Commands::Backup(args) => args.run(self), } } + + /// Convenience method to initialize backup manager from the cli arguments + pub fn meta(&self) -> io::Result> { + let metadata = Metadata { + server_type: self.server, + server_version: self.server_version.clone(), + }; + let dirs = vec![ + (PathBuf::from("config"), self.config.clone()), + (PathBuf::from("worlds"), self.world.clone()), + ]; + let mut meta = MetaManager::new(self.backup.clone(), dirs, metadata); + meta.add_all(&self.layers)?; + + Ok(meta) + } } From a4e2a1276ffbbf277609bf08e6df4ec800fb9c32 Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Fri, 23 Jun 2023 22:47:38 +0200 Subject: [PATCH 40/93] feat: restore backup chains using cli commands --- src/backup/manager/meta.rs | 15 ++++++- src/backup/manager/mod.rs | 29 +++++++++++++- src/backup/mod.rs | 51 ++++++++++++++++++++++-- src/cli/backup.rs | 82 +++++++++++++++++++++++++++++++++++++- src/cli/mod.rs | 6 +-- src/main.rs | 4 ++ src/server/mod.rs | 4 +- 7 files changed, 178 insertions(+), 13 deletions(-) diff --git a/src/backup/manager/meta.rs b/src/backup/manager/meta.rs index 6ba9c06..ff91310 100644 --- a/src/backup/manager/meta.rs +++ b/src/backup/manager/meta.rs @@ -9,7 +9,7 @@ use std::path::PathBuf; /// Manages a collection of backup layers, allowing them to be utilized as a single object. pub struct MetaManager where - T: Clone + Serialize + for<'de> Deserialize<'de>, + T: Clone + Serialize + for<'de> Deserialize<'de> + std::fmt::Debug, { backup_dir: PathBuf, dirs: Vec<(PathBuf, PathBuf)>, @@ -19,7 +19,7 @@ where impl MetaManager where - T: Clone + Serialize + for<'de> Deserialize<'de>, + T: Clone + Serialize + for<'de> Deserialize<'de> + std::fmt::Debug, { pub fn new>( backup_dir: P, @@ -116,4 +116,15 @@ where None } } + + /// Restore a backup for a specific layer + pub fn restore_backup( + &self, + layer: &str, + start_time: chrono::DateTime, + ) -> Option> { + self.managers + .get(layer) + .map(|manager| manager.restore_backup(start_time)) + } } diff --git a/src/backup/manager/mod.rs b/src/backup/manager/mod.rs index 32c452a..2b742f7 100644 --- a/src/backup/manager/mod.rs +++ b/src/backup/manager/mod.rs @@ -5,6 +5,8 @@ pub use config::ManagerConfig; pub use meta::MetaManager; use super::Backup; +use crate::other; +use chrono::SubsecRound; use chrono::Utc; use serde::Deserialize; use serde::Serialize; @@ -15,7 +17,7 @@ use std::path::PathBuf; /// Manages a single backup layer consisting of one or more chains of backups. pub struct Manager where - T: Clone + Serialize + for<'de> Deserialize<'de>, + T: Clone + Serialize + for<'de> Deserialize<'de> + std::fmt::Debug, { backup_dir: PathBuf, dirs: Vec<(PathBuf, PathBuf)>, @@ -28,7 +30,7 @@ where impl Manager where - T: Clone + Serialize + for<'de> Deserialize<'de>, + T: Clone + Serialize + for<'de> Deserialize<'de> + std::fmt::Debug, { const METADATA_FILE: &str = "alex.json"; @@ -156,4 +158,27 @@ where chrono::offset::Utc::now() } + + /// Restore the backup with the given start time by restoring its chain up to and including the + /// backup, in order. + pub fn restore_backup(&self, start_time: chrono::DateTime) -> io::Result<()> { + // Iterate over each chain, skipping elements until the element with the given start time + // is possibly found. + for chain in &self.chains { + // If we find the element in the chain, restore the entire chain up to and including + // the element + if let Some(index) = chain + .iter() + .position(|b| b.start_time.trunc_subsecs(0) == start_time) + { + for backup in chain.iter().take(index + 1) { + backup.restore(&self.backup_dir, &self.dirs)?; + } + + return Ok(()); + } + } + + Err(other("Unknown backup.")) + } } diff --git a/src/backup/mod.rs b/src/backup/mod.rs index 0168e67..3bc99df 100644 --- a/src/backup/mod.rs +++ b/src/backup/mod.rs @@ -8,6 +8,7 @@ pub use manager::ManagerConfig; pub use manager::MetaManager; use chrono::Utc; +use flate2::read::GzDecoder; use flate2::write::GzEncoder; use flate2::Compression; use path::PathExt; @@ -24,7 +25,7 @@ pub enum BackupType { } /// Represents a successful backup -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, Debug)] pub struct Backup { /// When the backup was started (also corresponds to the name) pub start_time: chrono::DateTime, @@ -36,6 +37,8 @@ pub struct Backup { } impl Backup<()> { + pub const FILENAME_FORMAT: &str = "%Y-%m-%d_%H-%M-%S.tar.gz"; + /// Return the path to a backup file by properly formatting the data. pub fn path>(backup_dir: P, start_time: chrono::DateTime) -> PathBuf { let backup_dir = backup_dir.as_ref(); @@ -46,8 +49,6 @@ impl Backup<()> { } impl Backup { - const FILENAME_FORMAT: &str = "%Y-%m-%d_%H-%M-%S.tar.gz"; - pub fn set_metadata(&mut self, metadata: T) { self.metadata = Some(metadata); } @@ -159,4 +160,48 @@ impl Backup { metadata: None, }) } + + pub fn restore>( + &self, + backup_dir: P, + dirs: &Vec<(PathBuf, PathBuf)>, + ) -> io::Result<()> { + let path = Backup::path(backup_dir, self.start_time); + let tar_gz = File::open(path)?; + let enc = GzDecoder::new(tar_gz); + let mut ar = tar::Archive::new(enc); + + // Unpack each file by matching it with one of the destination directories and extracting + // it to the right path + for entry in ar.entries()? { + let mut entry = entry?; + let entry_path_in_tar = entry.path()?.to_path_buf(); + + for (path_in_tar, dst_dir) in dirs { + if entry_path_in_tar.starts_with(path_in_tar) { + let dst_path = + dst_dir.join(entry_path_in_tar.strip_prefix(path_in_tar).unwrap()); + + // Ensure all parent directories are present + std::fs::create_dir_all(dst_path.parent().unwrap())?; + + entry.unpack(dst_path)?; + + break; + } + } + } + + // Remove any files + for (path_in_tar, dst_dir) in dirs { + if let Some(removed) = self.delta.removed.get(path_in_tar) { + for path in removed { + let dst_path = dst_dir.join(path); + std::fs::remove_file(dst_path)?; + } + } + } + + Ok(()) + } } diff --git a/src/cli/backup.rs b/src/cli/backup.rs index b71d992..32d45c4 100644 --- a/src/cli/backup.rs +++ b/src/cli/backup.rs @@ -1,11 +1,19 @@ +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::PathBuf; #[derive(Subcommand)] pub enum BackupCommands { - Create(BackupCreateArgs), + /// List all tracked backups List, + /// Manually create a new backup + Create(BackupCreateArgs), + /// Restore a backup + Restore(BackupRestoreArgs), } #[derive(Args)] @@ -20,11 +28,21 @@ pub struct BackupCreateArgs { layer: String, } +#[derive(Args)] +pub struct BackupRestoreArgs { + /// Path to the backup inside the backup directory + path: PathBuf, + /// Whether to overwrite the contents of the existing directories + #[arg(short, long, default_value_t = false)] + force: bool, +} + impl BackupArgs { pub fn run(&self, cli: &Cli) -> io::Result<()> { match &self.command { BackupCommands::Create(args) => args.run(cli), BackupCommands::List => Ok(()), + BackupCommands::Restore(args) => args.run(cli), } } } @@ -40,3 +58,65 @@ impl BackupCreateArgs { } } } + +impl BackupRestoreArgs { + pub fn run(&self, cli: &Cli) -> io::Result<()> { + let backup_dir = cli.backup.canonicalize()?; + + // Parse input path + let path = self.path.canonicalize()?; + + if !path.starts_with(&backup_dir) { + return Err(other("Provided file is not inside the backup directory.")); + } + + let layer = if let Some(parent) = 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) = 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.")); + }; + + let meta = cli.meta()?; + + // Clear previous contents of directories + let mut entries = cli + .config + .canonicalize()? + .read_dir()? + .chain(cli.world.canonicalize()?.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)?; + } + } + + // Restore the backup + if let Some(res) = meta.restore_backup(&layer, timestamp) { + res + } else { + Err(other("Unknown layer")) + } + } +} diff --git a/src/cli/mod.rs b/src/cli/mod.rs index eb4ba63..0594e8e 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -81,10 +81,10 @@ impl Cli { server_version: self.server_version.clone(), }; let dirs = vec![ - (PathBuf::from("config"), self.config.clone()), - (PathBuf::from("worlds"), self.world.clone()), + (PathBuf::from("config"), self.config.canonicalize()?), + (PathBuf::from("worlds"), self.world.canonicalize()?), ]; - let mut meta = MetaManager::new(self.backup.clone(), dirs, metadata); + let mut meta = MetaManager::new(self.backup.canonicalize()?, dirs, metadata); meta.add_all(&self.layers)?; Ok(meta) diff --git a/src/main.rs b/src/main.rs index edad9c3..f357d1b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,6 +8,10 @@ use crate::cli::Cli; use clap::Parser; use std::io; +pub fn other(msg: &str) -> io::Error { + io::Error::new(io::ErrorKind::Other, msg) +} + // fn commands_backup(cli: &Cli, args: &BackupArgs) -> io::Result<()> { // let metadata = server::Metadata { // server_type: cli.server, diff --git a/src/server/mod.rs b/src/server/mod.rs index 09f90a5..23a9ff5 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -8,7 +8,7 @@ use clap::ValueEnum; use serde::{Deserialize, Serialize}; use std::fmt; -#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Serialize, Deserialize)] +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Serialize, Deserialize, Debug)] pub enum ServerType { Unknown, Paper, @@ -29,7 +29,7 @@ impl fmt::Display for ServerType { } } -#[derive(Clone, Serialize, Deserialize)] +#[derive(Clone, Serialize, Deserialize, Debug)] pub struct Metadata { pub server_type: ServerType, pub server_version: String, From 0eda768c038c6ef8ac87c93e20a92ace73c39f9c Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Sat, 24 Jun 2023 12:16:53 +0200 Subject: [PATCH 41/93] chore: update readme --- .cargo/config.toml | 2 +- CHANGELOG.md | 3 ++ README.md | 105 ++++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 104 insertions(+), 6 deletions(-) diff --git a/.cargo/config.toml b/.cargo/config.toml index 37fecab..a5bfa9c 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,3 +1,3 @@ [alias] runs = "run -- --config data/config --backup data/backups --world data/worlds --layers 2min,2,4,4;3min,3,2,2" -runrs = "run --release -- paper 1.19.4-545 --config data/config --backup data/backups --world data/worlds --jar data/paper-1.19.4-525.jar" +runrs = "run --release -- --config data/config --backup data/backups --world data/worlds --layers 2min,2,4,4;3min,3,2,2" diff --git a/CHANGELOG.md b/CHANGELOG.md index c6009a6..7a3fc13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 same full backup * "backups to keep" has been replaced by "chains to keep" * Server type & version is now stored as metadata in the metadata file +* Backup layers + * Store multiple chains of backups in parallel, configuring each with + different parameters (son-father-grandfather principle) ### Changed diff --git a/README.md b/README.md index 4e00b78..dc96623 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,24 @@ Alex is a wrapper around a typical Minecraft server process. It acts as the parent process, and sits in between the user's input and the server's stdin. -This allows Alex to support additional commands that execute Rust code. +This allows Alex to support additional commands that execute Rust code, notably +creating periodic backups. + +## Installation + +Alex is distributed as statically compiled binaries for Linux amd64 and arm64. +These can be found +[here](https://git.rustybever.be/Chewing_Bever/alex/packages). + +### Dockerfiles + +You can easily install alex in your Docker images by letting Docker download it +for you. Add the following to your Dockerfile (replace with your required +version & architecture): + +```dockerfile +ADD "https://git.rustybever.be/api/packages/Chewing_Bever/generic/alex/0.2.2/alex-linux-amd64" /bin/alex +``` ## Why @@ -19,8 +36,86 @@ Afterwards, saving is enabled again with `save-on`. * Properly configures the process (working directory, optimisation flags) * Configure everything as CLI arguments or environment variables -## Installation +## Configuration -Alex is distributed as statically compiled binaries for Linux amd64 and arm64. -These can be found -[here](https://git.rustybever.be/Chewing_Bever/alex/packages). +Most information can be retrieved easily by looking at the help command: + +``` +Wrapper around Minecraft server processes, designed to complement Docker image installations. + +Usage: alex [OPTIONS] + +Commands: + run Run the server + backup Interact with the backup system without starting a server + help Print this message or the help of the given subcommand(s) + +Options: + --config + Directory where configs are stored, and where the server will run [env: ALEX_CONFIG_DIR=] [default: .] + --world + Directory where world files will be saved [env: ALEX_WORLD_DIR=] [default: ../worlds] + --backup + Directory where backups will be stored [env: ALEX_BACKUP_DIR=] [default: ../backups] + --layers + What backup layers to employ, provided as a list of tuples name,frequency,chains,chain_len delimited by semicolons (;) [env: ALEX_LAYERS=] + --server + Type of server [env: ALEX_SERVER=] [default: unknown] [possible values: unknown, paper, forge, vanilla] + --server-version + Version string for the server, e.g. 1.19.4-545 [env: ALEX_SERVER_VERSION=] [default: ] + -h, --help + Print help + -V, --version + Print version + +``` + +### Choosing layer parameters + +One part of the configuration that does require some clarification is the layer +system. Alex can manage an arbitrary number of backup layers, each having its +own configuration. These layers can either use incremental or full backups, +depending on how they're configured. + +These layers mostly correspond to the grandfather-father-son backup rotation +scheme. For example, one could have a layer that creates incremental backups +every 30 minutes, which are stored for 24 hours. This gives you 24 hours of +granular rollback in case your server suffers a crash. A second layer might +create a full backup every 24 hours, with backups being stored for 7 days. This +gives you 7 days worth of backups with the granularity of 24 hours. This +approach allows for greater versatility, while not having to store a large +amount of data. Thanks to incremental backups, frequent backups don't have to +take long at all. + +A layer consists of 4 pieces of metadata: + +* A name, which will be used in the file system and the in-game notifications +* The frequency, which describes in minutes how frequently a backup should be + created +* How many chains should be kept at all times +* How long each chain should be + +These last two require some clarification. In Alex, a "chain" describes an +initial full backup and zero or more incremental backups that are created from +that initial full backup. This concept exists because an incremental backup has +no real meaning if its ancestors are not known. To restore one of these chains, +all backups in the chain need to be restored in-order. Note that a chain length +of 1 disables incremental backups entirely. + +How many backups to keep is defined by how many chains should be stored. +Because an incremental backup needs to have its ancestors in order to be +restored, we can't simply "keep the last n backups", as this would break these +chains. Therefore, you configure how many backups to store using these chains. + +For example, if you configure a layer to store 5 chains of length 4, you will +have 20 archive files on disk, namely 5 full backups and 15 incremental +backups. Note that Alex applies these rules to *full* chains. An in-progress +chain does not count towards this total. Therefore, you can have up to `n-1` +additional archive files, with `n` being the chain length, on disk. + +To look at it from another perspective, say we wish to have a granularity of 30 +minutes for a timespan of 24 hours. Then we could configure the layer to only +save a single chain, with a chain length of 48. If we prefer to have a few full +backups instead of a long chain of incremental backups, we could instead use a +chain length of 12 and store 4 chains. Either way, the total comes out to 48, +which spans 24 hours if we make a backup every 30 minutes. From 5159bfdddd5fda8e8416972a12d7c9a7f1776c7d Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Sat, 24 Jun 2023 13:35:59 +0200 Subject: [PATCH 42/93] feat: basic list command --- src/backup/manager/meta.rs | 4 ++++ src/backup/manager/mod.rs | 5 +++++ src/cli/backup.rs | 32 ++++++++++++++++++++++++++++++-- 3 files changed, 39 insertions(+), 2 deletions(-) diff --git a/src/backup/manager/meta.rs b/src/backup/manager/meta.rs index ff91310..de82576 100644 --- a/src/backup/manager/meta.rs +++ b/src/backup/manager/meta.rs @@ -127,4 +127,8 @@ where .get(layer) .map(|manager| manager.restore_backup(start_time)) } + + pub fn managers(&self) -> &HashMap> { + &self.managers + } } diff --git a/src/backup/manager/mod.rs b/src/backup/manager/mod.rs index 2b742f7..47488d9 100644 --- a/src/backup/manager/mod.rs +++ b/src/backup/manager/mod.rs @@ -181,4 +181,9 @@ where Err(other("Unknown backup.")) } + + /// Get a reference to the underlying chains + pub fn chains(&self) -> &Vec>> { + &self.chains + } } diff --git a/src/cli/backup.rs b/src/cli/backup.rs index 32d45c4..44c7bca 100644 --- a/src/cli/backup.rs +++ b/src/cli/backup.rs @@ -9,7 +9,7 @@ use std::path::PathBuf; #[derive(Subcommand)] pub enum BackupCommands { /// List all tracked backups - List, + List(BackupListArgs), /// Manually create a new backup Create(BackupCreateArgs), /// Restore a backup @@ -28,6 +28,12 @@ pub struct BackupCreateArgs { 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 @@ -41,7 +47,7 @@ impl BackupArgs { pub fn run(&self, cli: &Cli) -> io::Result<()> { match &self.command { BackupCommands::Create(args) => args.run(cli), - BackupCommands::List => Ok(()), + BackupCommands::List(args) => args.run(cli), BackupCommands::Restore(args) => args.run(cli), } } @@ -120,3 +126,25 @@ impl BackupRestoreArgs { } } } + +impl BackupListArgs { + pub fn run(&self, cli: &Cli) -> io::Result<()> { + let meta = cli.meta()?; + + for (name, manager) in meta.managers().iter() { + println!("{}", name); + + for chain in manager.chains().iter().filter(|c| !c.is_empty()) { + let mut iter = chain.iter(); + let first = iter.next().unwrap(); + println!(" {}", first.start_time.format(Backup::FILENAME_FORMAT)); + + for backup in iter { + println!(" {}", backup.start_time.format(Backup::FILENAME_FORMAT)); + } + } + } + + Ok(()) + } +} From a4a03ca4c502e5e3b763d69521ed7c2c498a0168 Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Sat, 24 Jun 2023 13:47:55 +0200 Subject: [PATCH 43/93] feat: improve list view --- CHANGELOG.md | 2 +- src/backup/delta.rs | 10 ++++++++++ src/backup/mod.rs | 18 ++++++++++++++++++ src/cli/backup.rs | 5 ++--- 4 files changed, 31 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a3fc13..a973aea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -* `backup` CLI command * Incremental backups * Chain length describes how many incremental backups to create from the same full backup @@ -18,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Backup layers * Store multiple chains of backups in parallel, configuring each with different parameters (son-father-grandfather principle) +* CLI commands for creating, restoring & listing backups ### Changed diff --git a/src/backup/delta.rs b/src/backup/delta.rs index 34971f0..6e37e96 100644 --- a/src/backup/delta.rs +++ b/src/backup/delta.rs @@ -1,5 +1,6 @@ use serde::{Deserialize, Serialize}; use std::collections::{HashMap, HashSet}; +use std::fmt; use std::path::PathBuf; /// Represents the changes relative to the previous backup @@ -74,3 +75,12 @@ impl Delta { } } } + +impl fmt::Display for Delta { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let added_count: usize = self.added.values().map(|s| s.len()).sum(); + let removed_count: usize = self.removed.values().map(|s| s.len()).sum(); + + write!(f, "+{}-{}", added_count, removed_count) + } +} diff --git a/src/backup/mod.rs b/src/backup/mod.rs index 3bc99df..602df19 100644 --- a/src/backup/mod.rs +++ b/src/backup/mod.rs @@ -14,6 +14,7 @@ use flate2::Compression; use path::PathExt; use serde::{Deserialize, Serialize}; use std::collections::{HashMap, HashSet}; +use std::fmt; use std::fs::File; use std::io; use std::path::{Path, PathBuf}; @@ -205,3 +206,20 @@ impl Backup { Ok(()) } } + +impl fmt::Display for Backup { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let letter = match self.type_ { + BackupType::Full => 'F', + BackupType::Incremental => 'I', + }; + + write!( + f, + "{} ({}, {})", + self.start_time.format(Backup::FILENAME_FORMAT), + letter, + self.delta + ) + } +} diff --git a/src/cli/backup.rs b/src/cli/backup.rs index 44c7bca..4f64882 100644 --- a/src/cli/backup.rs +++ b/src/cli/backup.rs @@ -136,11 +136,10 @@ impl BackupListArgs { for chain in manager.chains().iter().filter(|c| !c.is_empty()) { let mut iter = chain.iter(); - let first = iter.next().unwrap(); - println!(" {}", first.start_time.format(Backup::FILENAME_FORMAT)); + println!(" {}", iter.next().unwrap()); for backup in iter { - println!(" {}", backup.start_time.format(Backup::FILENAME_FORMAT)); + println!(" {}", backup); } } } From c5193f0f3c40b1a7367db9dc5232929247d5eeb0 Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Mon, 3 Jul 2023 11:54:11 +0200 Subject: [PATCH 44/93] feat: store backup sizes in metadata file --- CHANGELOG.md | 3 ++- Dockerfile | 14 ++++++++------ src/backup/io_ext.rs | 43 +++++++++++++++++++++++++++++++++++++++++++ src/backup/mod.rs | 20 ++++++++++++++++++-- 4 files changed, 71 insertions(+), 9 deletions(-) create mode 100644 src/backup/io_ext.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index a973aea..d961db0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Chain length describes how many incremental backups to create from the same full backup * "backups to keep" has been replaced by "chains to keep" -* Server type & version is now stored as metadata in the metadata file +* Server type & version and backup size are now stored as metadata in the + metadata file * Backup layers * Store multiple chains of backups in parallel, configuring each with different parameters (son-father-grandfather principle) diff --git a/Dockerfile b/Dockerfile index dba2674..7c63117 100644 --- a/Dockerfile +++ b/Dockerfile @@ -47,13 +47,15 @@ COPY --from=builder /app/target/debug/alex /bin/alex RUN chmod +x /bin/alex # Default value to keep users from eating up all ram accidentally -ENV ALEX_XMS=1024 \ +ENV ALEX_CONFIG_DIR=/app/config \ + ALEX_WORLD_DIR=/app/worlds \ + ALEX_BACKUP_DIR=/app/backups \ + ALEX_SERVER=paper \ + ALEX_XMS=1024 \ ALEX_XMX=2048 \ ALEX_JAR=/app/server.jar \ - ALEX_CONFIG_DIR=/app/config \ - ALEX_WORLD_DIR=/app/worlds \ - ALEX_BACKUPS_DIR=/app/backups \ - ALEX_SERVER_VERSION="${MC_VERSION}-${PAPERMC_VERSION}" + ALEX_SERVER_VERSION="${MC_VERSION}-${PAPERMC_VERSION}" \ + ALEX_LAYERS="2min,2,4,4;3min,3,2,2" # Document exposed ports EXPOSE 25565 @@ -62,4 +64,4 @@ EXPOSE 25565 USER paper:paper ENTRYPOINT ["/bin/dumb-init", "--"] -CMD ["/bin/alex", "paper"] +CMD ["/bin/alex", "run"] diff --git a/src/backup/io_ext.rs b/src/backup/io_ext.rs new file mode 100644 index 0000000..30b919e --- /dev/null +++ b/src/backup/io_ext.rs @@ -0,0 +1,43 @@ +use std::io::{self, Write}; + +/// Wrapper around the Write trait that counts how many bytes have been written in total. +/// Heavily inspired by https://stackoverflow.com/a/42189386 +pub struct CountingWrite { + inner: W, + count: usize, +} + +impl CountingWrite +where + W: Write, +{ + pub fn new(writer: W) -> Self { + Self { + inner: writer, + count: 0, + } + } + + pub fn bytes_written(&self) -> usize { + self.count + } +} + +impl Write for CountingWrite +where + W: Write, +{ + fn write(&mut self, buf: &[u8]) -> io::Result { + let res = self.inner.write(buf); + + if let Ok(count) = res { + self.count += count; + } + + res + } + + fn flush(&mut self) -> io::Result<()> { + self.inner.flush() + } +} diff --git a/src/backup/mod.rs b/src/backup/mod.rs index 602df19..e1d6bfb 100644 --- a/src/backup/mod.rs +++ b/src/backup/mod.rs @@ -1,4 +1,5 @@ mod delta; +mod io_ext; pub mod manager; mod path; @@ -30,6 +31,7 @@ pub enum BackupType { pub struct Backup { /// When the backup was started (also corresponds to the name) pub start_time: chrono::DateTime, + pub size: usize, /// Type of the backup pub type_: BackupType, pub delta: Delta, @@ -84,7 +86,7 @@ impl Backup { let start_time = chrono::offset::Utc::now(); let path = Backup::path(backup_dir, start_time); - let tar_gz = File::create(path)?; + let tar_gz = io_ext::CountingWrite::new(File::create(path)?); let enc = GzEncoder::new(tar_gz, Compression::default()); let mut ar = tar::Builder::new(enc); @@ -104,9 +106,16 @@ impl Backup { delta.added.insert(dir_in_tar.to_path_buf(), added_files); } + let mut enc = ar.into_inner()?; + + // The docs recommend running try_finish before unwrapping using finish + enc.try_finish()?; + let tar_gz = enc.finish()?; + Ok(Backup { type_: BackupType::Full, start_time, + size: tar_gz.bytes_written(), delta, metadata: None, }) @@ -122,7 +131,7 @@ impl Backup { let start_time = chrono::offset::Utc::now(); let path = Backup::path(backup_dir, start_time); - let tar_gz = File::create(path)?; + let tar_gz = io_ext::CountingWrite::new(File::create(path)?); let enc = GzEncoder::new(tar_gz, Compression::default()); let mut ar = tar::Builder::new(enc); @@ -154,9 +163,16 @@ impl Backup { } } + let mut enc = ar.into_inner()?; + + // The docs recommend running try_finish before unwrapping using finish + enc.try_finish()?; + let tar_gz = enc.finish()?; + Ok(Backup { type_: BackupType::Incremental, start_time, + size: tar_gz.bytes_written(), delta, metadata: None, }) From bfd278abbe379e9c027b7ef4b6c9c0e268c29551 Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Mon, 3 Jul 2023 12:11:41 +0200 Subject: [PATCH 45/93] feat: show backup sizes in list command --- src/backup/mod.rs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/backup/mod.rs b/src/backup/mod.rs index e1d6bfb..f569697 100644 --- a/src/backup/mod.rs +++ b/src/backup/mod.rs @@ -20,6 +20,8 @@ use std::fs::File; use std::io; use std::path::{Path, PathBuf}; +const BYTE_SUFFIXES: [&str; 5] = ["B", "KiB", "MiB", "GiB", "TiB"]; + #[derive(Debug, PartialEq, Serialize, Deserialize)] pub enum BackupType { Full, @@ -230,11 +232,24 @@ impl fmt::Display for Backup { BackupType::Incremental => 'I', }; + // Pretty-print size + // If your backup is a petabyte or larger, this will crash and you need to re-evaluate your + // life choices + let mut index = 0; + let mut size = self.size as f64; + + while size >= 1024.0 { + index += 1; + size /= 1024.0; + } + write!( f, - "{} ({}, {})", + "{} ({}, {:.2}{}, {})", self.start_time.format(Backup::FILENAME_FORMAT), letter, + size, + BYTE_SUFFIXES[index], self.delta ) } From 2c256cf90401b24604b43733529ede98f348d6ab Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Mon, 3 Jul 2023 12:40:40 +0200 Subject: [PATCH 46/93] refactor: quick maths --- src/backup/mod.rs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/backup/mod.rs b/src/backup/mod.rs index f569697..cb9aa16 100644 --- a/src/backup/mod.rs +++ b/src/backup/mod.rs @@ -235,13 +235,8 @@ impl fmt::Display for Backup { // Pretty-print size // If your backup is a petabyte or larger, this will crash and you need to re-evaluate your // life choices - let mut index = 0; - let mut size = self.size as f64; - - while size >= 1024.0 { - index += 1; - size /= 1024.0; - } + let index = self.size.ilog(1024) as usize; + let size = self.size as f64 / (1024.0_f64.powi(index as i32)); write!( f, From f71db90922a8eaa3758e569e763d8f999caec53d Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Mon, 3 Jul 2023 12:59:50 +0200 Subject: [PATCH 47/93] feat: store end time as metadata --- src/backup/mod.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/backup/mod.rs b/src/backup/mod.rs index cb9aa16..ea7f2f9 100644 --- a/src/backup/mod.rs +++ b/src/backup/mod.rs @@ -33,6 +33,8 @@ pub enum BackupType { pub struct Backup { /// When the backup was started (also corresponds to the name) pub start_time: chrono::DateTime, + /// When the backup finished + pub end_time: chrono::DateTime, pub size: usize, /// Type of the backup pub type_: BackupType, @@ -117,6 +119,7 @@ impl Backup { Ok(Backup { type_: BackupType::Full, start_time, + end_time: chrono::Utc::now(), size: tar_gz.bytes_written(), delta, metadata: None, @@ -174,6 +177,7 @@ impl Backup { Ok(Backup { type_: BackupType::Incremental, start_time, + end_time: chrono::Utc::now(), size: tar_gz.bytes_written(), delta, metadata: None, @@ -237,12 +241,15 @@ impl fmt::Display for Backup { // life choices let index = self.size.ilog(1024) as usize; let size = self.size as f64 / (1024.0_f64.powi(index as i32)); + let duration = self.end_time - self.start_time; write!( f, - "{} ({}, {:.2}{}, {})", + "{} ({}, {}m{}s, {:.2}{}, {})", self.start_time.format(Backup::FILENAME_FORMAT), letter, + duration.num_seconds() / 60, + duration.num_seconds() % 60, size, BYTE_SUFFIXES[index], self.delta From 36c441b8c2c411554af8539f57fb86941270bb6d Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Tue, 4 Jul 2023 14:15:00 +0200 Subject: [PATCH 48/93] feat: respect backup list filter option --- src/cli/backup.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/cli/backup.rs b/src/cli/backup.rs index 4f64882..a77eae9 100644 --- a/src/cli/backup.rs +++ b/src/cli/backup.rs @@ -131,7 +131,12 @@ impl BackupListArgs { pub fn run(&self, cli: &Cli) -> io::Result<()> { let meta = cli.meta()?; - for (name, manager) in meta.managers().iter() { + // 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()) { From 55c5f2493705e32d1c52f324ce1ba541ae392d9d Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Tue, 4 Jul 2023 15:36:12 +0200 Subject: [PATCH 49/93] feat: specify output dirs for restore instead of using config & world --- src/backup/manager/meta.rs | 3 ++- src/backup/manager/mod.rs | 8 ++++++-- src/cli/backup.rs | 29 ++++++++++++++++++++++++----- 3 files changed, 32 insertions(+), 8 deletions(-) diff --git a/src/backup/manager/meta.rs b/src/backup/manager/meta.rs index de82576..2e0d703 100644 --- a/src/backup/manager/meta.rs +++ b/src/backup/manager/meta.rs @@ -122,10 +122,11 @@ where &self, layer: &str, start_time: chrono::DateTime, + dirs: &Vec<(PathBuf, PathBuf)>, ) -> Option> { self.managers .get(layer) - .map(|manager| manager.restore_backup(start_time)) + .map(|manager| manager.restore_backup(start_time, dirs)) } pub fn managers(&self) -> &HashMap> { diff --git a/src/backup/manager/mod.rs b/src/backup/manager/mod.rs index 47488d9..7969021 100644 --- a/src/backup/manager/mod.rs +++ b/src/backup/manager/mod.rs @@ -161,7 +161,11 @@ where /// Restore the backup with the given start time by restoring its chain up to and including the /// backup, in order. - pub fn restore_backup(&self, start_time: chrono::DateTime) -> io::Result<()> { + pub fn restore_backup( + &self, + start_time: chrono::DateTime, + dirs: &Vec<(PathBuf, PathBuf)>, + ) -> io::Result<()> { // Iterate over each chain, skipping elements until the element with the given start time // is possibly found. for chain in &self.chains { @@ -172,7 +176,7 @@ where .position(|b| b.start_time.trunc_subsecs(0) == start_time) { for backup in chain.iter().take(index + 1) { - backup.restore(&self.backup_dir, &self.dirs)?; + backup.restore(&self.backup_dir, dirs)?; } return Ok(()); diff --git a/src/cli/backup.rs b/src/cli/backup.rs index a77eae9..225fb42 100644 --- a/src/cli/backup.rs +++ b/src/cli/backup.rs @@ -38,9 +38,16 @@ pub struct BackupListArgs { pub struct BackupRestoreArgs { /// Path to the backup inside the backup directory 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 existing 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 { @@ -69,6 +76,15 @@ 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 path = self.path.canonicalize()?; @@ -97,11 +113,9 @@ impl BackupRestoreArgs { let meta = cli.meta()?; // Clear previous contents of directories - let mut entries = cli - .config - .canonicalize()? + let mut entries = output_config .read_dir()? - .chain(cli.world.canonicalize()?.read_dir()?) + .chain(output_worlds.read_dir()?) .peekable(); if entries.peek().is_some() && !self.force { @@ -118,8 +132,13 @@ impl BackupRestoreArgs { } } + 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) { + if let Some(res) = meta.restore_backup(&layer, timestamp, &dirs) { res } else { Err(other("Unknown layer")) From e6fa8a0eeb95fe84a88a4444598c0be7780e1e0f Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Tue, 4 Jul 2023 15:49:16 +0200 Subject: [PATCH 50/93] docs: add a few docstrings --- src/backup/manager/mod.rs | 3 ++- src/backup/mod.rs | 22 +++++++++++++++++++++- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/backup/manager/mod.rs b/src/backup/manager/mod.rs index 7969021..72743b7 100644 --- a/src/backup/manager/mod.rs +++ b/src/backup/manager/mod.rs @@ -53,7 +53,8 @@ where } } - /// Create a new backup with the expected type. + /// Create a new backup, either full or incremental, depending on the state of the current + /// chain. pub fn create_backup(&mut self) -> io::Result<()> { // We start a new chain if the current chain is complete, or if there isn't a first chain // yet diff --git a/src/backup/mod.rs b/src/backup/mod.rs index ea7f2f9..a4081f9 100644 --- a/src/backup/mod.rs +++ b/src/backup/mod.rs @@ -56,6 +56,7 @@ impl Backup<()> { } impl Backup { + /// Set the backup's metadata. pub fn set_metadata(&mut self, metadata: T) { self.metadata = Some(metadata); } @@ -126,7 +127,19 @@ impl Backup { }) } - /// Create a new incremental backup from a given previous backup + /// Create a new Incremental backup from the given state, populated with the given directories. + /// + /// # Arguments + /// + /// * `previous_state` - State the file system was in during the previous backup in the chain + /// * `previous_start_time` - Start time of the previous backup; used to filter files + /// * `backup_dir` - Directory to store archive in + /// * `dirs` - list of tuples `(path_in_tar, src_dir)` with `path_in_tar` the directory name + /// under which `src_dir`'s contents should be stored in the archive + /// + /// # Returns + /// + /// The `Backup` instance describing this new backup. pub fn create_from>( previous_state: HashMap>, previous_start_time: chrono::DateTime, @@ -184,6 +197,13 @@ impl Backup { }) } + /// Restore the backup by extracting its contents to the respective directories. + /// + /// # Arguments + /// + /// * `backup_dir` - Backup directory where the file is stored + /// * `dirs` - list of tuples `(path_in_tar, dst_dir)` with `dst_dir` the directory on-disk + /// where the files stored under `path_in_tar` inside the tarball should be extracted to. pub fn restore>( &self, backup_dir: P, From 75e9d7a9d288a907a2c3edb0641f29e4850b96ae Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Tue, 4 Jul 2023 15:55:52 +0200 Subject: [PATCH 51/93] chore: bumb version to 0.3.0 --- CHANGELOG.md | 2 ++ Cargo.lock | 2 +- Cargo.toml | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d961db0..2c9711d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://git.rustybever.be/Chewing_Bever/alex/src/branch/dev) +## [0.3.0](https://git.rustybever.be/Chewing_Bever/alex/src/tag/0.3.0) + ### Added * Incremental backups diff --git a/Cargo.lock b/Cargo.lock index 5ba48c9..bbe03a6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10,7 +10,7 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "alex" -version = "0.2.2" +version = "0.3.0" dependencies = [ "chrono", "clap", diff --git a/Cargo.toml b/Cargo.toml index 2ac6b39..4e3e88b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "alex" -version = "0.2.2" +version = "0.3.0" description = "Wrapper around Minecraft server processes, designed to complement Docker image installations." authors = ["Jef Roosens"] edition = "2021" From 6e216aa88f86923faa8e10dc7ee24dc7fb6e635b Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Tue, 4 Jul 2023 18:51:31 +0200 Subject: [PATCH 52/93] feat: define delta difference & strict difference --- src/backup/delta.rs | 95 ++++++++++++++++++++++++++++++++++++++++----- src/backup/mod.rs | 7 ++++ 2 files changed, 92 insertions(+), 10 deletions(-) diff --git a/src/backup/delta.rs b/src/backup/delta.rs index 6e37e96..b90ddfb 100644 --- a/src/backup/delta.rs +++ b/src/backup/delta.rs @@ -4,7 +4,7 @@ use std::fmt; use std::path::PathBuf; /// Represents the changes relative to the previous backup -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Clone)] pub struct Delta { /// What files were added/modified in each part of the tarball. pub added: HashMap>, @@ -23,38 +23,44 @@ impl Delta { } } - /// Update the current state so that its result becomes the merge of itself and the other - /// state. + /// Calculate the union of this delta with another delta. + /// + /// The union of two deltas is a delta that produces the same state as if you were to apply + /// both deltas in-order. Note that this operation is not commutative. #[allow(dead_code)] - pub fn merge(&mut self, delta: &Self) { + pub fn union(&self, delta: &Self) -> Self { + let mut out = self.clone(); + for (dir, added) in delta.added.iter() { // Files that were removed in the current state, but added in the new state, are no // longer removed - if let Some(orig_removed) = self.removed.get_mut(dir) { + if let Some(orig_removed) = out.removed.get_mut(dir) { orig_removed.retain(|k| !added.contains(k)); } // Newly added files are added to the state as well - if let Some(orig_added) = self.added.get_mut(dir) { + if let Some(orig_added) = out.added.get_mut(dir) { orig_added.extend(added.iter().cloned()); } else { - self.added.insert(dir.clone(), added.clone()); + out.added.insert(dir.clone(), added.clone()); } } for (dir, removed) in delta.removed.iter() { // Files that were originally added, but now deleted are removed from the added list - if let Some(orig_added) = self.added.get_mut(dir) { + if let Some(orig_added) = out.added.get_mut(dir) { orig_added.retain(|k| !removed.contains(k)); } // Newly removed files are added to the state as well - if let Some(orig_removed) = self.removed.get_mut(dir) { + if let Some(orig_removed) = out.removed.get_mut(dir) { orig_removed.extend(removed.iter().cloned()); } else { - self.removed.insert(dir.clone(), removed.clone()); + out.removed.insert(dir.clone(), removed.clone()); } } + + out } /// Modify the given state by applying this delta's changes to it @@ -74,6 +80,75 @@ impl Delta { } } } + + // Calculate the difference between this delta and the other delta. + // + // The difference simply means removing all adds and removes that are also performed in the + // other delta. + pub fn difference(&self, other: &Self) -> Self { + let mut out = self.clone(); + + for (dir, added) in out.added.iter_mut() { + // If files are added in the other delta, we don't add them in this delta + if let Some(other_added) = other.added.get(dir) { + added.retain(|k| !other_added.contains(k)); + }; + } + + for (dir, removed) in out.removed.iter_mut() { + // If files are removed in the other delta, we don't remove them in this delta either + if let Some(other_removed) = other.removed.get(dir) { + removed.retain(|k| !other_removed.contains(k)); + } + } + + out + } + + // Calculate the strict difference between this delta and the other delta. + // + // The strict difference is a difference where all operations that would be overwritten by the + // other delta are also removed (a.k.a. adding a file after removing it, or vice versa) + pub fn strict_difference(&self, other: &Self) -> Self { + let mut out = self.difference(other); + + for (dir, added) in out.added.iter_mut() { + // Remove additions that are removed in the other delta + if let Some(other_removed) = other.removed.get(dir) { + added.retain(|k| !other_removed.contains(k)); + } + } + + for (dir, removed) in out.removed.iter_mut() { + // Remove removals that are re-added in the other delta + if let Some(other_added) = other.added.get(dir) { + removed.retain(|k| !other_added.contains(k)); + } + } + + out + } + + /// Given a list of deltas, return a new list of the same length containing each delta, but + /// with all its successive deltas subtracted from it. This resulting list would produce the + /// same delta if merged, but with each delta only representing the parts of the final state + /// that it is the sole contributor to. + pub fn disjunct(deltas: &Vec) -> Vec { + let mut contributions = Vec::with_capacity(deltas.len()); + + for (i, delta) in deltas.iter().enumerate() { + // The base case for the contributions is every added file + let mut contribution = delta.clone(); + + for other_delta in &deltas[i + 1..] { + contribution = contribution.difference(other_delta); + } + + contributions.push(contribution); + } + + contributions + } } impl fmt::Display for Delta { diff --git a/src/backup/mod.rs b/src/backup/mod.rs index a4081f9..4515ba1 100644 --- a/src/backup/mod.rs +++ b/src/backup/mod.rs @@ -247,6 +247,13 @@ impl Backup { Ok(()) } + + pub fn open>(&self, backup_dir: P) -> io::Result>> { + let path = Backup::path(backup_dir, self.start_time); + let tar_gz = File::open(path)?; + let enc = GzDecoder::new(tar_gz); + Ok(tar::Archive::new(enc)) + } } impl fmt::Display for Backup { From 4ec336eb86bc20c435c3fdd23e14d476f3ab3ca9 Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Fri, 7 Jul 2023 16:59:51 +0200 Subject: [PATCH 53/93] feat: abstract State --- src/backup/delta.rs | 18 ---------- src/backup/manager/mod.rs | 6 ++-- src/backup/mod.rs | 18 +++------- src/backup/state.rs | 70 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 77 insertions(+), 35 deletions(-) create mode 100644 src/backup/state.rs diff --git a/src/backup/delta.rs b/src/backup/delta.rs index b90ddfb..61ceb1c 100644 --- a/src/backup/delta.rs +++ b/src/backup/delta.rs @@ -63,24 +63,6 @@ impl Delta { out } - /// Modify the given state by applying this delta's changes to it - pub fn apply(&self, state: &mut HashMap>) { - // First we add new files, then we remove the old ones - for (dir, added) in self.added.iter() { - if let Some(current) = state.get_mut(dir) { - current.extend(added.iter().cloned()); - } else { - state.insert(dir.clone(), added.clone()); - } - } - - for (dir, removed) in self.removed.iter() { - if let Some(current) = state.get_mut(dir) { - current.retain(|k| !removed.contains(k)); - } - } - } - // Calculate the difference between this delta and the other delta. // // The difference simply means removing all adds and removes that are also performed in the diff --git a/src/backup/manager/mod.rs b/src/backup/manager/mod.rs index 72743b7..0c25aad 100644 --- a/src/backup/manager/mod.rs +++ b/src/backup/manager/mod.rs @@ -4,7 +4,7 @@ mod meta; pub use config::ManagerConfig; pub use meta::MetaManager; -use super::Backup; +use super::{Backup, State}; use crate::other; use chrono::SubsecRound; use chrono::Utc; @@ -72,10 +72,10 @@ where let mut backup = if !current_chain.is_empty() { let previous_backup = current_chain.last().unwrap(); - let state = Backup::state(current_chain); + let previous_state = State::from(current_chain.iter().map(|b| &b.delta)); Backup::create_from( - state, + previous_state, previous_backup.start_time, &self.backup_dir, &self.dirs, diff --git a/src/backup/mod.rs b/src/backup/mod.rs index 4515ba1..0b5e322 100644 --- a/src/backup/mod.rs +++ b/src/backup/mod.rs @@ -2,11 +2,13 @@ mod delta; mod io_ext; pub mod manager; mod path; +mod state; use delta::Delta; pub use manager::Manager; pub use manager::ManagerConfig; pub use manager::MetaManager; +pub use state::State; use chrono::Utc; use flate2::read::GzDecoder; @@ -14,7 +16,7 @@ use flate2::write::GzEncoder; use flate2::Compression; use path::PathExt; use serde::{Deserialize, Serialize}; -use std::collections::{HashMap, HashSet}; +use std::collections::HashSet; use std::fmt; use std::fs::File; use std::io; @@ -61,18 +63,6 @@ impl Backup { self.metadata = Some(metadata); } - /// Resolve the state of the list of backups by applying their deltas in-order to an initially - /// empty state. - pub fn state(backups: &Vec) -> HashMap> { - let mut state: HashMap> = HashMap::new(); - - for backup in backups { - backup.delta.apply(&mut state); - } - - state - } - /// Create a new Full backup, populated with the given directories. /// /// # Arguments @@ -141,7 +131,7 @@ impl Backup { /// /// The `Backup` instance describing this new backup. pub fn create_from>( - previous_state: HashMap>, + previous_state: State, previous_start_time: chrono::DateTime, backup_dir: P, dirs: &Vec<(PathBuf, PathBuf)>, diff --git a/src/backup/state.rs b/src/backup/state.rs new file mode 100644 index 0000000..fa60738 --- /dev/null +++ b/src/backup/state.rs @@ -0,0 +1,70 @@ +use crate::backup::Delta; +use std::borrow::Borrow; +use std::collections::{HashMap, HashSet}; +use std::ops::Deref; +use std::path::PathBuf; + +/// Struct that represents a current state for a backup. This struct acts as a smart pointer around +/// a HashMap. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct State(HashMap>); + +impl State { + pub fn new() -> Self { + State(HashMap::new()) + } + + /// Apply the delta to the current state. + pub fn apply(&mut self, delta: &Delta) { + // First we add new files, then we remove the old ones + for (dir, added) in delta.added.iter() { + if let Some(current) = self.0.get_mut(dir) { + current.extend(added.iter().cloned()); + } else { + self.0.insert(dir.clone(), added.clone()); + } + } + + for (dir, removed) in delta.removed.iter() { + if let Some(current) = self.0.get_mut(dir) { + current.retain(|k| !removed.contains(k)); + } + } + } +} + +impl From for State +where + T: IntoIterator, + T::Item: Borrow, +{ + fn from(deltas: T) -> Self { + let mut state = State::new(); + + for delta in deltas { + state.apply(delta.borrow()); + } + + state + } +} + +impl AsRef>> for State { + fn as_ref(&self) -> &HashMap> { + &self.0 + } +} + +impl Deref for State { + type Target = HashMap>; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Default for State { + fn default() -> Self { + Self::new() + } +} From 80b814bcffbb65e674ef3b94b418635f2c9bfded Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Fri, 7 Jul 2023 18:06:15 +0200 Subject: [PATCH 54/93] feat: further use State abstraction --- src/backup/delta.rs | 36 ++++++++++++++++-------------------- src/backup/state.rs | 11 +++++++++-- 2 files changed, 25 insertions(+), 22 deletions(-) diff --git a/src/backup/delta.rs b/src/backup/delta.rs index 61ceb1c..f32f263 100644 --- a/src/backup/delta.rs +++ b/src/backup/delta.rs @@ -1,25 +1,24 @@ +use super::State; use serde::{Deserialize, Serialize}; -use std::collections::{HashMap, HashSet}; use std::fmt; -use std::path::PathBuf; /// Represents the changes relative to the previous backup #[derive(Debug, Serialize, Deserialize, Clone)] pub struct Delta { /// What files were added/modified in each part of the tarball. - pub added: HashMap>, + pub added: State, /// What files were removed in this backup, in comparison to the previous backup. For full /// backups, this will always be empty, as they do not consider previous backups. /// The map stores a separate list for each top-level directory, as the contents of these /// directories can come for different source directories. - pub removed: HashMap>, + pub removed: State, } impl Delta { pub fn new() -> Self { Self { - added: HashMap::new(), - removed: HashMap::new(), + added: Default::default(), + removed: Default::default(), } } @@ -111,23 +110,20 @@ impl Delta { out } - /// Given a list of deltas, return a new list of the same length containing each delta, but - /// with all its successive deltas subtracted from it. This resulting list would produce the - /// same delta if merged, but with each delta only representing the parts of the final state - /// that it is the sole contributor to. - pub fn disjunct(deltas: &Vec) -> Vec { - let mut contributions = Vec::with_capacity(deltas.len()); + pub fn contributions(deltas: &[Self]) -> Vec { + let mut contributions: Vec = Vec::with_capacity(deltas.len()); + // contributions[deltas.len() - 1] = deltas[deltas.len() - 1].added.clone(); - for (i, delta) in deltas.iter().enumerate() { - // The base case for the contributions is every added file - let mut contribution = delta.clone(); + // for (i, delta) in deltas.iter().enumerate() { + // // The base case for the contributions is every added file + // let mut contribution = delta.clone(); - for other_delta in &deltas[i + 1..] { - contribution = contribution.difference(other_delta); - } + // for other_delta in &deltas[i + 1..] { + // contribution = contribution.difference(other_delta); + // } - contributions.push(contribution); - } + // contributions.push(contribution); + // } contributions } diff --git a/src/backup/state.rs b/src/backup/state.rs index fa60738..dcd9c41 100644 --- a/src/backup/state.rs +++ b/src/backup/state.rs @@ -1,12 +1,13 @@ use crate::backup::Delta; +use serde::{Deserialize, Serialize}; use std::borrow::Borrow; use std::collections::{HashMap, HashSet}; -use std::ops::Deref; +use std::ops::{Deref, DerefMut}; use std::path::PathBuf; /// Struct that represents a current state for a backup. This struct acts as a smart pointer around /// a HashMap. -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct State(HashMap>); impl State { @@ -63,6 +64,12 @@ impl Deref for State { } } +impl DerefMut for State { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + impl Default for State { fn default() -> Self { Self::new() From 55673234731372df34649d77437efeb80aff7252 Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Fri, 7 Jul 2023 22:42:56 +0200 Subject: [PATCH 55/93] feat: initially working export command --- src/backup/delta.rs | 39 +++++++++++++++-------- src/backup/manager/meta.rs | 13 +++++++- src/backup/manager/mod.rs | 49 +++++++++++++++++++++++++++-- src/backup/mod.rs | 25 +++++++++++++++ src/backup/state.rs | 11 ++++++- src/cli/backup.rs | 63 ++++++++++++++++++++++++++++++++++++-- 6 files changed, 181 insertions(+), 19 deletions(-) diff --git a/src/backup/delta.rs b/src/backup/delta.rs index f32f263..1bc5477 100644 --- a/src/backup/delta.rs +++ b/src/backup/delta.rs @@ -1,5 +1,6 @@ use super::State; use serde::{Deserialize, Serialize}; +use std::borrow::Borrow; use std::fmt; /// Represents the changes relative to the previous backup @@ -26,7 +27,6 @@ impl Delta { /// /// The union of two deltas is a delta that produces the same state as if you were to apply /// both deltas in-order. Note that this operation is not commutative. - #[allow(dead_code)] pub fn union(&self, delta: &Self) -> Self { let mut out = self.clone(); @@ -110,20 +110,35 @@ impl Delta { out } - pub fn contributions(deltas: &[Self]) -> Vec { - let mut contributions: Vec = Vec::with_capacity(deltas.len()); - // contributions[deltas.len() - 1] = deltas[deltas.len() - 1].added.clone(); + /// Given a chain of deltas, ordered from last to first, calculate the "contribution" for each + /// state. + /// + /// The contribution of a delta in a given chain is defined as the parts of the state produced + /// by this chain that are actually provided by this delta. This comes down to calculating the + /// strict difference of this delta and all of its successive deltas. + pub fn contributions(deltas: I) -> Vec + where + I: IntoIterator, + I::Item: Borrow, + { + let mut contributions: Vec = Vec::new(); - // for (i, delta) in deltas.iter().enumerate() { - // // The base case for the contributions is every added file - // let mut contribution = delta.clone(); + let mut deltas = deltas.into_iter(); - // for other_delta in &deltas[i + 1..] { - // contribution = contribution.difference(other_delta); - // } + if let Some(first_delta) = deltas.next() { + // From last to first, we calculate the strict difference of the delta with the union of all its + // following deltas. The list of added files of this difference is the contribution for + // that delta. + contributions.push(first_delta.borrow().added.clone()); + let mut union_future = first_delta.borrow().clone(); - // contributions.push(contribution); - // } + for delta in deltas { + contributions.push(delta.borrow().strict_difference(&union_future).added); + union_future = union_future.union(delta.borrow()); + } + } + + // contributions.reverse(); contributions } diff --git a/src/backup/manager/meta.rs b/src/backup/manager/meta.rs index 2e0d703..4831f3f 100644 --- a/src/backup/manager/meta.rs +++ b/src/backup/manager/meta.rs @@ -4,7 +4,7 @@ use serde::Deserialize; use serde::Serialize; use std::collections::HashMap; use std::io; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; /// Manages a collection of backup layers, allowing them to be utilized as a single object. pub struct MetaManager @@ -129,6 +129,17 @@ where .map(|manager| manager.restore_backup(start_time, dirs)) } + pub fn export_backup>( + &self, + layer: &str, + start_time: chrono::DateTime, + output_path: P, + ) -> Option> { + self.managers + .get(layer) + .map(|manager| manager.export_backup(start_time, output_path)) + } + pub fn managers(&self) -> &HashMap> { &self.managers } diff --git a/src/backup/manager/mod.rs b/src/backup/manager/mod.rs index 0c25aad..6573376 100644 --- a/src/backup/manager/mod.rs +++ b/src/backup/manager/mod.rs @@ -4,15 +4,17 @@ mod meta; pub use config::ManagerConfig; pub use meta::MetaManager; -use super::{Backup, State}; +use super::{Backup, Delta, State}; use crate::other; use chrono::SubsecRound; use chrono::Utc; +use flate2::write::GzEncoder; +use flate2::Compression; use serde::Deserialize; use serde::Serialize; -use std::fs::File; +use std::fs::{File, OpenOptions}; use std::io; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; /// Manages a single backup layer consisting of one or more chains of backups. pub struct Manager @@ -187,6 +189,47 @@ where Err(other("Unknown backup.")) } + pub fn export_backup>( + &self, + start_time: chrono::DateTime, + output_path: P, + ) -> io::Result<()> { + // Iterate over each chain, skipping elements until the element with the given start time + // is possibly found. + for chain in &self.chains { + // If we find the element in the chain, restore the entire chain up to and including + // the element + if let Some(index) = chain + .iter() + .position(|b| b.start_time.trunc_subsecs(0) == start_time) + { + let contributions = + Delta::contributions(chain.iter().take(index + 1).map(|b| &b.delta).rev()); + + let tar_gz = OpenOptions::new() + .write(true) + .create(true) + .open(output_path.as_ref())?; + let enc = GzEncoder::new(tar_gz, Compression::default()); + let mut ar = tar::Builder::new(enc); + + for (contribution, backup) in + contributions.iter().rev().zip(chain.iter().take(index + 1)) + { + backup.append(&self.backup_dir, contribution, &mut ar)?; + } + + let mut enc = ar.into_inner()?; + enc.try_finish()?; + enc.finish()?; + + return Ok(()); + } + } + + Err(other("Unknown backup.")) + } + /// Get a reference to the underlying chains pub fn chains(&self) -> &Vec>> { &self.chains diff --git a/src/backup/mod.rs b/src/backup/mod.rs index 0b5e322..f456c0b 100644 --- a/src/backup/mod.rs +++ b/src/backup/mod.rs @@ -244,6 +244,31 @@ impl Backup { let enc = GzDecoder::new(tar_gz); Ok(tar::Archive::new(enc)) } + + /// Open this backup's archive and append all its files that are part of the provided state to + /// the archive file. + pub fn append>( + &self, + backup_dir: P, + state: &State, + ar: &mut tar::Builder>, + ) -> io::Result<()> { + let mut own_ar = self.open(backup_dir)?; + // println!("{:?}", &state); + + for entry in own_ar.entries()? { + let entry = entry?; + let entry_path_in_tar = entry.path()?.to_path_buf(); + + if state.contains(&entry_path_in_tar) { + println!("{:?}", &entry_path_in_tar); + let header = entry.header().clone(); + ar.append(&header, entry)?; + } + } + + Ok(()) + } } impl fmt::Display for Backup { diff --git a/src/backup/state.rs b/src/backup/state.rs index dcd9c41..4b09acf 100644 --- a/src/backup/state.rs +++ b/src/backup/state.rs @@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize}; use std::borrow::Borrow; use std::collections::{HashMap, HashSet}; use std::ops::{Deref, DerefMut}; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; /// Struct that represents a current state for a backup. This struct acts as a smart pointer around /// a HashMap. @@ -32,6 +32,15 @@ impl State { } } } + + /// Returns whether the provided relative path is part of the given state. + pub fn contains>(&self, path: P) -> bool { + let path = path.as_ref(); + + self.0.iter().any(|(dir, files)| { + path.starts_with(dir) && files.contains(path.strip_prefix(dir).unwrap()) + }) + } } impl From for State diff --git a/src/cli/backup.rs b/src/cli/backup.rs index 225fb42..dccf8d9 100644 --- a/src/cli/backup.rs +++ b/src/cli/backup.rs @@ -14,6 +14,8 @@ pub enum BackupCommands { Create(BackupCreateArgs), /// Restore a backup Restore(BackupRestoreArgs), + /// Export a backup into a full archive + Export(BackupExportArgs), } #[derive(Args)] @@ -36,13 +38,13 @@ pub struct BackupListArgs { #[derive(Args)] pub struct BackupRestoreArgs { - /// Path to the backup inside the backup directory + /// 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 existing directories + /// 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 @@ -50,12 +52,24 @@ pub struct BackupRestoreArgs { 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, +} + 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), } } } @@ -171,3 +185,48 @@ impl BackupListArgs { Ok(()) } } + +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 path = self.path.canonicalize()?; + + if !path.starts_with(&backup_dir) { + return Err(other("Provided file is not inside the backup directory.")); + } + + let layer = if let Some(parent) = 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) = 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.")); + }; + + let meta = cli.meta()?; + + if let Some(res) = meta.export_backup(&layer, timestamp, &self.output) { + res + } else { + Err(other("Unknown layer")) + } + } +} From fc8e8d37d397431d8cd9b4f85471cf7eb182b871 Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Sat, 8 Jul 2023 10:32:56 +0200 Subject: [PATCH 56/93] refactor: remove some code duplication --- src/backup/manager/mod.rs | 5 +- src/backup/mod.rs | 2 - src/cli/backup.rs | 135 +++++++++++++++++--------------------- 3 files changed, 63 insertions(+), 79 deletions(-) diff --git a/src/backup/manager/mod.rs b/src/backup/manager/mod.rs index 6573376..4cf01f0 100644 --- a/src/backup/manager/mod.rs +++ b/src/backup/manager/mod.rs @@ -220,10 +220,7 @@ where } let mut enc = ar.into_inner()?; - enc.try_finish()?; - enc.finish()?; - - return Ok(()); + return enc.try_finish(); } } diff --git a/src/backup/mod.rs b/src/backup/mod.rs index f456c0b..8eecc0f 100644 --- a/src/backup/mod.rs +++ b/src/backup/mod.rs @@ -254,14 +254,12 @@ impl Backup { ar: &mut tar::Builder>, ) -> io::Result<()> { let mut own_ar = self.open(backup_dir)?; - // println!("{:?}", &state); for entry in own_ar.entries()? { let entry = entry?; let entry_path_in_tar = entry.path()?.to_path_buf(); if state.contains(&entry_path_in_tar) { - println!("{:?}", &entry_path_in_tar); let header = entry.header().clone(); ar.append(&header, entry)?; } diff --git a/src/cli/backup.rs b/src/cli/backup.rs index dccf8d9..3a993fc 100644 --- a/src/cli/backup.rs +++ b/src/cli/backup.rs @@ -4,7 +4,7 @@ use crate::other; use chrono::{TimeZone, Utc}; use clap::{Args, Subcommand}; use std::io; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; #[derive(Subcommand)] pub enum BackupCommands { @@ -86,6 +86,63 @@ impl BackupCreateArgs { } } +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()?; @@ -100,29 +157,8 @@ impl BackupRestoreArgs { let output_worlds = self.output_worlds.canonicalize()?; // Parse input path - let path = self.path.canonicalize()?; - - if !path.starts_with(&backup_dir) { - return Err(other("Provided file is not inside the backup directory.")); - } - - let layer = if let Some(parent) = 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) = 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.")); - }; + let backup_path = self.path.canonicalize()?; + let (layer, timestamp) = parse_backup_path(&backup_dir, &backup_path)?; let meta = cli.meta()?; @@ -160,32 +196,6 @@ impl BackupRestoreArgs { } } -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(()) - } -} - impl BackupExportArgs { pub fn run(&self, cli: &Cli) -> io::Result<()> { let backup_dir = cli.backup.canonicalize()?; @@ -197,29 +207,8 @@ impl BackupExportArgs { } // Parse input path - let path = self.path.canonicalize()?; - - if !path.starts_with(&backup_dir) { - return Err(other("Provided file is not inside the backup directory.")); - } - - let layer = if let Some(parent) = 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) = 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.")); - }; + let backup_path = self.path.canonicalize()?; + let (layer, timestamp) = parse_backup_path(&backup_dir, &backup_path)?; let meta = cli.meta()?; From 1acfc9c4225231bf326f443d7c5d3744c495e0c7 Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Sat, 8 Jul 2023 13:30:11 +0200 Subject: [PATCH 57/93] refactor: have fun with rust's functional stuff --- CHANGELOG.md | 4 ++ src/backup/manager/mod.rs | 100 ++++++++++++++++++++++---------------- src/backup/path.rs | 13 +++-- 3 files changed, 67 insertions(+), 50 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c9711d..d4de051 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://git.rustybever.be/Chewing_Bever/alex/src/branch/dev) +### Added + +* Export command to export any backup as a new full backup + ## [0.3.0](https://git.rustybever.be/Chewing_Bever/alex/src/tag/0.3.0) ### Added diff --git a/src/backup/manager/mod.rs b/src/backup/manager/mod.rs index 4cf01f0..db53ad8 100644 --- a/src/backup/manager/mod.rs +++ b/src/backup/manager/mod.rs @@ -4,7 +4,7 @@ mod meta; pub use config::ManagerConfig; pub use meta::MetaManager; -use super::{Backup, Delta, State}; +use super::{Backup, BackupType, Delta, State}; use crate::other; use chrono::SubsecRound; use chrono::Utc; @@ -162,6 +162,25 @@ where chrono::offset::Utc::now() } + /// Search for a chain containing a backup with the specified start time. + /// + /// # Returns + /// + /// A tuple (chain, index) with index being the index of the found backup in the returned + /// chain. + fn find(&self, start_time: chrono::DateTime) -> Option<(&Vec>, usize)> { + for chain in &self.chains { + if let Some(index) = chain + .iter() + .position(|b| b.start_time.trunc_subsecs(0) == start_time) + { + return Some((chain, index)); + } + } + + None + } + /// Restore the backup with the given start time by restoring its chain up to and including the /// backup, in order. pub fn restore_backup( @@ -169,62 +188,57 @@ where start_time: chrono::DateTime, dirs: &Vec<(PathBuf, PathBuf)>, ) -> io::Result<()> { - // Iterate over each chain, skipping elements until the element with the given start time - // is possibly found. - for chain in &self.chains { - // If we find the element in the chain, restore the entire chain up to and including - // the element - if let Some(index) = chain - .iter() - .position(|b| b.start_time.trunc_subsecs(0) == start_time) - { + self.find(start_time) + .ok_or_else(|| other("Unknown layer.")) + .and_then(|(chain, index)| { for backup in chain.iter().take(index + 1) { backup.restore(&self.backup_dir, dirs)?; } - return Ok(()); - } - } - - Err(other("Unknown backup.")) + Ok(()) + }) } + /// Export the backup with the given start time as a new full archive. pub fn export_backup>( &self, start_time: chrono::DateTime, output_path: P, ) -> io::Result<()> { - // Iterate over each chain, skipping elements until the element with the given start time - // is possibly found. - for chain in &self.chains { - // If we find the element in the chain, restore the entire chain up to and including - // the element - if let Some(index) = chain - .iter() - .position(|b| b.start_time.trunc_subsecs(0) == start_time) - { - let contributions = - Delta::contributions(chain.iter().take(index + 1).map(|b| &b.delta).rev()); + self.find(start_time) + .ok_or_else(|| other("Unknown layer.")) + .and_then(|(chain, index)| { + match chain[index].type_ { + // A full backup is simply copied to the output path + BackupType::Full => std::fs::copy( + Backup::path(&self.backup_dir, chain[index].start_time), + output_path, + ) + .map(|_| ()), + // Incremental backups are exported one by one according to their contribution + BackupType::Incremental => { + let contributions = Delta::contributions( + chain.iter().take(index + 1).map(|b| &b.delta).rev(), + ); - let tar_gz = OpenOptions::new() - .write(true) - .create(true) - .open(output_path.as_ref())?; - let enc = GzEncoder::new(tar_gz, Compression::default()); - let mut ar = tar::Builder::new(enc); + let tar_gz = OpenOptions::new() + .write(true) + .create(true) + .open(output_path.as_ref())?; + let enc = GzEncoder::new(tar_gz, Compression::default()); + let mut ar = tar::Builder::new(enc); - for (contribution, backup) in - contributions.iter().rev().zip(chain.iter().take(index + 1)) - { - backup.append(&self.backup_dir, contribution, &mut ar)?; + for (contribution, backup) in + contributions.iter().rev().zip(chain.iter().take(index + 1)) + { + backup.append(&self.backup_dir, contribution, &mut ar)?; + } + + let mut enc = ar.into_inner()?; + enc.try_finish() + } } - - let mut enc = ar.into_inner()?; - return enc.try_finish(); - } - } - - Err(other("Unknown backup.")) + }) } /// Get a reference to the underlying chains diff --git a/src/backup/path.rs b/src/backup/path.rs index b8b5ae9..77c6883 100644 --- a/src/backup/path.rs +++ b/src/backup/path.rs @@ -129,16 +129,15 @@ pub trait PathExt { impl PathExt for Path { fn not_modified_since(&self, timestamp: chrono::DateTime) -> bool { - if let Ok(metadata) = self.metadata() { - if let Ok(last_modified) = metadata.modified() { + self.metadata() + .and_then(|m| m.modified()) + .map(|last_modified| { let t: chrono::DateTime = last_modified.into(); let t = t.with_timezone(&Local); - return t < timestamp; - } - } - - false + t < timestamp + }) + .unwrap_or(false) } fn read_dir_recursive(&self) -> io::Result { From 32d923e64bcb7150d7a76780ea5840d3c72f82a6 Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Sat, 8 Jul 2023 13:53:18 +0200 Subject: [PATCH 58/93] refactor: this is fun --- src/backup/manager/mod.rs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/backup/manager/mod.rs b/src/backup/manager/mod.rs index db53ad8..6a43f9f 100644 --- a/src/backup/manager/mod.rs +++ b/src/backup/manager/mod.rs @@ -153,13 +153,11 @@ where /// Calculate the next time a backup should be created. If no backup has been created yet, it /// will return now. pub fn next_scheduled_time(&self) -> chrono::DateTime { - if let Some(last_chain) = self.chains.last() { - if let Some(last_backup) = last_chain.last() { - return last_backup.start_time + self.frequency; - } - } - - chrono::offset::Utc::now() + self.chains + .last() + .and_then(|last_chain| last_chain.last()) + .map(|last_backup| last_backup.start_time + self.frequency) + .unwrap_or_else(chrono::offset::Utc::now) } /// Search for a chain containing a backup with the specified start time. From b924a054a6643ba94222c9430e424d1745435678 Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Sat, 8 Jul 2023 14:12:18 +0200 Subject: [PATCH 59/93] chore: bump version to 0.3.1 --- CHANGELOG.md | 2 ++ Cargo.lock | 2 +- Cargo.toml | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d4de051..918b0ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://git.rustybever.be/Chewing_Bever/alex/src/branch/dev) +## [0.3.1](https://git.rustybever.be/Chewing_Bever/alex/src/tag/0.3.1) + ### Added * Export command to export any backup as a new full backup diff --git a/Cargo.lock b/Cargo.lock index bbe03a6..16ec2ca 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10,7 +10,7 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "alex" -version = "0.3.0" +version = "0.3.1" dependencies = [ "chrono", "clap", diff --git a/Cargo.toml b/Cargo.toml index 4e3e88b..01d0593 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "alex" -version = "0.3.0" +version = "0.3.1" description = "Wrapper around Minecraft server processes, designed to complement Docker image installations." authors = ["Jef Roosens"] edition = "2021" From 6cdc18742ed841435b2b24a1b861e81bf8b967f9 Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Sat, 8 Jul 2023 14:50:18 +0200 Subject: [PATCH 60/93] feat: don't read non-contributing archives for export --- CHANGELOG.md | 5 +++++ src/backup/delta.rs | 11 +++++++++-- src/backup/manager/mod.rs | 11 +++++++++-- src/backup/state.rs | 8 ++++++++ 4 files changed, 31 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 918b0ea..62b6692 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://git.rustybever.be/Chewing_Bever/alex/src/branch/dev) +### Changed + +* Export command no longer reads backups that do not contribute to the final + state + ## [0.3.1](https://git.rustybever.be/Chewing_Bever/alex/src/tag/0.3.1) ### Added diff --git a/src/backup/delta.rs b/src/backup/delta.rs index 1bc5477..21f626c 100644 --- a/src/backup/delta.rs +++ b/src/backup/delta.rs @@ -18,11 +18,18 @@ pub struct Delta { impl Delta { pub fn new() -> Self { Self { - added: Default::default(), - removed: Default::default(), + added: State::new(), + removed: State::new(), } } + /// Returns whether the delta is empty by checking whether both its added and removed state + /// return true for their `is_empty`. + #[allow(dead_code)] + pub fn is_empty(&self) -> bool { + self.added.is_empty() && self.removed.is_empty() + } + /// Calculate the union of this delta with another delta. /// /// The union of two deltas is a delta that produces the same state as if you were to apply diff --git a/src/backup/manager/mod.rs b/src/backup/manager/mod.rs index 6a43f9f..7cbd43c 100644 --- a/src/backup/manager/mod.rs +++ b/src/backup/manager/mod.rs @@ -226,9 +226,16 @@ where let enc = GzEncoder::new(tar_gz, Compression::default()); let mut ar = tar::Builder::new(enc); - for (contribution, backup) in - contributions.iter().rev().zip(chain.iter().take(index + 1)) + // We only need to consider backups that have a non-empty contribution. + // This allows us to skip reading backups that have been completely + // overwritten by their successors anyways. + for (contribution, backup) in contributions + .iter() + .rev() + .zip(chain.iter().take(index + 1)) + .filter(|(contribution, _)| !contribution.is_empty()) { + println!("{}", &backup); backup.append(&self.backup_dir, contribution, &mut ar)?; } diff --git a/src/backup/state.rs b/src/backup/state.rs index 4b09acf..de4b2c0 100644 --- a/src/backup/state.rs +++ b/src/backup/state.rs @@ -41,6 +41,14 @@ impl State { path.starts_with(dir) && files.contains(path.strip_prefix(dir).unwrap()) }) } + + /// Returns whether the state is empty. + /// + /// Note that this does not necessarily mean that the state does not contain any sets, but + /// rather that any sets that it does contain are also empty. + pub fn is_empty(&self) -> bool { + self.0.values().all(|s| s.is_empty()) + } } impl From for State From 241bb4d68eb9fb432965de8fa5c3996419950a50 Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Sat, 8 Jul 2023 15:31:01 +0200 Subject: [PATCH 61/93] feat: add extract command --- CHANGELOG.md | 4 +++ src/backup/mod.rs | 66 +++++++++++++++++++++++++++++------------------ src/cli/backup.rs | 63 +++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 107 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 62b6692..bef4cf9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://git.rustybever.be/Chewing_Bever/alex/src/branch/dev) +### Added + +* Extract command for working with the output of export + ### Changed * Export command no longer reads backups that do not contribute to the final diff --git a/src/backup/mod.rs b/src/backup/mod.rs index 8eecc0f..6bd798f 100644 --- a/src/backup/mod.rs +++ b/src/backup/mod.rs @@ -55,6 +55,45 @@ impl Backup<()> { let filename = format!("{}", start_time.format(Self::FILENAME_FORMAT)); backup_dir.join(filename) } + + /// Extract an archive. + /// + /// # Arguments + /// + /// * `backup_path` - Path to the archive to extract + /// * `dirs` - list of tuples `(path_in_tar, dst_dir)` with `dst_dir` the directory on-disk + /// where the files stored under `path_in_tar` inside the tarball should be extracted to. + pub fn extract_archive>( + archive_path: P, + dirs: &Vec<(PathBuf, PathBuf)>, + ) -> io::Result<()> { + let tar_gz = File::open(archive_path)?; + let enc = GzDecoder::new(tar_gz); + let mut ar = tar::Archive::new(enc); + + // Unpack each file by matching it with one of the destination directories and extracting + // it to the right path + for entry in ar.entries()? { + let mut entry = entry?; + let entry_path_in_tar = entry.path()?.to_path_buf(); + + for (path_in_tar, dst_dir) in dirs { + if entry_path_in_tar.starts_with(path_in_tar) { + let dst_path = + dst_dir.join(entry_path_in_tar.strip_prefix(path_in_tar).unwrap()); + + // Ensure all parent directories are present + std::fs::create_dir_all(dst_path.parent().unwrap())?; + + entry.unpack(dst_path)?; + + break; + } + } + } + + Ok(()) + } } impl Backup { @@ -199,31 +238,8 @@ impl Backup { backup_dir: P, dirs: &Vec<(PathBuf, PathBuf)>, ) -> io::Result<()> { - let path = Backup::path(backup_dir, self.start_time); - let tar_gz = File::open(path)?; - let enc = GzDecoder::new(tar_gz); - let mut ar = tar::Archive::new(enc); - - // Unpack each file by matching it with one of the destination directories and extracting - // it to the right path - for entry in ar.entries()? { - let mut entry = entry?; - let entry_path_in_tar = entry.path()?.to_path_buf(); - - for (path_in_tar, dst_dir) in dirs { - if entry_path_in_tar.starts_with(path_in_tar) { - let dst_path = - dst_dir.join(entry_path_in_tar.strip_prefix(path_in_tar).unwrap()); - - // Ensure all parent directories are present - std::fs::create_dir_all(dst_path.parent().unwrap())?; - - entry.unpack(dst_path)?; - - break; - } - } - } + let backup_path = Backup::path(backup_dir, self.start_time); + Backup::extract_archive(backup_path, dirs)?; // Remove any files for (path_in_tar, dst_dir) in dirs { diff --git a/src/cli/backup.rs b/src/cli/backup.rs index 3a993fc..2ec1e2c 100644 --- a/src/cli/backup.rs +++ b/src/cli/backup.rs @@ -12,10 +12,13 @@ pub enum BackupCommands { List(BackupListArgs), /// Manually create a new backup Create(BackupCreateArgs), - /// Restore a backup + /// 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)] @@ -63,6 +66,22 @@ pub struct BackupExportArgs { 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 { @@ -70,6 +89,7 @@ impl BackupArgs { BackupCommands::List(args) => args.run(cli), BackupCommands::Restore(args) => args.run(cli), BackupCommands::Export(args) => args.run(cli), + BackupCommands::Extract(args) => args.run(cli), } } } @@ -219,3 +239,44 @@ impl BackupExportArgs { } } } + +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) + } +} From bfb264e8232c9414bc049d8f1841d049e9a09b2f Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Sat, 8 Jul 2023 15:34:24 +0200 Subject: [PATCH 62/93] docs: add some more help strings --- src/cli/backup.rs | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/src/cli/backup.rs b/src/cli/backup.rs index 2ec1e2c..6810e42 100644 --- a/src/cli/backup.rs +++ b/src/cli/backup.rs @@ -9,15 +9,29 @@ use std::path::{Path, PathBuf}; #[derive(Subcommand)] pub enum BackupCommands { /// List all tracked backups + /// + /// Note that this will only list backups for the layers currently configured, and will ignore + /// any other layers also present in the backup directory. List(BackupListArgs), /// Manually create a new backup + /// + /// Note that backups created using this command will count towards the length of a chain, and + /// can therefore shorten how far back in time your backups will be stored. Create(BackupCreateArgs), - /// Restore a backup including all of its required predecessors + /// Restore a backup + /// + /// This command will restore the selected backup by extracting its entire chain up to and + /// including the requested backup in-order. Restore(BackupRestoreArgs), /// Export a backup into a full archive + /// + /// Just like the restore command, this will extract each backup from the chain up to and + /// including the requested backup, but instead of writing the files to disk, they will be + /// recompressed into a new tarball, resulting in a new tarball containing a full backup. Export(BackupExportArgs), - /// Extract a single backup; meant as a convenience method for working with the output of - /// `export` + /// Extract an archive file, which is assumed to be a full backup. + /// + /// This command mostly exists as a convenience method for working with the output of `export`. Extract(BackupExtractArgs), } @@ -48,6 +62,9 @@ pub struct BackupRestoreArgs { /// Directory to store worlds in output_worlds: PathBuf, /// Whether to overwrite the contents of the output directories + /// + /// If set, the output directories will be completely cleared before trying to restore the + /// backup. #[arg(short, long, default_value_t = false)] force: bool, /// Create output directories if they don't exist @@ -75,6 +92,9 @@ pub struct BackupExtractArgs { /// Directory to store worlds in output_worlds: PathBuf, /// Whether to overwrite the contents of the output directories + /// + /// If set, the output directories will be completely cleared before trying to restore the + /// backup. #[arg(short, long, default_value_t = false)] force: bool, /// Create output directories if they don't exist From bf83357464ed010e4dd51513d06804734cebae2a Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Sat, 8 Jul 2023 16:11:36 +0200 Subject: [PATCH 63/93] feat: publish arch packages --- .woodpecker/arch-release.yml | 37 +++++++++++++++++++++++++++++++ CHANGELOG.md | 1 + PKGBUILD | 42 ++++++++++++++++++++++++++++++++++++ 3 files changed, 80 insertions(+) create mode 100644 .woodpecker/arch-release.yml create mode 100644 PKGBUILD diff --git a/.woodpecker/arch-release.yml b/.woodpecker/arch-release.yml new file mode 100644 index 0000000..ece2c52 --- /dev/null +++ b/.woodpecker/arch-release.yml @@ -0,0 +1,37 @@ +matrix: + PLATFORM: + - 'linux/amd64' + # - linux/arm64 + +platform: ${PLATFORM} +branches: [ main ] +skip_clone: true + +pipeline: + build: + image: 'menci/archlinuxarm:base-devel' + pull: true + commands: + - echo -e '[bur]\nServer = https://arch.r8r.be/$repo/$arch\nSigLevel = Optional' >> /etc/pacman.conf + # Update packages + - pacman -Syu --noconfirm + # Create non-root user to perform build & switch to their home + - groupadd -g 1000 builder + - useradd -mg builder builder + - chown -R builder:builder "$PWD" + - "echo 'builder ALL=(ALL) NOPASSWD: ALL' >> /etc/sudoers" + - su builder + - curl -OL "https://git.rustybever.be/Chewing_Bever/alex/raw/tag/$CI_COMMIT_TAG/PKGBUILD" + - makepkg -s --noconfirm --needed + when: + event: tag + + publish: + image: 'curlimages/curl' + commands: + # Publish the package + - 'for pkg in $(ls -1 *.pkg*); do curl -XPOST -T "$pkg" -H "X-API-KEY: $VIETER_API_KEY" https://arch.r8r.be/bur/publish; done' + secrets: + - vieter_api_key + when: + event: tag diff --git a/CHANGELOG.md b/CHANGELOG.md index bef4cf9..ebf8a14 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added * Extract command for working with the output of export +* Arch packages are now published to my bur repo ### Changed diff --git a/PKGBUILD b/PKGBUILD new file mode 100644 index 0000000..6bb6b6c --- /dev/null +++ b/PKGBUILD @@ -0,0 +1,42 @@ +# Maintainer: Jef Roosens + +pkgname='alex' +pkgver=0.3.1 +pkgdesc='Wrapper around Minecraft server processes, designed to complement Docker image installations.' +pkgrel=1 +arch=('x86_64' 'aarch64') + +url='https://git.rustybever.be/Chewing_Bever/alex' +license=('MIT') + +makedepends=('cargo') +depends=('glibc' 'gcc-libs' ) + +source=("${pkgname}-${pkgver}.tar.gz::https://git.rustybever.be/Chewing_Bever/alex/archive/${pkgver}.tar.gz") +sha512sums=('f88903bd99397f3e9a1c4a40febc65eace0d594cde8de20d54ed1cd0597512152111e7a20acaaa971309d5afe1ea267c7ef3b08f0d803237e8004808a83752c5') + +prepare() { + cd "${pkgname}" + + export RUSTUP_TOOLCHAIN=stable + cargo fetch --locked --target "$CARCH-unknown-linux-gnu" +} + +build() { + cd "${pkgname}" + + export RUSTUP_TOOLCHAIN=stable + export CARGO_TARGET_DIR=target + cargo build --frozen --release --all-features +} + +check() { + cd "${pkgname}" + + export RUSTUP_TOOLCHAIN=stable + cargo test --frozen --all-features +} + +package() { + install -Dm0755 -t "${pkgdir}/usr/bin/" "${pkgname}/target/release/${pkgname}" +} From 34d016fd3fd48eb9507183d17e07bd921f96addf Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Fri, 11 Aug 2023 21:26:17 +0200 Subject: [PATCH 64/93] feat: allow passing global configuration as TOML file --- Cargo.lock | 164 +++++++++++++++++++++++++++++++++++ Cargo.toml | 1 + alex-example.toml | 16 ++++ src/backup/manager/config.rs | 4 +- src/cli/backup.rs | 13 ++- src/cli/config.rs | 49 +++++++++++ src/cli/mod.rs | 115 ++++++++++++------------ src/cli/run.rs | 16 ++-- src/error.rs | 32 +++++++ src/main.rs | 11 ++- 10 files changed, 347 insertions(+), 74 deletions(-) create mode 100644 alex-example.toml create mode 100644 src/cli/config.rs create mode 100644 src/error.rs diff --git a/Cargo.lock b/Cargo.lock index 16ec2ca..1854dec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14,6 +14,7 @@ version = "0.3.1" dependencies = [ "chrono", "clap", + "figment", "flate2", "serde", "serde_json", @@ -85,6 +86,12 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "atomic" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59bdb34bc650a32731b31bd8f0829cc15d24a708ee31559e0bb34f2bc320cba" + [[package]] name = "autocfg" version = "1.1.0" @@ -194,6 +201,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + [[package]] name = "errno" version = "0.3.1" @@ -215,6 +228,20 @@ dependencies = [ "libc", ] +[[package]] +name = "figment" +version = "0.10.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4547e226f4c9ab860571e070a9034192b3175580ecea38da34fcdb53a018c9a5" +dependencies = [ + "atomic", + "pear", + "serde", + "toml", + "uncased", + "version_check", +] + [[package]] name = "filetime" version = "0.2.21" @@ -237,6 +264,12 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "hashbrown" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" + [[package]] name = "heck" version = "0.4.1" @@ -272,6 +305,22 @@ dependencies = [ "cc", ] +[[package]] +name = "indexmap" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "inlinable_string" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8fae54786f62fb2918dcfae3d568594e50eb9b5c25bf04371af6fe7516452fb" + [[package]] name = "io-lifetimes" version = "1.0.11" @@ -328,6 +377,12 @@ version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "518ef76f2f87365916b142844c16d8fefd85039bc5699050210a7778ee1cd1de" +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + [[package]] name = "miniz_oxide" version = "0.7.1" @@ -352,6 +407,29 @@ version = "1.17.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9670a07f94779e00908f3e686eab508878ebb390ba6e604d3a284c00e8d0487b" +[[package]] +name = "pear" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61a386cd715229d399604b50d1361683fe687066f42d56f54be995bc6868f71c" +dependencies = [ + "inlinable_string", + "pear_codegen", + "yansi", +] + +[[package]] +name = "pear_codegen" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da9f0f13dac8069c139e8300a6510e3f4143ecf5259c60b116a9b271b4ca0d54" +dependencies = [ + "proc-macro2", + "proc-macro2-diagnostics", + "quote", + "syn", +] + [[package]] name = "proc-macro2" version = "1.0.59" @@ -361,6 +439,19 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "proc-macro2-diagnostics" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "version_check", + "yansi", +] + [[package]] name = "quote" version = "1.0.28" @@ -430,6 +521,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_spanned" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96426c9936fd7a0124915f9185ea1d20aa9445cc9821142f0a73bc9207a2e186" +dependencies = [ + "serde", +] + [[package]] name = "signal-hook" version = "0.3.15" @@ -488,6 +588,49 @@ dependencies = [ "winapi", ] +[[package]] +name = "toml" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c17e963a819c331dcacd7ab957d80bc2b9a9c1e71c804826d2f283dd65306542" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.19.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8123f27e969974a3dfba720fdb560be359f57b44302d280ba72e76a74480e8a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + +[[package]] +name = "uncased" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b9bc53168a4be7402ab86c3aad243a84dd7381d09be0eddc81280c1da95ca68" +dependencies = [ + "version_check", +] + [[package]] name = "unicode-ident" version = "1.0.9" @@ -500,6 +643,12 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + [[package]] name = "wasi" version = "0.10.0+wasi-snapshot-preview1" @@ -657,6 +806,15 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" +[[package]] +name = "winnow" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f495880723d0999eb3500a9064d8dbcf836460b24c17df80ea7b5794053aac" +dependencies = [ + "memchr", +] + [[package]] name = "xattr" version = "0.2.3" @@ -665,3 +823,9 @@ checksum = "6d1526bbe5aaeb5eb06885f4d987bcdfa5e23187055de9b83fe00156a821fabc" dependencies = [ "libc", ] + +[[package]] +name = "yansi" +version = "1.0.0-rc" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ee746ad3851dd3bc40e4a028ab3b00b99278d929e48957bcb2d111874a7e43e" diff --git a/Cargo.toml b/Cargo.toml index 01d0593..777407a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ clap = { version = "4.3.1", features = ["derive", "env"] } signal-hook = "0.3.15" serde = { version = "1.0.164", features = ["derive", "rc"] } serde_json = "1.0.96" +figment = { version = "0.10.10", features = ["env", "toml"] } [profile.release] lto = "fat" diff --git a/alex-example.toml b/alex-example.toml new file mode 100644 index 0000000..d4b4122 --- /dev/null +++ b/alex-example.toml @@ -0,0 +1,16 @@ +config = "data/config" +world = "data/worlds" +backup = "data/backups" +server = "Paper" + +[[layers]] +name = "2min" +frequency = 2 +chains = 4 +chain_len = 4 + +[[layers]] +name = "3min" +frequency = 3 +chains = 2 +chain_len = 2 diff --git a/src/backup/manager/config.rs b/src/backup/manager/config.rs index 8adbd5a..75af34e 100644 --- a/src/backup/manager/config.rs +++ b/src/backup/manager/config.rs @@ -2,7 +2,9 @@ use std::error::Error; use std::fmt; use std::str::FromStr; -#[derive(Clone, Debug)] +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Serialize, Deserialize)] pub struct ManagerConfig { pub name: String, pub frequency: u32, diff --git a/src/cli/backup.rs b/src/cli/backup.rs index 6810e42..85c0cee 100644 --- a/src/cli/backup.rs +++ b/src/cli/backup.rs @@ -1,5 +1,4 @@ use crate::backup::Backup; -use crate::cli::Cli; use crate::other; use chrono::{TimeZone, Utc}; use clap::{Args, Subcommand}; @@ -103,7 +102,7 @@ pub struct BackupExtractArgs { } impl BackupArgs { - pub fn run(&self, cli: &Cli) -> io::Result<()> { + pub fn run(&self, cli: &super::Config) -> io::Result<()> { match &self.command { BackupCommands::Create(args) => args.run(cli), BackupCommands::List(args) => args.run(cli), @@ -115,7 +114,7 @@ impl BackupArgs { } impl BackupCreateArgs { - pub fn run(&self, cli: &Cli) -> io::Result<()> { + pub fn run(&self, cli: &super::Config) -> io::Result<()> { let mut meta = cli.meta()?; if let Some(res) = meta.create_backup(&self.layer) { @@ -127,7 +126,7 @@ impl BackupCreateArgs { } impl BackupListArgs { - pub fn run(&self, cli: &Cli) -> io::Result<()> { + pub fn run(&self, cli: &super::Config) -> io::Result<()> { let meta = cli.meta()?; // A bit scuffed? Sure @@ -184,7 +183,7 @@ fn parse_backup_path( } impl BackupRestoreArgs { - pub fn run(&self, cli: &Cli) -> io::Result<()> { + pub fn run(&self, cli: &super::Config) -> io::Result<()> { let backup_dir = cli.backup.canonicalize()?; // Create directories if needed @@ -237,7 +236,7 @@ impl BackupRestoreArgs { } impl BackupExportArgs { - pub fn run(&self, cli: &Cli) -> io::Result<()> { + pub fn run(&self, cli: &super::Config) -> io::Result<()> { let backup_dir = cli.backup.canonicalize()?; if self.make { @@ -261,7 +260,7 @@ impl BackupExportArgs { } impl BackupExtractArgs { - pub fn run(&self, _cli: &Cli) -> io::Result<()> { + pub fn run(&self, _cli: &super::Config) -> io::Result<()> { // Create directories if needed if self.make { std::fs::create_dir_all(&self.output_config)?; diff --git a/src/cli/config.rs b/src/cli/config.rs new file mode 100644 index 0000000..da80269 --- /dev/null +++ b/src/cli/config.rs @@ -0,0 +1,49 @@ +use std::{io, path::PathBuf}; + +use serde::{Deserialize, Serialize}; + +use crate::{ + backup::{ManagerConfig, MetaManager}, + server::{Metadata, ServerType}, +}; + +#[derive(Serialize, Deserialize, Debug)] +pub struct Config { + pub config: PathBuf, + pub world: PathBuf, + pub backup: PathBuf, + pub layers: Vec, + pub server: ServerType, + pub server_version: String, +} + +impl Default for Config { + fn default() -> Self { + Self { + config: PathBuf::from("."), + world: PathBuf::from("../worlds"), + backup: PathBuf::from("../backups"), + layers: Vec::new(), + server: ServerType::Unknown, + server_version: String::from(""), + } + } +} + +impl Config { + /// Convenience method to initialize backup manager from the cli arguments + pub fn meta(&self) -> io::Result> { + let metadata = Metadata { + server_type: self.server, + server_version: self.server_version.clone(), + }; + let dirs = vec![ + (PathBuf::from("config"), self.config.canonicalize()?), + (PathBuf::from("worlds"), self.world.canonicalize()?), + ]; + let mut meta = MetaManager::new(self.backup.canonicalize()?, dirs, metadata); + meta.add_all(&self.layers)?; + + Ok(meta) + } +} diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 0594e8e..6ba3f94 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -1,4 +1,5 @@ mod backup; +mod config; mod run; pub use crate::backup::MetaManager; @@ -8,7 +9,13 @@ pub use run::RunArgs; use crate::backup::ManagerConfig; use crate::server::ServerType; -use clap::{Parser, Subcommand}; +use clap::{Args, Parser, Subcommand}; +pub use config::Config; +use figment::{ + providers::{Env, Format, Serialized, Toml}, + Figment, +}; +use serde::{Deserialize, Serialize}; use std::io; use std::path::PathBuf; @@ -20,73 +27,71 @@ pub enum Commands { Backup(BackupArgs), } -#[derive(Parser)] -#[command(author, version, about, long_about = None)] -pub struct Cli { - #[command(subcommand)] - pub command: Commands, +#[derive(Args, Serialize, Deserialize, Clone)] +pub struct CliArgs { /// Directory where configs are stored, and where the server will run - #[arg( - long, - value_name = "CONFIG_DIR", - default_value = ".", - env = "ALEX_CONFIG_DIR", - global = true - )] - pub config: PathBuf, + #[arg(long, value_name = "CONFIG_DIR", global = true)] + #[serde(skip_serializing_if = "::std::option::Option::is_none")] + pub config: Option, + /// Directory where world files will be saved - #[arg( - long, - value_name = "WORLD_DIR", - default_value = "../worlds", - env = "ALEX_WORLD_DIR", - global = true - )] - pub world: PathBuf, + #[arg(long, value_name = "WORLD_DIR", global = true)] + #[serde(skip_serializing_if = "::std::option::Option::is_none")] + pub world: Option, + /// Directory where backups will be stored - #[arg( - long, - value_name = "BACKUP_DIR", - default_value = "../backups", - env = "ALEX_BACKUP_DIR", - global = true - )] - pub backup: PathBuf, + #[arg(long, value_name = "BACKUP_DIR", global = true)] + #[serde(skip_serializing_if = "::std::option::Option::is_none")] + pub backup: Option, /// What backup layers to employ, provided as a list of tuples name,frequency,chains,chain_len /// delimited by semicolons (;). - #[arg(long, env = "ALEX_LAYERS", global = true, value_delimiter = ';')] - pub layers: Vec, + #[arg(long, global = true, value_delimiter = ';')] + #[serde(skip_serializing_if = "::std::option::Option::is_none")] + pub layers: Option>, /// Type of server - #[arg(long, default_value = "unknown", env = "ALEX_SERVER", global = true)] - pub server: ServerType, + #[arg(long, global = true)] + #[serde(skip_serializing_if = "::std::option::Option::is_none")] + pub server: Option, + /// Version string for the server, e.g. 1.19.4-545 - #[arg(long, default_value = "", env = "ALEX_SERVER_VERSION", global = true)] - pub server_version: String, + #[arg(long, global = true)] + #[serde(skip_serializing_if = "::std::option::Option::is_none")] + pub server_version: Option, +} + +#[derive(Parser, Serialize)] +#[command(author, version, about, long_about = None)] +pub struct Cli { + #[command(subcommand)] + #[serde(skip)] + pub command: Commands, + + /// Path to a TOML configuration file + #[arg(long = "config-file", global = true)] + pub config_file: Option, + + #[command(flatten)] + pub args: CliArgs, } impl Cli { - pub fn run(&self) -> io::Result<()> { + pub fn run(&self) -> crate::Result<()> { + let toml_file = self + .config_file + .clone() + .unwrap_or(PathBuf::from(Env::var_or("ALEX_CONFIG_FILE", ""))); + let config: Config = Figment::new() + .merge(Serialized::defaults(Config::default())) + .merge(Toml::file(toml_file)) + .merge(Env::prefixed("ALEX_")) + .merge(Serialized::defaults(&self.args)) + .extract()?; + match &self.command { - Commands::Run(args) => args.run(self), - Commands::Backup(args) => args.run(self), + Commands::Run(args) => args.run(&config), + Commands::Backup(args) => Ok(args.run(&config)?), } } - - /// Convenience method to initialize backup manager from the cli arguments - pub fn meta(&self) -> io::Result> { - let metadata = Metadata { - server_type: self.server, - server_version: self.server_version.clone(), - }; - let dirs = vec![ - (PathBuf::from("config"), self.config.canonicalize()?), - (PathBuf::from("worlds"), self.world.canonicalize()?), - ]; - let mut meta = MetaManager::new(self.backup.canonicalize()?, dirs, metadata); - meta.add_all(&self.layers)?; - - Ok(meta) - } } diff --git a/src/cli/run.rs b/src/cli/run.rs index 986fe8a..4e577b8 100644 --- a/src/cli/run.rs +++ b/src/cli/run.rs @@ -57,16 +57,16 @@ fn backups_thread(counter: Arc>) { } impl RunArgs { - pub fn run(&self, cli: &Cli) -> io::Result<()> { + pub fn run(&self, config: &super::Config) -> crate::Result<()> { let (_, mut signals) = signals::install_signal_handlers()?; - let mut cmd = server::ServerCommand::new(cli.server, &cli.server_version) + let mut cmd = server::ServerCommand::new(config.server, &config.server_version) .java(&self.java) .jar(self.jar.clone()) - .config(cli.config.clone()) - .world(cli.world.clone()) - .backup(cli.backup.clone()) - .managers(cli.layers.clone()) + .config(config.config.clone()) + .world(config.world.clone()) + .backup(config.backup.clone()) + .managers(config.layers.clone()) .xms(self.xms) .xmx(self.xmx); cmd.canonicalize()?; @@ -79,7 +79,7 @@ impl RunArgs { let counter = Arc::new(Mutex::new(cmd.spawn()?)); - if !cli.layers.is_empty() { + if !config.layers.is_empty() { let clone = Arc::clone(&counter); std::thread::spawn(move || backups_thread(clone)); } @@ -89,6 +89,6 @@ impl RunArgs { std::thread::spawn(move || stdin::handle_stdin(clone)); // Signal handler loop exits the process when necessary - signals::handle_signals(&mut signals, counter) + Ok(signals::handle_signals(&mut signals, counter)?) } } diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..0ea7532 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,32 @@ +use std::{fmt, io}; + +pub type Result = std::result::Result; + +#[derive(Debug)] +pub enum Error { + IO(io::Error), + Figment(figment::Error), +} + +impl fmt::Display for Error { + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Error::IO(err) => write!(fmt, "{}", err), + Error::Figment(err) => write!(fmt, "{}", err), + } + } +} + +impl std::error::Error for Error {} + +impl From for Error { + fn from(err: io::Error) -> Self { + Error::IO(err) + } +} + +impl From for Error { + fn from(err: figment::Error) -> Self { + Error::Figment(err) + } +} diff --git a/src/main.rs b/src/main.rs index f357d1b..245a796 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,13 +1,17 @@ mod backup; mod cli; +mod error; mod server; mod signals; mod stdin; -use crate::cli::Cli; -use clap::Parser; use std::io; +use clap::Parser; + +use crate::cli::Cli; +pub use error::{Error, Result}; + pub fn other(msg: &str) -> io::Error { io::Error::new(io::ErrorKind::Other, msg) } @@ -32,7 +36,8 @@ pub fn other(msg: &str) -> io::Error { // // manager.remove_old_backups() // } -fn main() -> io::Result<()> { +fn main() -> crate::Result<()> { let cli = Cli::parse(); + cli.run() } From db3bba5a422b41f3abc8be90b0f39a1aed68291d Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Fri, 11 Aug 2023 23:13:17 +0200 Subject: [PATCH 65/93] feat: also allow run args to be passed from toml file --- CHANGELOG.md | 1 + Cargo.toml | 2 +- alex-example.toml | 20 ++++----- src/cli/mod.rs | 31 ++++++++----- src/cli/run/config.rs | 22 ++++++++++ src/cli/{run.rs => run/mod.rs} | 80 +++++++++++++++++++--------------- 6 files changed, 99 insertions(+), 57 deletions(-) create mode 100644 src/cli/run/config.rs rename src/cli/{run.rs => run/mod.rs} (58%) diff --git a/CHANGELOG.md b/CHANGELOG.md index ebf8a14..553fd69 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Extract command for working with the output of export * Arch packages are now published to my bur repo +* Allow passing configuration variables from TOML file ### Changed diff --git a/Cargo.toml b/Cargo.toml index 777407a..09a8b1b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,7 @@ flate2 = "1.0.26" chrono = { version = "0.4.26", features = ["serde"] } clap = { version = "4.3.1", features = ["derive", "env"] } signal-hook = "0.3.15" -serde = { version = "1.0.164", features = ["derive", "rc"] } +serde = { version = "1.0.164", features = ["derive"] } serde_json = "1.0.96" figment = { version = "0.10.10", features = ["env", "toml"] } diff --git a/alex-example.toml b/alex-example.toml index d4b4122..234a7bf 100644 --- a/alex-example.toml +++ b/alex-example.toml @@ -3,14 +3,14 @@ world = "data/worlds" backup = "data/backups" server = "Paper" -[[layers]] -name = "2min" -frequency = 2 -chains = 4 -chain_len = 4 +# [[layers]] +# name = "2min" +# frequency = 2 +# chains = 4 +# chain_len = 4 -[[layers]] -name = "3min" -frequency = 3 -chains = 2 -chain_len = 2 +# [[layers]] +# name = "3min" +# frequency = 3 +# chains = 2 +# chain_len = 2 diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 6ba3f94..7a76ea1 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -5,7 +5,7 @@ mod run; pub use crate::backup::MetaManager; pub use crate::server::Metadata; pub use backup::{BackupArgs, BackupCommands}; -pub use run::RunArgs; +pub use run::RunCli; use crate::backup::ManagerConfig; use crate::server::ServerType; @@ -16,13 +16,12 @@ use figment::{ Figment, }; use serde::{Deserialize, Serialize}; -use std::io; use std::path::PathBuf; #[derive(Subcommand)] pub enum Commands { /// Run the server - Run(RunArgs), + Run(RunCli), /// Interact with the backup system without starting a server Backup(BackupArgs), } @@ -78,20 +77,28 @@ pub struct Cli { impl Cli { pub fn run(&self) -> crate::Result<()> { + let config = self.config(&self.args)?; + + match &self.command { + Commands::Run(args) => args.run(self, &config), + Commands::Backup(args) => Ok(args.run(&config)?), + } + } + + pub fn config(&self, args: &U) -> Result + where + T: Default + Serialize + for<'de> Deserialize<'de>, + U: Serialize, + { let toml_file = self .config_file .clone() .unwrap_or(PathBuf::from(Env::var_or("ALEX_CONFIG_FILE", ""))); - let config: Config = Figment::new() - .merge(Serialized::defaults(Config::default())) + Figment::new() + .merge(Serialized::defaults(T::default())) .merge(Toml::file(toml_file)) .merge(Env::prefixed("ALEX_")) - .merge(Serialized::defaults(&self.args)) - .extract()?; - - match &self.command { - Commands::Run(args) => args.run(&config), - Commands::Backup(args) => Ok(args.run(&config)?), - } + .merge(Serialized::defaults(args)) + .extract() } } diff --git a/src/cli/run/config.rs b/src/cli/run/config.rs new file mode 100644 index 0000000..5783451 --- /dev/null +++ b/src/cli/run/config.rs @@ -0,0 +1,22 @@ +use std::path::PathBuf; + +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Debug)] +pub struct Config { + pub jar: PathBuf, + pub java: String, + pub xms: u64, + pub xmx: u64, +} + +impl Default for Config { + fn default() -> Self { + Self { + jar: PathBuf::from("server.jar"), + java: String::from("java"), + xms: 1024, + xmx: 2048, + } + } +} diff --git a/src/cli/run.rs b/src/cli/run/mod.rs similarity index 58% rename from src/cli/run.rs rename to src/cli/run/mod.rs index 4e577b8..fb48491 100644 --- a/src/cli/run.rs +++ b/src/cli/run/mod.rs @@ -1,33 +1,19 @@ -use crate::cli::Cli; +mod config; + use crate::server; use crate::signals; use crate::stdin; use clap::Args; -use std::io; +use config::Config; use std::path::PathBuf; use std::sync::{Arc, Mutex}; +use serde::{Deserialize, Serialize}; + #[derive(Args)] -pub struct RunArgs { - /// Server jar to execute - #[arg( - long, - value_name = "JAR_PATH", - default_value = "server.jar", - env = "ALEX_JAR" - )] - pub jar: PathBuf, - - /// Java command to run the server jar with - #[arg(long, value_name = "JAVA_CMD", default_value_t = String::from("java"), env = "ALEX_JAVA")] - pub java: String, - - /// XMS value in megabytes for the server instance - #[arg(long, default_value_t = 1024, env = "ALEX_XMS")] - pub xms: u64, - /// XMX value in megabytes for the server instance - #[arg(long, default_value_t = 2048, env = "ALEX_XMX")] - pub xmx: u64, +pub struct RunCli { + #[command(flatten)] + pub args: RunArgs, /// Don't actually run the server, but simply output the server configuration that would have /// been ran @@ -35,6 +21,29 @@ pub struct RunArgs { pub dry: bool, } +#[derive(Args, Serialize, Deserialize, Clone)] +pub struct RunArgs { + /// Server jar to execute + #[arg(long, value_name = "JAR_PATH")] + #[serde(skip_serializing_if = "::std::option::Option::is_none")] + pub jar: Option, + + /// Java command to run the server jar with + #[arg(long, value_name = "JAVA_CMD")] + #[serde(skip_serializing_if = "::std::option::Option::is_none")] + pub java: Option, + + /// XMS value in megabytes for the server instance + #[arg(long)] + #[serde(skip_serializing_if = "::std::option::Option::is_none")] + pub xms: Option, + + /// XMX value in megabytes for the server instance + #[arg(long)] + #[serde(skip_serializing_if = "::std::option::Option::is_none")] + pub xmx: Option, +} + fn backups_thread(counter: Arc>) { loop { let next_scheduled_time = { @@ -56,19 +65,22 @@ fn backups_thread(counter: Arc>) { } } -impl RunArgs { - pub fn run(&self, config: &super::Config) -> crate::Result<()> { +impl RunCli { + pub fn run(&self, cli: &super::Cli, global: &super::Config) -> crate::Result<()> { + let config: Config = cli.config(&self.args)?; + println!("{:?}", config); + let (_, mut signals) = signals::install_signal_handlers()?; - let mut cmd = server::ServerCommand::new(config.server, &config.server_version) - .java(&self.java) - .jar(self.jar.clone()) - .config(config.config.clone()) - .world(config.world.clone()) - .backup(config.backup.clone()) - .managers(config.layers.clone()) - .xms(self.xms) - .xmx(self.xmx); + let mut cmd = server::ServerCommand::new(global.server, &global.server_version) + .java(&config.java) + .jar(config.jar.clone()) + .config(global.config.clone()) + .world(global.world.clone()) + .backup(global.backup.clone()) + .managers(global.layers.clone()) + .xms(config.xms) + .xmx(config.xmx); cmd.canonicalize()?; if self.dry { @@ -79,7 +91,7 @@ impl RunArgs { let counter = Arc::new(Mutex::new(cmd.spawn()?)); - if !config.layers.is_empty() { + if !global.layers.is_empty() { let clone = Arc::clone(&counter); std::thread::spawn(move || backups_thread(clone)); } From a51ff3937d48f78ee48748f795bfc5539c0fddf3 Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Fri, 11 Aug 2023 23:24:14 +0200 Subject: [PATCH 66/93] refactor: reorder imports --- src/backup/delta.rs | 7 ++--- src/backup/manager/config.rs | 4 +-- src/backup/manager/meta.rs | 15 ++++++----- src/backup/manager/mod.rs | 22 +++++++-------- src/backup/mod.rs | 25 ++++++++--------- src/backup/path.rs | 13 +++++---- src/backup/state.rs | 14 ++++++---- src/cli/backup.rs | 9 ++++--- src/cli/mod.rs | 52 +++++++++++++++++------------------- src/cli/run/mod.rs | 15 ++++++----- src/signals.rs | 15 ++++++----- src/stdin.rs | 6 +++-- 12 files changed, 106 insertions(+), 91 deletions(-) diff --git a/src/backup/delta.rs b/src/backup/delta.rs index 21f626c..1260b43 100644 --- a/src/backup/delta.rs +++ b/src/backup/delta.rs @@ -1,7 +1,8 @@ -use super::State; +use std::{borrow::Borrow, fmt}; + use serde::{Deserialize, Serialize}; -use std::borrow::Borrow; -use std::fmt; + +use super::State; /// Represents the changes relative to the previous backup #[derive(Debug, Serialize, Deserialize, Clone)] diff --git a/src/backup/manager/config.rs b/src/backup/manager/config.rs index 75af34e..a07be56 100644 --- a/src/backup/manager/config.rs +++ b/src/backup/manager/config.rs @@ -1,6 +1,4 @@ -use std::error::Error; -use std::fmt; -use std::str::FromStr; +use std::{error::Error, fmt, str::FromStr}; use serde::{Deserialize, Serialize}; diff --git a/src/backup/manager/meta.rs b/src/backup/manager/meta.rs index 4831f3f..a649764 100644 --- a/src/backup/manager/meta.rs +++ b/src/backup/manager/meta.rs @@ -1,10 +1,13 @@ -use super::{Manager, ManagerConfig}; +use std::{ + collections::HashMap, + io, + path::{Path, PathBuf}, +}; + use chrono::Utc; -use serde::Deserialize; -use serde::Serialize; -use std::collections::HashMap; -use std::io; -use std::path::{Path, PathBuf}; +use serde::{Deserialize, Serialize}; + +use super::{Manager, ManagerConfig}; /// Manages a collection of backup layers, allowing them to be utilized as a single object. pub struct MetaManager diff --git a/src/backup/manager/mod.rs b/src/backup/manager/mod.rs index 7cbd43c..ec91a3d 100644 --- a/src/backup/manager/mod.rs +++ b/src/backup/manager/mod.rs @@ -1,20 +1,20 @@ mod config; mod meta; -pub use config::ManagerConfig; -pub use meta::MetaManager; +use std::{ + fs::{File, OpenOptions}, + io, + path::{Path, PathBuf}, +}; + +use chrono::{SubsecRound, Utc}; +use flate2::{write::GzEncoder, Compression}; +use serde::{Deserialize, Serialize}; use super::{Backup, BackupType, Delta, State}; use crate::other; -use chrono::SubsecRound; -use chrono::Utc; -use flate2::write::GzEncoder; -use flate2::Compression; -use serde::Deserialize; -use serde::Serialize; -use std::fs::{File, OpenOptions}; -use std::io; -use std::path::{Path, PathBuf}; +pub use config::ManagerConfig; +pub use meta::MetaManager; /// Manages a single backup layer consisting of one or more chains of backups. pub struct Manager diff --git a/src/backup/mod.rs b/src/backup/mod.rs index 6bd798f..c833b55 100644 --- a/src/backup/mod.rs +++ b/src/backup/mod.rs @@ -4,23 +4,24 @@ pub mod manager; mod path; mod state; +use std::{ + collections::HashSet, + fmt, + fs::File, + io, + path::{Path, PathBuf}, +}; + +use chrono::Utc; +use flate2::{read::GzDecoder, write::GzEncoder, Compression}; +use serde::{Deserialize, Serialize}; + use delta::Delta; pub use manager::Manager; pub use manager::ManagerConfig; pub use manager::MetaManager; -pub use state::State; - -use chrono::Utc; -use flate2::read::GzDecoder; -use flate2::write::GzEncoder; -use flate2::Compression; use path::PathExt; -use serde::{Deserialize, Serialize}; -use std::collections::HashSet; -use std::fmt; -use std::fs::File; -use std::io; -use std::path::{Path, PathBuf}; +pub use state::State; const BYTE_SUFFIXES: [&str; 5] = ["B", "KiB", "MiB", "GiB", "TiB"]; diff --git a/src/backup/path.rs b/src/backup/path.rs index 77c6883..d8c2cec 100644 --- a/src/backup/path.rs +++ b/src/backup/path.rs @@ -1,9 +1,12 @@ +use std::{ + collections::HashSet, + ffi::OsString, + fs::{self, DirEntry}, + io, + path::{Path, PathBuf}, +}; + use chrono::{Local, Utc}; -use std::collections::HashSet; -use std::ffi::OsString; -use std::fs::DirEntry; -use std::path::{Path, PathBuf}; -use std::{fs, io}; pub struct ReadDirRecursive { ignored: HashSet, diff --git a/src/backup/state.rs b/src/backup/state.rs index de4b2c0..1f81abb 100644 --- a/src/backup/state.rs +++ b/src/backup/state.rs @@ -1,9 +1,13 @@ -use crate::backup::Delta; +use std::{ + borrow::Borrow, + collections::{HashMap, HashSet}, + ops::{Deref, DerefMut}, + path::{Path, PathBuf}, +}; + use serde::{Deserialize, Serialize}; -use std::borrow::Borrow; -use std::collections::{HashMap, HashSet}; -use std::ops::{Deref, DerefMut}; -use std::path::{Path, PathBuf}; + +use crate::backup::Delta; /// Struct that represents a current state for a backup. This struct acts as a smart pointer around /// a HashMap. diff --git a/src/cli/backup.rs b/src/cli/backup.rs index 85c0cee..548facc 100644 --- a/src/cli/backup.rs +++ b/src/cli/backup.rs @@ -1,10 +1,11 @@ -use crate::backup::Backup; -use crate::other; -use chrono::{TimeZone, Utc}; -use clap::{Args, Subcommand}; use std::io; use std::path::{Path, PathBuf}; +use chrono::{TimeZone, Utc}; +use clap::{Args, Subcommand}; + +use crate::{backup::Backup, other}; + #[derive(Subcommand)] pub enum BackupCommands { /// List all tracked backups diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 7a76ea1..4eaf6f8 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -2,28 +2,33 @@ mod backup; mod config; mod run; -pub use crate::backup::MetaManager; -pub use crate::server::Metadata; -pub use backup::{BackupArgs, BackupCommands}; -pub use run::RunCli; +use std::path::PathBuf; -use crate::backup::ManagerConfig; -use crate::server::ServerType; use clap::{Args, Parser, Subcommand}; -pub use config::Config; use figment::{ providers::{Env, Format, Serialized, Toml}, Figment, }; use serde::{Deserialize, Serialize}; -use std::path::PathBuf; -#[derive(Subcommand)] -pub enum Commands { - /// Run the server - Run(RunCli), - /// Interact with the backup system without starting a server - Backup(BackupArgs), +use crate::{backup::ManagerConfig, server::ServerType}; +use backup::BackupArgs; +use config::Config; +use run::RunCli; + +#[derive(Parser, Serialize)] +#[command(author, version, about, long_about = None)] +pub struct Cli { + #[command(subcommand)] + #[serde(skip)] + pub command: Commands, + + /// Path to a TOML configuration file + #[arg(long = "config-file", global = true)] + pub config_file: Option, + + #[command(flatten)] + pub args: CliArgs, } #[derive(Args, Serialize, Deserialize, Clone)] @@ -60,19 +65,12 @@ pub struct CliArgs { pub server_version: Option, } -#[derive(Parser, Serialize)] -#[command(author, version, about, long_about = None)] -pub struct Cli { - #[command(subcommand)] - #[serde(skip)] - pub command: Commands, - - /// Path to a TOML configuration file - #[arg(long = "config-file", global = true)] - pub config_file: Option, - - #[command(flatten)] - pub args: CliArgs, +#[derive(Subcommand)] +pub enum Commands { + /// Run the server + Run(RunCli), + /// Interact with the backup system without starting a server + Backup(BackupArgs), } impl Cli { diff --git a/src/cli/run/mod.rs b/src/cli/run/mod.rs index fb48491..21c6a0a 100644 --- a/src/cli/run/mod.rs +++ b/src/cli/run/mod.rs @@ -1,15 +1,16 @@ mod config; -use crate::server; -use crate::signals; -use crate::stdin; -use clap::Args; -use config::Config; -use std::path::PathBuf; -use std::sync::{Arc, Mutex}; +use std::{ + path::PathBuf, + sync::{Arc, Mutex}, +}; +use clap::Args; use serde::{Deserialize, Serialize}; +use crate::{server, signals, stdin}; +use config::Config; + #[derive(Args)] pub struct RunCli { #[command(flatten)] diff --git a/src/signals.rs b/src/signals.rs index 61c38f9..5462a75 100644 --- a/src/signals.rs +++ b/src/signals.rs @@ -1,10 +1,13 @@ -use std::io; -use std::sync::atomic::AtomicBool; -use std::sync::{Arc, Mutex}; +use std::{ + io, + sync::{atomic::AtomicBool, Arc, Mutex}, +}; -use signal_hook::consts::TERM_SIGNALS; -use signal_hook::flag; -use signal_hook::iterator::{Signals, SignalsInfo}; +use signal_hook::{ + consts::TERM_SIGNALS, + flag, + iterator::{Signals, SignalsInfo}, +}; use crate::server; diff --git a/src/stdin.rs b/src/stdin.rs index f9a22e8..4a7f761 100644 --- a/src/stdin.rs +++ b/src/stdin.rs @@ -1,5 +1,7 @@ -use std::io; -use std::sync::{Arc, Mutex}; +use std::{ + io, + sync::{Arc, Mutex}, +}; use crate::server; From b3d1cec078557a1f8f47517d3121471e55577fa9 Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Sat, 12 Aug 2023 11:44:35 +0200 Subject: [PATCH 67/93] feat: granular locking for proper concurrent access to server process --- CHANGELOG.md | 1 + src/cli/run/mod.rs | 25 +++++++++++-------------- src/server/process.rs | 40 ++++++++++++++++++++++------------------ src/signals.rs | 10 ++++------ src/stdin.rs | 17 +++++------------ 5 files changed, 43 insertions(+), 50 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 553fd69..e2e02ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Export command no longer reads backups that do not contribute to the final state +* Running backups no longer block stdin input or shutdown ## [0.3.1](https://git.rustybever.be/Chewing_Bever/alex/src/tag/0.3.1) diff --git a/src/cli/run/mod.rs b/src/cli/run/mod.rs index 21c6a0a..4a11692 100644 --- a/src/cli/run/mod.rs +++ b/src/cli/run/mod.rs @@ -1,9 +1,6 @@ mod config; -use std::{ - path::PathBuf, - sync::{Arc, Mutex}, -}; +use std::{path::PathBuf, sync::Arc}; use clap::Args; use serde::{Deserialize, Serialize}; @@ -45,11 +42,15 @@ pub struct RunArgs { pub xmx: Option, } -fn backups_thread(counter: Arc>) { +fn backups_thread(server: Arc) { loop { let next_scheduled_time = { - let server = counter.lock().unwrap(); - server.backups.next_scheduled_time().unwrap() + server + .backups + .read() + .unwrap() + .next_scheduled_time() + .unwrap() }; let now = chrono::offset::Utc::now(); @@ -57,12 +58,8 @@ fn backups_thread(counter: Arc>) { std::thread::sleep((next_scheduled_time - now).to_std().unwrap()); } - { - let mut server = counter.lock().unwrap(); - - // We explicitely ignore the error here, as we don't want the thread to fail - let _ = server.backup(); - } + // We explicitely ignore the error here, as we don't want the thread to fail + let _ = server.backup(); } } @@ -90,7 +87,7 @@ impl RunCli { return Ok(()); } - let counter = Arc::new(Mutex::new(cmd.spawn()?)); + let counter = Arc::new(cmd.spawn()?); if !global.layers.is_empty() { let clone = Arc::clone(&counter); diff --git a/src/server/process.rs b/src/server/process.rs index 7748c1d..e7ff3d5 100644 --- a/src/server/process.rs +++ b/src/server/process.rs @@ -1,22 +1,21 @@ -use crate::backup::MetaManager; -use crate::server::Metadata; -use std::io::Write; -use std::process::Child; +use std::{io::Write, process::Child, sync::RwLock}; + +use crate::{backup::MetaManager, server::Metadata}; pub struct ServerProcess { - child: Child, - pub backups: MetaManager, + child: RwLock, + pub backups: RwLock>, } impl ServerProcess { pub fn new(manager: MetaManager, child: Child) -> ServerProcess { ServerProcess { - child, - backups: manager, + child: RwLock::new(child), + backups: RwLock::new(manager), } } - pub fn send_command(&mut self, cmd: &str) -> std::io::Result<()> { + pub fn send_command(&self, cmd: &str) -> std::io::Result<()> { match cmd.trim() { "stop" | "exit" => self.stop()?, "backup" => self.backup()?, @@ -26,29 +25,34 @@ impl ServerProcess { Ok(()) } - fn custom(&mut self, cmd: &str) -> std::io::Result<()> { - let mut stdin = self.child.stdin.as_ref().unwrap(); + fn custom(&self, cmd: &str) -> std::io::Result<()> { + let child = self.child.write().unwrap(); + let mut stdin = child.stdin.as_ref().unwrap(); stdin.write_all(format!("{}\n", cmd.trim()).as_bytes())?; stdin.flush()?; Ok(()) } - pub fn stop(&mut self) -> std::io::Result<()> { + pub fn stop(&self) -> std::io::Result<()> { self.custom("stop")?; - self.child.wait()?; + + self.child.write().unwrap().wait()?; Ok(()) } - pub fn kill(&mut self) -> std::io::Result<()> { - self.child.kill() + pub fn kill(&self) -> std::io::Result<()> { + self.child.write().unwrap().kill() } /// Perform a backup by disabling the server's save feature and flushing its data, before /// creating an archive file. - pub fn backup(&mut self) -> std::io::Result<()> { - let layer_name = String::from(self.backups.next_scheduled_layer().unwrap()); + pub fn backup(&self) -> std::io::Result<()> { + // We explicitely lock this entire function to prevent parallel backups + let mut backups = self.backups.write().unwrap(); + + let layer_name = String::from(backups.next_scheduled_layer().unwrap()); self.custom(&format!("say starting backup for layer '{}'", layer_name))?; // Make sure the server isn't modifying the files during the backup @@ -60,7 +64,7 @@ impl ServerProcess { std::thread::sleep(std::time::Duration::from_secs(10)); let start_time = chrono::offset::Utc::now(); - let res = self.backups.perform_backup_cycle(); + let res = backups.perform_backup_cycle(); // The server's save feature needs to be enabled again even if the archive failed to create self.custom("save-on")?; diff --git a/src/signals.rs b/src/signals.rs index 5462a75..87e5c73 100644 --- a/src/signals.rs +++ b/src/signals.rs @@ -1,6 +1,6 @@ use std::{ io, - sync::{atomic::AtomicBool, Arc, Mutex}, + sync::{atomic::AtomicBool, Arc}, }; use signal_hook::{ @@ -37,7 +37,7 @@ pub fn install_signal_handlers() -> io::Result<(Arc, SignalsInfo)> { /// Loop that handles terminating signals as they come in. pub fn handle_signals( signals: &mut SignalsInfo, - counter: Arc>, + server: Arc, ) -> io::Result<()> { let mut force = false; @@ -49,17 +49,15 @@ pub fn handle_signals( // This will currently not work, as the initial stop command will block the kill from // happening. if force { - let mut server = counter.lock().unwrap(); return server.kill(); } // The stop command runs in a separate thread to avoid blocking the signal handling loop. // After stopping the server, the thread terminates the process. else { - let clone = Arc::clone(&counter); + let clone = Arc::clone(&server); std::thread::spawn(move || { - let mut server = clone.lock().unwrap(); - let _ = server.stop(); + let _ = clone.stop(); std::process::exit(0); }); } diff --git a/src/stdin.rs b/src/stdin.rs index 4a7f761..8317671 100644 --- a/src/stdin.rs +++ b/src/stdin.rs @@ -1,11 +1,8 @@ -use std::{ - io, - sync::{Arc, Mutex}, -}; +use std::{io, sync::Arc}; use crate::server; -pub fn handle_stdin(counter: Arc>) { +pub fn handle_stdin(server: Arc) { let stdin = io::stdin(); let input = &mut String::new(); @@ -16,13 +13,9 @@ pub fn handle_stdin(counter: Arc>) { continue; }; - { - let mut server = counter.lock().unwrap(); - - if let Err(e) = server.send_command(input) { - println!("{}", e); - }; - } + if let Err(e) = server.send_command(input) { + println!("{}", e); + }; if input.trim() == "stop" { std::process::exit(0); From 5f6366078cc0e608f7fd16a33d95e7bc3ec09863 Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Sat, 12 Aug 2023 13:46:46 +0200 Subject: [PATCH 68/93] fix: properly parse layers env var --- src/cli/mod.rs | 29 +++++++++++++++++++++++------ src/cli/run/mod.rs | 1 - src/server/mod.rs | 1 + 3 files changed, 24 insertions(+), 7 deletions(-) diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 4eaf6f8..0bf55f9 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -2,7 +2,7 @@ mod backup; mod config; mod run; -use std::path::PathBuf; +use std::{path::PathBuf, str::FromStr}; use clap::{Args, Parser, Subcommand}; use figment::{ @@ -83,7 +83,7 @@ impl Cli { } } - pub fn config(&self, args: &U) -> Result + pub fn config(&self, args: &U) -> crate::Result where T: Default + Serialize + for<'de> Deserialize<'de>, U: Serialize, @@ -92,11 +92,28 @@ impl Cli { .config_file .clone() .unwrap_or(PathBuf::from(Env::var_or("ALEX_CONFIG_FILE", ""))); - Figment::new() + + let mut figment = Figment::new() .merge(Serialized::defaults(T::default())) .merge(Toml::file(toml_file)) - .merge(Env::prefixed("ALEX_")) - .merge(Serialized::defaults(args)) - .extract() + .merge(Env::prefixed("ALEX_").ignore(&["ALEX_LAYERS"])); + + // Layers need to be parsed separately, as the env var format is different than the one + // serde expects + if let Some(layers_env) = Env::var("ALEX_LAYERS") { + let res = layers_env + .split(';') + .map(ManagerConfig::from_str) + .collect::>(); + + if res.iter().any(|e| e.is_err()) { + return Err(crate::other("Invalid layer configuration").into()); + } + + let layers: Vec<_> = res.iter().flatten().collect(); + figment = figment.merge(Serialized::default("layers", layers)); + } + + Ok(figment.merge(Serialized::defaults(args)).extract()?) } } diff --git a/src/cli/run/mod.rs b/src/cli/run/mod.rs index 4a11692..37ddd1b 100644 --- a/src/cli/run/mod.rs +++ b/src/cli/run/mod.rs @@ -66,7 +66,6 @@ fn backups_thread(server: Arc) { impl RunCli { pub fn run(&self, cli: &super::Cli, global: &super::Config) -> crate::Result<()> { let config: Config = cli.config(&self.args)?; - println!("{:?}", config); let (_, mut signals) = signals::install_signal_handlers()?; diff --git a/src/server/mod.rs b/src/server/mod.rs index 23a9ff5..2939af7 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -9,6 +9,7 @@ use serde::{Deserialize, Serialize}; use std::fmt; #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Serialize, Deserialize, Debug)] +#[serde(rename_all = "lowercase")] pub enum ServerType { Unknown, Paper, From 8f190c489b079f1495fc3acb2ff7b8d3b364b507 Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Sat, 12 Aug 2023 13:58:13 +0200 Subject: [PATCH 69/93] chore: update changelog --- CHANGELOG.md | 2 ++ Dockerfile | 6 +++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e2e02ad..652c541 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Export command no longer reads backups that do not contribute to the final state * Running backups no longer block stdin input or shutdown +* Env vars `ALEX_CONFIG_DIR`, `ALEX_WORLD_DIR` and `ALEX_BACKUP_DIR` renamed to + `ALEX_CONFIG`, `ALEX_WORLD` and `ALEX_BACKUP` respectively ## [0.3.1](https://git.rustybever.be/Chewing_Bever/alex/src/tag/0.3.1) diff --git a/Dockerfile b/Dockerfile index 7c63117..819cbf2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -47,9 +47,9 @@ COPY --from=builder /app/target/debug/alex /bin/alex RUN chmod +x /bin/alex # Default value to keep users from eating up all ram accidentally -ENV ALEX_CONFIG_DIR=/app/config \ - ALEX_WORLD_DIR=/app/worlds \ - ALEX_BACKUP_DIR=/app/backups \ +ENV ALEX_CONFIG=/app/config \ + ALEX_WORLD=/app/worlds \ + ALEX_BACKUP=/app/backups \ ALEX_SERVER=paper \ ALEX_XMS=1024 \ ALEX_XMX=2048 \ From 5bdd4e21b0e9d767045bc24d9875786a7fba2478 Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Sat, 12 Aug 2023 14:09:22 +0200 Subject: [PATCH 70/93] chore(ci): modernize config --- .woodpecker/arch-release.yml | 14 +++++++------- .woodpecker/build.yml | 15 ++++++++------- .woodpecker/clippy.yml | 15 ++++++++------- .woodpecker/lint.yml | 15 ++++++++------- .woodpecker/release.yml | 15 +++++++-------- 5 files changed, 38 insertions(+), 36 deletions(-) diff --git a/.woodpecker/arch-release.yml b/.woodpecker/arch-release.yml index ece2c52..1bd073f 100644 --- a/.woodpecker/arch-release.yml +++ b/.woodpecker/arch-release.yml @@ -3,11 +3,15 @@ matrix: - 'linux/amd64' # - linux/arm64 -platform: ${PLATFORM} -branches: [ main ] +labels: + platform: ${PLATFORM} + +when: + event: tag + skip_clone: true -pipeline: +steps: build: image: 'menci/archlinuxarm:base-devel' pull: true @@ -23,8 +27,6 @@ pipeline: - su builder - curl -OL "https://git.rustybever.be/Chewing_Bever/alex/raw/tag/$CI_COMMIT_TAG/PKGBUILD" - makepkg -s --noconfirm --needed - when: - event: tag publish: image: 'curlimages/curl' @@ -33,5 +35,3 @@ pipeline: - 'for pkg in $(ls -1 *.pkg*); do curl -XPOST -T "$pkg" -H "X-API-KEY: $VIETER_API_KEY" https://arch.r8r.be/bur/publish; done' secrets: - vieter_api_key - when: - event: tag diff --git a/.woodpecker/build.yml b/.woodpecker/build.yml index a1722be..768e2c8 100644 --- a/.woodpecker/build.yml +++ b/.woodpecker/build.yml @@ -3,19 +3,20 @@ matrix: - 'amd64' - 'arm64' -platform: "linux/${ARCH}" +labels: + platform: "linux/${ARCH}" -branches: - exclude: [main] +when: + branch: + exclude: [main] + event: push -pipeline: +steps: build: - image: 'rust:1.70-alpine3.18' + image: 'rust:1.71-alpine3.18' commands: - apk add --no-cache build-base - cargo build --verbose - cargo test --verbose # Binaries, even debug ones, should be statically compiled - '[ "$(readelf -d target/debug/alex | grep NEEDED | wc -l)" = 0 ]' - when: - event: [push] diff --git a/.woodpecker/clippy.yml b/.woodpecker/clippy.yml index 61b150d..5f23363 100644 --- a/.woodpecker/clippy.yml +++ b/.woodpecker/clippy.yml @@ -1,13 +1,14 @@ -platform: 'linux/amd64' +labels: + platform: 'linux/amd64' -branches: - exclude: [main] +when: + branch: + exclude: [ main ] + event: push -pipeline: +steps: clippy: - image: 'rust:1.70' + image: 'rust:1.71' commands: - rustup component add clippy - cargo clippy -- --no-deps -Dwarnings - when: - event: [push] diff --git a/.woodpecker/lint.yml b/.woodpecker/lint.yml index b74d26d..15e45dc 100644 --- a/.woodpecker/lint.yml +++ b/.woodpecker/lint.yml @@ -1,13 +1,14 @@ -platform: 'linux/amd64' +labels: + platform: 'linux/amd64' -branches: - exclude: [main] +when: + branch: + exclude: [ main ] + event: push -pipeline: +steps: lint: - image: 'rust:1.70' + image: 'rust:1.71' commands: - rustup component add rustfmt - cargo fmt -- --check - when: - event: [push] diff --git a/.woodpecker/release.yml b/.woodpecker/release.yml index f7d44db..892dced 100644 --- a/.woodpecker/release.yml +++ b/.woodpecker/release.yml @@ -3,20 +3,21 @@ matrix: - 'linux/amd64' - 'linux/arm64' -platform: ${PLATFORM} -branches: [ main ] +labels: + platform: ${PLATFORM} -pipeline: +when: + event: tag + +steps: build: - image: 'rust:1.70-alpine3.18' + image: 'rust:1.71-alpine3.18' commands: - apk add --no-cache build-base - cargo build --release --verbose # Ensure the release binary is also statically compiled - '[ "$(readelf -d target/release/alex | grep NEEDED | wc -l)" = 0 ]' - du -h target/release/alex - when: - event: tag publish: image: 'curlimages/curl' @@ -28,5 +29,3 @@ pipeline: --user "Chewing_Bever:$GITEA_PASSWORD" --upload-file target/release/alex https://git.rustybever.be/api/packages/Chewing_Bever/generic/alex/"${CI_COMMIT_TAG}"/alex-"$(echo '${PLATFORM}' | sed 's:/:-:g')" - when: - event: tag From f2e781dd5a7868fd7181e8d5d41427827ad7d26a Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Sat, 12 Aug 2023 14:50:06 +0200 Subject: [PATCH 71/93] chore: bump dependency versions --- Cargo.lock | 187 ++++++++++++++++++++++++++--------------------------- 1 file changed, 91 insertions(+), 96 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1854dec..e03b1d5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -54,15 +54,15 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41ed9a86bf92ae6580e0a31281f65a1b1d867c0cc68d5346e2ae128dddfa6a7d" +checksum = "3a30da5c5f2d5e72842e00bcb57657162cdabef0931f40e2deb9b4140440cecd" [[package]] name = "anstyle-parse" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e765fd216e48e067936442276d1d57399e37bce53c264d6fefbe298080cb57ee" +checksum = "938874ff5980b03a87c5524b3ae5b59cf99b1d6bc836848df7bc5ada9643c333" dependencies = [ "utf8parse", ] @@ -78,9 +78,9 @@ dependencies = [ [[package]] name = "anstyle-wincon" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "180abfa45703aebe0093f79badacc01b8fd4ea2e35118747e5811127f926e188" +checksum = "c677ab05e09154296dd37acecd46420c17b9713e8366facafa8fc0885167cf4c" dependencies = [ "anstyle", "windows-sys", @@ -104,6 +104,12 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitflags" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" + [[package]] name = "bumpalo" version = "3.13.0" @@ -112,9 +118,12 @@ checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" [[package]] name = "cc" -version = "1.0.79" +version = "1.0.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" +checksum = "305fe645edc1442a0fa8b6726ba61d422798d37a52e12eaecf4b022ebbb88f01" +dependencies = [ + "libc", +] [[package]] name = "cfg-if" @@ -140,9 +149,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.3.1" +version = "4.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ed2379f8603fa2b7509891660e802b88c70a79a6427a70abb5968054de2c28" +checksum = "c27cdf28c0f604ba3f512b0c9a409f8de8513e4816705deb0498b627e7c3a3fd" dependencies = [ "clap_builder", "clap_derive", @@ -151,22 +160,21 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.3.1" +version = "4.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72394f3339a76daf211e57d4bcb374410f3965dcc606dd0e03738c7888766980" +checksum = "08a9f1ab5e9f01a9b81f202e8562eb9a10de70abf9eaeac1be465c28b75aa4aa" dependencies = [ "anstream", "anstyle", - "bitflags", "clap_lex", "strsim", ] [[package]] name = "clap_derive" -version = "4.3.1" +version = "4.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59e9ef9a08ee1c0e1f2e162121665ac45ac3783b0f897db7244ae75ad9a8f65b" +checksum = "54a9bb5758fc5dfe728d1019941681eccaf0cf8a4189b692a0ee2f2ecf90a050" dependencies = [ "heck", "proc-macro2", @@ -209,9 +217,9 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" +checksum = "6b30f669a7961ef1631673d2766cc92f52d64f7ef354d4fe0ddfd30ed52f0f4f" dependencies = [ "errno-dragonfly", "libc", @@ -244,9 +252,9 @@ dependencies = [ [[package]] name = "filetime" -version = "0.2.21" +version = "0.2.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cbc844cecaee9d4443931972e1289c8ff485cb4cc2767cb03ca139ed6885153" +checksum = "d4029edd3e734da6fe05b6cd7bd2960760a616bd2ddd0d59a0124746d6272af0" dependencies = [ "cfg-if", "libc", @@ -278,15 +286,15 @@ checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" [[package]] name = "hermit-abi" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" +checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" [[package]] name = "iana-time-zone" -version = "0.1.56" +version = "0.1.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0722cd7114b7de04316e7ea5456a0bbb20e4adb46fd27a3697adb812cff0f37c" +checksum = "2fad5b825842d2b38bd206f3e81d6957625fd7f0a361e345c30e01a0ae2dd613" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -321,61 +329,49 @@ version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8fae54786f62fb2918dcfae3d568594e50eb9b5c25bf04371af6fe7516452fb" -[[package]] -name = "io-lifetimes" -version = "1.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" -dependencies = [ - "hermit-abi", - "libc", - "windows-sys", -] - [[package]] name = "is-terminal" -version = "0.4.7" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adcf93614601c8129ddf72e2d5633df827ba6551541c6d8c59520a371475be1f" +checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" dependencies = [ "hermit-abi", - "io-lifetimes", "rustix", "windows-sys", ] [[package]] name = "itoa" -version = "1.0.6" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" +checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" [[package]] name = "js-sys" -version = "0.3.63" +version = "0.3.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f37a4a5928311ac501dee68b3c7613a1037d0edb30c8e5427bd832d55d1b790" +checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" dependencies = [ "wasm-bindgen", ] [[package]] name = "libc" -version = "0.2.144" +version = "0.2.147" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b00cc1c228a6782d0f076e7b232802e0c5689d41bb5df366f2a6b6621cfdfe1" +checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" [[package]] name = "linux-raw-sys" -version = "0.3.8" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" +checksum = "57bcfdad1b858c2db7c38303a6d2ad4dfaf5eb53dfeb0910128b2c26d6158503" [[package]] name = "log" -version = "0.4.18" +version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "518ef76f2f87365916b142844c16d8fefd85039bc5699050210a7778ee1cd1de" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" [[package]] name = "memchr" @@ -394,18 +390,18 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" dependencies = [ "autocfg", ] [[package]] name = "once_cell" -version = "1.17.2" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9670a07f94779e00908f3e686eab508878ebb390ba6e604d3a284c00e8d0487b" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" [[package]] name = "pear" @@ -432,9 +428,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.59" +version = "1.0.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6aeca18b86b413c660b781aa319e4e2648a3e6f9eadc9b47e9038e6fe9f3451b" +checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" dependencies = [ "unicode-ident", ] @@ -454,31 +450,30 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.28" +version = "1.0.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9ab9c7eadfd8df19006f1cf1a4aed13540ed5cbc047010ece5826e10825488" +checksum = "50f3b39ccfb720540debaa0164757101c08ecb8d326b15358ce76a62c7e85965" dependencies = [ "proc-macro2", ] [[package]] name = "redox_syscall" -version = "0.2.16" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" dependencies = [ - "bitflags", + "bitflags 1.3.2", ] [[package]] name = "rustix" -version = "0.37.19" +version = "0.38.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acf8729d8542766f1b2cf77eb034d52f40d375bb8b615d0b147089946e16613d" +checksum = "19ed4fa021d81c8392ce04db050a3da9a60299050b7ae1cf482d862b54a7218f" dependencies = [ - "bitflags", + "bitflags 2.4.0", "errno", - "io-lifetimes", "libc", "linux-raw-sys", "windows-sys", @@ -486,24 +481,24 @@ dependencies = [ [[package]] name = "ryu" -version = "1.0.13" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" +checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" [[package]] name = "serde" -version = "1.0.164" +version = "1.0.183" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e8c8cf938e98f769bc164923b06dce91cea1751522f46f8466461af04c9027d" +checksum = "32ac8da02677876d532745a130fc9d8e6edfa81a269b107c5b00829b91d8eb3c" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.164" +version = "1.0.183" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9735b638ccc51c28bf6914d90a2e9725b377144fc612c49a611fddd1b631d68" +checksum = "aafe972d60b0b9bee71a91b92fee2d4fb3c9d7e8f6b179aa99f27203d99a4816" dependencies = [ "proc-macro2", "quote", @@ -512,9 +507,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.96" +version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1" +checksum = "076066c5f1078eac5b722a31827a8832fe108bed65dfa75e233c89f8206e976c" dependencies = [ "itoa", "ryu", @@ -532,9 +527,9 @@ dependencies = [ [[package]] name = "signal-hook" -version = "0.3.15" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "732768f1176d21d09e076c23a93123d40bba92d50c4058da34d45c8de8e682b9" +checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" dependencies = [ "libc", "signal-hook-registry", @@ -557,9 +552,9 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "syn" -version = "2.0.18" +version = "2.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32d41677bcbe24c20c52e7c70b0d8db04134c5d1066bf98662e2871ad200ea3e" +checksum = "04361975b3f5e348b2189d8dc55bc942f278b2d482a6a0365de5bdd62d351567" dependencies = [ "proc-macro2", "quote", @@ -568,9 +563,9 @@ dependencies = [ [[package]] name = "tar" -version = "0.4.38" +version = "0.4.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b55807c0344e1e6c04d7c965f5289c39a8d94ae23ed5c0b57aabac549f871c6" +checksum = "b16afcea1f22891c49a00c751c7b63b2233284064f11a200fc624137c51e2ddb" dependencies = [ "filetime", "libc", @@ -633,9 +628,9 @@ dependencies = [ [[package]] name = "unicode-ident" -version = "1.0.9" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15811caf2415fb889178633e7724bad2509101cde276048e013b9def5e51fa0" +checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" [[package]] name = "utf8parse" @@ -657,9 +652,9 @@ checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" [[package]] name = "wasm-bindgen" -version = "0.2.86" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bba0e8cb82ba49ff4e229459ff22a191bbe9a1cb3a341610c9c33efc27ddf73" +checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -667,9 +662,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.86" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19b04bc93f9d6bdee709f6bd2118f57dd6679cf1176a1af464fca3ab0d66d8fb" +checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" dependencies = [ "bumpalo", "log", @@ -682,9 +677,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.86" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14d6b024f1a526bb0234f52840389927257beb670610081360e5a03c5df9c258" +checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -692,9 +687,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.86" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e128beba882dd1eb6200e1dc92ae6c5dbaa4311aa7bb211ca035779e5efc39f8" +checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ "proc-macro2", "quote", @@ -705,9 +700,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.86" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed9d5b4305409d1fc9482fee2d7f9bcbf24b3972bf59817ef757e23982242a93" +checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" [[package]] name = "winapi" @@ -751,9 +746,9 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.48.0" +version = "0.48.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" +checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f" dependencies = [ "windows_aarch64_gnullvm", "windows_aarch64_msvc", @@ -808,24 +803,24 @@ checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" [[package]] name = "winnow" -version = "0.5.7" +version = "0.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19f495880723d0999eb3500a9064d8dbcf836460b24c17df80ea7b5794053aac" +checksum = "5504cc7644f4b593cbc05c4a55bf9bd4e94b867c3c0bd440934174d50482427d" dependencies = [ "memchr", ] [[package]] name = "xattr" -version = "0.2.3" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d1526bbe5aaeb5eb06885f4d987bcdfa5e23187055de9b83fe00156a821fabc" +checksum = "f4686009f71ff3e5c4dbcf1a282d0a44db3f021ba69350cd42086b3e5f1c6985" dependencies = [ "libc", ] [[package]] name = "yansi" -version = "1.0.0-rc" +version = "1.0.0-rc.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ee746ad3851dd3bc40e4a028ab3b00b99278d929e48957bcb2d111874a7e43e" +checksum = "1367295b8f788d371ce2dbc842c7b709c73ee1364d30351dd300ec2203b12377" From 9ce2417528aec8b20942ca908a48d028ebcbd511 Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Sat, 12 Aug 2023 15:06:58 +0200 Subject: [PATCH 72/93] chore: bump versions to 0.4.0 --- CHANGELOG.md | 2 ++ Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 652c541..44603b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://git.rustybever.be/Chewing_Bever/alex/src/branch/dev) +## [0.4.0](https://git.rustybever.be/Chewing_Bever/alex/src/tag/0.4.0) + ### Added * Extract command for working with the output of export diff --git a/Cargo.toml b/Cargo.toml index 09a8b1b..f37c2ca 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "alex" -version = "0.3.1" +version = "0.4.0" description = "Wrapper around Minecraft server processes, designed to complement Docker image installations." authors = ["Jef Roosens"] edition = "2021" From 3cddea19c3a5daaa249629f9e3dbefd1808fe6d5 Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Sat, 12 Aug 2023 15:47:05 +0200 Subject: [PATCH 73/93] chore: revert to old platform syntax --- .woodpecker/build.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.woodpecker/build.yml b/.woodpecker/build.yml index 768e2c8..357ab76 100644 --- a/.woodpecker/build.yml +++ b/.woodpecker/build.yml @@ -3,8 +3,7 @@ matrix: - 'amd64' - 'arm64' -labels: - platform: "linux/${ARCH}" +platform: "linux/${ARCH}" when: branch: From d3cb29b52eaef2777b617246dd41a57f9041246c Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Sun, 13 Aug 2023 10:17:21 +0200 Subject: [PATCH 74/93] chore: move PKGBUILD to separate repo --- CHANGELOG.md | 7 +++++++ Cargo.lock | 2 +- Cargo.toml | 2 +- PKGBUILD | 42 ------------------------------------------ README.md | 14 ++++++++++++++ 5 files changed, 23 insertions(+), 44 deletions(-) delete mode 100644 PKGBUILD diff --git a/CHANGELOG.md b/CHANGELOG.md index 44603b6..5305b42 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://git.rustybever.be/Chewing_Bever/alex/src/branch/dev) +## [0.4.1](https://git.rustybever.be/Chewing_Bever/alex/src/tag/0.4.1) + +### Changed + +* Moved PKGBUILD to separate repo +* Properly update lock file + ## [0.4.0](https://git.rustybever.be/Chewing_Bever/alex/src/tag/0.4.0) ### Added diff --git a/Cargo.lock b/Cargo.lock index e03b1d5..788d506 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10,7 +10,7 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "alex" -version = "0.3.1" +version = "0.4.1" dependencies = [ "chrono", "clap", diff --git a/Cargo.toml b/Cargo.toml index f37c2ca..9e625f4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "alex" -version = "0.4.0" +version = "0.4.1" description = "Wrapper around Minecraft server processes, designed to complement Docker image installations." authors = ["Jef Roosens"] edition = "2021" diff --git a/PKGBUILD b/PKGBUILD deleted file mode 100644 index 6bb6b6c..0000000 --- a/PKGBUILD +++ /dev/null @@ -1,42 +0,0 @@ -# Maintainer: Jef Roosens - -pkgname='alex' -pkgver=0.3.1 -pkgdesc='Wrapper around Minecraft server processes, designed to complement Docker image installations.' -pkgrel=1 -arch=('x86_64' 'aarch64') - -url='https://git.rustybever.be/Chewing_Bever/alex' -license=('MIT') - -makedepends=('cargo') -depends=('glibc' 'gcc-libs' ) - -source=("${pkgname}-${pkgver}.tar.gz::https://git.rustybever.be/Chewing_Bever/alex/archive/${pkgver}.tar.gz") -sha512sums=('f88903bd99397f3e9a1c4a40febc65eace0d594cde8de20d54ed1cd0597512152111e7a20acaaa971309d5afe1ea267c7ef3b08f0d803237e8004808a83752c5') - -prepare() { - cd "${pkgname}" - - export RUSTUP_TOOLCHAIN=stable - cargo fetch --locked --target "$CARCH-unknown-linux-gnu" -} - -build() { - cd "${pkgname}" - - export RUSTUP_TOOLCHAIN=stable - export CARGO_TARGET_DIR=target - cargo build --frozen --release --all-features -} - -check() { - cd "${pkgname}" - - export RUSTUP_TOOLCHAIN=stable - cargo test --frozen --all-features -} - -package() { - install -Dm0755 -t "${pkgdir}/usr/bin/" "${pkgname}/target/release/${pkgname}" -} diff --git a/README.md b/README.md index dc96623..979bd1e 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,20 @@ Alex is distributed as statically compiled binaries for Linux amd64 and arm64. These can be found [here](https://git.rustybever.be/Chewing_Bever/alex/packages). +### Arch + +Arch users can install prebuilt `x86_64` & `aarch64` packages from my `bur` +repository. Add the following at the bottom of your `pacman.conf`: + +```toml +[bur] +Server = https://arch.r8r.be/$repo/$arch +SigLevel = Optional +``` + +If you prefer building the package yourself, the PKGBUILD can be found +[here](https://git.rustybever.be/bur/alex-mc). + ### Dockerfiles You can easily install alex in your Docker images by letting Docker download it From d23227dd0bf067ab2f0c30bb4cec378fdca18497 Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Sun, 13 Aug 2023 10:18:53 +0200 Subject: [PATCH 75/93] fix(ci): use old platform --- .woodpecker/arch-release.yml | 37 ------------------------------------ .woodpecker/clippy.yml | 3 +-- .woodpecker/lint.yml | 3 +-- .woodpecker/release.yml | 3 +-- 4 files changed, 3 insertions(+), 43 deletions(-) delete mode 100644 .woodpecker/arch-release.yml diff --git a/.woodpecker/arch-release.yml b/.woodpecker/arch-release.yml deleted file mode 100644 index 1bd073f..0000000 --- a/.woodpecker/arch-release.yml +++ /dev/null @@ -1,37 +0,0 @@ -matrix: - PLATFORM: - - 'linux/amd64' - # - linux/arm64 - -labels: - platform: ${PLATFORM} - -when: - event: tag - -skip_clone: true - -steps: - build: - image: 'menci/archlinuxarm:base-devel' - pull: true - commands: - - echo -e '[bur]\nServer = https://arch.r8r.be/$repo/$arch\nSigLevel = Optional' >> /etc/pacman.conf - # Update packages - - pacman -Syu --noconfirm - # Create non-root user to perform build & switch to their home - - groupadd -g 1000 builder - - useradd -mg builder builder - - chown -R builder:builder "$PWD" - - "echo 'builder ALL=(ALL) NOPASSWD: ALL' >> /etc/sudoers" - - su builder - - curl -OL "https://git.rustybever.be/Chewing_Bever/alex/raw/tag/$CI_COMMIT_TAG/PKGBUILD" - - makepkg -s --noconfirm --needed - - publish: - image: 'curlimages/curl' - commands: - # Publish the package - - 'for pkg in $(ls -1 *.pkg*); do curl -XPOST -T "$pkg" -H "X-API-KEY: $VIETER_API_KEY" https://arch.r8r.be/bur/publish; done' - secrets: - - vieter_api_key diff --git a/.woodpecker/clippy.yml b/.woodpecker/clippy.yml index 5f23363..7bdc59c 100644 --- a/.woodpecker/clippy.yml +++ b/.woodpecker/clippy.yml @@ -1,5 +1,4 @@ -labels: - platform: 'linux/amd64' +platform: 'linux/amd64' when: branch: diff --git a/.woodpecker/lint.yml b/.woodpecker/lint.yml index 15e45dc..be6f331 100644 --- a/.woodpecker/lint.yml +++ b/.woodpecker/lint.yml @@ -1,5 +1,4 @@ -labels: - platform: 'linux/amd64' +platform: 'linux/amd64' when: branch: diff --git a/.woodpecker/release.yml b/.woodpecker/release.yml index 892dced..32524de 100644 --- a/.woodpecker/release.yml +++ b/.woodpecker/release.yml @@ -3,8 +3,7 @@ matrix: - 'linux/amd64' - 'linux/arm64' -labels: - platform: ${PLATFORM} +platform: ${PLATFORM} when: event: tag From abafd9a28c3cca922725a592977c65d16c9d8556 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Wed, 30 Apr 2025 15:03:15 +0200 Subject: [PATCH 76/93] refactor: split backup and alex into separate crate; set up workspace --- Cargo.lock | 17 +- Cargo.toml | 38 +- alex/Cargo.lock | 871 +++++++++++++++++++ alex/Cargo.toml | 14 + {src => alex/src}/cli/backup.rs | 3 +- {src => alex/src}/cli/config.rs | 6 +- {src => alex/src}/cli/mod.rs | 3 +- {src => alex/src}/cli/run/config.rs | 0 {src => alex/src}/cli/run/mod.rs | 0 {src => alex/src}/error.rs | 0 {src => alex/src}/main.rs | 1 - {src => alex/src}/server/command.rs | 4 +- {src => alex/src}/server/mod.rs | 0 {src => alex/src}/server/process.rs | 3 +- {src => alex/src}/signals.rs | 0 {src => alex/src}/stdin.rs | 0 backup/Cargo.lock | 547 ++++++++++++ backup/Cargo.toml | 14 + {src/backup => backup/src}/delta.rs | 0 {src/backup => backup/src}/io_ext.rs | 0 src/backup/mod.rs => backup/src/lib.rs | 4 + {src/backup => backup/src}/manager/config.rs | 0 {src/backup => backup/src}/manager/meta.rs | 0 {src/backup => backup/src}/manager/mod.rs | 0 {src/backup => backup/src}/path.rs | 0 {src/backup => backup/src}/state.rs | 2 +- 26 files changed, 1494 insertions(+), 33 deletions(-) create mode 100644 alex/Cargo.lock create mode 100644 alex/Cargo.toml rename {src => alex/src}/cli/backup.rs (99%) rename {src => alex/src}/cli/config.rs (93%) rename {src => alex/src}/cli/mod.rs (98%) rename {src => alex/src}/cli/run/config.rs (100%) rename {src => alex/src}/cli/run/mod.rs (100%) rename {src => alex/src}/error.rs (100%) rename {src => alex/src}/main.rs (98%) rename {src => alex/src}/server/command.rs (98%) rename {src => alex/src}/server/mod.rs (100%) rename {src => alex/src}/server/process.rs (98%) rename {src => alex/src}/signals.rs (100%) rename {src => alex/src}/stdin.rs (100%) create mode 100644 backup/Cargo.lock create mode 100644 backup/Cargo.toml rename {src/backup => backup/src}/delta.rs (100%) rename {src/backup => backup/src}/io_ext.rs (100%) rename src/backup/mod.rs => backup/src/lib.rs (99%) rename {src/backup => backup/src}/manager/config.rs (100%) rename {src/backup => backup/src}/manager/meta.rs (100%) rename {src/backup => backup/src}/manager/mod.rs (100%) rename {src/backup => backup/src}/path.rs (100%) rename {src/backup => backup/src}/state.rs (98%) diff --git a/Cargo.lock b/Cargo.lock index 788d506..fb849ea 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10,16 +10,14 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "alex" -version = "0.4.1" +version = "0.4.2" dependencies = [ + "backup", "chrono", "clap", "figment", - "flate2", "serde", - "serde_json", "signal-hook", - "tar", ] [[package]] @@ -98,6 +96,17 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "backup" +version = "0.4.2" +dependencies = [ + "chrono", + "flate2", + "serde", + "serde_json", + "tar", +] + [[package]] name = "bitflags" version = "1.3.2" diff --git a/Cargo.toml b/Cargo.toml index 9e625f4..0b3b6b0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,26 +1,28 @@ -[package] -name = "alex" -version = "0.4.1" -description = "Wrapper around Minecraft server processes, designed to complement Docker image installations." +[workspace] +resolver = "2" +members = [ + 'backup', + 'alex' +] + +[workspace.package] +version = "0.4.2" authors = ["Jef Roosens"] edition = "2021" +# [package] +# name = "alex" +# version = "0.4.1" +# description = "Wrapper around Minecraft server processes, designed to complement Docker image installations." + # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html -[dependencies] -# Used for creating tarballs for backups -tar = "0.4.38" -# Used to compress said tarballs using gzip -flate2 = "1.0.26" +[workspace.dependencies] chrono = { version = "0.4.26", features = ["serde"] } -clap = { version = "4.3.1", features = ["derive", "env"] } -signal-hook = "0.3.15" serde = { version = "1.0.164", features = ["derive"] } -serde_json = "1.0.96" -figment = { version = "0.10.10", features = ["env", "toml"] } -[profile.release] -lto = "fat" -codegen-units = 1 -panic = "abort" -strip = true +# [profile.release] +# lto = "fat" +# codegen-units = 1 +# panic = "abort" +# strip = true diff --git a/alex/Cargo.lock b/alex/Cargo.lock new file mode 100644 index 0000000..e98ba32 --- /dev/null +++ b/alex/Cargo.lock @@ -0,0 +1,871 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + +[[package]] +name = "alex" +version = "0.1.0" +dependencies = [ + "backup", + "chrono", + "clap", + "figment", + "serde", + "signal-hook", +] + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" + +[[package]] +name = "anstyle-parse" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" +dependencies = [ + "anstyle", + "once_cell", + "windows-sys", +] + +[[package]] +name = "atomic" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d818003e740b63afc82337e3160717f4f63078720a810b7b903e70a5d1d2994" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "backup" +version = "0.4.1" +dependencies = [ + "chrono", + "flate2", + "serde", + "serde_json", + "tar", +] + +[[package]] +name = "bitflags" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" + +[[package]] +name = "bumpalo" +version = "3.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" + +[[package]] +name = "bytemuck" +version = "1.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9134a6ef01ce4b366b50689c94f82c14bc72bc5d0386829828a2e2752ef7958c" + +[[package]] +name = "cc" +version = "1.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04da6a0d40b948dfc4fa8f5bbf402b0fc1a64a28dbf7d12ffd683550f2c1b63a" +dependencies = [ + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "clap" +version = "4.5.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eccb054f56cbd38340b380d4a8e69ef1f02f1af43db2f0cc817a4774d80ae071" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efd9466fac8543255d3b1fcad4762c5e116ffe808c8a3043d4263cd4fd4862a2" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" + +[[package]] +name = "colorchoice" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "figment" +version = "0.10.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cb01cd46b0cf372153850f4c6c272d9cbea2da513e07538405148f95bd789f3" +dependencies = [ + "atomic", + "pear", + "serde", + "toml", + "uncased", + "version_check", +] + +[[package]] +name = "filetime" +version = "0.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586" +dependencies = [ + "cfg-if", + "libc", + "libredox", + "windows-sys", +] + +[[package]] +name = "flate2" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "hashbrown" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "iana-time-zone" +version = "0.1.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[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 = "indexmap" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "inlinable_string" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8fae54786f62fb2918dcfae3d568594e50eb9b5c25bf04371af6fe7516452fb" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.172" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" + +[[package]] +name = "libredox" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +dependencies = [ + "bitflags", + "libc", + "redox_syscall", +] + +[[package]] +name = "linux-raw-sys" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" + +[[package]] +name = "log" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "miniz_oxide" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" +dependencies = [ + "adler2", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "pear" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdeeaa00ce488657faba8ebf44ab9361f9365a97bd39ffb8a60663f57ff4b467" +dependencies = [ + "inlinable_string", + "pear_codegen", + "yansi", +] + +[[package]] +name = "pear_codegen" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bab5b985dc082b345f812b7df84e1bef27e7207b39e448439ba8bd69c93f147" +dependencies = [ + "proc-macro2", + "proc-macro2-diagnostics", + "quote", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "proc-macro2-diagnostics" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "version_check", + "yansi", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f103c6d277498fbceb16e84d317e2a400f160f46904d5f5410848c829511a3" +dependencies = [ + "bitflags", +] + +[[package]] +name = "rustix" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d97817398dd4bb2e6da002002db259209759911da105da92bec29ccb12cf58bf" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "rustversion" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.140" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_spanned" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +dependencies = [ + "serde", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" +dependencies = [ + "libc", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tar" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a" +dependencies = [ + "filetime", + "libc", + "xattr", +] + +[[package]] +name = "toml" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05ae329d1f08c4d17a59bed7ff5b5a769d062e64a62d34a3261b219e62cd5aae" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3da5db5a963e24bc68be8b17b6fa82814bb22ee8660f192bb182771d498f09a3" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "310068873db2c5b3e7659d2cc35d21855dbafa50d1ce336397c666e3cb08137e" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "toml_write", + "winnow", +] + +[[package]] +name = "toml_write" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfb942dfe1d8e29a7ee7fcbde5bd2b9a25fb89aa70caea2eba3bee836ff41076" + +[[package]] +name = "uncased" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1b88fcfe09e89d3866a5c11019378088af2d24c3fbd4f0543f96b479ec90697" +dependencies = [ + "version_check", +] + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "windows-core" +version = "0.61.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" + +[[package]] +name = "windows-result" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cb8234a863ea0e8cd7284fcdd4f145233eb00fee02bbdd9861aec44e6477bc5" +dependencies = [ + "memchr", +] + +[[package]] +name = "xattr" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d65cbf2f12c15564212d48f4e3dfb87923d25d611f2aed18f4cb23f0413d89e" +dependencies = [ + "libc", + "rustix", +] + +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" diff --git a/alex/Cargo.toml b/alex/Cargo.toml new file mode 100644 index 0000000..ccbf9d2 --- /dev/null +++ b/alex/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "alex" +version.workspace = true +edition.workspace = true + +[dependencies] +backup = { path = "../backup" } + +chrono.workspace = true +serde.workspace = true + +clap = { version = "4.3.1", features = ["derive", "env"] } +signal-hook = "0.3.15" +figment = { version = "0.10.10", features = ["env", "toml"] } diff --git a/src/cli/backup.rs b/alex/src/cli/backup.rs similarity index 99% rename from src/cli/backup.rs rename to alex/src/cli/backup.rs index 548facc..a587a3d 100644 --- a/src/cli/backup.rs +++ b/alex/src/cli/backup.rs @@ -4,7 +4,8 @@ use std::path::{Path, PathBuf}; use chrono::{TimeZone, Utc}; use clap::{Args, Subcommand}; -use crate::{backup::Backup, other}; +use crate::other; +use backup::Backup; #[derive(Subcommand)] pub enum BackupCommands { diff --git a/src/cli/config.rs b/alex/src/cli/config.rs similarity index 93% rename from src/cli/config.rs rename to alex/src/cli/config.rs index da80269..b940e96 100644 --- a/src/cli/config.rs +++ b/alex/src/cli/config.rs @@ -2,10 +2,8 @@ use std::{io, path::PathBuf}; use serde::{Deserialize, Serialize}; -use crate::{ - backup::{ManagerConfig, MetaManager}, - server::{Metadata, ServerType}, -}; +use crate::server::{Metadata, ServerType}; +use backup::{ManagerConfig, MetaManager}; #[derive(Serialize, Deserialize, Debug)] pub struct Config { diff --git a/src/cli/mod.rs b/alex/src/cli/mod.rs similarity index 98% rename from src/cli/mod.rs rename to alex/src/cli/mod.rs index 0bf55f9..47d12f3 100644 --- a/src/cli/mod.rs +++ b/alex/src/cli/mod.rs @@ -11,7 +11,8 @@ use figment::{ }; use serde::{Deserialize, Serialize}; -use crate::{backup::ManagerConfig, server::ServerType}; +use crate::server::ServerType; +use ::backup::ManagerConfig; use backup::BackupArgs; use config::Config; use run::RunCli; diff --git a/src/cli/run/config.rs b/alex/src/cli/run/config.rs similarity index 100% rename from src/cli/run/config.rs rename to alex/src/cli/run/config.rs diff --git a/src/cli/run/mod.rs b/alex/src/cli/run/mod.rs similarity index 100% rename from src/cli/run/mod.rs rename to alex/src/cli/run/mod.rs diff --git a/src/error.rs b/alex/src/error.rs similarity index 100% rename from src/error.rs rename to alex/src/error.rs diff --git a/src/main.rs b/alex/src/main.rs similarity index 98% rename from src/main.rs rename to alex/src/main.rs index 245a796..f4bf25d 100644 --- a/src/main.rs +++ b/alex/src/main.rs @@ -1,4 +1,3 @@ -mod backup; mod cli; mod error; mod server; diff --git a/src/server/command.rs b/alex/src/server/command.rs similarity index 98% rename from src/server/command.rs rename to alex/src/server/command.rs index 258bb86..0c94732 100644 --- a/src/server/command.rs +++ b/alex/src/server/command.rs @@ -1,6 +1,6 @@ -use crate::backup::ManagerConfig; -use crate::backup::MetaManager; use crate::server::{Metadata, ServerProcess, ServerType}; +use backup::ManagerConfig; +use backup::MetaManager; use std::fmt; use std::fs::File; use std::io::Write; diff --git a/src/server/mod.rs b/alex/src/server/mod.rs similarity index 100% rename from src/server/mod.rs rename to alex/src/server/mod.rs diff --git a/src/server/process.rs b/alex/src/server/process.rs similarity index 98% rename from src/server/process.rs rename to alex/src/server/process.rs index e7ff3d5..ca34a0c 100644 --- a/src/server/process.rs +++ b/alex/src/server/process.rs @@ -1,6 +1,7 @@ use std::{io::Write, process::Child, sync::RwLock}; -use crate::{backup::MetaManager, server::Metadata}; +use crate::server::Metadata; +use backup::MetaManager; pub struct ServerProcess { child: RwLock, diff --git a/src/signals.rs b/alex/src/signals.rs similarity index 100% rename from src/signals.rs rename to alex/src/signals.rs diff --git a/src/stdin.rs b/alex/src/stdin.rs similarity index 100% rename from src/stdin.rs rename to alex/src/stdin.rs diff --git a/backup/Cargo.lock b/backup/Cargo.lock new file mode 100644 index 0000000..a9afc37 --- /dev/null +++ b/backup/Cargo.lock @@ -0,0 +1,547 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "backup" +version = "0.4.1" +dependencies = [ + "chrono", + "flate2", + "serde", + "serde_json", + "tar", +] + +[[package]] +name = "bitflags" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" + +[[package]] +name = "bumpalo" +version = "3.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" + +[[package]] +name = "cc" +version = "1.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04da6a0d40b948dfc4fa8f5bbf402b0fc1a64a28dbf7d12ffd683550f2c1b63a" +dependencies = [ + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "errno" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "filetime" +version = "0.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586" +dependencies = [ + "cfg-if", + "libc", + "libredox", + "windows-sys", +] + +[[package]] +name = "flate2" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[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 = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.172" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" + +[[package]] +name = "libredox" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +dependencies = [ + "bitflags", + "libc", + "redox_syscall", +] + +[[package]] +name = "linux-raw-sys" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" + +[[package]] +name = "log" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "miniz_oxide" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" +dependencies = [ + "adler2", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "proc-macro2" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f103c6d277498fbceb16e84d317e2a400f160f46904d5f5410848c829511a3" +dependencies = [ + "bitflags", +] + +[[package]] +name = "rustix" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d97817398dd4bb2e6da002002db259209759911da105da92bec29ccb12cf58bf" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "rustversion" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.140" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "syn" +version = "2.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tar" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a" +dependencies = [ + "filetime", + "libc", + "xattr", +] + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "windows-core" +version = "0.61.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" + +[[package]] +name = "windows-result" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "xattr" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d65cbf2f12c15564212d48f4e3dfb87923d25d611f2aed18f4cb23f0413d89e" +dependencies = [ + "libc", + "rustix", +] diff --git a/backup/Cargo.toml b/backup/Cargo.toml new file mode 100644 index 0000000..0a03a1b --- /dev/null +++ b/backup/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "backup" +version.workspace = true +edition.workspace = true + +[dependencies] +chrono.workspace = true +serde.workspace = true + +# Used for creating tarballs for backups +tar = "0.4.38" +# Used to compress said tarballs using gzip +flate2 = "1.0.26" +serde_json = "1.0.96" diff --git a/src/backup/delta.rs b/backup/src/delta.rs similarity index 100% rename from src/backup/delta.rs rename to backup/src/delta.rs diff --git a/src/backup/io_ext.rs b/backup/src/io_ext.rs similarity index 100% rename from src/backup/io_ext.rs rename to backup/src/io_ext.rs diff --git a/src/backup/mod.rs b/backup/src/lib.rs similarity index 99% rename from src/backup/mod.rs rename to backup/src/lib.rs index c833b55..f61773d 100644 --- a/src/backup/mod.rs +++ b/backup/src/lib.rs @@ -25,6 +25,10 @@ pub use state::State; const BYTE_SUFFIXES: [&str; 5] = ["B", "KiB", "MiB", "GiB", "TiB"]; +pub fn other(msg: &str) -> io::Error { + io::Error::new(io::ErrorKind::Other, msg) +} + #[derive(Debug, PartialEq, Serialize, Deserialize)] pub enum BackupType { Full, diff --git a/src/backup/manager/config.rs b/backup/src/manager/config.rs similarity index 100% rename from src/backup/manager/config.rs rename to backup/src/manager/config.rs diff --git a/src/backup/manager/meta.rs b/backup/src/manager/meta.rs similarity index 100% rename from src/backup/manager/meta.rs rename to backup/src/manager/meta.rs diff --git a/src/backup/manager/mod.rs b/backup/src/manager/mod.rs similarity index 100% rename from src/backup/manager/mod.rs rename to backup/src/manager/mod.rs diff --git a/src/backup/path.rs b/backup/src/path.rs similarity index 100% rename from src/backup/path.rs rename to backup/src/path.rs diff --git a/src/backup/state.rs b/backup/src/state.rs similarity index 98% rename from src/backup/state.rs rename to backup/src/state.rs index 1f81abb..c6a992f 100644 --- a/src/backup/state.rs +++ b/backup/src/state.rs @@ -7,7 +7,7 @@ use std::{ use serde::{Deserialize, Serialize}; -use crate::backup::Delta; +use crate::Delta; /// Struct that represents a current state for a backup. This struct acts as a smart pointer around /// a HashMap. From 08e77034a78f038fc82a2a9a50736db7b790f4ba Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Wed, 30 Apr 2025 15:15:51 +0200 Subject: [PATCH 77/93] refactor: add Justfile; fix some clippy lints --- Justfile | 35 +++++++++++++++++++++++++++++++++++ backup/src/delta.rs | 9 +-------- backup/src/lib.rs | 4 ++-- backup/src/state.rs | 14 ++------------ 4 files changed, 40 insertions(+), 22 deletions(-) create mode 100644 Justfile diff --git a/Justfile b/Justfile new file mode 100644 index 0000000..511d61f --- /dev/null +++ b/Justfile @@ -0,0 +1,35 @@ +[group('build')] +build pkg: + cargo build --package {{ pkg }} +alias b := build + +[group('build')] +build-all: + cargo build --workspace +alias ba := build-all + +[group('test')] +test pkg: + cargo test --package {{ pkg }} +alias t := test + +[group('test')] +test-all: + cargo test --workspace +alias ta := test-all + +[group('check')] +check pkg: + cargo fmt --check --package {{ pkg }} + cargo clippy --package {{ pkg }} -- \ + --no-deps \ + --deny 'clippy::all' +alias c := check + +[group('check')] +check-all: + cargo fmt --check --all + cargo clippy --all -- \ + --no-deps \ + --deny 'clippy::all' +alias ca := check-all diff --git a/backup/src/delta.rs b/backup/src/delta.rs index 1260b43..6bdff88 100644 --- a/backup/src/delta.rs +++ b/backup/src/delta.rs @@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize}; use super::State; /// Represents the changes relative to the previous backup -#[derive(Debug, Serialize, Deserialize, Clone)] +#[derive(Debug, Serialize, Deserialize, Clone, Default)] pub struct Delta { /// What files were added/modified in each part of the tarball. pub added: State, @@ -17,13 +17,6 @@ pub struct Delta { } impl Delta { - pub fn new() -> Self { - Self { - added: State::new(), - removed: State::new(), - } - } - /// Returns whether the delta is empty by checking whether both its added and removed state /// return true for their `is_empty`. #[allow(dead_code)] diff --git a/backup/src/lib.rs b/backup/src/lib.rs index f61773d..6569f83 100644 --- a/backup/src/lib.rs +++ b/backup/src/lib.rs @@ -129,7 +129,7 @@ impl Backup { let enc = GzEncoder::new(tar_gz, Compression::default()); let mut ar = tar::Builder::new(enc); - let mut delta = Delta::new(); + let mut delta = Delta::default(); for (dir_in_tar, src_dir) in dirs { let mut added_files: HashSet = HashSet::new(); @@ -187,7 +187,7 @@ impl Backup { let enc = GzEncoder::new(tar_gz, Compression::default()); let mut ar = tar::Builder::new(enc); - let mut delta = Delta::new(); + let mut delta = Delta::default(); for (dir_in_tar, src_dir) in dirs { let mut all_files: HashSet = HashSet::new(); diff --git a/backup/src/state.rs b/backup/src/state.rs index c6a992f..c0f66b9 100644 --- a/backup/src/state.rs +++ b/backup/src/state.rs @@ -11,14 +11,10 @@ use crate::Delta; /// Struct that represents a current state for a backup. This struct acts as a smart pointer around /// a HashMap. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] pub struct State(HashMap>); impl State { - pub fn new() -> Self { - State(HashMap::new()) - } - /// Apply the delta to the current state. pub fn apply(&mut self, delta: &Delta) { // First we add new files, then we remove the old ones @@ -61,7 +57,7 @@ where T::Item: Borrow, { fn from(deltas: T) -> Self { - let mut state = State::new(); + let mut state = State::default(); for delta in deltas { state.apply(delta.borrow()); @@ -90,9 +86,3 @@ impl DerefMut for State { &mut self.0 } } - -impl Default for State { - fn default() -> Self { - Self::new() - } -} From 3ae19e21681bdc76afe3a5156bcc5345f04ecf94 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Wed, 30 Apr 2025 15:30:57 +0200 Subject: [PATCH 78/93] fix(backup): work with temporary file while writing json metadata file --- CHANGELOG.md | 5 +++++ backup/src/manager/mod.rs | 16 +++++++++++++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5305b42..810214c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://git.rustybever.be/Chewing_Bever/alex/src/branch/dev) +## Fixed + +* Fix bug where JSON metadata file can be corrupted if crash occurs while + writing (data is now written to a temporary file before atomically renaming) + ## [0.4.1](https://git.rustybever.be/Chewing_Bever/alex/src/tag/0.4.1) ### Changed diff --git a/backup/src/manager/mod.rs b/backup/src/manager/mod.rs index ec91a3d..f8bbc73 100644 --- a/backup/src/manager/mod.rs +++ b/backup/src/manager/mod.rs @@ -122,10 +122,24 @@ where } /// Write the in-memory state to disk. + /// + /// The state is first written to a temporary file before being (atomically, depending on the + /// file system) renamed to the final path. pub fn save(&self) -> io::Result<()> { - let json_file = File::create(self.backup_dir.join(Self::METADATA_FILE))?; + let dest_path = self.backup_dir.join(Self::METADATA_FILE); + + let dest_ext = dest_path + .extension() + .map(|ext| ext.to_string_lossy().to_string()) + .unwrap_or(String::new()); + let temp_path = dest_path.with_extension(format!("{dest_ext}.temp")); + + let json_file = File::create(&temp_path)?; serde_json::to_writer(json_file, &self.chains)?; + // Rename temp file to the destination path after writing was successful + std::fs::rename(temp_path, dest_path)?; + Ok(()) } From 6a8725489ea700e9a61a4d189b80f55e7b353ecc Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Wed, 30 Apr 2025 17:40:28 +0200 Subject: [PATCH 79/93] chore: bump dependencies --- CHANGELOG.md | 2 +- Cargo.lock | 512 +++++++++++++++++++++++++--------------------- Cargo.toml | 7 - alex/Cargo.toml | 3 +- backup/Cargo.toml | 2 +- 5 files changed, 278 insertions(+), 248 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 810214c..f95f877 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://git.rustybever.be/Chewing_Bever/alex/src/branch/dev) -## Fixed +### Fixed * Fix bug where JSON metadata file can be corrupted if crash occurs while writing (data is now written to a temporary file before atomically renaming) diff --git a/Cargo.lock b/Cargo.lock index fb849ea..a46a1ae 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3,10 +3,10 @@ version = 3 [[package]] -name = "adler" -version = "1.0.2" +name = "adler2" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" [[package]] name = "alex" @@ -37,64 +37,68 @@ dependencies = [ [[package]] name = "anstream" -version = "0.3.2" +version = "0.6.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ca84f3628370c59db74ee214b3263d58f9aadd9b4fe7e711fd87dc452b7f163" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", - "is-terminal", + "is_terminal_polyfill", "utf8parse", ] [[package]] name = "anstyle" -version = "1.0.1" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a30da5c5f2d5e72842e00bcb57657162cdabef0931f40e2deb9b4140440cecd" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" [[package]] name = "anstyle-parse" -version = "0.2.1" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "938874ff5980b03a87c5524b3ae5b59cf99b1d6bc836848df7bc5ada9643c333" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.0.0" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" dependencies = [ "windows-sys", ] [[package]] name = "anstyle-wincon" -version = "1.0.2" +version = "3.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c677ab05e09154296dd37acecd46420c17b9713e8366facafa8fc0885167cf4c" +checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" dependencies = [ "anstyle", + "once_cell", "windows-sys", ] [[package]] name = "atomic" -version = "0.5.3" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c59bdb34bc650a32731b31bd8f0829cc15d24a708ee31559e0bb34f2bc320cba" +checksum = "8d818003e740b63afc82337e3160717f4f63078720a810b7b903e70a5d1d2994" +dependencies = [ + "bytemuck", +] [[package]] name = "autocfg" -version = "1.1.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "backup" @@ -109,29 +113,29 @@ dependencies = [ [[package]] name = "bitflags" -version = "1.3.2" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "bitflags" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" +checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" [[package]] name = "bumpalo" -version = "3.13.0" +version = "3.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" +checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" + +[[package]] +name = "bytemuck" +version = "1.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9134a6ef01ce4b366b50689c94f82c14bc72bc5d0386829828a2e2752ef7958c" [[package]] name = "cc" -version = "1.0.82" +version = "1.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "305fe645edc1442a0fa8b6726ba61d422798d37a52e12eaecf4b022ebbb88f01" +checksum = "04da6a0d40b948dfc4fa8f5bbf402b0fc1a64a28dbf7d12ffd683550f2c1b63a" dependencies = [ - "libc", + "shlex", ] [[package]] @@ -142,36 +146,34 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.26" +version = "0.4.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec837a71355b28f6556dbd569b37b3f363091c0bd4b2e735674521b4c5fd9bc5" +checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" dependencies = [ "android-tzdata", "iana-time-zone", "js-sys", "num-traits", "serde", - "time", "wasm-bindgen", - "winapi", + "windows-link", ] [[package]] name = "clap" -version = "4.3.21" +version = "4.5.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c27cdf28c0f604ba3f512b0c9a409f8de8513e4816705deb0498b627e7c3a3fd" +checksum = "eccb054f56cbd38340b380d4a8e69ef1f02f1af43db2f0cc817a4774d80ae071" dependencies = [ "clap_builder", "clap_derive", - "once_cell", ] [[package]] name = "clap_builder" -version = "4.3.21" +version = "4.5.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08a9f1ab5e9f01a9b81f202e8562eb9a10de70abf9eaeac1be465c28b75aa4aa" +checksum = "efd9466fac8543255d3b1fcad4762c5e116ffe808c8a3043d4263cd4fd4862a2" dependencies = [ "anstream", "anstyle", @@ -181,9 +183,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.3.12" +version = "4.5.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54a9bb5758fc5dfe728d1019941681eccaf0cf8a4189b692a0ee2f2ecf90a050" +checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" dependencies = [ "heck", "proc-macro2", @@ -193,63 +195,52 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.5.0" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" +checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" [[package]] name = "colorchoice" -version = "1.0.0" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" [[package]] name = "core-foundation-sys" -version = "0.8.4" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "crc32fast" -version = "1.3.2" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" dependencies = [ "cfg-if", ] [[package]] name = "equivalent" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.2" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b30f669a7961ef1631673d2766cc92f52d64f7ef354d4fe0ddfd30ed52f0f4f" +checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e" dependencies = [ - "errno-dragonfly", "libc", "windows-sys", ] -[[package]] -name = "errno-dragonfly" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" -dependencies = [ - "cc", - "libc", -] - [[package]] name = "figment" -version = "0.10.10" +version = "0.10.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4547e226f4c9ab860571e070a9034192b3175580ecea38da34fcdb53a018c9a5" +checksum = "8cb01cd46b0cf372153850f4c6c272d9cbea2da513e07538405148f95bd789f3" dependencies = [ "atomic", "pear", @@ -261,21 +252,21 @@ dependencies = [ [[package]] name = "filetime" -version = "0.2.22" +version = "0.2.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4029edd3e734da6fe05b6cd7bd2960760a616bd2ddd0d59a0124746d6272af0" +checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "libredox", "windows-sys", ] [[package]] name = "flate2" -version = "1.0.26" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b9429470923de8e8cbd4d2dc513535400b4b3fef0319fb5c4e1f520a7bef743" +checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece" dependencies = [ "crc32fast", "miniz_oxide", @@ -283,34 +274,29 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.14.0" +version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" [[package]] name = "heck" -version = "0.4.1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" - -[[package]] -name = "hermit-abi" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "iana-time-zone" -version = "0.1.57" +version = "0.1.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fad5b825842d2b38bd206f3e81d6957625fd7f0a361e345c30e01a0ae2dd613" +checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", + "log", "wasm-bindgen", - "windows", + "windows-core", ] [[package]] @@ -324,9 +310,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.0.0" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" +checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" dependencies = [ "equivalent", "hashbrown", @@ -339,84 +325,91 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8fae54786f62fb2918dcfae3d568594e50eb9b5c25bf04371af6fe7516452fb" [[package]] -name = "is-terminal" -version = "0.4.9" +name = "is_terminal_polyfill" +version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" -dependencies = [ - "hermit-abi", - "rustix", - "windows-sys", -] +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" [[package]] name = "itoa" -version = "1.0.9" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "js-sys" -version = "0.3.64" +version = "0.3.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" dependencies = [ + "once_cell", "wasm-bindgen", ] [[package]] name = "libc" -version = "0.2.147" +version = "0.2.172" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" +checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" + +[[package]] +name = "libredox" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +dependencies = [ + "bitflags", + "libc", + "redox_syscall", +] [[package]] name = "linux-raw-sys" -version = "0.4.5" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57bcfdad1b858c2db7c38303a6d2ad4dfaf5eb53dfeb0910128b2c26d6158503" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" [[package]] name = "log" -version = "0.4.20" +version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" [[package]] name = "memchr" -version = "2.5.0" +version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "miniz_oxide" -version = "0.7.1" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" dependencies = [ - "adler", + "adler2", ] [[package]] name = "num-traits" -version = "0.2.16" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] [[package]] name = "once_cell" -version = "1.18.0" +version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "pear" -version = "0.2.7" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61a386cd715229d399604b50d1361683fe687066f42d56f54be995bc6868f71c" +checksum = "bdeeaa00ce488657faba8ebf44ab9361f9365a97bd39ffb8a60663f57ff4b467" dependencies = [ "inlinable_string", "pear_codegen", @@ -425,9 +418,9 @@ dependencies = [ [[package]] name = "pear_codegen" -version = "0.2.7" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da9f0f13dac8069c139e8300a6510e3f4143ecf5259c60b116a9b271b4ca0d54" +checksum = "4bab5b985dc082b345f812b7df84e1bef27e7207b39e448439ba8bd69c93f147" dependencies = [ "proc-macro2", "proc-macro2-diagnostics", @@ -437,9 +430,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.66" +version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" dependencies = [ "unicode-ident", ] @@ -459,29 +452,29 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.32" +version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50f3b39ccfb720540debaa0164757101c08ecb8d326b15358ce76a62c7e85965" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" dependencies = [ "proc-macro2", ] [[package]] name = "redox_syscall" -version = "0.3.5" +version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +checksum = "d2f103c6d277498fbceb16e84d317e2a400f160f46904d5f5410848c829511a3" dependencies = [ - "bitflags 1.3.2", + "bitflags", ] [[package]] name = "rustix" -version = "0.38.8" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19ed4fa021d81c8392ce04db050a3da9a60299050b7ae1cf482d862b54a7218f" +checksum = "d97817398dd4bb2e6da002002db259209759911da105da92bec29ccb12cf58bf" dependencies = [ - "bitflags 2.4.0", + "bitflags", "errno", "libc", "linux-raw-sys", @@ -489,25 +482,31 @@ dependencies = [ ] [[package]] -name = "ryu" -version = "1.0.15" +name = "rustversion" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" +checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" [[package]] name = "serde" -version = "1.0.183" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32ac8da02677876d532745a130fc9d8e6edfa81a269b107c5b00829b91d8eb3c" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.183" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aafe972d60b0b9bee71a91b92fee2d4fb3c9d7e8f6b179aa99f27203d99a4816" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", @@ -516,24 +515,31 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.104" +version = "1.0.140" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "076066c5f1078eac5b722a31827a8832fe108bed65dfa75e233c89f8206e976c" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" dependencies = [ "itoa", + "memchr", "ryu", "serde", ] [[package]] name = "serde_spanned" -version = "0.6.3" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96426c9936fd7a0124915f9185ea1d20aa9445cc9821142f0a73bc9207a2e186" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" dependencies = [ "serde", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "signal-hook" version = "0.3.17" @@ -546,24 +552,24 @@ dependencies = [ [[package]] name = "signal-hook-registry" -version = "1.4.1" +version = "1.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" dependencies = [ "libc", ] [[package]] name = "strsim" -version = "0.10.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" -version = "2.0.28" +version = "2.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04361975b3f5e348b2189d8dc55bc942f278b2d482a6a0365de5bdd62d351567" +checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" dependencies = [ "proc-macro2", "quote", @@ -572,31 +578,20 @@ dependencies = [ [[package]] name = "tar" -version = "0.4.40" +version = "0.4.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b16afcea1f22891c49a00c751c7b63b2233284064f11a200fc624137c51e2ddb" +checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a" dependencies = [ "filetime", "libc", "xattr", ] -[[package]] -name = "time" -version = "0.1.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" -dependencies = [ - "libc", - "wasi", - "winapi", -] - [[package]] name = "toml" -version = "0.7.6" +version = "0.8.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c17e963a819c331dcacd7ab957d80bc2b9a9c1e71c804826d2f283dd65306542" +checksum = "05ae329d1f08c4d17a59bed7ff5b5a769d062e64a62d34a3261b219e62cd5aae" dependencies = [ "serde", "serde_spanned", @@ -606,78 +601,80 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.3" +version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +checksum = "3da5db5a963e24bc68be8b17b6fa82814bb22ee8660f192bb182771d498f09a3" dependencies = [ "serde", ] [[package]] name = "toml_edit" -version = "0.19.14" +version = "0.22.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8123f27e969974a3dfba720fdb560be359f57b44302d280ba72e76a74480e8a" +checksum = "310068873db2c5b3e7659d2cc35d21855dbafa50d1ce336397c666e3cb08137e" dependencies = [ "indexmap", "serde", "serde_spanned", "toml_datetime", + "toml_write", "winnow", ] [[package]] -name = "uncased" -version = "0.9.9" +name = "toml_write" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b9bc53168a4be7402ab86c3aad243a84dd7381d09be0eddc81280c1da95ca68" +checksum = "bfb942dfe1d8e29a7ee7fcbde5bd2b9a25fb89aa70caea2eba3bee836ff41076" + +[[package]] +name = "uncased" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1b88fcfe09e89d3866a5c11019378088af2d24c3fbd4f0543f96b479ec90697" dependencies = [ "version_check", ] [[package]] name = "unicode-ident" -version = "1.0.11" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" [[package]] name = "utf8parse" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "version_check" -version = "0.9.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" - -[[package]] -name = "wasi" -version = "0.10.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "wasm-bindgen" -version = "0.2.87" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" dependencies = [ "cfg-if", + "once_cell", + "rustversion", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.87" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" dependencies = [ "bumpalo", "log", - "once_cell", "proc-macro2", "quote", "syn", @@ -686,9 +683,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.87" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -696,9 +693,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.87" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", @@ -709,59 +706,91 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.87" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", + "unicode-ident", ] [[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" +name = "windows-core" +version = "0.61.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 = "windows" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980" dependencies = [ - "windows-targets", + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" + +[[package]] +name = "windows-result" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97" +dependencies = [ + "windows-link", ] [[package]] name = "windows-sys" -version = "0.48.0" +version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ "windows-targets", ] [[package]] name = "windows-targets" -version = "0.48.1" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ "windows_aarch64_gnullvm", "windows_aarch64_msvc", "windows_i686_gnu", + "windows_i686_gnullvm", "windows_i686_msvc", "windows_x86_64_gnu", "windows_x86_64_gnullvm", @@ -770,66 +799,73 @@ dependencies = [ [[package]] name = "windows_aarch64_gnullvm" -version = "0.48.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" -version = "0.48.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" -version = "0.48.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_msvc" -version = "0.48.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_x86_64_gnu" -version = "0.48.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnullvm" -version = "0.48.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" -version = "0.48.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" -version = "0.5.10" +version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5504cc7644f4b593cbc05c4a55bf9bd4e94b867c3c0bd440934174d50482427d" +checksum = "6cb8234a863ea0e8cd7284fcdd4f145233eb00fee02bbdd9861aec44e6477bc5" dependencies = [ "memchr", ] [[package]] name = "xattr" -version = "1.0.1" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4686009f71ff3e5c4dbcf1a282d0a44db3f021ba69350cd42086b3e5f1c6985" +checksum = "0d65cbf2f12c15564212d48f4e3dfb87923d25d611f2aed18f4cb23f0413d89e" dependencies = [ "libc", + "rustix", ] [[package]] name = "yansi" -version = "1.0.0-rc.1" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1367295b8f788d371ce2dbc842c7b709c73ee1364d30351dd300ec2203b12377" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" diff --git a/Cargo.toml b/Cargo.toml index 0b3b6b0..4df3c3d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,13 +10,6 @@ version = "0.4.2" authors = ["Jef Roosens"] edition = "2021" -# [package] -# name = "alex" -# version = "0.4.1" -# description = "Wrapper around Minecraft server processes, designed to complement Docker image installations." - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - [workspace.dependencies] chrono = { version = "0.4.26", features = ["serde"] } serde = { version = "1.0.164", features = ["derive"] } diff --git a/alex/Cargo.toml b/alex/Cargo.toml index ccbf9d2..7f7cac8 100644 --- a/alex/Cargo.toml +++ b/alex/Cargo.toml @@ -1,5 +1,6 @@ [package] name = "alex" +description = "Wrapper around Minecraft server processes, designed to complement Docker image installations." version.workspace = true edition.workspace = true @@ -9,6 +10,6 @@ backup = { path = "../backup" } chrono.workspace = true serde.workspace = true -clap = { version = "4.3.1", features = ["derive", "env"] } +clap = { version = "4.5.37", features = ["derive", "env"] } signal-hook = "0.3.15" figment = { version = "0.10.10", features = ["env", "toml"] } diff --git a/backup/Cargo.toml b/backup/Cargo.toml index 0a03a1b..80e5804 100644 --- a/backup/Cargo.toml +++ b/backup/Cargo.toml @@ -10,5 +10,5 @@ serde.workspace = true # Used for creating tarballs for backups tar = "0.4.38" # Used to compress said tarballs using gzip -flate2 = "1.0.26" +flate2 = "1.1.1" serde_json = "1.0.96" From e8a92d7e0799c1bf8c90ce0c5b1c86f528c6c159 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Sat, 17 May 2025 20:10:23 +0200 Subject: [PATCH 80/93] chore: organize justfiles --- .gitignore | 1 + Justfile | 44 +++++++++++++++++--------------------------- alex/Justfile | 16 ++++++++++++++++ backup/Justfile | 16 ++++++++++++++++ 4 files changed, 50 insertions(+), 27 deletions(-) create mode 100644 alex/Justfile create mode 100644 backup/Justfile diff --git a/.gitignore b/.gitignore index 3695da7..0f92b30 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,4 @@ target/ # testing files *.jar data*/ +*.log diff --git a/Justfile b/Justfile index 511d61f..76be6db 100644 --- a/Justfile +++ b/Justfile @@ -1,35 +1,25 @@ -[group('build')] -build pkg: - cargo build --package {{ pkg }} +build: + cargo build --frozen --workspace alias b := build -[group('build')] -build-all: - cargo build --workspace -alias ba := build-all - -[group('test')] -test pkg: - cargo test --package {{ pkg }} +test: + cargo test --frozen --workspace alias t := test -[group('test')] -test-all: - cargo test --workspace -alias ta := test-all - -[group('check')] -check pkg: - cargo fmt --check --package {{ pkg }} - cargo clippy --package {{ pkg }} -- \ +check: + cargo fmt --check --all + cargo clippy \ + --frozen \ + --all -- \ --no-deps \ --deny 'clippy::all' alias c := check -[group('check')] -check-all: - cargo fmt --check --all - cargo clippy --all -- \ - --no-deps \ - --deny 'clippy::all' -alias ca := check-all +fetch: + cargo fetch --locked + +clean: + cargo clean + +doc: + cargo doc --workspace --frozen diff --git a/alex/Justfile b/alex/Justfile new file mode 100644 index 0000000..cc02572 --- /dev/null +++ b/alex/Justfile @@ -0,0 +1,16 @@ +build: + cargo build --frozen +alias b := build + +test: + cargo test --frozen +alias t := test + +check: + cargo fmt --check + cargo clippy \ + --frozen \ + -- \ + --no-deps \ + --deny 'clippy::all' +alias c := check diff --git a/backup/Justfile b/backup/Justfile new file mode 100644 index 0000000..cc02572 --- /dev/null +++ b/backup/Justfile @@ -0,0 +1,16 @@ +build: + cargo build --frozen +alias b := build + +test: + cargo test --frozen +alias t := test + +check: + cargo fmt --check + cargo clippy \ + --frozen \ + -- \ + --no-deps \ + --deny 'clippy::all' +alias c := check From 638e228ba45be7f9dd4547f0003c3e91ce62b0e3 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Tue, 13 May 2025 10:06:49 +0200 Subject: [PATCH 81/93] chore: add publish functionality to justfile --- .cargo/config.toml | 4 ++++ Justfile | 35 +++++++++++++++++++++++++++++++++++ alex-example.toml | 24 +++++++++++++----------- 3 files changed, 52 insertions(+), 11 deletions(-) diff --git a/.cargo/config.toml b/.cargo/config.toml index a5bfa9c..4b1cf60 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,3 +1,7 @@ [alias] runs = "run -- --config data/config --backup data/backups --world data/worlds --layers 2min,2,4,4;3min,3,2,2" runrs = "run --release -- --config data/config --backup data/backups --world data/worlds --layers 2min,2,4,4;3min,3,2,2" + +[target.aarch64-unknown-linux-musl] +linker = "aarch64-linux-gnu-gcc" +runner = "qemu-aarch64" diff --git a/Justfile b/Justfile index 76be6db..54943ad 100644 --- a/Justfile +++ b/Justfile @@ -1,7 +1,16 @@ +[group('build')] build: cargo build --frozen --workspace alias b := build +[group('build')] +build-release target: + cargo build \ + --release \ + --frozen \ + --workspace \ + --target '{{ target }}' + test: cargo test --frozen --workspace alias t := test @@ -23,3 +32,29 @@ clean: doc: cargo doc --workspace --frozen + +run: + mkdir -p data + cargo run --frozen --package alex -- run \ + --config data/config \ + --backup data/backups \ + --world data/worlds \ + --jar ./paper-1.21.5-77.jar \ + --java '/usr/lib/jvm/java-21-openjdk/bin/java' \ + --layers '2min,2,4,4;3min,3,2,2' + +publish-release-binaries tag: (build-release 'x86_64-unknown-linux-musl') (build-release 'aarch64-unknown-linux-musl') + # Check the binaries are proper static binaries + [ "$(readelf -d target/x86_64-unknown-linux-musl/release/alex | grep NEEDED | wc -l)" = 0 ] + [ "$(readelf -d target/aarch64-unknown-linux-musl/release/alex | grep NEEDED | wc -l)" = 0 ] + + curl \ + --netrc \ + --fail \ + --upload-file target/x86_64-unknown-linux-musl/release/alex \ + https://git.rustybever.be/api/packages/Chewing_Bever/generic/alex/"{{ tag }}"/alex-linux-amd64 + curl \ + --netrc \ + --fail \ + --upload-file target/aarch64-unknown-linux-musl/release/alex \ + https://git.rustybever.be/api/packages/Chewing_Bever/generic/alex/"{{ tag }}"/alex-linux-arm64 diff --git a/alex-example.toml b/alex-example.toml index 234a7bf..3157870 100644 --- a/alex-example.toml +++ b/alex-example.toml @@ -1,16 +1,18 @@ config = "data/config" world = "data/worlds" backup = "data/backups" -server = "Paper" +server = "paper" +jar = './paper-1.21.5-77.jar' +java = '/usr/lib/jvm/java-21-openjdk/bin/java' -# [[layers]] -# name = "2min" -# frequency = 2 -# chains = 4 -# chain_len = 4 +[[layers]] +name = "2min" +frequency = 2 +chains = 4 +chain_len = 4 -# [[layers]] -# name = "3min" -# frequency = 3 -# chains = 2 -# chain_len = 2 +[[layers]] +name = "3min" +frequency = 3 +chains = 2 +chain_len = 2 From 5f7376ebb17856850016b99fe761b1f0587a0ed7 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Sat, 24 May 2025 12:05:36 +0200 Subject: [PATCH 82/93] chore: bump versions to 0.4.2 --- CHANGELOG.md | 6 ++++-- Cargo.toml | 10 +++++----- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f95f877..5b6fa3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://git.rustybever.be/Chewing_Bever/alex/src/branch/dev) +## [0.4.2](https://git.rustybever.be/Chewing_Bever/alex/src/tag/0.4.2) + ### Fixed * Fix bug where JSON metadata file can be corrupted if crash occurs while @@ -79,7 +81,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed -* JVM flags now narrowely follow Aikar's specifications +* JVM flags now narrowly follow Aikar's specifications ## [0.2.0](https://git.rustybever.be/Chewing_Bever/alex/src/tag/0.2.0) @@ -89,7 +91,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * A single stop signal will trigger the Java process to shut down, but Alex still expects to be run from a utility such as dumb-init * Properly back up entire config directory -* Inject Java optimisation flags +* Inject Java optimization flags ## [0.1.1](https://git.rustybever.be/Chewing_Bever/alex/src/tag/0.1.1) diff --git a/Cargo.toml b/Cargo.toml index 4df3c3d..4f14331 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,8 +14,8 @@ edition = "2021" chrono = { version = "0.4.26", features = ["serde"] } serde = { version = "1.0.164", features = ["derive"] } -# [profile.release] -# lto = "fat" -# codegen-units = 1 -# panic = "abort" -# strip = true +[profile.release] +lto = "fat" +codegen-units = 1 +panic = "abort" +strip = true From 3f00eee61e7733041739244f2dc3bd023067bf72 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Mon, 19 May 2025 10:48:39 +0200 Subject: [PATCH 83/93] feat(backup): add delta mutation methods; start union tests --- backup/src/delta.rs | 102 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 99 insertions(+), 3 deletions(-) diff --git a/backup/src/delta.rs b/backup/src/delta.rs index 6bdff88..dde906b 100644 --- a/backup/src/delta.rs +++ b/backup/src/delta.rs @@ -1,11 +1,15 @@ -use std::{borrow::Borrow, fmt}; +use std::{ + borrow::Borrow, + fmt, + path::{Path, PathBuf}, +}; use serde::{Deserialize, Serialize}; use super::State; /// Represents the changes relative to the previous backup -#[derive(Debug, Serialize, Deserialize, Clone, Default)] +#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq)] pub struct Delta { /// What files were added/modified in each part of the tarball. pub added: State, @@ -19,7 +23,6 @@ pub struct Delta { impl Delta { /// Returns whether the delta is empty by checking whether both its added and removed state /// return true for their `is_empty`. - #[allow(dead_code)] pub fn is_empty(&self) -> bool { self.added.is_empty() && self.removed.is_empty() } @@ -143,6 +146,58 @@ impl Delta { contributions } + + /// Append the given files to the directory's list of added files + pub fn append_added(&mut self, dir: impl Into, files: I) + where + I: IntoIterator, + I::Item: Into, + { + let dir: PathBuf = dir.into(); + let files = files.into_iter().map(Into::into); + + if let Some(dir_files) = self.added.get_mut(&dir) { + dir_files.extend(files); + } else { + self.added.insert(dir, files.collect()); + } + } + + /// Wrapper around the `append_added` method for a builder-style construction of delta's + pub fn with_added(mut self, dir: impl Into, files: I) -> Self + where + I: IntoIterator, + I::Item: Into, + { + self.append_added(dir, files); + self + } + + /// Append the given files to the directory's list of removed files + pub fn append_removed(&mut self, dir: impl Into, files: I) + where + I: IntoIterator, + I::Item: Into, + { + let dir: PathBuf = dir.into(); + let files = files.into_iter().map(Into::into); + + if let Some(dir_files) = self.removed.get_mut(&dir) { + dir_files.extend(files); + } else { + self.removed.insert(dir, files.collect()); + } + } + + /// Wrapper around the `append_removed` method for a builder-style construction of delta's + pub fn with_removed(mut self, dir: impl Into, files: I) -> Self + where + I: IntoIterator, + I::Item: Into, + { + self.append_removed(dir, files); + self + } } impl fmt::Display for Delta { @@ -153,3 +208,44 @@ impl fmt::Display for Delta { write!(f, "+{}-{}", added_count, removed_count) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_union_disjunct_dirs() { + let a = Delta::default() + .with_added("dir_added_1", ["file1", "file2"]) + .with_removed("dir_removed_1", ["file1", "file2"]); + let b = Delta::default() + .with_added("dir_added_3", ["file1", "file2"]) + .with_removed("dir_removed_3", ["file1", "file2"]); + + let expected = Delta::default() + .with_added("dir_added_1", ["file1", "file2"]) + .with_added("dir_added_3", ["file1", "file2"]) + .with_removed("dir_removed_1", ["file1", "file2"]) + .with_removed("dir_removed_3", ["file1", "file2"]); + + assert_eq!(expected, a.union(&b)); + assert_eq!(expected, b.union(&a)); + } + + #[test] + fn test_union_disjunct_files() { + let a = Delta::default() + .with_added("dir_added_1", ["file1", "file2"]) + .with_removed("dir_removed_1", ["file1", "file2"]); + let b = Delta::default() + .with_added("dir_added_1", ["file3", "file4"]) + .with_removed("dir_removed_1", ["file3", "file4"]); + + let expected = Delta::default() + .with_added("dir_added_1", ["file1", "file2", "file3", "file4"]) + .with_removed("dir_removed_1", ["file1", "file2", "file3", "file4"]); + + assert_eq!(expected, a.union(&b)); + assert_eq!(expected, b.union(&a)); + } +} From 22a6e68c7cf2e2e314ba3d0f154452cbf14c6d1c Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Mon, 19 May 2025 11:45:49 +0200 Subject: [PATCH 84/93] feat(backup): implement mutation methods and specialized PartialEq for State --- backup/src/delta.rs | 24 ++------------ backup/src/state.rs | 76 ++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 78 insertions(+), 22 deletions(-) diff --git a/backup/src/delta.rs b/backup/src/delta.rs index dde906b..85b36a0 100644 --- a/backup/src/delta.rs +++ b/backup/src/delta.rs @@ -1,8 +1,4 @@ -use std::{ - borrow::Borrow, - fmt, - path::{Path, PathBuf}, -}; +use std::{borrow::Borrow, fmt, path::PathBuf}; use serde::{Deserialize, Serialize}; @@ -153,14 +149,7 @@ impl Delta { I: IntoIterator, I::Item: Into, { - let dir: PathBuf = dir.into(); - let files = files.into_iter().map(Into::into); - - if let Some(dir_files) = self.added.get_mut(&dir) { - dir_files.extend(files); - } else { - self.added.insert(dir, files.collect()); - } + self.added.append_dir(dir, files); } /// Wrapper around the `append_added` method for a builder-style construction of delta's @@ -179,14 +168,7 @@ impl Delta { I: IntoIterator, I::Item: Into, { - let dir: PathBuf = dir.into(); - let files = files.into_iter().map(Into::into); - - if let Some(dir_files) = self.removed.get_mut(&dir) { - dir_files.extend(files); - } else { - self.removed.insert(dir, files.collect()); - } + self.removed.append_dir(dir, files); } /// Wrapper around the `append_removed` method for a builder-style construction of delta's diff --git a/backup/src/state.rs b/backup/src/state.rs index c0f66b9..b94370f 100644 --- a/backup/src/state.rs +++ b/backup/src/state.rs @@ -11,7 +11,7 @@ use crate::Delta; /// Struct that represents a current state for a backup. This struct acts as a smart pointer around /// a HashMap. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] +#[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct State(HashMap>); impl State { @@ -49,8 +49,52 @@ impl State { pub fn is_empty(&self) -> bool { self.0.values().all(|s| s.is_empty()) } + + pub fn append_dir(&mut self, dir: impl Into, files: I) + where + I: IntoIterator, + I::Item: Into, + { + let dir = dir.into(); + let files = files.into_iter().map(Into::into); + + if let Some(dir_files) = self.0.get_mut(&dir) { + dir_files.extend(files); + } else { + self.0.insert(dir, files.collect()); + } + } + + pub fn with_dir(mut self, dir: impl Into, files: I) -> Self + where + I: IntoIterator, + I::Item: Into, + { + self.append_dir(dir, files); + self + } } +impl PartialEq for State { + fn eq(&self, other: &Self) -> bool { + let self_non_empty = self.0.values().filter(|files| !files.is_empty()).count(); + let other_non_empty = other.0.values().filter(|files| !files.is_empty()).count(); + + if self_non_empty != other_non_empty { + return false; + } + + // If both states have the same number of non-empty directories, then comparing each + // directory of one with the other will only be true if their list of non-empty directories + // is identical. + self.0 + .iter() + .all(|(dir, files)| files.is_empty() || other.0.get(dir).map_or(false, |v| v == files)) + } +} + +impl Eq for State {} + impl From for State where T: IntoIterator, @@ -86,3 +130,33 @@ impl DerefMut for State { &mut self.0 } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_eq() { + let a = State::default().with_dir("dir1", ["file1", "file2"]); + let b = State::default().with_dir("dir1", ["file1", "file2"]); + + assert_eq!(a, b); + + let b = b.with_dir("dir2", ["file3"]); + + assert_ne!(a, b); + } + + #[test] + fn test_eq_empty_dirs() { + let a = State::default().with_dir("dir1", ["file1", "file2"]); + let b = State::default() + .with_dir("dir1", ["file1", "file2"]) + .with_dir("dir2", Vec::::new()); + + assert_eq!(a, b); + + let b = b.with_dir("dir2", ["file3"]); + assert_ne!(a, b); + } +} From 15c4839a819b9dd6ae92c8cc9aa10654e6b47d77 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Mon, 19 May 2025 11:46:19 +0200 Subject: [PATCH 85/93] test(backup): add delta union test --- backup/src/delta.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/backup/src/delta.rs b/backup/src/delta.rs index 85b36a0..cc831ac 100644 --- a/backup/src/delta.rs +++ b/backup/src/delta.rs @@ -230,4 +230,16 @@ mod tests { assert_eq!(expected, a.union(&b)); assert_eq!(expected, b.union(&a)); } + + #[test] + fn test_union_full_revert() { + let a = Delta::default().with_added("dir_1", ["file1", "file2"]); + let b = Delta::default().with_removed("dir_1", ["file1", "file2"]); + + let expected = Delta::default().with_removed("dir_1", ["file1", "file2"]); + assert_eq!(expected, a.union(&b)); + + let expected = Delta::default().with_added("dir_1", ["file1", "file2"]); + assert_eq!(expected, b.union(&a)); + } } From 5f43d7b8b1d7fae17d1f33f5fed357d0809d38fe Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Sat, 24 May 2025 23:23:46 +0200 Subject: [PATCH 86/93] test(backup): add some more delta tests --- backup/src/delta.rs | 52 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/backup/src/delta.rs b/backup/src/delta.rs index cc831ac..993dd3a 100644 --- a/backup/src/delta.rs +++ b/backup/src/delta.rs @@ -242,4 +242,56 @@ mod tests { let expected = Delta::default().with_added("dir_1", ["file1", "file2"]); assert_eq!(expected, b.union(&a)); } + + #[test] + fn test_difference() { + let a = Delta::default() + .with_added("dir1", ["file1", "file2"]) + .with_removed("dir1", ["file3", "file4"]); + let b = Delta::default() + .with_added("dir1", ["file1"]) + .with_removed("dir1", ["file3"]); + let expected = Delta::default() + .with_added("dir1", ["file2"]) + .with_removed("dir1", ["file4"]); + + assert_eq!(a.difference(&b), expected); + assert_eq!(b.difference(&a), Delta::default()); + } + + #[test] + fn test_strict_difference() { + let a = Delta::default() + .with_added("dir1", ["file1", "file2"]) + .with_removed("dir1", ["file3", "file4"]); + let b = Delta::default() + .with_added("dir1", ["file1", "file4"]) + .with_removed("dir1", ["file3"]); + let expected = Delta::default().with_added("dir1", ["file2"]); + + assert_eq!(a.strict_difference(&b), expected); + assert_eq!(b.strict_difference(&a), Delta::default()); + } + + #[test] + fn test_contributions() { + let deltas = [ + Delta::default().with_added("dir1", ["file4"]), + Delta::default().with_added("dir1", ["file1", "file2"]), + Delta::default() + .with_added("dir1", ["file1"]) + .with_added("dir2", ["file3"]), + Delta::default() + .with_added("dir1", ["file2"]) + .with_removed("dir2", ["file3"]), + ]; + let expected = [ + State::default().with_dir("dir1", ["file2"]), + State::default().with_dir("dir1", ["file1"]), + State::default(), + State::default().with_dir("dir1", ["file4"]), + ]; + + assert_eq!(Delta::contributions(deltas.iter().rev()), expected); + } } From f2a0b6230fe419e61710e6dee2a2b6f9554b560e Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Sat, 24 May 2025 23:39:54 +0200 Subject: [PATCH 87/93] refactor(backup): delta contributions function no longer takes reversed input --- backup/src/delta.rs | 22 ++++++++++------------ backup/src/manager/mod.rs | 6 ++---- 2 files changed, 12 insertions(+), 16 deletions(-) diff --git a/backup/src/delta.rs b/backup/src/delta.rs index 993dd3a..95ec391 100644 --- a/backup/src/delta.rs +++ b/backup/src/delta.rs @@ -110,20 +110,19 @@ impl Delta { out } - /// Given a chain of deltas, ordered from last to first, calculate the "contribution" for each - /// state. + /// Given a chain of deltas, calculate the "contribution" for each state. /// - /// The contribution of a delta in a given chain is defined as the parts of the state produced - /// by this chain that are actually provided by this delta. This comes down to calculating the - /// strict difference of this delta and all of its successive deltas. + /// For each delta, its contribution is the part of its added and removed files that isn't + /// overwritten by any of its following deltas. pub fn contributions(deltas: I) -> Vec where I: IntoIterator, + I::IntoIter: DoubleEndedIterator, I::Item: Borrow, { let mut contributions: Vec = Vec::new(); - let mut deltas = deltas.into_iter(); + let mut deltas = deltas.into_iter().rev(); if let Some(first_delta) = deltas.next() { // From last to first, we calculate the strict difference of the delta with the union of all its @@ -138,8 +137,7 @@ impl Delta { } } - // contributions.reverse(); - + contributions.reverse(); contributions } @@ -286,12 +284,12 @@ mod tests { .with_removed("dir2", ["file3"]), ]; let expected = [ - State::default().with_dir("dir1", ["file2"]), - State::default().with_dir("dir1", ["file1"]), - State::default(), State::default().with_dir("dir1", ["file4"]), + State::default(), + State::default().with_dir("dir1", ["file1"]), + State::default().with_dir("dir1", ["file2"]), ]; - assert_eq!(Delta::contributions(deltas.iter().rev()), expected); + assert_eq!(Delta::contributions(deltas), expected); } } diff --git a/backup/src/manager/mod.rs b/backup/src/manager/mod.rs index f8bbc73..f13c69f 100644 --- a/backup/src/manager/mod.rs +++ b/backup/src/manager/mod.rs @@ -229,9 +229,8 @@ where .map(|_| ()), // Incremental backups are exported one by one according to their contribution BackupType::Incremental => { - let contributions = Delta::contributions( - chain.iter().take(index + 1).map(|b| &b.delta).rev(), - ); + let contributions = + Delta::contributions(chain.iter().take(index + 1).map(|b| &b.delta)); let tar_gz = OpenOptions::new() .write(true) @@ -245,7 +244,6 @@ where // overwritten by their successors anyways. for (contribution, backup) in contributions .iter() - .rev() .zip(chain.iter().take(index + 1)) .filter(|(contribution, _)| !contribution.is_empty()) { From b4dce4b69acac5a087e2af1f05e8ba2091dc232d Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Tue, 31 Mar 2026 19:14:17 +0200 Subject: [PATCH 88/93] feat(papermc_api): implement wrapper for PaperMC builds API --- Cargo.lock | 615 +++++++++++++++++++++++++++++++-- Cargo.toml | 3 +- papermc-api/Cargo.toml | 10 + papermc-api/examples/routes.rs | 19 + papermc-api/src/error.rs | 22 ++ papermc-api/src/lib.rs | 217 ++++++++++++ papermc-api/src/models.rs | 90 +++++ 7 files changed, 952 insertions(+), 24 deletions(-) create mode 100644 papermc-api/Cargo.toml create mode 100644 papermc-api/examples/routes.rs create mode 100644 papermc-api/src/error.rs create mode 100644 papermc-api/src/lib.rs create mode 100644 papermc-api/src/models.rs diff --git a/Cargo.lock b/Cargo.lock index a46a1ae..6e569bb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "adler2" @@ -71,7 +71,7 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" dependencies = [ - "windows-sys", + "windows-sys 0.59.0", ] [[package]] @@ -82,7 +82,7 @@ checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" dependencies = [ "anstyle", "once_cell", - "windows-sys", + "windows-sys 0.59.0", ] [[package]] @@ -112,10 +112,16 @@ dependencies = [ ] [[package]] -name = "bitflags" -version = "2.9.0" +name = "base64" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" [[package]] name = "bumpalo" @@ -130,11 +136,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9134a6ef01ce4b366b50689c94f82c14bc72bc5d0386829828a2e2752ef7958c" [[package]] -name = "cc" -version = "1.2.20" +name = "bytes" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04da6a0d40b948dfc4fa8f5bbf402b0fc1a64a28dbf7d12ffd683550f2c1b63a" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cc" +version = "1.2.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1e928d4b69e3077709075a938a05ffbedfa53a84c8f766efbf8220bb1ff60e1" dependencies = [ + "find-msvc-tools", "shlex", ] @@ -205,6 +218,35 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + +[[package]] +name = "cookie_store" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fc4bff745c9b4c7fb1e97b25d13153da2bc7796260141df62378998d070207f" +dependencies = [ + "cookie", + "document-features", + "idna", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "time", + "url", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -220,6 +262,35 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "deranged" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -233,7 +304,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.59.0", ] [[package]] @@ -259,9 +330,15 @@ dependencies = [ "cfg-if", "libc", "libredox", - "windows-sys", + "windows-sys 0.59.0", ] +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + [[package]] name = "flate2" version = "1.1.1" @@ -272,6 +349,26 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "hashbrown" version = "0.15.2" @@ -284,6 +381,22 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + [[package]] name = "iana-time-zone" version = "0.1.63" @@ -308,6 +421,108 @@ dependencies = [ "cc", ] +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + [[package]] name = "indexmap" version = "2.9.0" @@ -369,6 +584,18 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + [[package]] name = "log" version = "0.4.27" @@ -390,6 +617,12 @@ dependencies = [ "adler2", ] +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + [[package]] name = "num-traits" version = "0.2.19" @@ -405,6 +638,16 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "papermc-api" +version = "0.4.2" +dependencies = [ + "chrono", + "serde", + "serde_json", + "ureq", +] + [[package]] name = "pear" version = "0.2.9" @@ -428,6 +671,27 @@ dependencies = [ "syn", ] +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "proc-macro2" version = "1.0.95" @@ -468,6 +732,20 @@ dependencies = [ "bitflags", ] +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "rustix" version = "1.0.5" @@ -478,7 +756,42 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustls" +version = "0.23.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" +dependencies = [ + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", ] [[package]] @@ -488,25 +801,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" [[package]] -name = "ryu" -version = "1.0.20" +name = "serde" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] [[package]] -name = "serde" -version = "1.0.219" +name = "serde_core" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.219" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", @@ -515,14 +832,15 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.140" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ "itoa", "memchr", - "ryu", "serde", + "serde_core", + "zmij", ] [[package]] @@ -559,12 +877,30 @@ dependencies = [ "libc", ] +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + [[package]] name = "strsim" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "syn" version = "2.0.101" @@ -576,6 +912,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "tar" version = "0.4.44" @@ -587,6 +934,47 @@ dependencies = [ "xattr", ] +[[package]] +name = "time" +version = "0.3.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9e442fc33d7fdb45aa9bfeb312c095964abdf596f7567261062b2a7107aaabd" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b36ee98fd31ec7426d599183e8fe26932a8dc1fb76ddb6214d05493377d34ca" + +[[package]] +name = "time-macros" +version = "0.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e552d1249bf61ac2a52db88179fd0673def1e1ad8243a00d9ec9ed71fee3dd" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + [[package]] name = "toml" version = "0.8.22" @@ -643,6 +1031,68 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "ureq" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dea7109cdcd5864d4eeb1b58a1648dc9bf520360d7af16ec26d0a9354bafcfc0" +dependencies = [ + "base64", + "cookie_store", + "flate2", + "log", + "percent-encoding", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "ureq-proto", + "utf8-zero", + "webpki-roots", +] + +[[package]] +name = "ureq-proto" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e994ba84b0bd1b1b0cf92878b7ef898a5c1760108fe7b6010327e274917a808c" +dependencies = [ + "base64", + "http", + "httparse", + "log", +] + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8-zero" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8c0a043c9540bae7c578c88f91dda8bd82e59ae27c21baca69c8b191aaf5a6e" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + [[package]] name = "utf8parse" version = "0.2.2" @@ -655,6 +1105,12 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + [[package]] name = "wasm-bindgen" version = "0.2.100" @@ -713,6 +1169,15 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "webpki-roots" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "windows-core" version = "0.61.0" @@ -772,6 +1237,15 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-sys" version = "0.59.0" @@ -854,6 +1328,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + [[package]] name = "xattr" version = "1.5.0" @@ -869,3 +1349,92 @@ name = "yansi" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml index 4f14331..6317a33 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,8 @@ resolver = "2" members = [ 'backup', - 'alex' + 'alex', + 'papermc-api' ] [workspace.package] diff --git a/papermc-api/Cargo.toml b/papermc-api/Cargo.toml new file mode 100644 index 0000000..9c8a2a0 --- /dev/null +++ b/papermc-api/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "papermc-api" +version.workspace = true +edition.workspace = true + +[dependencies] +serde.workspace = true +chrono.workspace = true +serde_json = "1.0.149" +ureq = { version = "3.3.0", features = ["json"] } diff --git a/papermc-api/examples/routes.rs b/papermc-api/examples/routes.rs new file mode 100644 index 0000000..5628546 --- /dev/null +++ b/papermc-api/examples/routes.rs @@ -0,0 +1,19 @@ +fn main() { + let client = papermc_api::Client::new(); + let projects = client.projects().unwrap(); + + for project in projects { + println!("project: {:?}", project); + } + + let versions = client.project("paper").versions().unwrap(); + for version in versions { + println!("version: {:?}", version); + } + + let latest = client.project("paper").version("1.21.1").latest().unwrap(); + println!("latest: {:?}", latest); + + let builds = client.project("paper").version("1.21.10").builds().unwrap(); + println!("number of builds: {}", builds.len()); +} diff --git a/papermc-api/src/error.rs b/papermc-api/src/error.rs new file mode 100644 index 0000000..3e9fa21 --- /dev/null +++ b/papermc-api/src/error.rs @@ -0,0 +1,22 @@ +#[derive(Debug)] +pub enum Error { + Ureq(ureq::Error), + BadBody, +} + +impl std::error::Error for Error {} + +impl From for Error { + fn from(value: ureq::Error) -> Self { + Self::Ureq(value) + } +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Ureq(err) => err.fmt(f), + Self::BadBody => f.write_str("bad response body"), + } + } +} diff --git a/papermc-api/src/lib.rs b/papermc-api/src/lib.rs new file mode 100644 index 0000000..dbb0cf9 --- /dev/null +++ b/papermc-api/src/lib.rs @@ -0,0 +1,217 @@ +use serde_json::Value; + +use crate::{ + error::Error, + models::{Build, BuildCommit, BuildDownload, Java, Project, Version}, +}; + +mod error; +mod models; + +pub struct Client { + agent: ureq::Agent, +} + +pub const BASE_URL: &str = "https://fill.papermc.io/v3"; + +impl Client { + pub fn new() -> Self { + Self { + agent: ureq::agent(), + } + } + + pub fn projects(&self) -> Result, Error> { + let mut res = self.agent.get(format!("{}/projects", BASE_URL)).call()?; + let body_json: Value = res.body_mut().read_json()?; + + let projects = body_json["projects"].as_array().ok_or(Error::BadBody)?; + projects + .into_iter() + .map(|p| { + Ok(Project { + id: p["project"]["id"] + .as_str() + .map(|s| s.to_string()) + .ok_or(Error::BadBody)?, + name: p["project"]["name"] + .as_str() + .map(|s| s.to_string()) + .ok_or(Error::BadBody)?, + // Flatten map of versions into one array + versions: p["versions"] + .as_object() + .ok_or(Error::BadBody)? + .into_iter() + .map(|(_, versions)| versions.as_array().ok_or(Error::BadBody)) + // Collect into error to propagate error of any of the versions + .collect::, _>>()? + .into_iter() + .flatten() + .map(|v| v.as_str().ok_or(Error::BadBody).map(|s| s.to_string())) + .collect::>()?, + }) + }) + .collect() + } + + pub fn project<'a>(&'a self, project: &'a str) -> ProjectQuery<'a> { + return ProjectQuery { + agent: &self.agent, + project, + }; + } +} + +pub struct ProjectQuery<'a> { + agent: &'a ureq::Agent, + project: &'a str, +} + +impl<'a> ProjectQuery<'a> { + pub fn versions(&self) -> Result, Error> { + let mut res = self + .agent + .get(format!("{}/projects/{}/versions", BASE_URL, self.project)) + .call()?; + let body_json: Value = res.body_mut().read_json()?; + + let versions = body_json["versions"].as_array().ok_or(Error::BadBody)?; + versions + .into_iter() + .map(|v| { + Ok(Version { + id: v["version"]["id"] + .as_str() + .map(String::from) + .ok_or(Error::BadBody)?, + support_status: v["version"]["support"]["status"] + .as_str() + .ok_or(Error::BadBody)? + .parse() + .map_err(|_| Error::BadBody)?, + java: Java { + minimum_version: v["version"]["java"]["version"]["minimum"] + .as_u64() + .ok_or(Error::BadBody)?, + recommended_flags: v["version"]["java"]["flags"]["recommended"] + .as_array() + .ok_or(Error::BadBody)? + .into_iter() + .map(|v| v.as_str().map(String::from).ok_or(Error::BadBody)) + .collect::>()?, + }, + builds: v["builds"] + .as_array() + .ok_or(Error::BadBody)? + .into_iter() + .map(|v| v.as_u64().ok_or(Error::BadBody)) + .collect::>()?, + }) + }) + .collect() + } + + pub fn version(&self, version: &'a str) -> VersionQuery<'a> { + VersionQuery { + agent: self.agent, + project: self.project, + version, + } + } +} + +pub struct VersionQuery<'a> { + agent: &'a ureq::Agent, + project: &'a str, + version: &'a str, +} + +impl<'a> VersionQuery<'a> { + pub fn latest(&self) -> Result { + let mut res = self + .agent + .get(format!( + "{}/projects/{}/versions/{}/builds/latest", + BASE_URL, self.project, self.version + )) + .call()?; + let body_json: Value = res.body_mut().read_json()?; + + parse_build_json(&body_json) + } + + pub fn builds(&self) -> Result, Error> { + let mut res = self + .agent + .get(format!( + "{}/projects/{}/versions/{}/builds", + BASE_URL, self.project, self.version + )) + .call()?; + let body_json: Value = res.body_mut().read_json()?; + + body_json + .as_array() + .ok_or(Error::BadBody)? + .into_iter() + .map(parse_build_json) + .collect() + } +} + +fn parse_build_json(value: &Value) -> Result { + Ok(Build { + id: value["id"].as_u64().ok_or(Error::BadBody)?, + time: chrono::DateTime::parse_from_rfc3339(value["time"].as_str().ok_or(Error::BadBody)?) + .map_err(|_| Error::BadBody)? + .into(), + channel: value["channel"] + .as_str() + .ok_or(Error::BadBody)? + .parse() + .map_err(|_| Error::BadBody)?, + commits: value["commits"] + .as_array() + .ok_or(Error::BadBody)? + .into_iter() + .map(|build| { + Ok(BuildCommit { + sha: build["sha"].as_str().ok_or(Error::BadBody)?.to_string(), + time: chrono::DateTime::parse_from_rfc3339( + build["time"].as_str().ok_or(Error::BadBody)?, + ) + .map_err(|_| Error::BadBody)? + .into(), + message: build["message"].as_str().ok_or(Error::BadBody)?.to_string(), + }) + }) + .collect::>()?, + downloads: value["downloads"] + .as_object() + .ok_or(Error::BadBody)? + .into_iter() + .map(|(key, build)| { + Ok(( + key.to_string(), + BuildDownload { + name: build["name"].as_str().ok_or(Error::BadBody)?.to_string(), + size: build["size"].as_u64().ok_or(Error::BadBody)?, + url: build["url"].as_str().ok_or(Error::BadBody)?.to_string(), + checksums: build["checksums"] + .as_object() + .ok_or(Error::BadBody)? + .into_iter() + .map(|(key, value)| { + Ok(( + key.to_string(), + value.as_str().ok_or(Error::BadBody)?.to_string(), + )) + }) + .collect::>()?, + }, + )) + }) + .collect::>()?, + }) +} diff --git a/papermc-api/src/models.rs b/papermc-api/src/models.rs new file mode 100644 index 0000000..c73ecf9 --- /dev/null +++ b/papermc-api/src/models.rs @@ -0,0 +1,90 @@ +use std::{collections::HashMap, str::FromStr}; + +use chrono::{DateTime, Utc}; + +#[derive(Debug)] +pub struct Project { + pub id: String, + pub name: String, + pub versions: Vec, +} + +#[derive(Debug)] +pub enum SupportStatus { + Supported, + Unsupported, +} + +pub struct SupportStatusParseError; + +impl FromStr for SupportStatus { + type Err = SupportStatusParseError; + + fn from_str(s: &str) -> Result { + match s { + "SUPPORTED" => Ok(Self::Supported), + "UNSUPPORTED" => Ok(Self::Unsupported), + _ => Err(SupportStatusParseError), + } + } +} + +#[derive(Debug)] +pub struct Java { + pub minimum_version: u64, + pub recommended_flags: Vec, +} + +#[derive(Debug)] +pub struct Version { + pub id: String, + pub support_status: SupportStatus, + pub java: Java, + pub builds: Vec, +} + +#[derive(Debug)] +pub struct Build { + pub id: u64, + pub time: DateTime, + pub channel: BuildChannel, + pub commits: Vec, + pub downloads: HashMap, +} + +#[derive(Debug)] +pub enum BuildChannel { + Alpha, + Beta, + Stable, +} + +pub struct BuildChannelParseError; + +impl FromStr for BuildChannel { + type Err = BuildChannelParseError; + + fn from_str(s: &str) -> Result { + match s { + "STABLE" => Ok(Self::Stable), + "BETA" => Ok(Self::Beta), + "ALPHA" => Ok(Self::Alpha), + _ => Err(BuildChannelParseError), + } + } +} + +#[derive(Debug)] +pub struct BuildCommit { + pub sha: String, + pub time: DateTime, + pub message: String, +} + +#[derive(Debug)] +pub struct BuildDownload { + pub name: String, + pub checksums: HashMap, + pub size: u64, + pub url: String, +} From f2c4b97626a7fb6beba99077fe957142d3b1ef86 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Mon, 13 Apr 2026 22:31:24 +0200 Subject: [PATCH 89/93] feat(papermc_api): add routes for per-version, project and build information --- papermc-api/src/lib.rs | 158 ++++++++++++++++++++++---------------- papermc-api/src/models.rs | 21 ++++- 2 files changed, 111 insertions(+), 68 deletions(-) diff --git a/papermc-api/src/lib.rs b/papermc-api/src/lib.rs index dbb0cf9..d9881c8 100644 --- a/papermc-api/src/lib.rs +++ b/papermc-api/src/lib.rs @@ -1,9 +1,8 @@ use serde_json::Value; -use crate::{ - error::Error, - models::{Build, BuildCommit, BuildDownload, Java, Project, Version}, -}; +pub use error::Error; + +use crate::models::{Build, BuildCommit, BuildDownload, Java, Project, Version}; mod error; mod models; @@ -26,33 +25,7 @@ impl Client { let body_json: Value = res.body_mut().read_json()?; let projects = body_json["projects"].as_array().ok_or(Error::BadBody)?; - projects - .into_iter() - .map(|p| { - Ok(Project { - id: p["project"]["id"] - .as_str() - .map(|s| s.to_string()) - .ok_or(Error::BadBody)?, - name: p["project"]["name"] - .as_str() - .map(|s| s.to_string()) - .ok_or(Error::BadBody)?, - // Flatten map of versions into one array - versions: p["versions"] - .as_object() - .ok_or(Error::BadBody)? - .into_iter() - .map(|(_, versions)| versions.as_array().ok_or(Error::BadBody)) - // Collect into error to propagate error of any of the versions - .collect::, _>>()? - .into_iter() - .flatten() - .map(|v| v.as_str().ok_or(Error::BadBody).map(|s| s.to_string())) - .collect::>()?, - }) - }) - .collect() + projects.into_iter().map(parse_project_json).collect() } pub fn project<'a>(&'a self, project: &'a str) -> ProjectQuery<'a> { @@ -69,6 +42,16 @@ pub struct ProjectQuery<'a> { } impl<'a> ProjectQuery<'a> { + pub fn info(&self) -> Result { + let mut res = self + .agent + .get(format!("{}/projects/{}", BASE_URL, self.project)) + .call()?; + let body_json: Value = res.body_mut().read_json()?; + + parse_project_json(&body_json) + } + pub fn versions(&self) -> Result, Error> { let mut res = self .agent @@ -77,39 +60,7 @@ impl<'a> ProjectQuery<'a> { let body_json: Value = res.body_mut().read_json()?; let versions = body_json["versions"].as_array().ok_or(Error::BadBody)?; - versions - .into_iter() - .map(|v| { - Ok(Version { - id: v["version"]["id"] - .as_str() - .map(String::from) - .ok_or(Error::BadBody)?, - support_status: v["version"]["support"]["status"] - .as_str() - .ok_or(Error::BadBody)? - .parse() - .map_err(|_| Error::BadBody)?, - java: Java { - minimum_version: v["version"]["java"]["version"]["minimum"] - .as_u64() - .ok_or(Error::BadBody)?, - recommended_flags: v["version"]["java"]["flags"]["recommended"] - .as_array() - .ok_or(Error::BadBody)? - .into_iter() - .map(|v| v.as_str().map(String::from).ok_or(Error::BadBody)) - .collect::>()?, - }, - builds: v["builds"] - .as_array() - .ok_or(Error::BadBody)? - .into_iter() - .map(|v| v.as_u64().ok_or(Error::BadBody)) - .collect::>()?, - }) - }) - .collect() + versions.into_iter().map(parse_version_json).collect() } pub fn version(&self, version: &'a str) -> VersionQuery<'a> { @@ -128,17 +79,17 @@ pub struct VersionQuery<'a> { } impl<'a> VersionQuery<'a> { - pub fn latest(&self) -> Result { + pub fn info(&self) -> Result { let mut res = self .agent .get(format!( - "{}/projects/{}/versions/{}/builds/latest", + "{}/projects/{}/versions/{}", BASE_URL, self.project, self.version )) .call()?; let body_json: Value = res.body_mut().read_json()?; - parse_build_json(&body_json) + parse_version_json(&body_json) } pub fn builds(&self) -> Result, Error> { @@ -158,6 +109,79 @@ impl<'a> VersionQuery<'a> { .map(parse_build_json) .collect() } + + pub fn build(&self, build: &str) -> Result { + let mut res = self + .agent + .get(format!( + "{}/projects/{}/versions/{}/builds/{}", + BASE_URL, self.project, self.version, build + )) + .call()?; + let body_json: Value = res.body_mut().read_json()?; + + parse_build_json(&body_json) + } + + pub fn latest(&self) -> Result { + return self.build("latest"); + } +} + +fn parse_project_json(value: &Value) -> Result { + Ok(Project { + id: value["project"]["id"] + .as_str() + .map(|s| s.to_string()) + .ok_or(Error::BadBody)?, + name: value["project"]["name"] + .as_str() + .map(|s| s.to_string()) + .ok_or(Error::BadBody)?, + // Flatten map of versions into one array + versions: value["versions"] + .as_object() + .ok_or(Error::BadBody)? + .into_iter() + .map(|(_, versions)| versions.as_array().ok_or(Error::BadBody)) + // Collect into error to propagate error of any of the versions + .collect::, _>>()? + .into_iter() + .flatten() + .map(|v| v.as_str().ok_or(Error::BadBody).map(|s| s.to_string())) + .collect::>()?, + }) +} + +fn parse_version_json(value: &Value) -> Result { + Ok(Version { + id: value["version"]["id"] + .as_str() + .map(String::from) + .ok_or(Error::BadBody)?, + support_status: value["version"]["support"]["status"] + .as_str() + .ok_or(Error::BadBody)? + .parse() + .map_err(|_| Error::BadBody)?, + java: Java { + minimum_version: value["version"]["java"]["version"]["minimum"] + .as_u64() + .ok_or(Error::BadBody)?, + recommended_flags: value["version"]["java"]["flags"]["recommended"] + .as_array() + .ok_or(Error::BadBody)? + .into_iter() + .map(|v| v.as_str().map(String::from).ok_or(Error::BadBody)) + .collect::>()?, + }, + builds: value["builds"] + .as_array() + .ok_or(Error::BadBody)? + .into_iter() + .map(|v| v.as_u64().ok_or(Error::BadBody)) + .collect::>()?, + }) } fn parse_build_json(value: &Value) -> Result { diff --git a/papermc-api/src/models.rs b/papermc-api/src/models.rs index c73ecf9..c28f7e9 100644 --- a/papermc-api/src/models.rs +++ b/papermc-api/src/models.rs @@ -1,4 +1,4 @@ -use std::{collections::HashMap, str::FromStr}; +use std::{collections::HashMap, fmt::Display, str::FromStr}; use chrono::{DateTime, Utc}; @@ -29,6 +29,15 @@ impl FromStr for SupportStatus { } } +impl Display for SupportStatus { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Supported => f.write_str("SUPPORTED"), + Self::Unsupported => f.write_str("UNSUPPORTED"), + } + } +} + #[derive(Debug)] pub struct Java { pub minimum_version: u64, @@ -74,6 +83,16 @@ impl FromStr for BuildChannel { } } +impl Display for BuildChannel { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Alpha => f.write_str("ALPHA"), + Self::Beta => f.write_str("BETA"), + Self::Stable => f.write_str("STABLE"), + } + } +} + #[derive(Debug)] pub struct BuildCommit { pub sha: String, From d38cb4ce39f1e532c927fe926e1b1ab36d5ae10c Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Mon, 13 Apr 2026 22:44:17 +0200 Subject: [PATCH 90/93] feat(cli): add commands to list, view and download PaperMC builds --- CHANGELOG.md | 6 ++ Cargo.lock | 2 + alex/Cargo.toml | 2 + alex/src/cli/mod.rs | 5 + alex/src/cli/papermc.rs | 213 ++++++++++++++++++++++++++++++++++++++++ alex/src/error.rs | 2 + 6 files changed, 230 insertions(+) create mode 100644 alex/src/cli/papermc.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b6fa3a..4b9fb7d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://git.rustybever.be/Chewing_Bever/alex/src/branch/dev) +### Added + +* CLI commands to interact with the PaperMC API + * list and view available Minecraft versions and builds per version + * Download jars to simplify manual server updating + ## [0.4.2](https://git.rustybever.be/Chewing_Bever/alex/src/tag/0.4.2) ### Fixed diff --git a/Cargo.lock b/Cargo.lock index 6e569bb..c1afa68 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -16,8 +16,10 @@ dependencies = [ "chrono", "clap", "figment", + "papermc-api", "serde", "signal-hook", + "ureq", ] [[package]] diff --git a/alex/Cargo.toml b/alex/Cargo.toml index 7f7cac8..71c1c93 100644 --- a/alex/Cargo.toml +++ b/alex/Cargo.toml @@ -6,6 +6,7 @@ edition.workspace = true [dependencies] backup = { path = "../backup" } +papermc-api = { path = "../papermc-api" } chrono.workspace = true serde.workspace = true @@ -13,3 +14,4 @@ serde.workspace = true clap = { version = "4.5.37", features = ["derive", "env"] } signal-hook = "0.3.15" figment = { version = "0.10.10", features = ["env", "toml"] } +ureq = "3.3.0" diff --git a/alex/src/cli/mod.rs b/alex/src/cli/mod.rs index 47d12f3..92724ea 100644 --- a/alex/src/cli/mod.rs +++ b/alex/src/cli/mod.rs @@ -1,5 +1,6 @@ mod backup; mod config; +mod papermc; mod run; use std::{path::PathBuf, str::FromStr}; @@ -72,6 +73,9 @@ pub enum Commands { Run(RunCli), /// Interact with the backup system without starting a server Backup(BackupArgs), + /// Interact with the PaperMC API and download new JARs + #[command(name = "papermc")] + PaperMC(papermc::Cli), } impl Cli { @@ -81,6 +85,7 @@ impl Cli { match &self.command { Commands::Run(args) => args.run(self, &config), Commands::Backup(args) => Ok(args.run(&config)?), + Commands::PaperMC(cli) => cli.run(), } } diff --git a/alex/src/cli/papermc.rs b/alex/src/cli/papermc.rs new file mode 100644 index 0000000..66fc724 --- /dev/null +++ b/alex/src/cli/papermc.rs @@ -0,0 +1,213 @@ +use std::path::{Path, PathBuf}; + +use chrono::Local; +use clap::{Args, Subcommand}; + +#[derive(Args)] +pub struct Cli { + #[command(subcommand)] + pub command: Commands, +} + +#[derive(Subcommand)] +pub enum Commands { + /// Show information or a specific version or build + Show(ShowArgs), + /// List the available versions, or builds for a specific version + List(ListArgs), + /// Download the jar for a specific build + Download(DownloadBuildArgs), +} + +#[derive(Args)] +pub struct ShowArgs { + /// Version to show information for + version: String, + + /// Build within version to show information for + build: Option, +} + +#[derive(Args)] +pub struct ListArgs { + /// If provided, list the available builds for this version + version: Option, +} + +#[derive(Args)] +pub struct DownloadBuildArgs { + /// Version of build to download + version: String, + /// Build number for build to download + build: String, + + /// Path to store the new JAR file in; stores JAR in the local directory if not specified + #[arg(short, long, value_name = "OUT_PATH")] + out: Option, +} + +impl Cli { + pub fn run(&self) -> crate::Result<()> { + match &self.command { + Commands::Show(ShowArgs { + version, + build: None, + }) => show_version(&version), + Commands::List(ListArgs { version: None }) => list_versions(), + Commands::List(ListArgs { + version: Some(version), + }) => list_builds(version), + Commands::Show(ShowArgs { + version, + build: Some(build), + }) => show_build(&version, &build), + Commands::Download(args) => { + download_build(&args.version, &args.build, args.out.as_deref()) + } + } + + Ok(()) + } +} + +fn show_version(version_str: &str) { + let client = papermc_api::Client::new(); + + let version = match client.project("paper").version(version_str).info() { + Ok(version) => version, + Err(err) => { + println!("failed to query API: {err}"); + return; + } + }; + + println!("id : {}", version.id); + println!("status : {}", version.support_status); + println!("builds : {}", version.builds.len()); + println!("Min. Java : {}", version.java.minimum_version); + println!("Java flags: {}", version.java.recommended_flags.join(" ")) +} + +fn show_build(version_str: &str, build_str: &str) { + let client = papermc_api::Client::new(); + + let build = match client + .project("paper") + .version(version_str) + .build(build_str) + { + Ok(build) => build, + Err(err) => { + println!("failed to query API: {err}"); + return; + } + }; + + println!("id : {}", build.id); + println!("time : {}", build.time.with_timezone(&Local)); + println!("channel: {}", build.channel); + println!("commits:"); + + for commit in build.commits { + println!("- SHA : {}", commit.sha); + println!(" time : {}", commit.time.with_timezone(&Local)); + println!(" message: {}", commit.message); + } + + println!("downloads:"); + + for (name, download) in build.downloads.iter() { + println!(" {name}:"); + println!(" name: {}", download.name); + println!(" size: {}", download.size); + println!(" URL : {}", download.url); + println!(" checksums:"); + + for (name, value) in download.checksums.iter() { + println!(" - {}: {}", name, value); + } + } +} + +fn list_versions() { + let client = papermc_api::Client::new(); + + match client.project("paper").versions() { + Ok(versions) => { + for version in versions { + println!("{} ({})", version.id, version.support_status); + } + } + Err(err) => { + println!("failed to query API: {err}"); + } + } +} + +fn list_builds(version: &str) { + let client = papermc_api::Client::new(); + + match client.project("paper").version(version).builds() { + Ok(builds) => { + for build in builds { + println!("- id : {}", build.id); + println!(" time : {}", build.time.with_timezone(&Local)); + println!(" channel: {}", build.channel); + } + } + Err(err) => { + println!("failed to query API: {err}"); + } + } +} + +fn download_build(version: &str, build: &str, out_path: Option<&Path>) { + let client = papermc_api::Client::new(); + let build = match client.project("paper").version(version).build(build) { + Ok(build) => build, + Err(err) => { + println!("failed to query API: {err}"); + return; + } + }; + + let filename = format!("paper-{}-{}.jar", version, build.id); + let dest_path = match out_path { + Some(path) if path.is_dir() => path.join(filename), + Some(path) => path.to_path_buf(), + None => PathBuf::from(filename), + }; + + let download_url = match build + .downloads + .get("server:default") + .or(build.downloads.values().next()) + { + Some(download) => &download.url, + None => { + println!("no download URLs found for build."); + return; + } + }; + + let mut f = match std::fs::File::create(dest_path) { + Ok(f) => f, + Err(err) => { + println!("failed to create destination file: {err}"); + return; + } + }; + + let mut res = match ureq::get(download_url).call() { + Ok(res) => res, + Err(err) => { + println!("failed to download file: {err}"); + return; + } + }; + + let mut reader = res.body_mut().as_reader(); + if let Err(err) = std::io::copy(&mut reader, &mut f) { + println!("failed to download file: {err}"); + } +} diff --git a/alex/src/error.rs b/alex/src/error.rs index 0ea7532..b46a49e 100644 --- a/alex/src/error.rs +++ b/alex/src/error.rs @@ -6,6 +6,7 @@ pub type Result = std::result::Result; pub enum Error { IO(io::Error), Figment(figment::Error), + Other(Box), } impl fmt::Display for Error { @@ -13,6 +14,7 @@ impl fmt::Display for Error { match self { Error::IO(err) => write!(fmt, "{}", err), Error::Figment(err) => write!(fmt, "{}", err), + Error::Other(err) => write!(fmt, "{}", err), } } } From 7987bef3849e12557a533f69c1bc1f151021d0eb Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Tue, 14 Apr 2026 21:04:57 +0200 Subject: [PATCH 91/93] refactor(papermc_api): fix Clippy diagnostics --- papermc-api/src/lib.rs | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/papermc-api/src/lib.rs b/papermc-api/src/lib.rs index d9881c8..1de90b5 100644 --- a/papermc-api/src/lib.rs +++ b/papermc-api/src/lib.rs @@ -13,6 +13,12 @@ pub struct Client { pub const BASE_URL: &str = "https://fill.papermc.io/v3"; +impl Default for Client { + fn default() -> Self { + Self::new() + } +} + impl Client { pub fn new() -> Self { Self { @@ -25,14 +31,14 @@ impl Client { let body_json: Value = res.body_mut().read_json()?; let projects = body_json["projects"].as_array().ok_or(Error::BadBody)?; - projects.into_iter().map(parse_project_json).collect() + projects.iter().map(parse_project_json).collect() } pub fn project<'a>(&'a self, project: &'a str) -> ProjectQuery<'a> { - return ProjectQuery { + ProjectQuery { agent: &self.agent, project, - }; + } } } @@ -60,7 +66,7 @@ impl<'a> ProjectQuery<'a> { let body_json: Value = res.body_mut().read_json()?; let versions = body_json["versions"].as_array().ok_or(Error::BadBody)?; - versions.into_iter().map(parse_version_json).collect() + versions.iter().map(parse_version_json).collect() } pub fn version(&self, version: &'a str) -> VersionQuery<'a> { @@ -105,7 +111,7 @@ impl<'a> VersionQuery<'a> { body_json .as_array() .ok_or(Error::BadBody)? - .into_iter() + .iter() .map(parse_build_json) .collect() } @@ -124,7 +130,7 @@ impl<'a> VersionQuery<'a> { } pub fn latest(&self) -> Result { - return self.build("latest"); + self.build("latest") } } @@ -142,7 +148,7 @@ fn parse_project_json(value: &Value) -> Result { versions: value["versions"] .as_object() .ok_or(Error::BadBody)? - .into_iter() + .iter() .map(|(_, versions)| versions.as_array().ok_or(Error::BadBody)) // Collect into error to propagate error of any of the versions .collect::, _>>()? @@ -171,14 +177,14 @@ fn parse_version_json(value: &Value) -> Result { recommended_flags: value["version"]["java"]["flags"]["recommended"] .as_array() .ok_or(Error::BadBody)? - .into_iter() + .iter() .map(|v| v.as_str().map(String::from).ok_or(Error::BadBody)) .collect::>()?, }, builds: value["builds"] .as_array() .ok_or(Error::BadBody)? - .into_iter() + .iter() .map(|v| v.as_u64().ok_or(Error::BadBody)) .collect::>()?, }) @@ -198,7 +204,7 @@ fn parse_build_json(value: &Value) -> Result { commits: value["commits"] .as_array() .ok_or(Error::BadBody)? - .into_iter() + .iter() .map(|build| { Ok(BuildCommit { sha: build["sha"].as_str().ok_or(Error::BadBody)?.to_string(), @@ -214,7 +220,7 @@ fn parse_build_json(value: &Value) -> Result { downloads: value["downloads"] .as_object() .ok_or(Error::BadBody)? - .into_iter() + .iter() .map(|(key, build)| { Ok(( key.to_string(), From f0d552266142ba20addff7fb71844d613f8caf57 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Tue, 14 Apr 2026 21:30:27 +0200 Subject: [PATCH 92/93] chore: update dependencies; bump version to 0.5.0 --- CHANGELOG.md | 2 + Cargo.lock | 425 ++++++++++++++++++++++++++------------------------- Cargo.toml | 2 +- Justfile | 3 +- 4 files changed, 218 insertions(+), 214 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b9fb7d..aaec0f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://git.rustybever.be/Chewing_Bever/alex/src/branch/dev) +## [0.5.0](https://git.rustybever.be/Chewing_Bever/alex/src/tag/0.5.0) + ### Added * CLI commands to interact with the PaperMC API diff --git a/Cargo.lock b/Cargo.lock index c1afa68..a71e4e1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,13 +4,13 @@ version = 4 [[package]] name = "adler2" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "alex" -version = "0.4.2" +version = "0.5.0" dependencies = [ "backup", "chrono", @@ -22,12 +22,6 @@ dependencies = [ "ureq", ] -[[package]] -name = "android-tzdata" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" - [[package]] name = "android_system_properties" version = "0.1.5" @@ -39,9 +33,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.18" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" dependencies = [ "anstyle", "anstyle-parse", @@ -54,57 +48,57 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.10" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anstyle-parse" -version = "0.2.6" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.2" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "anstyle-wincon" -version = "3.0.7" +version = "3.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", - "once_cell", - "windows-sys 0.59.0", + "once_cell_polyfill", + "windows-sys 0.61.2", ] [[package]] name = "atomic" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d818003e740b63afc82337e3160717f4f63078720a810b7b903e70a5d1d2994" +checksum = "a89cbf775b137e9b968e67227ef7f775587cde3fd31b0d8599dbd0f598a48340" dependencies = [ "bytemuck", ] [[package]] name = "autocfg" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "backup" -version = "0.4.2" +version = "0.5.0" dependencies = [ "chrono", "flate2", @@ -121,21 +115,21 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bitflags" -version = "2.11.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" [[package]] name = "bumpalo" -version = "3.17.0" +version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" [[package]] name = "bytemuck" -version = "1.23.0" +version = "1.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9134a6ef01ce4b366b50689c94f82c14bc72bc5d0386829828a2e2752ef7958c" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" [[package]] name = "bytes" @@ -145,9 +139,9 @@ checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "cc" -version = "1.2.58" +version = "1.2.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1e928d4b69e3077709075a938a05ffbedfa53a84c8f766efbf8220bb1ff60e1" +checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20" dependencies = [ "find-msvc-tools", "shlex", @@ -155,17 +149,16 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.0" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "chrono" -version = "0.4.41" +version = "0.4.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" dependencies = [ - "android-tzdata", "iana-time-zone", "js-sys", "num-traits", @@ -176,9 +169,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.37" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eccb054f56cbd38340b380d4a8e69ef1f02f1af43db2f0cc817a4774d80ae071" +checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" dependencies = [ "clap_builder", "clap_derive", @@ -186,9 +179,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.37" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efd9466fac8543255d3b1fcad4762c5e116ffe808c8a3043d4263cd4fd4862a2" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" dependencies = [ "anstream", "anstyle", @@ -198,9 +191,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.32" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" +checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a" dependencies = [ "heck", "proc-macro2", @@ -210,15 +203,15 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.4" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] name = "colorchoice" -version = "1.0.3" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "cookie" @@ -233,9 +226,9 @@ dependencies = [ [[package]] name = "cookie_store" -version = "0.22.0" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fc4bff745c9b4c7fb1e97b25d13153da2bc7796260141df62378998d070207f" +checksum = "15b2c103cf610ec6cae3da84a766285b42fd16aad564758459e6ecf128c75206" dependencies = [ "cookie", "document-features", @@ -257,18 +250,18 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "crc32fast" -version = "1.4.2" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" dependencies = [ "cfg-if", ] [[package]] name = "deranged" -version = "0.5.5" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" dependencies = [ "powerfmt", ] @@ -301,12 +294,12 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.11" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -325,14 +318,13 @@ dependencies = [ [[package]] name = "filetime" -version = "0.2.25" +version = "0.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586" +checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db" dependencies = [ "cfg-if", "libc", "libredox", - "windows-sys 0.59.0", ] [[package]] @@ -343,9 +335,9 @@ checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "flate2" -version = "1.1.1" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" dependencies = [ "crc32fast", "miniz_oxide", @@ -373,9 +365,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.15.2" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" [[package]] name = "heck" @@ -401,9 +393,9 @@ checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "iana-time-zone" -version = "0.1.63" +version = "0.1.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -425,12 +417,13 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" dependencies = [ "displaydoc", "potential_utf", + "utf8_iter", "yoke", "zerofrom", "zerovec", @@ -438,9 +431,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" dependencies = [ "displaydoc", "litemap", @@ -451,9 +444,9 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" dependencies = [ "icu_collections", "icu_normalizer_data", @@ -465,15 +458,15 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" [[package]] name = "icu_properties" -version = "2.1.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" dependencies = [ "icu_collections", "icu_locale_core", @@ -485,15 +478,15 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.1.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" [[package]] name = "icu_provider" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" dependencies = [ "displaydoc", "icu_locale_core", @@ -527,9 +520,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.9.0" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", "hashbrown", @@ -543,21 +536,21 @@ checksum = "c8fae54786f62fb2918dcfae3d568594e50eb9b5c25bf04371af6fe7516452fb" [[package]] name = "is_terminal_polyfill" -version = "1.70.1" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] name = "itoa" -version = "1.0.15" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "js-sys" -version = "0.3.77" +version = "0.3.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca" dependencies = [ "once_cell", "wasm-bindgen", @@ -565,32 +558,33 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.172" +version = "0.2.185" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" +checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f" [[package]] name = "libredox" -version = "0.1.3" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" dependencies = [ "bitflags", "libc", + "plain", "redox_syscall", ] [[package]] name = "linux-raw-sys" -version = "0.9.4" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" [[package]] name = "litrs" @@ -600,30 +594,31 @@ checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" [[package]] name = "log" -version = "0.4.27" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "memchr" -version = "2.7.4" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "miniz_oxide" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", + "simd-adler32", ] [[package]] name = "num-conv" -version = "0.1.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" [[package]] name = "num-traits" @@ -636,13 +631,19 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "papermc-api" -version = "0.4.2" +version = "0.5.0" dependencies = [ "chrono", "serde", @@ -680,10 +681,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] -name = "potential_utf" -version = "0.1.4" +name = "plain" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" dependencies = [ "zerovec", ] @@ -696,9 +703,9 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "proc-macro2" -version = "1.0.95" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] @@ -718,18 +725,18 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.40" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] [[package]] name = "redox_syscall" -version = "0.5.11" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2f103c6d277498fbceb16e84d317e2a400f160f46904d5f5410848c829511a3" +checksum = "f450ad9c3b1da563fb6948a8e0fb0fb9269711c9c73d9ea1de5058c79c8d643a" dependencies = [ "bitflags", ] @@ -750,22 +757,22 @@ dependencies = [ [[package]] name = "rustix" -version = "1.0.5" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d97817398dd4bb2e6da002002db259209759911da105da92bec29ccb12cf58bf" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ "bitflags", "errno", "libc", "linux-raw-sys", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "rustls" -version = "0.23.37" +version = "0.23.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" +checksum = "69f9466fb2c14ea04357e91413efb882e2a6d4a406e625449bc0a5d360d53a21" dependencies = [ "log", "once_cell", @@ -787,9 +794,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.10" +version = "0.103.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef" +checksum = "8279bb85272c9f10811ae6a6c547ff594d6a7f3c6c6b02ee9726d1d0dcfcdd06" dependencies = [ "ring", "rustls-pki-types", @@ -798,9 +805,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.20" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "serde" @@ -847,9 +854,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "0.6.8" +version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" dependencies = [ "serde", ] @@ -862,9 +869,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook" -version = "0.3.17" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" dependencies = [ "libc", "signal-hook-registry", @@ -872,13 +879,20 @@ dependencies = [ [[package]] name = "signal-hook-registry" -version = "1.4.5" +version = "1.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" dependencies = [ + "errno", "libc", ] +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + [[package]] name = "smallvec" version = "1.15.1" @@ -905,9 +919,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.101" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -927,9 +941,9 @@ dependencies = [ [[package]] name = "tar" -version = "0.4.44" +version = "0.4.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a" +checksum = "22692a6476a21fa75fdfc11d452fda482af402c008cdbaf3476414e122040973" dependencies = [ "filetime", "libc", @@ -938,9 +952,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.45" +version = "0.3.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9e442fc33d7fdb45aa9bfeb312c095964abdf596f7567261062b2a7107aaabd" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ "deranged", "itoa", @@ -953,15 +967,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b36ee98fd31ec7426d599183e8fe26932a8dc1fb76ddb6214d05493377d34ca" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" [[package]] name = "time-macros" -version = "0.2.25" +version = "0.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71e552d1249bf61ac2a52db88179fd0673def1e1ad8243a00d9ec9ed71fee3dd" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" dependencies = [ "num-conv", "time-core", @@ -969,9 +983,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" dependencies = [ "displaydoc", "zerovec", @@ -979,9 +993,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.22" +version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05ae329d1f08c4d17a59bed7ff5b5a769d062e64a62d34a3261b219e62cd5aae" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" dependencies = [ "serde", "serde_spanned", @@ -991,18 +1005,18 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.9" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3da5db5a963e24bc68be8b17b6fa82814bb22ee8660f192bb182771d498f09a3" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" dependencies = [ "serde", ] [[package]] name = "toml_edit" -version = "0.22.26" +version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "310068873db2c5b3e7659d2cc35d21855dbafa50d1ce336397c666e3cb08137e" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ "indexmap", "serde", @@ -1014,9 +1028,9 @@ dependencies = [ [[package]] name = "toml_write" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfb942dfe1d8e29a7ee7fcbde5bd2b9a25fb89aa70caea2eba3bee836ff41076" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" [[package]] name = "uncased" @@ -1029,9 +1043,9 @@ dependencies = [ [[package]] name = "unicode-ident" -version = "1.0.18" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "untrusted" @@ -1115,35 +1129,22 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasm-bindgen" -version = "0.2.100" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89" dependencies = [ "cfg-if", "once_cell", "rustversion", "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" -dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.100" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1151,22 +1152,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.100" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904" dependencies = [ + "bumpalo", "proc-macro2", "quote", "syn", - "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.100" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129" dependencies = [ "unicode-ident", ] @@ -1182,9 +1183,9 @@ dependencies = [ [[package]] name = "windows-core" -version = "0.61.0" +version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ "windows-implement", "windows-interface", @@ -1195,9 +1196,9 @@ dependencies = [ [[package]] name = "windows-implement" -version = "0.60.0" +version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", @@ -1206,9 +1207,9 @@ dependencies = [ [[package]] name = "windows-interface" -version = "0.59.1" +version = "0.59.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", @@ -1217,24 +1218,24 @@ dependencies = [ [[package]] name = "windows-link" -version = "0.1.1" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-result" -version = "0.3.2" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" dependencies = [ "windows-link", ] [[package]] name = "windows-strings" -version = "0.4.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" dependencies = [ "windows-link", ] @@ -1250,11 +1251,11 @@ dependencies = [ [[package]] name = "windows-sys" -version = "0.59.0" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ - "windows-targets", + "windows-link", ] [[package]] @@ -1323,24 +1324,24 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" -version = "0.7.7" +version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6cb8234a863ea0e8cd7284fcdd4f145233eb00fee02bbdd9861aec44e6477bc5" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" dependencies = [ "memchr", ] [[package]] name = "writeable" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" [[package]] name = "xattr" -version = "1.5.0" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d65cbf2f12c15564212d48f4e3dfb87923d25d611f2aed18f4cb23f0413d89e" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" dependencies = [ "libc", "rustix", @@ -1354,9 +1355,9 @@ checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" [[package]] name = "yoke" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" dependencies = [ "stable_deref_trait", "yoke-derive", @@ -1365,9 +1366,9 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", @@ -1377,18 +1378,18 @@ dependencies = [ [[package]] name = "zerofrom" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", @@ -1404,9 +1405,9 @@ checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" [[package]] name = "zerotrie" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" dependencies = [ "displaydoc", "yoke", @@ -1415,9 +1416,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.5" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" dependencies = [ "yoke", "zerofrom", @@ -1426,9 +1427,9 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 6317a33..66f7b00 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,7 @@ members = [ ] [workspace.package] -version = "0.4.2" +version = "0.5.0" authors = ["Jef Roosens"] edition = "2021" diff --git a/Justfile b/Justfile index 54943ad..75af725 100644 --- a/Justfile +++ b/Justfile @@ -43,7 +43,8 @@ run: --java '/usr/lib/jvm/java-21-openjdk/bin/java' \ --layers '2min,2,4,4;3min,3,2,2' -publish-release-binaries tag: (build-release 'x86_64-unknown-linux-musl') (build-release 'aarch64-unknown-linux-musl') +# Publish the binaries and packages for a new release +publish-release tag: (build-release 'x86_64-unknown-linux-musl') (build-release 'aarch64-unknown-linux-musl') # Check the binaries are proper static binaries [ "$(readelf -d target/x86_64-unknown-linux-musl/release/alex | grep NEEDED | wc -l)" = 0 ] [ "$(readelf -d target/aarch64-unknown-linux-musl/release/alex | grep NEEDED | wc -l)" = 0 ] From e1bf2cb4672589865f4391ba8cf480b2594c0ec9 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Sun, 19 Apr 2026 16:03:43 +0200 Subject: [PATCH 93/93] chore: added Debian packaging using cargo-deb --- CHANGELOG.md | 4 ++++ Cargo.toml | 1 + Justfile | 44 ++++++++++++++++++++++++++++++++------------ alex/Cargo.toml | 2 ++ 4 files changed, 39 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aaec0f2..8e1ad49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://git.rustybever.be/Chewing_Bever/alex/src/branch/dev) +### Added + +* Debian packages are now available in the [package registry](https://git.rustybever.be/Chewing_Bever/alex/packages) + ## [0.5.0](https://git.rustybever.be/Chewing_Bever/alex/src/tag/0.5.0) ### Added diff --git a/Cargo.toml b/Cargo.toml index 66f7b00..f834642 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ members = [ version = "0.5.0" authors = ["Jef Roosens"] edition = "2021" +license-file = "LICENSE" [workspace.dependencies] chrono = { version = "0.4.26", features = ["serde"] } diff --git a/Justfile b/Justfile index 75af725..54180ad 100644 --- a/Justfile +++ b/Justfile @@ -1,20 +1,25 @@ +# Build the local development build [group('build')] build: cargo build --frozen --workspace alias b := build +# Build release binaries for the supported architectures [group('build')] -build-release target: +build-release: cargo build \ --release \ --frozen \ --workspace \ - --target '{{ target }}' + --target x86_64-unknown-linux-musl \ + --target aarch64-unknown-linux-musl +# Run all tests in the workspace test: cargo test --frozen --workspace alias t := test +# Run cargofmt and clippy check: cargo fmt --check --all cargo clippy \ @@ -23,6 +28,7 @@ check: --no-deps \ --deny 'clippy::all' alias c := check +alias lint := check fetch: cargo fetch --locked @@ -43,19 +49,33 @@ run: --java '/usr/lib/jvm/java-21-openjdk/bin/java' \ --layers '2min,2,4,4;3min,3,2,2' +# Package the static release binaries as a Debian package +[group('package')] +package-deb: build-release + cargo deb \ + --package alex \ + --frozen \ + --no-build \ + --target x86_64-unknown-linux-musl \ + --target aarch64-unknown-linux-musl + # Publish the binaries and packages for a new release -publish-release tag: (build-release 'x86_64-unknown-linux-musl') (build-release 'aarch64-unknown-linux-musl') +[group('package')] +publish-release tag: build-release package-deb # Check the binaries are proper static binaries [ "$(readelf -d target/x86_64-unknown-linux-musl/release/alex | grep NEEDED | wc -l)" = 0 ] [ "$(readelf -d target/aarch64-unknown-linux-musl/release/alex | grep NEEDED | wc -l)" = 0 ] curl \ - --netrc \ - --fail \ - --upload-file target/x86_64-unknown-linux-musl/release/alex \ - https://git.rustybever.be/api/packages/Chewing_Bever/generic/alex/"{{ tag }}"/alex-linux-amd64 - curl \ - --netrc \ - --fail \ - --upload-file target/aarch64-unknown-linux-musl/release/alex \ - https://git.rustybever.be/api/packages/Chewing_Bever/generic/alex/"{{ tag }}"/alex-linux-arm64 + --parallel --fail-early \ + --netrc --upload-file target/x86_64-unknown-linux-musl/release/alex \ + https://git.rustybever.be/api/packages/Chewing_Bever/generic/alex/"{{ tag }}"/alex-linux-amd64 \ + --next \ + --netrc --upload-file target/aarch64-unknown-linux-musl/release/alex \ + https://git.rustybever.be/api/packages/Chewing_Bever/generic/alex/"{{ tag }}"/alex-linux-arm64 \ + --next \ + --netrc --upload-file target/debian/alex_{{ tag }}-1_amd64.deb \ + https://git.rustybever.be/api/packages/Chewing_Bever/debian/pool/any/main/upload \ + --next \ + --netrc --upload-file target/debian/alex_{{ tag }}-1_arm64.deb \ + https://git.rustybever.be/api/packages/Chewing_Bever/debian/pool/any/main/upload > /dev/null diff --git a/alex/Cargo.toml b/alex/Cargo.toml index 71c1c93..ff5df75 100644 --- a/alex/Cargo.toml +++ b/alex/Cargo.toml @@ -3,6 +3,8 @@ name = "alex" description = "Wrapper around Minecraft server processes, designed to complement Docker image installations." version.workspace = true edition.workspace = true +authors.workspace = true +license-file.workspace = true [dependencies] backup = { path = "../backup" }