Compare commits
36 Commits
Author | SHA1 | Date |
---|---|---|
|
f2a0b6230f | |
|
5f43d7b8b1 | |
|
15c4839a81 | |
|
22a6e68c7c | |
|
3f00eee61e | |
|
5f7376ebb1 | |
|
638e228ba4 | |
|
e8a92d7e07 | |
|
6a8725489e | |
|
3ae19e2168 | |
|
08e77034a7 | |
|
abafd9a28c | |
|
d23227dd0b | |
|
d3cb29b52e | |
|
3cddea19c3 | |
|
9ce2417528 | |
|
f2e781dd5a | |
|
5bdd4e21b0 | |
|
8f190c489b | |
|
5f6366078c | |
|
b3d1cec078 | |
|
a51ff3937d | |
|
db3bba5a42 | |
|
34d016fd3f | |
|
bf83357464 | |
|
bfb264e823 | |
|
241bb4d68e | |
|
6cdc18742e | |
|
b924a054a6 | |
|
32d923e64b | |
|
1acfc9c422 | |
|
fc8e8d37d3 | |
|
5567323473 | |
|
80b814bcff | |
|
4ec336eb86 | |
|
6e216aa88f |
|
@ -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"
|
||||
|
|
|
@ -20,3 +20,4 @@ target/
|
|||
# testing files
|
||||
*.jar
|
||||
data*/
|
||||
*.log
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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
|
||||
|
|
40
CHANGELOG.md
40
CHANGELOG.md
|
@ -7,6 +7,42 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
## [Unreleased](https://git.rustybever.be/Chewing_Bever/alex/src/branch/dev)
|
||||
|
||||
## [0.4.2](https://git.rustybever.be/Chewing_Bever/alex/src/tag/0.4.2)
|
||||
|
||||
### Fixed
|
||||
|
||||
* Fix bug where JSON metadata file can be corrupted if crash occurs while
|
||||
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
|
||||
|
||||
* Export command to export any backup as a new full backup
|
||||
|
||||
## [0.3.0](https://git.rustybever.be/Chewing_Bever/alex/src/tag/0.3.0)
|
||||
|
||||
### Added
|
||||
|
@ -45,7 +81,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
### Changed
|
||||
|
||||
* JVM flags now narrowely follow Aikar's specifications
|
||||
* JVM flags now narrowly follow Aikar's specifications
|
||||
|
||||
## [0.2.0](https://git.rustybever.be/Chewing_Bever/alex/src/tag/0.2.0)
|
||||
|
||||
|
@ -55,7 +91,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
* A single stop signal will trigger the Java process to shut down, but Alex
|
||||
still expects to be run from a utility such as dumb-init
|
||||
* Properly back up entire config directory
|
||||
* Inject Java optimisation flags
|
||||
* Inject Java optimization flags
|
||||
|
||||
## [0.1.1](https://git.rustybever.be/Chewing_Bever/alex/src/tag/0.1.1)
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load Diff
26
Cargo.toml
26
Cargo.toml
|
@ -1,22 +1,18 @@
|
|||
[package]
|
||||
name = "alex"
|
||||
version = "0.3.0"
|
||||
description = "Wrapper around Minecraft server processes, designed to complement Docker image installations."
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = [
|
||||
'backup',
|
||||
'alex'
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.4.2"
|
||||
authors = ["Jef Roosens"]
|
||||
edition = "2021"
|
||||
|
||||
# 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"
|
||||
|
|
|
@ -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 \
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
[group('build')]
|
||||
build:
|
||||
cargo build --frozen --workspace
|
||||
alias b := build
|
||||
|
||||
[group('build')]
|
||||
build-release target:
|
||||
cargo build \
|
||||
--release \
|
||||
--frozen \
|
||||
--workspace \
|
||||
--target '{{ target }}'
|
||||
|
||||
test:
|
||||
cargo test --frozen --workspace
|
||||
alias t := test
|
||||
|
||||
check:
|
||||
cargo fmt --check --all
|
||||
cargo clippy \
|
||||
--frozen \
|
||||
--all -- \
|
||||
--no-deps \
|
||||
--deny 'clippy::all'
|
||||
alias c := 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'
|
||||
|
||||
publish-release-binaries tag: (build-release 'x86_64-unknown-linux-musl') (build-release 'aarch64-unknown-linux-musl')
|
||||
# Check the binaries are proper static binaries
|
||||
[ "$(readelf -d target/x86_64-unknown-linux-musl/release/alex | grep NEEDED | wc -l)" = 0 ]
|
||||
[ "$(readelf -d target/aarch64-unknown-linux-musl/release/alex | grep NEEDED | wc -l)" = 0 ]
|
||||
|
||||
curl \
|
||||
--netrc \
|
||||
--fail \
|
||||
--upload-file target/x86_64-unknown-linux-musl/release/alex \
|
||||
https://git.rustybever.be/api/packages/Chewing_Bever/generic/alex/"{{ tag }}"/alex-linux-amd64
|
||||
curl \
|
||||
--netrc \
|
||||
--fail \
|
||||
--upload-file target/aarch64-unknown-linux-musl/release/alex \
|
||||
https://git.rustybever.be/api/packages/Chewing_Bever/generic/alex/"{{ tag }}"/alex-linux-arm64
|
14
README.md
14
README.md
|
@ -11,6 +11,20 @@ Alex is distributed as statically compiled binaries for Linux amd64 and arm64.
|
|||
These can be found
|
||||
[here](https://git.rustybever.be/Chewing_Bever/alex/packages).
|
||||
|
||||
### Arch
|
||||
|
||||
Arch users can install prebuilt `x86_64` & `aarch64` packages from my `bur`
|
||||
repository. Add the following at the bottom of your `pacman.conf`:
|
||||
|
||||
```toml
|
||||
[bur]
|
||||
Server = https://arch.r8r.be/$repo/$arch
|
||||
SigLevel = Optional
|
||||
```
|
||||
|
||||
If you prefer building the package yourself, the PKGBUILD can be found
|
||||
[here](https://git.rustybever.be/bur/alex-mc).
|
||||
|
||||
### Dockerfiles
|
||||
|
||||
You can easily install alex in your Docker images by letting Docker download it
|
||||
|
|
|
@ -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
|
|
@ -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"
|
|
@ -0,0 +1,15 @@
|
|||
[package]
|
||||
name = "alex"
|
||||
description = "Wrapper around Minecraft server processes, designed to complement Docker image installations."
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
backup = { path = "../backup" }
|
||||
|
||||
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"] }
|
|
@ -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
|
|
@ -0,0 +1,303 @@
|
|||
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)]
|
||||
pub struct BackupArgs {
|
||||
#[command(subcommand)]
|
||||
pub command: BackupCommands,
|
||||
}
|
||||
|
||||
#[derive(Args)]
|
||||
pub struct BackupCreateArgs {
|
||||
/// What layer to create a backup in
|
||||
layer: String,
|
||||
}
|
||||
|
||||
#[derive(Args)]
|
||||
pub struct BackupListArgs {
|
||||
/// What layer to list
|
||||
layer: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Args)]
|
||||
pub struct BackupRestoreArgs {
|
||||
/// Path to the backup inside the backup directory to restore
|
||||
path: PathBuf,
|
||||
/// Directory to store config in
|
||||
output_config: PathBuf,
|
||||
/// Directory to store worlds in
|
||||
output_worlds: PathBuf,
|
||||
/// Whether to overwrite the contents of the 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,
|
||||
}
|
||||
|
||||
#[derive(Args)]
|
||||
pub struct BackupExportArgs {
|
||||
/// Path to the backup inside the backup directory to export
|
||||
path: PathBuf,
|
||||
/// Path to store the exported archive
|
||||
output: PathBuf,
|
||||
/// Create output directories if they don't exist
|
||||
#[arg(short, long, default_value_t = false)]
|
||||
make: bool,
|
||||
}
|
||||
|
||||
#[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: &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: &super::Config) -> io::Result<()> {
|
||||
let mut meta = cli.meta()?;
|
||||
|
||||
if let Some(res) = meta.create_backup(&self.layer) {
|
||||
res
|
||||
} else {
|
||||
Err(io::Error::new(io::ErrorKind::Other, "Unknown layer"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BackupListArgs {
|
||||
pub fn run(&self, cli: &super::Config) -> io::Result<()> {
|
||||
let meta = cli.meta()?;
|
||||
|
||||
// A bit scuffed? Sure
|
||||
for (name, manager) in meta
|
||||
.managers()
|
||||
.iter()
|
||||
.filter(|(name, _)| self.layer.is_none() || &self.layer.as_ref().unwrap() == name)
|
||||
{
|
||||
println!("{}", name);
|
||||
|
||||
for chain in manager.chains().iter().filter(|c| !c.is_empty()) {
|
||||
let mut iter = chain.iter();
|
||||
println!(" {}", iter.next().unwrap());
|
||||
|
||||
for backup in iter {
|
||||
println!(" {}", backup);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Tries to parse the given path as the path to a backup inside the backup directory with a
|
||||
/// formatted timestamp.
|
||||
fn parse_backup_path(
|
||||
backup_dir: &Path,
|
||||
backup_path: &Path,
|
||||
) -> io::Result<(String, chrono::DateTime<Utc>)> {
|
||||
if !backup_path.starts_with(backup_dir) {
|
||||
return Err(other("Provided file is not inside the backup directory."));
|
||||
}
|
||||
|
||||
let layer = if let Some(parent) = backup_path.parent() {
|
||||
// Backup files should be stored nested inside a layer's folder
|
||||
if parent != backup_dir {
|
||||
parent.file_name().unwrap().to_string_lossy()
|
||||
} else {
|
||||
return Err(other("Invalid path."));
|
||||
}
|
||||
} else {
|
||||
return Err(other("Invalid path."));
|
||||
};
|
||||
|
||||
let timestamp = if let Some(filename) = backup_path.file_name() {
|
||||
Utc.datetime_from_str(&filename.to_string_lossy(), Backup::FILENAME_FORMAT)
|
||||
.map_err(|_| other("Invalid filename."))?
|
||||
} else {
|
||||
return Err(other("Invalid filename."));
|
||||
};
|
||||
|
||||
Ok((layer.to_string(), timestamp))
|
||||
}
|
||||
|
||||
impl BackupRestoreArgs {
|
||||
pub fn run(&self, cli: &super::Config) -> io::Result<()> {
|
||||
let backup_dir = cli.backup.canonicalize()?;
|
||||
|
||||
// Create directories if needed
|
||||
if self.make {
|
||||
std::fs::create_dir_all(&self.output_config)?;
|
||||
std::fs::create_dir_all(&self.output_worlds)?;
|
||||
}
|
||||
|
||||
let output_config = self.output_config.canonicalize()?;
|
||||
let output_worlds = self.output_worlds.canonicalize()?;
|
||||
|
||||
// Parse input path
|
||||
let backup_path = self.path.canonicalize()?;
|
||||
let (layer, timestamp) = parse_backup_path(&backup_dir, &backup_path)?;
|
||||
|
||||
let meta = cli.meta()?;
|
||||
|
||||
// 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),
|
||||
];
|
||||
|
||||
// Restore the backup
|
||||
if let Some(res) = meta.restore_backup(&layer, timestamp, &dirs) {
|
||||
res
|
||||
} else {
|
||||
Err(other("Unknown layer"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BackupExportArgs {
|
||||
pub fn run(&self, cli: &super::Config) -> io::Result<()> {
|
||||
let backup_dir = cli.backup.canonicalize()?;
|
||||
|
||||
if self.make {
|
||||
if let Some(parent) = &self.output.parent() {
|
||||
std::fs::create_dir_all(parent)?;
|
||||
}
|
||||
}
|
||||
|
||||
// Parse input path
|
||||
let backup_path = self.path.canonicalize()?;
|
||||
let (layer, timestamp) = parse_backup_path(&backup_dir, &backup_path)?;
|
||||
|
||||
let meta = cli.meta()?;
|
||||
|
||||
if let Some(res) = meta.export_backup(&layer, timestamp, &self.output) {
|
||||
res
|
||||
} else {
|
||||
Err(other("Unknown layer"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,120 @@
|
|||
mod backup;
|
||||
mod config;
|
||||
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),
|
||||
}
|
||||
|
||||
impl Cli {
|
||||
pub fn run(&self) -> crate::Result<()> {
|
||||
let config = self.config(&self.args)?;
|
||||
|
||||
match &self.command {
|
||||
Commands::Run(args) => args.run(self, &config),
|
||||
Commands::Backup(args) => Ok(args.run(&config)?),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn config<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()?)
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)?)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
use std::{fmt, io};
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
IO(io::Error),
|
||||
Figment(figment::Error),
|
||||
}
|
||||
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Error::IO(err) => write!(fmt, "{}", err),
|
||||
Error::Figment(err) => write!(fmt, "{}", err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for Error {}
|
||||
|
||||
impl From<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)
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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;
|
|
@ -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,
|
|
@ -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")?;
|
|
@ -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);
|
||||
});
|
||||
}
|
|
@ -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);
|
|
@ -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",
|
||||
]
|
|
@ -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"
|
|
@ -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
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -2,26 +2,33 @@ mod delta;
|
|||
mod io_ext;
|
||||
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 chrono::Utc;
|
||||
use flate2::read::GzDecoder;
|
||||
use flate2::write::GzEncoder;
|
||||
use flate2::Compression;
|
||||
use path::PathExt;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::fmt;
|
||||
use std::fs::File;
|
||||
use std::io;
|
||||
use std::path::{Path, PathBuf};
|
||||
pub use state::State;
|
||||
|
||||
const BYTE_SUFFIXES: [&str; 5] = ["B", "KiB", "MiB", "GiB", "TiB"];
|
||||
|
||||
pub fn other(msg: &str) -> io::Error {
|
||||
io::Error::new(io::ErrorKind::Other, msg)
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub enum BackupType {
|
||||
Full,
|
||||
|
@ -53,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> {
|
||||
|
@ -61,18 +107,6 @@ impl<T: Clone> Backup<T> {
|
|||
self.metadata = Some(metadata);
|
||||
}
|
||||
|
||||
/// Resolve the state of the list of backups by applying their deltas in-order to an initially
|
||||
/// empty state.
|
||||
pub fn state(backups: &Vec<Self>) -> HashMap<PathBuf, HashSet<PathBuf>> {
|
||||
let mut state: HashMap<PathBuf, HashSet<PathBuf>> = HashMap::new();
|
||||
|
||||
for backup in backups {
|
||||
backup.delta.apply(&mut state);
|
||||
}
|
||||
|
||||
state
|
||||
}
|
||||
|
||||
/// Create a new Full backup, populated with the given directories.
|
||||
///
|
||||
/// # Arguments
|
||||
|
@ -95,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();
|
||||
|
@ -141,7 +175,7 @@ impl<T: Clone> Backup<T> {
|
|||
///
|
||||
/// The `Backup` instance describing this new backup.
|
||||
pub fn create_from<P: AsRef<Path>>(
|
||||
previous_state: HashMap<PathBuf, HashSet<PathBuf>>,
|
||||
previous_state: State,
|
||||
previous_start_time: chrono::DateTime<Utc>,
|
||||
backup_dir: P,
|
||||
dirs: &Vec<(PathBuf, PathBuf)>,
|
||||
|
@ -153,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();
|
||||
|
@ -209,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 {
|
||||
|
@ -247,6 +258,36 @@ impl<T: Clone> Backup<T> {
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn open<P: AsRef<Path>>(&self, backup_dir: P) -> io::Result<tar::Archive<GzDecoder<File>>> {
|
||||
let path = Backup::path(backup_dir, self.start_time);
|
||||
let tar_gz = File::open(path)?;
|
||||
let enc = GzDecoder::new(tar_gz);
|
||||
Ok(tar::Archive::new(enc))
|
||||
}
|
||||
|
||||
/// Open this backup's archive and append all its files that are part of the provided state to
|
||||
/// the archive file.
|
||||
pub fn append<P: AsRef<Path>>(
|
||||
&self,
|
||||
backup_dir: P,
|
||||
state: &State,
|
||||
ar: &mut tar::Builder<GzEncoder<File>>,
|
||||
) -> io::Result<()> {
|
||||
let mut own_ar = self.open(backup_dir)?;
|
||||
|
||||
for entry in own_ar.entries()? {
|
||||
let entry = entry?;
|
||||
let entry_path_in_tar = entry.path()?.to_path_buf();
|
||||
|
||||
if state.contains(&entry_path_in_tar) {
|
||||
let header = entry.header().clone();
|
||||
ar.append(&header, entry)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Clone> fmt::Display for Backup<T> {
|
|
@ -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,
|
|
@ -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::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>
|
||||
|
@ -129,6 +132,17 @@ where
|
|||
.map(|manager| manager.restore_backup(start_time, dirs))
|
||||
}
|
||||
|
||||
pub fn export_backup<P: AsRef<Path>>(
|
||||
&self,
|
||||
layer: &str,
|
||||
start_time: chrono::DateTime<Utc>,
|
||||
output_path: P,
|
||||
) -> Option<io::Result<()>> {
|
||||
self.managers
|
||||
.get(layer)
|
||||
.map(|manager| manager.export_backup(start_time, output_path))
|
||||
}
|
||||
|
||||
pub fn managers(&self) -> &HashMap<String, Manager<T>> {
|
||||
&self.managers
|
||||
}
|
|
@ -1,19 +1,21 @@
|
|||
mod config;
|
||||
mod meta;
|
||||
|
||||
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;
|
||||
pub use config::ManagerConfig;
|
||||
pub use meta::MetaManager;
|
||||
|
||||
use super::Backup;
|
||||
use crate::other;
|
||||
use chrono::SubsecRound;
|
||||
use chrono::Utc;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::fs::File;
|
||||
use std::io;
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Manages a single backup layer consisting of one or more chains of backups.
|
||||
pub struct Manager<T>
|
||||
where
|
||||
|
@ -72,10 +74,10 @@ where
|
|||
|
||||
let mut backup = if !current_chain.is_empty() {
|
||||
let previous_backup = current_chain.last().unwrap();
|
||||
let state = Backup::state(current_chain);
|
||||
let previous_state = State::from(current_chain.iter().map(|b| &b.delta));
|
||||
|
||||
Backup::create_from(
|
||||
state,
|
||||
previous_state,
|
||||
previous_backup.start_time,
|
||||
&self.backup_dir,
|
||||
&self.dirs,
|
||||
|
@ -120,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(())
|
||||
}
|
||||
|
||||
|
@ -151,13 +167,30 @@ where
|
|||
/// Calculate the next time a backup should be created. If no backup has been created yet, it
|
||||
/// will return now.
|
||||
pub fn next_scheduled_time(&self) -> chrono::DateTime<Utc> {
|
||||
if let Some(last_chain) = self.chains.last() {
|
||||
if let Some(last_backup) = last_chain.last() {
|
||||
return last_backup.start_time + self.frequency;
|
||||
self.chains
|
||||
.last()
|
||||
.and_then(|last_chain| last_chain.last())
|
||||
.map(|last_backup| last_backup.start_time + self.frequency)
|
||||
.unwrap_or_else(chrono::offset::Utc::now)
|
||||
}
|
||||
|
||||
/// Search for a chain containing a backup with the specified start time.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A tuple (chain, index) with index being the index of the found backup in the returned
|
||||
/// chain.
|
||||
fn find(&self, start_time: chrono::DateTime<Utc>) -> Option<(&Vec<Backup<T>>, usize)> {
|
||||
for chain in &self.chains {
|
||||
if let Some(index) = chain
|
||||
.iter()
|
||||
.position(|b| b.start_time.trunc_subsecs(0) == start_time)
|
||||
{
|
||||
return Some((chain, index));
|
||||
}
|
||||
}
|
||||
|
||||
chrono::offset::Utc::now()
|
||||
None
|
||||
}
|
||||
|
||||
/// Restore the backup with the given start time by restoring its chain up to and including the
|
||||
|
@ -167,24 +200,62 @@ where
|
|||
start_time: chrono::DateTime<Utc>,
|
||||
dirs: &Vec<(PathBuf, PathBuf)>,
|
||||
) -> io::Result<()> {
|
||||
// Iterate over each chain, skipping elements until the element with the given start time
|
||||
// is possibly found.
|
||||
for chain in &self.chains {
|
||||
// If we find the element in the chain, restore the entire chain up to and including
|
||||
// the element
|
||||
if let Some(index) = chain
|
||||
.iter()
|
||||
.position(|b| b.start_time.trunc_subsecs(0) == start_time)
|
||||
{
|
||||
self.find(start_time)
|
||||
.ok_or_else(|| other("Unknown layer."))
|
||||
.and_then(|(chain, index)| {
|
||||
for backup in chain.iter().take(index + 1) {
|
||||
backup.restore(&self.backup_dir, dirs)?;
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
Err(other("Unknown backup."))
|
||||
/// Export the backup with the given start time as a new full archive.
|
||||
pub fn export_backup<P: AsRef<Path>>(
|
||||
&self,
|
||||
start_time: chrono::DateTime<Utc>,
|
||||
output_path: P,
|
||||
) -> io::Result<()> {
|
||||
self.find(start_time)
|
||||
.ok_or_else(|| other("Unknown layer."))
|
||||
.and_then(|(chain, index)| {
|
||||
match chain[index].type_ {
|
||||
// A full backup is simply copied to the output path
|
||||
BackupType::Full => std::fs::copy(
|
||||
Backup::path(&self.backup_dir, chain[index].start_time),
|
||||
output_path,
|
||||
)
|
||||
.map(|_| ()),
|
||||
// Incremental backups are exported one by one according to their contribution
|
||||
BackupType::Incremental => {
|
||||
let contributions =
|
||||
Delta::contributions(chain.iter().take(index + 1).map(|b| &b.delta));
|
||||
|
||||
let tar_gz = OpenOptions::new()
|
||||
.write(true)
|
||||
.create(true)
|
||||
.open(output_path.as_ref())?;
|
||||
let enc = GzEncoder::new(tar_gz, Compression::default());
|
||||
let mut ar = tar::Builder::new(enc);
|
||||
|
||||
// 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)?;
|
||||
}
|
||||
|
||||
let mut enc = ar.into_inner()?;
|
||||
enc.try_finish()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Get a reference to the underlying chains
|
|
@ -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>,
|
||||
|
@ -129,16 +132,15 @@ pub trait PathExt {
|
|||
|
||||
impl PathExt for Path {
|
||||
fn not_modified_since(&self, timestamp: chrono::DateTime<Utc>) -> bool {
|
||||
if let Ok(metadata) = self.metadata() {
|
||||
if let Ok(last_modified) = metadata.modified() {
|
||||
self.metadata()
|
||||
.and_then(|m| m.modified())
|
||||
.map(|last_modified| {
|
||||
let t: chrono::DateTime<Utc> = last_modified.into();
|
||||
let t = t.with_timezone(&Local);
|
||||
|
||||
return t < timestamp;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
t < timestamp
|
||||
})
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn read_dir_recursive(&self) -> io::Result<ReadDirRecursive> {
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -1,86 +0,0 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::fmt;
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Represents the changes relative to the previous backup
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Delta {
|
||||
/// What files were added/modified in each part of the tarball.
|
||||
pub added: HashMap<PathBuf, HashSet<PathBuf>>,
|
||||
/// What files were removed in this backup, in comparison to the previous backup. For full
|
||||
/// backups, this will always be empty, as they do not consider previous backups.
|
||||
/// The map stores a separate list for each top-level directory, as the contents of these
|
||||
/// directories can come for different source directories.
|
||||
pub removed: HashMap<PathBuf, HashSet<PathBuf>>,
|
||||
}
|
||||
|
||||
impl Delta {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
added: HashMap::new(),
|
||||
removed: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Update the current state so that its result becomes the merge of itself and the other
|
||||
/// state.
|
||||
#[allow(dead_code)]
|
||||
pub fn merge(&mut self, delta: &Self) {
|
||||
for (dir, added) in delta.added.iter() {
|
||||
// Files that were removed in the current state, but added in the new state, are no
|
||||
// longer removed
|
||||
if let Some(orig_removed) = self.removed.get_mut(dir) {
|
||||
orig_removed.retain(|k| !added.contains(k));
|
||||
}
|
||||
|
||||
// Newly added files are added to the state as well
|
||||
if let Some(orig_added) = self.added.get_mut(dir) {
|
||||
orig_added.extend(added.iter().cloned());
|
||||
} else {
|
||||
self.added.insert(dir.clone(), added.clone());
|
||||
}
|
||||
}
|
||||
|
||||
for (dir, removed) in delta.removed.iter() {
|
||||
// Files that were originally added, but now deleted are removed from the added list
|
||||
if let Some(orig_added) = self.added.get_mut(dir) {
|
||||
orig_added.retain(|k| !removed.contains(k));
|
||||
}
|
||||
|
||||
// Newly removed files are added to the state as well
|
||||
if let Some(orig_removed) = self.removed.get_mut(dir) {
|
||||
orig_removed.extend(removed.iter().cloned());
|
||||
} else {
|
||||
self.removed.insert(dir.clone(), removed.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Modify the given state by applying this delta's changes to it
|
||||
pub fn apply(&self, state: &mut HashMap<PathBuf, HashSet<PathBuf>>) {
|
||||
// First we add new files, then we remove the old ones
|
||||
for (dir, added) in self.added.iter() {
|
||||
if let Some(current) = state.get_mut(dir) {
|
||||
current.extend(added.iter().cloned());
|
||||
} else {
|
||||
state.insert(dir.clone(), added.clone());
|
||||
}
|
||||
}
|
||||
|
||||
for (dir, removed) in self.removed.iter() {
|
||||
if let Some(current) = state.get_mut(dir) {
|
||||
current.retain(|k| !removed.contains(k));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
|
@ -1,173 +0,0 @@
|
|||
use crate::backup::Backup;
|
||||
use crate::cli::Cli;
|
||||
use crate::other;
|
||||
use chrono::{TimeZone, Utc};
|
||||
use clap::{Args, Subcommand};
|
||||
use std::io;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub enum BackupCommands {
|
||||
/// List all tracked backups
|
||||
List(BackupListArgs),
|
||||
/// Manually create a new backup
|
||||
Create(BackupCreateArgs),
|
||||
/// Restore a backup
|
||||
Restore(BackupRestoreArgs),
|
||||
}
|
||||
|
||||
#[derive(Args)]
|
||||
pub struct BackupArgs {
|
||||
#[command(subcommand)]
|
||||
pub command: BackupCommands,
|
||||
}
|
||||
|
||||
#[derive(Args)]
|
||||
pub struct BackupCreateArgs {
|
||||
/// What layer to create a backup in
|
||||
layer: String,
|
||||
}
|
||||
|
||||
#[derive(Args)]
|
||||
pub struct BackupListArgs {
|
||||
/// What layer to list
|
||||
layer: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Args)]
|
||||
pub struct BackupRestoreArgs {
|
||||
/// Path to the backup inside the backup directory
|
||||
path: PathBuf,
|
||||
/// Directory to store config in
|
||||
output_config: PathBuf,
|
||||
/// Directory to store worlds in
|
||||
output_worlds: PathBuf,
|
||||
/// Whether to overwrite the contents of the existing directories
|
||||
#[arg(short, long, default_value_t = false)]
|
||||
force: bool,
|
||||
/// Create output directories if they don't exist
|
||||
#[arg(short, long, default_value_t = false)]
|
||||
make: bool,
|
||||
}
|
||||
|
||||
impl BackupArgs {
|
||||
pub fn run(&self, cli: &Cli) -> io::Result<()> {
|
||||
match &self.command {
|
||||
BackupCommands::Create(args) => args.run(cli),
|
||||
BackupCommands::List(args) => args.run(cli),
|
||||
BackupCommands::Restore(args) => args.run(cli),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BackupCreateArgs {
|
||||
pub fn run(&self, cli: &Cli) -> io::Result<()> {
|
||||
let mut meta = cli.meta()?;
|
||||
|
||||
if let Some(res) = meta.create_backup(&self.layer) {
|
||||
res
|
||||
} else {
|
||||
Err(io::Error::new(io::ErrorKind::Other, "Unknown layer"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BackupRestoreArgs {
|
||||
pub fn run(&self, cli: &Cli) -> io::Result<()> {
|
||||
let backup_dir = cli.backup.canonicalize()?;
|
||||
|
||||
// Create directories if needed
|
||||
if self.make {
|
||||
std::fs::create_dir_all(&self.output_config)?;
|
||||
std::fs::create_dir_all(&self.output_worlds)?;
|
||||
}
|
||||
|
||||
let output_config = self.output_config.canonicalize()?;
|
||||
let output_worlds = self.output_worlds.canonicalize()?;
|
||||
|
||||
// Parse input path
|
||||
let path = self.path.canonicalize()?;
|
||||
|
||||
if !path.starts_with(&backup_dir) {
|
||||
return Err(other("Provided file is not inside the backup directory."));
|
||||
}
|
||||
|
||||
let layer = if let Some(parent) = path.parent() {
|
||||
// Backup files should be stored nested inside a layer's folder
|
||||
if parent != backup_dir {
|
||||
parent.file_name().unwrap().to_string_lossy()
|
||||
} else {
|
||||
return Err(other("Invalid path."));
|
||||
}
|
||||
} else {
|
||||
return Err(other("Invalid path."));
|
||||
};
|
||||
|
||||
let timestamp = if let Some(filename) = path.file_name() {
|
||||
Utc.datetime_from_str(&filename.to_string_lossy(), Backup::FILENAME_FORMAT)
|
||||
.map_err(|_| other("Invalid filename."))?
|
||||
} else {
|
||||
return Err(other("Invalid filename."));
|
||||
};
|
||||
|
||||
let meta = cli.meta()?;
|
||||
|
||||
// Clear previous contents of directories
|
||||
let mut entries = 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),
|
||||
];
|
||||
|
||||
// Restore the backup
|
||||
if let Some(res) = meta.restore_backup(&layer, timestamp, &dirs) {
|
||||
res
|
||||
} else {
|
||||
Err(other("Unknown layer"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BackupListArgs {
|
||||
pub fn run(&self, cli: &Cli) -> io::Result<()> {
|
||||
let meta = cli.meta()?;
|
||||
|
||||
// A bit scuffed? Sure
|
||||
for (name, manager) in meta
|
||||
.managers()
|
||||
.iter()
|
||||
.filter(|(name, _)| self.layer.is_none() || &self.layer.as_ref().unwrap() == name)
|
||||
{
|
||||
println!("{}", name);
|
||||
|
||||
for chain in manager.chains().iter().filter(|c| !c.is_empty()) {
|
||||
let mut iter = chain.iter();
|
||||
println!(" {}", iter.next().unwrap());
|
||||
|
||||
for backup in iter {
|
||||
println!(" {}", backup);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue