Compare commits

...

34 commits
0.3.1 ... main

Author SHA1 Message Date
e1bf2cb467
chore: added Debian packaging using cargo-deb 2026-04-19 16:03:43 +02:00
f0d5522661
chore: update dependencies; bump version to 0.5.0 2026-04-14 21:30:27 +02:00
7987bef384
refactor(papermc_api): fix Clippy diagnostics 2026-04-14 21:04:57 +02:00
d38cb4ce39
feat(cli): add commands to list, view and download PaperMC builds 2026-04-14 20:57:33 +02:00
f2c4b97626
feat(papermc_api): add routes for per-version, project and build information 2026-04-13 22:31:24 +02:00
b4dce4b69a
feat(papermc_api): implement wrapper for PaperMC builds API 2026-03-31 19:14:17 +02:00
f2a0b6230f
refactor(backup): delta contributions function no longer takes reversed input 2025-05-24 23:39:54 +02:00
5f43d7b8b1
test(backup): add some more delta tests 2025-05-24 23:23:46 +02:00
15c4839a81
test(backup): add delta union test 2025-05-24 17:20:25 +02:00
22a6e68c7c
feat(backup): implement mutation methods and specialized PartialEq for State 2025-05-24 17:20:25 +02:00
3f00eee61e
feat(backup): add delta mutation methods; start union tests 2025-05-24 17:20:25 +02:00
5f7376ebb1
chore: bump versions to 0.4.2 2025-05-24 17:20:25 +02:00
638e228ba4
chore: add publish functionality to justfile 2025-05-17 20:16:46 +02:00
e8a92d7e07
chore: organize justfiles 2025-05-17 20:10:23 +02:00
6a8725489e
chore: bump dependencies 2025-04-30 17:40:28 +02:00
3ae19e2168
fix(backup): work with temporary file while writing json metadata file 2025-04-30 15:30:57 +02:00
08e77034a7
refactor: add Justfile; fix some clippy lints 2025-04-30 15:15:51 +02:00
abafd9a28c
refactor: split backup and alex into separate crate; set up workspace 2025-04-30 15:14:34 +02:00
d23227dd0b
fix(ci): use old platform
All checks were successful
ci/woodpecker/tag/release Pipeline was successful
ci/woodpecker/push/lint Pipeline was successful
ci/woodpecker/push/clippy Pipeline was successful
ci/woodpecker/push/build Pipeline was successful
2023-08-13 10:19:29 +02:00
d3cb29b52e
chore: move PKGBUILD to separate repo 2023-08-13 10:19:29 +02:00
3cddea19c3
chore: revert to old platform syntax
All checks were successful
ci/woodpecker/push/lint Pipeline was successful
ci/woodpecker/push/clippy Pipeline was successful
ci/woodpecker/push/build Pipeline was successful
2023-08-12 15:47:05 +02:00
9ce2417528
chore: bump versions to 0.4.0
Some checks failed
ci/woodpecker/tag/arch-release Pipeline was successful
ci/woodpecker/tag/release Pipeline was successful
ci/woodpecker/push/clippy Pipeline failed
ci/woodpecker/push/lint Pipeline failed
ci/woodpecker/push/build Pipeline failed
2023-08-12 15:06:58 +02:00
f2e781dd5a
chore: bump dependency versions
All checks were successful
ci/woodpecker/push/lint Pipeline was successful
ci/woodpecker/push/build Pipeline was successful
ci/woodpecker/push/clippy Pipeline was successful
2023-08-12 14:50:06 +02:00
5bdd4e21b0
chore(ci): modernize config
All checks were successful
ci/woodpecker/push/clippy Pipeline was successful
ci/woodpecker/push/build Pipeline was successful
ci/woodpecker/push/lint Pipeline was successful
2023-08-12 14:09:22 +02:00
8f190c489b
chore: update changelog 2023-08-12 13:58:13 +02:00
5f6366078c
fix: properly parse layers env var
All checks were successful
ci/woodpecker/push/lint Pipeline was successful
ci/woodpecker/push/clippy Pipeline was successful
ci/woodpecker/push/build Pipeline was successful
2023-08-12 13:46:46 +02:00
b3d1cec078
feat: granular locking for proper concurrent access to server process
All checks were successful
ci/woodpecker/push/clippy Pipeline was successful
ci/woodpecker/push/lint Pipeline was successful
ci/woodpecker/push/build Pipeline was successful
2023-08-12 11:44:35 +02:00
a51ff3937d
refactor: reorder imports
All checks were successful
ci/woodpecker/push/lint Pipeline was successful
ci/woodpecker/push/clippy Pipeline was successful
ci/woodpecker/push/build Pipeline was successful
2023-08-11 23:24:14 +02:00
db3bba5a42
feat: also allow run args to be passed from toml file 2023-08-11 23:13:17 +02:00
34d016fd3f
feat: allow passing global configuration as TOML file 2023-08-11 21:26:17 +02:00
bf83357464
feat: publish arch packages
All checks were successful
ci/woodpecker/push/lint Pipeline was successful
ci/woodpecker/push/clippy Pipeline was successful
ci/woodpecker/push/build Pipeline was successful
2023-07-08 16:11:36 +02:00
bfb264e823
docs: add some more help strings
All checks were successful
ci/woodpecker/push/lint Pipeline was successful
ci/woodpecker/push/clippy Pipeline was successful
ci/woodpecker/push/build Pipeline was successful
2023-07-08 15:34:24 +02:00
241bb4d68e
feat: add extract command 2023-07-08 15:31:01 +02:00
6cdc18742e
feat: don't read non-contributing archives for export 2023-07-08 14:50:18 +02:00
49 changed files with 4358 additions and 827 deletions

View file

@ -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"

1
.gitignore vendored
View file

@ -20,3 +20,4 @@ target/
# testing files
*.jar
data*/
*.log

View file

@ -5,17 +5,17 @@ matrix:
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]

View file

@ -1,13 +1,13 @@
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]

View file

@ -1,13 +1,13 @@
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]

View file

@ -4,19 +4,19 @@ matrix:
- 'linux/arm64'
platform: ${PLATFORM}
branches: [ main ]
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 +28,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

View file

@ -7,6 +7,48 @@ 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
* 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
* 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
* 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
* 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
* 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)
### Added
@ -51,7 +93,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)
@ -61,7 +103,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)

1238
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,22 +1,20 @@
[package]
name = "alex"
version = "0.3.1"
description = "Wrapper around Minecraft server processes, designed to complement Docker image installations."
[workspace]
resolver = "2"
members = [
'backup',
'alex',
'papermc-api'
]
[workspace.package]
version = "0.5.0"
authors = ["Jef Roosens"]
edition = "2021"
license-file = "LICENSE"
# 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", "rc"] }
serde_json = "1.0.96"
serde = { version = "1.0.164", features = ["derive"] }
[profile.release]
lto = "fat"

View file

@ -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 \

81
Justfile Normal file
View file

@ -0,0 +1,81 @@
# 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:
cargo build \
--release \
--frozen \
--workspace \
--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 \
--frozen \
--all -- \
--no-deps \
--deny 'clippy::all'
alias c := check
alias lint := check
fetch:
cargo fetch --locked
clean:
cargo 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'
# 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
[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 \
--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

View file

@ -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

18
alex-example.toml Normal file
View file

@ -0,0 +1,18 @@
config = "data/config"
world = "data/worlds"
backup = "data/backups"
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 = "3min"
frequency = 3
chains = 2
chain_len = 2

871
alex/Cargo.lock generated Normal file
View file

@ -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"

19
alex/Cargo.toml Normal file
View file

@ -0,0 +1,19 @@
[package]
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" }
papermc-api = { path = "../papermc-api" }
chrono.workspace = true
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"

16
alex/Justfile Normal file
View file

@ -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

View file

@ -1,21 +1,39 @@
use crate::backup::Backup;
use crate::cli::Cli;
use crate::other;
use chrono::{TimeZone, Utc};
use clap::{Args, Subcommand};
use std::io;
use std::path::{Path, PathBuf};
use chrono::{TimeZone, Utc};
use clap::{Args, Subcommand};
use crate::other;
use backup::Backup;
#[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
///
/// 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 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),
}
#[derive(Args)]
@ -45,6 +63,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
@ -63,19 +84,39 @@ 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
///
/// 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
#[arg(short, long, default_value_t = false)]
make: bool,
}
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),
BackupCommands::Restore(args) => args.run(cli),
BackupCommands::Export(args) => args.run(cli),
BackupCommands::Extract(args) => args.run(cli),
}
}
}
impl BackupCreateArgs {
pub fn run(&self, cli: &Cli) -> io::Result<()> {
pub fn run(&self, cli: &super::Config) -> io::Result<()> {
let mut meta = cli.meta()?;
if let Some(res) = meta.create_backup(&self.layer) {
@ -87,7 +128,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
@ -144,7 +185,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
@ -197,7 +238,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 {
@ -219,3 +260,44 @@ impl BackupExportArgs {
}
}
}
impl BackupExtractArgs {
pub fn run(&self, _cli: &super::Config) -> 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)
}
}

47
alex/src/cli/config.rs Normal file
View file

@ -0,0 +1,47 @@
use std::{io, path::PathBuf};
use serde::{Deserialize, Serialize};
use crate::server::{Metadata, ServerType};
use backup::{ManagerConfig, MetaManager};
#[derive(Serialize, Deserialize, Debug)]
pub struct Config {
pub config: PathBuf,
pub world: PathBuf,
pub backup: PathBuf,
pub layers: Vec<ManagerConfig>,
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<MetaManager<Metadata>> {
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)
}
}

125
alex/src/cli/mod.rs Normal file
View file

@ -0,0 +1,125 @@
mod backup;
mod config;
mod papermc;
mod run;
use std::{path::PathBuf, str::FromStr};
use clap::{Args, Parser, Subcommand};
use figment::{
providers::{Env, Format, Serialized, Toml},
Figment,
};
use serde::{Deserialize, Serialize};
use crate::server::ServerType;
use ::backup::ManagerConfig;
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<PathBuf>,
#[command(flatten)]
pub args: CliArgs,
}
#[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", global = true)]
#[serde(skip_serializing_if = "::std::option::Option::is_none")]
pub config: Option<PathBuf>,
/// Directory where world files will be saved
#[arg(long, value_name = "WORLD_DIR", global = true)]
#[serde(skip_serializing_if = "::std::option::Option::is_none")]
pub world: Option<PathBuf>,
/// Directory where backups will be stored
#[arg(long, value_name = "BACKUP_DIR", global = true)]
#[serde(skip_serializing_if = "::std::option::Option::is_none")]
pub backup: Option<PathBuf>,
/// What backup layers to employ, provided as a list of tuples name,frequency,chains,chain_len
/// delimited by semicolons (;).
#[arg(long, global = true, value_delimiter = ';')]
#[serde(skip_serializing_if = "::std::option::Option::is_none")]
pub layers: Option<Vec<ManagerConfig>>,
/// Type of server
#[arg(long, global = true)]
#[serde(skip_serializing_if = "::std::option::Option::is_none")]
pub server: Option<ServerType>,
/// Version string for the server, e.g. 1.19.4-545
#[arg(long, global = true)]
#[serde(skip_serializing_if = "::std::option::Option::is_none")]
pub server_version: Option<String>,
}
#[derive(Subcommand)]
pub enum Commands {
/// Run the server
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 {
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)?),
Commands::PaperMC(cli) => cli.run(),
}
}
pub fn config<T, U>(&self, args: &U) -> crate::Result<T>
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 mut figment = Figment::new()
.merge(Serialized::defaults(T::default()))
.merge(Toml::file(toml_file))
.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::<Vec<_>>();
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()?)
}
}

213
alex/src/cli/papermc.rs Normal file
View file

@ -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<String>,
}
#[derive(Args)]
pub struct ListArgs {
/// If provided, list the available builds for this version
version: Option<String>,
}
#[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<PathBuf>,
}
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}");
}
}

View file

@ -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,
}
}
}

103
alex/src/cli/run/mod.rs Normal file
View file

@ -0,0 +1,103 @@
mod config;
use std::{path::PathBuf, sync::Arc};
use clap::Args;
use serde::{Deserialize, Serialize};
use crate::{server, signals, stdin};
use config::Config;
#[derive(Args)]
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
#[arg(short, long, default_value_t = false)]
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<PathBuf>,
/// 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<String>,
/// XMS value in megabytes for the server instance
#[arg(long)]
#[serde(skip_serializing_if = "::std::option::Option::is_none")]
pub xms: Option<u64>,
/// XMX value in megabytes for the server instance
#[arg(long)]
#[serde(skip_serializing_if = "::std::option::Option::is_none")]
pub xmx: Option<u64>,
}
fn backups_thread(server: Arc<server::ServerProcess>) {
loop {
let next_scheduled_time = {
server
.backups
.read()
.unwrap()
.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());
}
// We explicitely ignore the error here, as we don't want the thread to fail
let _ = server.backup();
}
}
impl RunCli {
pub fn run(&self, cli: &super::Cli, global: &super::Config) -> crate::Result<()> {
let config: Config = cli.config(&self.args)?;
let (_, mut signals) = signals::install_signal_handlers()?;
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 {
print!("{}", cmd);
return Ok(());
}
let counter = Arc::new(cmd.spawn()?);
if !global.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
Ok(signals::handle_signals(&mut signals, counter)?)
}
}

34
alex/src/error.rs Normal file
View file

@ -0,0 +1,34 @@
use std::{fmt, io};
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug)]
pub enum Error {
IO(io::Error),
Figment(figment::Error),
Other(Box<dyn std::error::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),
Error::Other(err) => write!(fmt, "{}", err),
}
}
}
impl std::error::Error for Error {}
impl From<io::Error> for Error {
fn from(err: io::Error) -> Self {
Error::IO(err)
}
}
impl From<figment::Error> for Error {
fn from(err: figment::Error) -> Self {
Error::Figment(err)
}
}

View file

@ -1,13 +1,16 @@
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 +35,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()
}

View file

@ -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;

View file

@ -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,

View file

@ -1,22 +1,22 @@
use crate::backup::MetaManager;
use std::{io::Write, process::Child, sync::RwLock};
use crate::server::Metadata;
use std::io::Write;
use std::process::Child;
use backup::MetaManager;
pub struct ServerProcess {
child: Child,
pub backups: MetaManager<Metadata>,
child: RwLock<Child>,
pub backups: RwLock<MetaManager<Metadata>>,
}
impl ServerProcess {
pub fn new(manager: MetaManager<Metadata>, 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 +26,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 +65,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")?;

View file

@ -1,10 +1,13 @@
use std::io;
use std::sync::atomic::AtomicBool;
use std::sync::{Arc, Mutex};
use std::{
io,
sync::{atomic::AtomicBool, Arc},
};
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;
@ -34,7 +37,7 @@ pub fn install_signal_handlers() -> io::Result<(Arc<AtomicBool>, SignalsInfo)> {
/// Loop that handles terminating signals as they come in.
pub fn handle_signals(
signals: &mut SignalsInfo,
counter: Arc<Mutex<server::ServerProcess>>,
server: Arc<server::ServerProcess>,
) -> io::Result<()> {
let mut force = false;
@ -46,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);
});
}

View file

@ -1,9 +1,8 @@
use std::io;
use std::sync::{Arc, Mutex};
use std::{io, sync::Arc};
use crate::server;
pub fn handle_stdin(counter: Arc<Mutex<server::ServerProcess>>) {
pub fn handle_stdin(server: Arc<server::ServerProcess>) {
let stdin = io::stdin();
let input = &mut String::new();
@ -14,13 +13,9 @@ pub fn handle_stdin(counter: Arc<Mutex<server::ServerProcess>>) {
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);

547
backup/Cargo.lock generated Normal file
View file

@ -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",
]

14
backup/Cargo.toml Normal file
View file

@ -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.1.1"
serde_json = "1.0.96"

16
backup/Justfile Normal file
View file

@ -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

295
backup/src/delta.rs Normal file
View file

@ -0,0 +1,295 @@
use std::{borrow::Borrow, fmt, path::PathBuf};
use serde::{Deserialize, Serialize};
use super::State;
/// Represents the changes relative to the previous backup
#[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,
/// 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: State,
}
impl Delta {
/// Returns whether the delta is empty by checking whether both its added and removed state
/// return true for their `is_empty`.
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
/// both deltas in-order. Note that this operation is not commutative.
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) = 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) = out.added.get_mut(dir) {
orig_added.extend(added.iter().cloned());
} else {
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) = 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) = out.removed.get_mut(dir) {
orig_removed.extend(removed.iter().cloned());
} else {
out.removed.insert(dir.clone(), removed.clone());
}
}
out
}
// 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 chain of deltas, calculate the "contribution" for each state.
///
/// 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<I>(deltas: I) -> Vec<State>
where
I: IntoIterator,
I::IntoIter: DoubleEndedIterator,
I::Item: Borrow<Delta>,
{
let mut contributions: Vec<State> = Vec::new();
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
// 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();
for delta in deltas {
contributions.push(delta.borrow().strict_difference(&union_future).added);
union_future = union_future.union(delta.borrow());
}
}
contributions.reverse();
contributions
}
/// Append the given files to the directory's list of added files
pub fn append_added<I>(&mut self, dir: impl Into<PathBuf>, files: I)
where
I: IntoIterator,
I::Item: Into<PathBuf>,
{
self.added.append_dir(dir, files);
}
/// Wrapper around the `append_added` method for a builder-style construction of delta's
pub fn with_added<I>(mut self, dir: impl Into<PathBuf>, files: I) -> Self
where
I: IntoIterator,
I::Item: Into<PathBuf>,
{
self.append_added(dir, files);
self
}
/// Append the given files to the directory's list of removed files
pub fn append_removed<I>(&mut self, dir: impl Into<PathBuf>, files: I)
where
I: IntoIterator,
I::Item: Into<PathBuf>,
{
self.removed.append_dir(dir, files);
}
/// Wrapper around the `append_removed` method for a builder-style construction of delta's
pub fn with_removed<I>(mut self, dir: impl Into<PathBuf>, files: I) -> Self
where
I: IntoIterator,
I::Item: Into<PathBuf>,
{
self.append_removed(dir, files);
self
}
}
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)
}
}
#[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));
}
#[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));
}
#[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", ["file4"]),
State::default(),
State::default().with_dir("dir1", ["file1"]),
State::default().with_dir("dir1", ["file2"]),
];
assert_eq!(Delta::contributions(deltas), expected);
}
}

View file

@ -4,26 +4,31 @@ 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;
use path::PathExt;
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};
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,
@ -55,6 +60,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<P: AsRef<Path>>(
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<T: Clone> Backup<T> {
@ -85,7 +129,7 @@ impl<T: Clone> Backup<T> {
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<PathBuf> = HashSet::new();
@ -143,7 +187,7 @@ impl<T: Clone> Backup<T> {
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<PathBuf> = HashSet::new();
@ -199,31 +243,8 @@ impl<T: Clone> Backup<T> {
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 {

View file

@ -1,8 +1,8 @@
use std::error::Error;
use std::fmt;
use std::str::FromStr;
use std::{error::Error, fmt, str::FromStr};
#[derive(Clone, Debug)]
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ManagerConfig {
pub name: String,
pub frequency: u32,

View file

@ -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<T>

View file

@ -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<T>
@ -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(())
}
@ -215,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)
@ -226,9 +239,15 @@ 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()
.zip(chain.iter().take(index + 1))
.filter(|(contribution, _)| !contribution.is_empty())
{
println!("{}", &backup);
backup.append(&self.backup_dir, contribution, &mut ar)?;
}

View file

@ -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<OsString>,

162
backup/src/state.rs Normal file
View file

@ -0,0 +1,162 @@
use std::{
borrow::Borrow,
collections::{HashMap, HashSet},
ops::{Deref, DerefMut},
path::{Path, PathBuf},
};
use serde::{Deserialize, Serialize};
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, Serialize, Deserialize, Default)]
pub struct State(HashMap<PathBuf, HashSet<PathBuf>>);
impl State {
/// 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));
}
}
}
/// Returns whether the provided relative path is part of the given state.
pub fn contains<P: AsRef<Path>>(&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())
})
}
/// 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())
}
pub fn append_dir<I>(&mut self, dir: impl Into<PathBuf>, files: I)
where
I: IntoIterator,
I::Item: Into<PathBuf>,
{
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<I>(mut self, dir: impl Into<PathBuf>, files: I) -> Self
where
I: IntoIterator,
I::Item: Into<PathBuf>,
{
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<T> From<T> for State
where
T: IntoIterator,
T::Item: Borrow<Delta>,
{
fn from(deltas: T) -> Self {
let mut state = State::default();
for delta in deltas {
state.apply(delta.borrow());
}
state
}
}
impl AsRef<HashMap<PathBuf, HashSet<PathBuf>>> for State {
fn as_ref(&self) -> &HashMap<PathBuf, HashSet<PathBuf>> {
&self.0
}
}
impl Deref for State {
type Target = HashMap<PathBuf, HashSet<PathBuf>>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for State {
fn deref_mut(&mut self) -> &mut Self::Target {
&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::<PathBuf>::new());
assert_eq!(a, b);
let b = b.with_dir("dir2", ["file3"]);
assert_ne!(a, b);
}
}

10
papermc-api/Cargo.toml Normal file
View file

@ -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"] }

View file

@ -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());
}

22
papermc-api/src/error.rs Normal file
View file

@ -0,0 +1,22 @@
#[derive(Debug)]
pub enum Error {
Ureq(ureq::Error),
BadBody,
}
impl std::error::Error for Error {}
impl From<ureq::Error> 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"),
}
}
}

247
papermc-api/src/lib.rs Normal file
View file

@ -0,0 +1,247 @@
use serde_json::Value;
pub use error::Error;
use crate::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 Default for Client {
fn default() -> Self {
Self::new()
}
}
impl Client {
pub fn new() -> Self {
Self {
agent: ureq::agent(),
}
}
pub fn projects(&self) -> Result<Vec<Project>, 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.iter().map(parse_project_json).collect()
}
pub fn project<'a>(&'a self, project: &'a str) -> ProjectQuery<'a> {
ProjectQuery {
agent: &self.agent,
project,
}
}
}
pub struct ProjectQuery<'a> {
agent: &'a ureq::Agent,
project: &'a str,
}
impl<'a> ProjectQuery<'a> {
pub fn info(&self) -> Result<Project, Error> {
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<Vec<Version>, 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.iter().map(parse_version_json).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 info(&self) -> Result<Version, Error> {
let mut res = self
.agent
.get(format!(
"{}/projects/{}/versions/{}",
BASE_URL, self.project, self.version
))
.call()?;
let body_json: Value = res.body_mut().read_json()?;
parse_version_json(&body_json)
}
pub fn builds(&self) -> Result<Vec<Build>, 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)?
.iter()
.map(parse_build_json)
.collect()
}
pub fn build(&self, build: &str) -> Result<Build, Error> {
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<Build, Error> {
self.build("latest")
}
}
fn parse_project_json(value: &Value) -> Result<Project, Error> {
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)?
.iter()
.map(|(_, versions)| versions.as_array().ok_or(Error::BadBody))
// Collect into error to propagate error of any of the versions
.collect::<Result<Vec<_>, _>>()?
.into_iter()
.flatten()
.map(|v| v.as_str().ok_or(Error::BadBody).map(|s| s.to_string()))
.collect::<Result<_, _>>()?,
})
}
fn parse_version_json(value: &Value) -> Result<Version, Error> {
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)?
.iter()
.map(|v| v.as_str().map(String::from).ok_or(Error::BadBody))
.collect::<Result<_, _>>()?,
},
builds: value["builds"]
.as_array()
.ok_or(Error::BadBody)?
.iter()
.map(|v| v.as_u64().ok_or(Error::BadBody))
.collect::<Result<_, _>>()?,
})
}
fn parse_build_json(value: &Value) -> Result<Build, Error> {
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)?
.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::<Result<_, Error>>()?,
downloads: value["downloads"]
.as_object()
.ok_or(Error::BadBody)?
.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::<Result<_, Error>>()?,
},
))
})
.collect::<Result<_, Error>>()?,
})
}

109
papermc-api/src/models.rs Normal file
View file

@ -0,0 +1,109 @@
use std::{collections::HashMap, fmt::Display, str::FromStr};
use chrono::{DateTime, Utc};
#[derive(Debug)]
pub struct Project {
pub id: String,
pub name: String,
pub versions: Vec<String>,
}
#[derive(Debug)]
pub enum SupportStatus {
Supported,
Unsupported,
}
pub struct SupportStatusParseError;
impl FromStr for SupportStatus {
type Err = SupportStatusParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"SUPPORTED" => Ok(Self::Supported),
"UNSUPPORTED" => Ok(Self::Unsupported),
_ => Err(SupportStatusParseError),
}
}
}
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,
pub recommended_flags: Vec<String>,
}
#[derive(Debug)]
pub struct Version {
pub id: String,
pub support_status: SupportStatus,
pub java: Java,
pub builds: Vec<u64>,
}
#[derive(Debug)]
pub struct Build {
pub id: u64,
pub time: DateTime<Utc>,
pub channel: BuildChannel,
pub commits: Vec<BuildCommit>,
pub downloads: HashMap<String, BuildDownload>,
}
#[derive(Debug)]
pub enum BuildChannel {
Alpha,
Beta,
Stable,
}
pub struct BuildChannelParseError;
impl FromStr for BuildChannel {
type Err = BuildChannelParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"STABLE" => Ok(Self::Stable),
"BETA" => Ok(Self::Beta),
"ALPHA" => Ok(Self::Alpha),
_ => Err(BuildChannelParseError),
}
}
}
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,
pub time: DateTime<Utc>,
pub message: String,
}
#[derive(Debug)]
pub struct BuildDownload {
pub name: String,
pub checksums: HashMap<String, String>,
pub size: u64,
pub url: String,
}

View file

@ -1,154 +0,0 @@
use super::State;
use serde::{Deserialize, Serialize};
use std::borrow::Borrow;
use std::fmt;
/// 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: 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: State,
}
impl Delta {
pub fn new() -> Self {
Self {
added: Default::default(),
removed: Default::default(),
}
}
/// 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.
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) = 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) = out.added.get_mut(dir) {
orig_added.extend(added.iter().cloned());
} else {
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) = 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) = out.removed.get_mut(dir) {
orig_removed.extend(removed.iter().cloned());
} else {
out.removed.insert(dir.clone(), removed.clone());
}
}
out
}
// 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 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<I>(deltas: I) -> Vec<State>
where
I: IntoIterator,
I::Item: Borrow<Delta>,
{
let mut contributions: Vec<State> = Vec::new();
let mut deltas = deltas.into_iter();
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();
for delta in deltas {
contributions.push(delta.borrow().strict_difference(&union_future).added);
union_future = union_future.union(delta.borrow());
}
}
// contributions.reverse();
contributions
}
}
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)
}
}

View file

@ -1,86 +0,0 @@
use crate::backup::Delta;
use serde::{Deserialize, Serialize};
use std::borrow::Borrow;
use std::collections::{HashMap, HashSet};
use std::ops::{Deref, DerefMut};
use std::path::{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, Serialize, Deserialize)]
pub struct State(HashMap<PathBuf, HashSet<PathBuf>>);
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));
}
}
}
/// Returns whether the provided relative path is part of the given state.
pub fn contains<P: AsRef<Path>>(&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<T> From<T> for State
where
T: IntoIterator,
T::Item: Borrow<Delta>,
{
fn from(deltas: T) -> Self {
let mut state = State::new();
for delta in deltas {
state.apply(delta.borrow());
}
state
}
}
impl AsRef<HashMap<PathBuf, HashSet<PathBuf>>> for State {
fn as_ref(&self) -> &HashMap<PathBuf, HashSet<PathBuf>> {
&self.0
}
}
impl Deref for State {
type Target = HashMap<PathBuf, HashSet<PathBuf>>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for State {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl Default for State {
fn default() -> Self {
Self::new()
}
}

View file

@ -1,92 +0,0 @@
mod backup;
mod run;
pub use crate::backup::MetaManager;
pub use crate::server::Metadata;
pub use backup::{BackupArgs, BackupCommands};
pub use run::RunArgs;
use crate::backup::ManagerConfig;
use crate::server::ServerType;
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 {
#[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,
/// 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<ManagerConfig>,
/// Type of 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", global = true)]
pub server_version: String,
}
impl Cli {
pub fn run(&self) -> io::Result<()> {
match &self.command {
Commands::Run(args) => args.run(self),
Commands::Backup(args) => args.run(self),
}
}
/// Convenience method to initialize backup manager from the cli arguments
pub fn meta(&self) -> io::Result<MetaManager<Metadata>> {
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)
}
}

View file

@ -1,94 +0,0 @@
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<Mutex<server::ServerProcess>>) {
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)
}
}