Compare commits
87 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 | |
|
75e9d7a9d2 | |
|
e6fa8a0eeb | |
|
55c5f24937 | |
|
36c441b8c2 | |
|
f71db90922 | |
|
2c256cf904 | |
|
bfd278abbe | |
|
c5193f0f3c | |
|
a4a03ca4c5 | |
|
5159bfdddd | |
|
0eda768c03 | |
|
a4e2a1276f | |
|
e373fc85f1 | |
|
1cfe13674d | |
|
d5cea49c8b | |
|
03e21fda87 | |
|
29636ffcdb | |
|
a236c36a4f | |
|
0a459ee30b | |
|
4e8d0a8d25 | |
|
188fb30343 | |
|
53dc3783ca | |
|
ef631fab1d | |
|
74a0b91fd1 | |
|
b48c531d80 | |
|
b51d951688 | |
|
bb7b57899b | |
|
f7235fb342 | |
|
5275356353 | |
|
27d7e681c3 | |
|
8add96b39b | |
|
d204c68400 | |
|
a9e7b215d1 | |
|
fcc111b4ef | |
|
b7a678e32f | |
|
703a25e8be | |
|
29d6713486 | |
|
4958257f6e | |
|
90aa929b73 | |
|
9ce8199d5f | |
|
375a68fbd6 | |
|
ce3dcdd4b1 | |
|
5ae23c931a | |
|
b08ba3853f | |
|
acb3cfd8e6 | |
|
45d736d1bb | |
|
69ce8616d5 | |
|
50cdd3115f | |
|
0faa6a8578 | |
|
f5fc8b588f | |
|
640364405f |
|
@ -1,2 +1,7 @@
|
|||
[alias]
|
||||
runs = "run -- paper --config data/config --backup data/backups --world data/worlds --jar data/paper.jar"
|
||||
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"
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
*
|
||||
|
||||
!Cargo.toml
|
||||
!Cargo.lock
|
||||
!src/
|
|
@ -19,4 +19,5 @@ target/
|
|||
|
||||
# testing files
|
||||
*.jar
|
||||
data/
|
||||
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
|
||||
|
|
86
CHANGELOG.md
86
CHANGELOG.md
|
@ -7,6 +7,92 @@ 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
|
||||
|
||||
* Incremental backups
|
||||
* Chain length describes how many incremental backups to create from the
|
||||
same full backup
|
||||
* "backups to keep" has been replaced by "chains to keep"
|
||||
* Server type & version and backup size are now stored as metadata in the
|
||||
metadata file
|
||||
* Backup layers
|
||||
* Store multiple chains of backups in parallel, configuring each with
|
||||
different parameters (son-father-grandfather principle)
|
||||
* CLI commands for creating, restoring & listing backups
|
||||
|
||||
### Changed
|
||||
|
||||
* Running the server now uses the `run` CLI subcommand
|
||||
* `server_type` and `server_version` arguments are now optional flags
|
||||
|
||||
### Removed
|
||||
|
||||
* `max_backups` setting
|
||||
|
||||
## [0.2.2](https://git.rustybever.be/Chewing_Bever/alex/src/tag/0.2.2)
|
||||
|
||||
### Fixed
|
||||
|
||||
* Use correct env var for backup directory
|
||||
|
||||
## [0.2.1](https://git.rustybever.be/Chewing_Bever/alex/src/tag/0.2.1)
|
||||
|
||||
### Added
|
||||
|
||||
* `--dry` flag to inspect command that will be run
|
||||
|
||||
### Changed
|
||||
|
||||
* JVM flags now narrowly follow Aikar's specifications
|
||||
|
||||
## [0.2.0](https://git.rustybever.be/Chewing_Bever/alex/src/tag/0.2.0)
|
||||
|
||||
### Added
|
||||
|
||||
* Rudimentary signal handling for gently stopping server
|
||||
* A single stop signal will trigger the Java process to shut down, but Alex
|
||||
still expects to be run from a utility such as dumb-init
|
||||
* Properly back up entire config directory
|
||||
* Inject Java optimization flags
|
||||
|
||||
## [0.1.1](https://git.rustybever.be/Chewing_Bever/alex/src/tag/0.1.1)
|
||||
|
||||
### Changed
|
||||
|
|
File diff suppressed because it is too large
Load Diff
26
Cargo.toml
26
Cargo.toml
|
@ -1,20 +1,18 @@
|
|||
[package]
|
||||
name = "alex"
|
||||
version = "0.1.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"
|
||||
# Used for backup filenames
|
||||
chrono = "0.4.26"
|
||||
clap = { version = "4.3.1", features = ["derive", "env"] }
|
||||
[workspace.dependencies]
|
||||
chrono = { version = "0.4.26", features = ["serde"] }
|
||||
serde = { version = "1.0.164", features = ["derive"] }
|
||||
|
||||
[profile.release]
|
||||
lto = "fat"
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
FROM rust:1.70-alpine3.18 AS builder
|
||||
|
||||
ARG DI_VER=1.2.5
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY . ./
|
||||
|
||||
RUN apk add --no-cache build-base unzip curl && \
|
||||
curl -Lo - "https://github.com/Yelp/dumb-init/archive/refs/tags/v${DI_VER}.tar.gz" | tar -xzf - && \
|
||||
cd "dumb-init-${DI_VER}" && \
|
||||
make SHELL=/bin/sh && \
|
||||
mv dumb-init ..
|
||||
|
||||
RUN cargo build && \
|
||||
[ "$(readelf -d target/debug/alex | grep NEEDED | wc -l)" = 0 ]
|
||||
|
||||
|
||||
# We use ${:-} instead of a default value because the argument is always passed
|
||||
# to the build, it'll just be blank most likely
|
||||
FROM eclipse-temurin:18-jre-alpine
|
||||
|
||||
# Build arguments
|
||||
ARG MC_VERSION=1.19.4
|
||||
ARG PAPERMC_VERSION=525
|
||||
|
||||
RUN addgroup -Sg 1000 paper && \
|
||||
adduser -SHG paper -u 1000 paper
|
||||
|
||||
# Create worlds and config directory
|
||||
WORKDIR /app
|
||||
RUN mkdir -p worlds config/cache backups
|
||||
|
||||
# Download server file
|
||||
ADD "https://papermc.io/api/v2/projects/paper/versions/$MC_VERSION/builds/$PAPERMC_VERSION/downloads/paper-$MC_VERSION-$PAPERMC_VERSION.jar" server.jar
|
||||
|
||||
# Make sure the server user can access all necessary folders
|
||||
RUN chown -R paper:paper /app
|
||||
|
||||
# Store the cache in an anonymous volume, which means it won't get stored in the other volumes
|
||||
VOLUME /app/config/cache
|
||||
VOLUME /app/backups
|
||||
|
||||
COPY --from=builder /app/dumb-init /bin/dumb-init
|
||||
COPY --from=builder /app/target/debug/alex /bin/alex
|
||||
|
||||
RUN chmod +x /bin/alex
|
||||
|
||||
# Default value to keep users from eating up all ram accidentally
|
||||
ENV ALEX_CONFIG=/app/config \
|
||||
ALEX_WORLD=/app/worlds \
|
||||
ALEX_BACKUP=/app/backups \
|
||||
ALEX_SERVER=paper \
|
||||
ALEX_XMS=1024 \
|
||||
ALEX_XMX=2048 \
|
||||
ALEX_JAR=/app/server.jar \
|
||||
ALEX_SERVER_VERSION="${MC_VERSION}-${PAPERMC_VERSION}" \
|
||||
ALEX_LAYERS="2min,2,4,4;3min,3,2,2"
|
||||
|
||||
# Document exposed ports
|
||||
EXPOSE 25565
|
||||
|
||||
# Switch to non-root user
|
||||
USER paper:paper
|
||||
|
||||
ENTRYPOINT ["/bin/dumb-init", "--"]
|
||||
CMD ["/bin/alex", "run"]
|
|
@ -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
|
136
README.md
136
README.md
|
@ -1,3 +1,135 @@
|
|||
# mc-wrapper
|
||||
# Alex
|
||||
|
||||
A wrapper around a standard Minecraft server, written in Rust.
|
||||
Alex is a wrapper around a typical Minecraft server process. It acts as the
|
||||
parent process, and sits in between the user's input and the server's stdin.
|
||||
This allows Alex to support additional commands that execute Rust code, notably
|
||||
creating periodic backups.
|
||||
|
||||
## Installation
|
||||
|
||||
Alex is distributed as statically compiled binaries for Linux amd64 and arm64.
|
||||
These can be found
|
||||
[here](https://git.rustybever.be/Chewing_Bever/alex/packages).
|
||||
|
||||
### 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
|
||||
for you. Add the following to your Dockerfile (replace with your required
|
||||
version & architecture):
|
||||
|
||||
```dockerfile
|
||||
ADD "https://git.rustybever.be/api/packages/Chewing_Bever/generic/alex/0.2.2/alex-linux-amd64" /bin/alex
|
||||
```
|
||||
|
||||
## Why
|
||||
|
||||
The primary usecase for this is backups. A common problem I've had with
|
||||
Minecraft backups is that they fail, because the server is writing to one of
|
||||
the region files as the backup is being created. Alex solves this be sending
|
||||
`save-off` and `save-all` to the server, before creating the tarball.
|
||||
Afterwards, saving is enabled again with `save-on`.
|
||||
|
||||
## Features
|
||||
|
||||
* Create safe backups as gzip-compressed tarballs using the `backup` command
|
||||
* Automatically create backups periodically
|
||||
* Properly configures the process (working directory, optimisation flags)
|
||||
* Configure everything as CLI arguments or environment variables
|
||||
|
||||
## Configuration
|
||||
|
||||
Most information can be retrieved easily by looking at the help command:
|
||||
|
||||
```
|
||||
Wrapper around Minecraft server processes, designed to complement Docker image installations.
|
||||
|
||||
Usage: alex [OPTIONS] <COMMAND>
|
||||
|
||||
Commands:
|
||||
run Run the server
|
||||
backup Interact with the backup system without starting a server
|
||||
help Print this message or the help of the given subcommand(s)
|
||||
|
||||
Options:
|
||||
--config <CONFIG_DIR>
|
||||
Directory where configs are stored, and where the server will run [env: ALEX_CONFIG_DIR=] [default: .]
|
||||
--world <WORLD_DIR>
|
||||
Directory where world files will be saved [env: ALEX_WORLD_DIR=] [default: ../worlds]
|
||||
--backup <BACKUP_DIR>
|
||||
Directory where backups will be stored [env: ALEX_BACKUP_DIR=] [default: ../backups]
|
||||
--layers <LAYERS>
|
||||
What backup layers to employ, provided as a list of tuples name,frequency,chains,chain_len delimited by semicolons (;) [env: ALEX_LAYERS=]
|
||||
--server <SERVER>
|
||||
Type of server [env: ALEX_SERVER=] [default: unknown] [possible values: unknown, paper, forge, vanilla]
|
||||
--server-version <SERVER_VERSION>
|
||||
Version string for the server, e.g. 1.19.4-545 [env: ALEX_SERVER_VERSION=] [default: ]
|
||||
-h, --help
|
||||
Print help
|
||||
-V, --version
|
||||
Print version
|
||||
|
||||
```
|
||||
|
||||
### Choosing layer parameters
|
||||
|
||||
One part of the configuration that does require some clarification is the layer
|
||||
system. Alex can manage an arbitrary number of backup layers, each having its
|
||||
own configuration. These layers can either use incremental or full backups,
|
||||
depending on how they're configured.
|
||||
|
||||
These layers mostly correspond to the grandfather-father-son backup rotation
|
||||
scheme. For example, one could have a layer that creates incremental backups
|
||||
every 30 minutes, which are stored for 24 hours. This gives you 24 hours of
|
||||
granular rollback in case your server suffers a crash. A second layer might
|
||||
create a full backup every 24 hours, with backups being stored for 7 days. This
|
||||
gives you 7 days worth of backups with the granularity of 24 hours. This
|
||||
approach allows for greater versatility, while not having to store a large
|
||||
amount of data. Thanks to incremental backups, frequent backups don't have to
|
||||
take long at all.
|
||||
|
||||
A layer consists of 4 pieces of metadata:
|
||||
|
||||
* A name, which will be used in the file system and the in-game notifications
|
||||
* The frequency, which describes in minutes how frequently a backup should be
|
||||
created
|
||||
* How many chains should be kept at all times
|
||||
* How long each chain should be
|
||||
|
||||
These last two require some clarification. In Alex, a "chain" describes an
|
||||
initial full backup and zero or more incremental backups that are created from
|
||||
that initial full backup. This concept exists because an incremental backup has
|
||||
no real meaning if its ancestors are not known. To restore one of these chains,
|
||||
all backups in the chain need to be restored in-order. Note that a chain length
|
||||
of 1 disables incremental backups entirely.
|
||||
|
||||
How many backups to keep is defined by how many chains should be stored.
|
||||
Because an incremental backup needs to have its ancestors in order to be
|
||||
restored, we can't simply "keep the last n backups", as this would break these
|
||||
chains. Therefore, you configure how many backups to store using these chains.
|
||||
|
||||
For example, if you configure a layer to store 5 chains of length 4, you will
|
||||
have 20 archive files on disk, namely 5 full backups and 15 incremental
|
||||
backups. Note that Alex applies these rules to *full* chains. An in-progress
|
||||
chain does not count towards this total. Therefore, you can have up to `n-1`
|
||||
additional archive files, with `n` being the chain length, on disk.
|
||||
|
||||
To look at it from another perspective, say we wish to have a granularity of 30
|
||||
minutes for a timespan of 24 hours. Then we could configure the layer to only
|
||||
save a single chain, with a chain length of 48. If we prefer to have a few full
|
||||
backups instead of a long chain of incremental backups, we could instead use a
|
||||
chain length of 12 and store 4 chains. Either way, the total comes out to 48,
|
||||
which spans 24 hours if we make a backup every 30 minutes.
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
mod cli;
|
||||
mod error;
|
||||
mod server;
|
||||
mod signals;
|
||||
mod stdin;
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// fn commands_backup(cli: &Cli, args: &BackupArgs) -> io::Result<()> {
|
||||
// let metadata = server::Metadata {
|
||||
// server_type: cli.server,
|
||||
// server_version: cli.server_version.clone(),
|
||||
// };
|
||||
// let dirs = vec![
|
||||
// (PathBuf::from("config"), cli.config.clone()),
|
||||
// (PathBuf::from("worlds"), cli.world.clone()),
|
||||
// ];
|
||||
// let mut meta = MetaManager::new(cli.backup.clone(), dirs, metadata);
|
||||
// meta.add_all(&cli.layers)?;
|
||||
|
||||
// match &args.command {
|
||||
// BackupCommands::List => ()
|
||||
// }
|
||||
|
||||
// // manager.create_backup()?;
|
||||
// // manager.remove_old_backups()
|
||||
// }
|
||||
|
||||
fn main() -> crate::Result<()> {
|
||||
let cli = Cli::parse();
|
||||
|
||||
cli.run()
|
||||
}
|
|
@ -0,0 +1,210 @@
|
|||
use crate::server::{Metadata, ServerProcess, ServerType};
|
||||
use backup::ManagerConfig;
|
||||
use backup::MetaManager;
|
||||
use std::fmt;
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::{Command, Stdio};
|
||||
|
||||
pub struct ServerCommand {
|
||||
type_: ServerType,
|
||||
version: String,
|
||||
java: String,
|
||||
jar: PathBuf,
|
||||
config_dir: PathBuf,
|
||||
world_dir: PathBuf,
|
||||
backup_dir: PathBuf,
|
||||
xms: u64,
|
||||
xmx: u64,
|
||||
managers: Vec<ManagerConfig>,
|
||||
}
|
||||
|
||||
impl ServerCommand {
|
||||
pub fn new(type_: ServerType, version: &str) -> Self {
|
||||
ServerCommand {
|
||||
type_,
|
||||
version: String::from(version),
|
||||
java: String::from("java"),
|
||||
jar: PathBuf::from("server.jar"),
|
||||
config_dir: PathBuf::from("config"),
|
||||
world_dir: PathBuf::from("worlds"),
|
||||
backup_dir: PathBuf::from("backups"),
|
||||
xms: 1024,
|
||||
xmx: 2048,
|
||||
managers: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn java(mut self, java: &str) -> Self {
|
||||
self.java = String::from(java);
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
pub fn jar<T: AsRef<Path>>(mut self, path: T) -> Self {
|
||||
self.jar = PathBuf::from(path.as_ref());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn config<T: AsRef<Path>>(mut self, path: T) -> Self {
|
||||
self.config_dir = PathBuf::from(path.as_ref());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn world<T: AsRef<Path>>(mut self, path: T) -> Self {
|
||||
self.world_dir = PathBuf::from(path.as_ref());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn backup<T: AsRef<Path>>(mut self, path: T) -> Self {
|
||||
self.backup_dir = PathBuf::from(path.as_ref());
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
pub fn xms(mut self, v: u64) -> Self {
|
||||
self.xms = v;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn xmx(mut self, v: u64) -> Self {
|
||||
self.xmx = v;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn managers(mut self, configs: Vec<ManagerConfig>) -> Self {
|
||||
self.managers = configs;
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
fn accept_eula(&self) -> std::io::Result<()> {
|
||||
let mut eula_path = self.config_dir.clone();
|
||||
eula_path.push("eula.txt");
|
||||
let mut eula_file = File::create(eula_path)?;
|
||||
eula_file.write_all(b"eula=true")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Canonicalize all paths to absolute paths. Without this command, all paths will be
|
||||
/// interpreted relatively from the config directory.
|
||||
pub fn canonicalize(&mut self) -> std::io::Result<()> {
|
||||
// To avoid any issues, we use absolute paths for everything when spawning the process
|
||||
self.jar = self.jar.canonicalize()?;
|
||||
self.config_dir = self.config_dir.canonicalize()?;
|
||||
self.world_dir = self.world_dir.canonicalize()?;
|
||||
self.backup_dir = self.backup_dir.canonicalize()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn create_cmd(&self) -> std::process::Command {
|
||||
let mut cmd = Command::new(&self.java);
|
||||
|
||||
// Apply JVM optimisation flags
|
||||
// https://aikar.co/2018/07/02/tuning-the-jvm-g1gc-garbage-collector-flags-for-minecraft/
|
||||
cmd.arg(format!("-Xms{}M", self.xms))
|
||||
.arg(format!("-Xmx{}M", self.xmx))
|
||||
.args([
|
||||
"-XX:+UseG1GC",
|
||||
"-XX:+ParallelRefProcEnabled",
|
||||
"-XX:MaxGCPauseMillis=200",
|
||||
"-XX:+UnlockExperimentalVMOptions",
|
||||
"-XX:+DisableExplicitGC",
|
||||
"-XX:+AlwaysPreTouch",
|
||||
]);
|
||||
|
||||
if self.xms > 12 * 1024 {
|
||||
cmd.args([
|
||||
"-XX:G1NewSizePercent=40",
|
||||
"-XX:G1MaxNewSizePercent=50",
|
||||
"-XX:G1HeapRegionSize=16M",
|
||||
"-XX:G1ReservePercent=15",
|
||||
]);
|
||||
} else {
|
||||
cmd.args([
|
||||
"-XX:G1NewSizePercent=30",
|
||||
"-XX:G1MaxNewSizePercent=40",
|
||||
"-XX:G1HeapRegionSize=8M",
|
||||
"-XX:G1ReservePercent=20",
|
||||
]);
|
||||
}
|
||||
|
||||
cmd.args(["-XX:G1HeapWastePercent=5", "-XX:G1MixedGCCountTarget=4"]);
|
||||
|
||||
if self.xms > 12 * 1024 {
|
||||
cmd.args(["-XX:InitiatingHeapOccupancyPercent=20"]);
|
||||
} else {
|
||||
cmd.args(["-XX:InitiatingHeapOccupancyPercent=15"]);
|
||||
}
|
||||
|
||||
cmd.args([
|
||||
"-XX:G1MixedGCLiveThresholdPercent=90",
|
||||
"-XX:G1RSetUpdatingPauseTimePercent=5",
|
||||
"-XX:SurvivorRatio=32",
|
||||
"-XX:+PerfDisableSharedMem",
|
||||
"-XX:MaxTenuringThreshold=1",
|
||||
"-Dusing.aikars.flags=https://mcflags.emc.gs",
|
||||
"-Daikars.new.flags=true",
|
||||
]);
|
||||
|
||||
cmd.current_dir(&self.config_dir)
|
||||
.arg("-jar")
|
||||
.arg(&self.jar)
|
||||
.arg("--universe")
|
||||
.arg(&self.world_dir)
|
||||
.arg("--nogui")
|
||||
.stdin(Stdio::piped());
|
||||
|
||||
cmd
|
||||
}
|
||||
|
||||
pub fn spawn(&mut self) -> std::io::Result<ServerProcess> {
|
||||
let metadata = Metadata {
|
||||
server_type: self.type_,
|
||||
server_version: self.version.clone(),
|
||||
};
|
||||
|
||||
let dirs = vec![
|
||||
(PathBuf::from("config"), self.config_dir.clone()),
|
||||
(PathBuf::from("worlds"), self.world_dir.clone()),
|
||||
];
|
||||
|
||||
let mut meta = MetaManager::new(self.backup_dir.clone(), dirs, metadata);
|
||||
meta.add_all(&self.managers)?;
|
||||
|
||||
let mut cmd = self.create_cmd();
|
||||
self.accept_eula()?;
|
||||
let child = cmd.spawn()?;
|
||||
|
||||
Ok(ServerProcess::new(meta, child))
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ServerCommand {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let cmd = self.create_cmd();
|
||||
|
||||
writeln!(f, "Command: {}", self.java)?;
|
||||
writeln!(f, "Working dir: {}", self.config_dir.as_path().display())?;
|
||||
|
||||
// Print command env vars
|
||||
writeln!(f, "Environment:")?;
|
||||
|
||||
for (key, val) in cmd.get_envs().filter(|(_, v)| v.is_some()) {
|
||||
let val = val.unwrap();
|
||||
writeln!(f, " {}={}", key.to_string_lossy(), val.to_string_lossy())?;
|
||||
}
|
||||
|
||||
// Print command arguments
|
||||
writeln!(f, "Arguments:")?;
|
||||
|
||||
for arg in cmd.get_args() {
|
||||
writeln!(f, " {}", arg.to_string_lossy())?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
mod command;
|
||||
mod process;
|
||||
|
||||
pub use command::ServerCommand;
|
||||
pub use process::ServerProcess;
|
||||
|
||||
use clap::ValueEnum;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Serialize, Deserialize, Debug)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum ServerType {
|
||||
Unknown,
|
||||
Paper,
|
||||
Forge,
|
||||
Vanilla,
|
||||
}
|
||||
|
||||
impl fmt::Display for ServerType {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let s = match self {
|
||||
ServerType::Unknown => "Unknown",
|
||||
ServerType::Paper => "PaperMC",
|
||||
ServerType::Forge => "Forge",
|
||||
ServerType::Vanilla => "Vanilla",
|
||||
};
|
||||
|
||||
write!(f, "{}", s)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||
pub struct Metadata {
|
||||
pub server_type: ServerType,
|
||||
pub server_version: String,
|
||||
}
|
|
@ -0,0 +1,95 @@
|
|||
use std::{io::Write, process::Child, sync::RwLock};
|
||||
|
||||
use crate::server::Metadata;
|
||||
use backup::MetaManager;
|
||||
|
||||
pub struct ServerProcess {
|
||||
child: RwLock<Child>,
|
||||
pub backups: RwLock<MetaManager<Metadata>>,
|
||||
}
|
||||
|
||||
impl ServerProcess {
|
||||
pub fn new(manager: MetaManager<Metadata>, child: Child) -> ServerProcess {
|
||||
ServerProcess {
|
||||
child: RwLock::new(child),
|
||||
backups: RwLock::new(manager),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn send_command(&self, cmd: &str) -> std::io::Result<()> {
|
||||
match cmd.trim() {
|
||||
"stop" | "exit" => self.stop()?,
|
||||
"backup" => self.backup()?,
|
||||
s => self.custom(s)?,
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
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(&self) -> std::io::Result<()> {
|
||||
self.custom("stop")?;
|
||||
|
||||
self.child.write().unwrap().wait()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
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(&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
|
||||
self.custom("save-off")?;
|
||||
self.custom("save-all")?;
|
||||
|
||||
// TODO implement a better mechanism
|
||||
// We wait some time to (hopefully) ensure the save-all call has completed
|
||||
std::thread::sleep(std::time::Duration::from_secs(10));
|
||||
|
||||
let start_time = chrono::offset::Utc::now();
|
||||
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")?;
|
||||
self.custom("save-all")?;
|
||||
|
||||
let duration = chrono::offset::Utc::now() - start_time;
|
||||
let duration_str = format!(
|
||||
"{}m{}s",
|
||||
duration.num_seconds() / 60,
|
||||
duration.num_seconds() % 60
|
||||
);
|
||||
|
||||
if res.is_ok() {
|
||||
self.custom(&format!(
|
||||
"say backup created for layer '{}' in {}",
|
||||
layer_name, duration_str
|
||||
))?;
|
||||
} else {
|
||||
self.custom(&format!(
|
||||
"an error occured after {} while creating backup for layer '{}'",
|
||||
duration_str, layer_name
|
||||
))?;
|
||||
}
|
||||
|
||||
res
|
||||
}
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
use std::{
|
||||
io,
|
||||
sync::{atomic::AtomicBool, Arc},
|
||||
};
|
||||
|
||||
use signal_hook::{
|
||||
consts::TERM_SIGNALS,
|
||||
flag,
|
||||
iterator::{Signals, SignalsInfo},
|
||||
};
|
||||
|
||||
use crate::server;
|
||||
|
||||
/// Install the required signal handlers for terminating signals.
|
||||
pub fn install_signal_handlers() -> io::Result<(Arc<AtomicBool>, SignalsInfo)> {
|
||||
let term = Arc::new(AtomicBool::new(false));
|
||||
|
||||
// For each terminating signal, we register both a shutdown handler and a handler that sets an
|
||||
// atomic bool. With this, the process will get killed immediately once it receives a second
|
||||
// termination signal (e.g. a double ctrl-c).
|
||||
// https://docs.rs/signal-hook/0.3.15/signal_hook/#a-complex-signal-handling-with-a-background-thread
|
||||
for sig in TERM_SIGNALS {
|
||||
// When terminated by a second term signal, exit with exit code 1.
|
||||
// This will do nothing the first time (because term_now is false).
|
||||
flag::register_conditional_shutdown(*sig, 1, Arc::clone(&term))?;
|
||||
// But this will "arm" the above for the second time, by setting it to true.
|
||||
// The order of registering these is important, if you put this one first, it will
|
||||
// first arm and then terminate ‒ all in the first round.
|
||||
flag::register(*sig, Arc::clone(&term))?;
|
||||
}
|
||||
|
||||
let signals = TERM_SIGNALS;
|
||||
|
||||
Ok((term, Signals::new(signals)?))
|
||||
}
|
||||
|
||||
/// Loop that handles terminating signals as they come in.
|
||||
pub fn handle_signals(
|
||||
signals: &mut SignalsInfo,
|
||||
server: Arc<server::ServerProcess>,
|
||||
) -> io::Result<()> {
|
||||
let mut force = false;
|
||||
|
||||
// We only register terminating signals, so we don't need to differentiate between what kind of
|
||||
// signal came in
|
||||
for _ in signals {
|
||||
// If term is already true, this is the second signal, meaning we kill the process
|
||||
// immediately.
|
||||
// This will currently not work, as the initial stop command will block the kill from
|
||||
// happening.
|
||||
if force {
|
||||
return server.kill();
|
||||
}
|
||||
// The stop command runs in a separate thread to avoid blocking the signal handling loop.
|
||||
// After stopping the server, the thread terminates the process.
|
||||
else {
|
||||
let clone = Arc::clone(&server);
|
||||
|
||||
std::thread::spawn(move || {
|
||||
let _ = clone.stop();
|
||||
std::process::exit(0);
|
||||
});
|
||||
}
|
||||
|
||||
force = true;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
use std::{io, sync::Arc};
|
||||
|
||||
use crate::server;
|
||||
|
||||
pub fn handle_stdin(server: Arc<server::ServerProcess>) {
|
||||
let stdin = io::stdin();
|
||||
let input = &mut String::new();
|
||||
|
||||
loop {
|
||||
input.clear();
|
||||
|
||||
if stdin.read_line(input).is_err() {
|
||||
continue;
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
use std::io::{self, Write};
|
||||
|
||||
/// Wrapper around the Write trait that counts how many bytes have been written in total.
|
||||
/// Heavily inspired by https://stackoverflow.com/a/42189386
|
||||
pub struct CountingWrite<W> {
|
||||
inner: W,
|
||||
count: usize,
|
||||
}
|
||||
|
||||
impl<W> CountingWrite<W>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
pub fn new(writer: W) -> Self {
|
||||
Self {
|
||||
inner: writer,
|
||||
count: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn bytes_written(&self) -> usize {
|
||||
self.count
|
||||
}
|
||||
}
|
||||
|
||||
impl<W> Write for CountingWrite<W>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||
let res = self.inner.write(buf);
|
||||
|
||||
if let Ok(count) = res {
|
||||
self.count += count;
|
||||
}
|
||||
|
||||
res
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
self.inner.flush()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,319 @@
|
|||
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 path::PathExt;
|
||||
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,
|
||||
Incremental,
|
||||
}
|
||||
|
||||
/// Represents a successful backup
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct Backup<T: Clone> {
|
||||
/// When the backup was started (also corresponds to the name)
|
||||
pub start_time: chrono::DateTime<Utc>,
|
||||
/// When the backup finished
|
||||
pub end_time: chrono::DateTime<Utc>,
|
||||
pub size: usize,
|
||||
/// Type of the backup
|
||||
pub type_: BackupType,
|
||||
pub delta: Delta,
|
||||
/// Additional metadata that can be associated with a given backup
|
||||
pub metadata: Option<T>,
|
||||
}
|
||||
|
||||
impl Backup<()> {
|
||||
pub const FILENAME_FORMAT: &str = "%Y-%m-%d_%H-%M-%S.tar.gz";
|
||||
|
||||
/// Return the path to a backup file by properly formatting the data.
|
||||
pub fn path<P: AsRef<Path>>(backup_dir: P, start_time: chrono::DateTime<Utc>) -> PathBuf {
|
||||
let backup_dir = backup_dir.as_ref();
|
||||
|
||||
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> {
|
||||
/// Set the backup's metadata.
|
||||
pub fn set_metadata(&mut self, metadata: T) {
|
||||
self.metadata = Some(metadata);
|
||||
}
|
||||
|
||||
/// Create a new Full backup, populated with the given directories.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `backup_dir` - Directory to store archive in
|
||||
/// * `dirs` - list of tuples `(path_in_tar, src_dir)` with `path_in_tar` the directory name
|
||||
/// under which `src_dir`'s contents should be stored in the archive
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// The `Backup` instance describing this new backup.
|
||||
pub fn create<P: AsRef<Path>>(
|
||||
backup_dir: P,
|
||||
dirs: &Vec<(PathBuf, PathBuf)>,
|
||||
) -> io::Result<Self> {
|
||||
let start_time = chrono::offset::Utc::now();
|
||||
|
||||
let path = Backup::path(backup_dir, start_time);
|
||||
let tar_gz = io_ext::CountingWrite::new(File::create(path)?);
|
||||
let enc = GzEncoder::new(tar_gz, Compression::default());
|
||||
let mut ar = tar::Builder::new(enc);
|
||||
|
||||
let mut delta = Delta::default();
|
||||
|
||||
for (dir_in_tar, src_dir) in dirs {
|
||||
let mut added_files: HashSet<PathBuf> = HashSet::new();
|
||||
|
||||
for entry in src_dir.read_dir_recursive()?.ignored("cache").files() {
|
||||
let path = entry?.path();
|
||||
let stripped = path.strip_prefix(src_dir).unwrap();
|
||||
|
||||
ar.append_path_with_name(&path, dir_in_tar.join(stripped))?;
|
||||
added_files.insert(stripped.to_path_buf());
|
||||
}
|
||||
|
||||
delta.added.insert(dir_in_tar.to_path_buf(), added_files);
|
||||
}
|
||||
|
||||
let mut enc = ar.into_inner()?;
|
||||
|
||||
// The docs recommend running try_finish before unwrapping using finish
|
||||
enc.try_finish()?;
|
||||
let tar_gz = enc.finish()?;
|
||||
|
||||
Ok(Backup {
|
||||
type_: BackupType::Full,
|
||||
start_time,
|
||||
end_time: chrono::Utc::now(),
|
||||
size: tar_gz.bytes_written(),
|
||||
delta,
|
||||
metadata: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Create a new Incremental backup from the given state, populated with the given directories.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `previous_state` - State the file system was in during the previous backup in the chain
|
||||
/// * `previous_start_time` - Start time of the previous backup; used to filter files
|
||||
/// * `backup_dir` - Directory to store archive in
|
||||
/// * `dirs` - list of tuples `(path_in_tar, src_dir)` with `path_in_tar` the directory name
|
||||
/// under which `src_dir`'s contents should be stored in the archive
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// The `Backup` instance describing this new backup.
|
||||
pub fn create_from<P: AsRef<Path>>(
|
||||
previous_state: State,
|
||||
previous_start_time: chrono::DateTime<Utc>,
|
||||
backup_dir: P,
|
||||
dirs: &Vec<(PathBuf, PathBuf)>,
|
||||
) -> io::Result<Self> {
|
||||
let start_time = chrono::offset::Utc::now();
|
||||
|
||||
let path = Backup::path(backup_dir, start_time);
|
||||
let tar_gz = io_ext::CountingWrite::new(File::create(path)?);
|
||||
let enc = GzEncoder::new(tar_gz, Compression::default());
|
||||
let mut ar = tar::Builder::new(enc);
|
||||
|
||||
let mut delta = Delta::default();
|
||||
|
||||
for (dir_in_tar, src_dir) in dirs {
|
||||
let mut all_files: HashSet<PathBuf> = HashSet::new();
|
||||
let mut added_files: HashSet<PathBuf> = HashSet::new();
|
||||
|
||||
for entry in src_dir.read_dir_recursive()?.ignored("cache").files() {
|
||||
let path = entry?.path();
|
||||
let stripped = path.strip_prefix(src_dir).unwrap();
|
||||
|
||||
if !path.not_modified_since(previous_start_time) {
|
||||
ar.append_path_with_name(&path, dir_in_tar.join(stripped))?;
|
||||
added_files.insert(stripped.to_path_buf());
|
||||
}
|
||||
|
||||
all_files.insert(stripped.to_path_buf());
|
||||
}
|
||||
|
||||
delta.added.insert(dir_in_tar.clone(), added_files);
|
||||
|
||||
if let Some(previous_files) = previous_state.get(dir_in_tar) {
|
||||
delta.removed.insert(
|
||||
dir_in_tar.to_path_buf(),
|
||||
previous_files.difference(&all_files).cloned().collect(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let mut enc = ar.into_inner()?;
|
||||
|
||||
// The docs recommend running try_finish before unwrapping using finish
|
||||
enc.try_finish()?;
|
||||
let tar_gz = enc.finish()?;
|
||||
|
||||
Ok(Backup {
|
||||
type_: BackupType::Incremental,
|
||||
start_time,
|
||||
end_time: chrono::Utc::now(),
|
||||
size: tar_gz.bytes_written(),
|
||||
delta,
|
||||
metadata: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Restore the backup by extracting its contents to the respective directories.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `backup_dir` - Backup directory where the file is stored
|
||||
/// * `dirs` - list of tuples `(path_in_tar, dst_dir)` with `dst_dir` the directory on-disk
|
||||
/// where the files stored under `path_in_tar` inside the tarball should be extracted to.
|
||||
pub fn restore<P: AsRef<Path>>(
|
||||
&self,
|
||||
backup_dir: P,
|
||||
dirs: &Vec<(PathBuf, PathBuf)>,
|
||||
) -> io::Result<()> {
|
||||
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 {
|
||||
if let Some(removed) = self.delta.removed.get(path_in_tar) {
|
||||
for path in removed {
|
||||
let dst_path = dst_dir.join(path);
|
||||
std::fs::remove_file(dst_path)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
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> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let letter = match self.type_ {
|
||||
BackupType::Full => 'F',
|
||||
BackupType::Incremental => 'I',
|
||||
};
|
||||
|
||||
// Pretty-print size
|
||||
// If your backup is a petabyte or larger, this will crash and you need to re-evaluate your
|
||||
// life choices
|
||||
let index = self.size.ilog(1024) as usize;
|
||||
let size = self.size as f64 / (1024.0_f64.powi(index as i32));
|
||||
let duration = self.end_time - self.start_time;
|
||||
|
||||
write!(
|
||||
f,
|
||||
"{} ({}, {}m{}s, {:.2}{}, {})",
|
||||
self.start_time.format(Backup::FILENAME_FORMAT),
|
||||
letter,
|
||||
duration.num_seconds() / 60,
|
||||
duration.num_seconds() % 60,
|
||||
size,
|
||||
BYTE_SUFFIXES[index],
|
||||
self.delta
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
use std::{error::Error, fmt, str::FromStr};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct ManagerConfig {
|
||||
pub name: String,
|
||||
pub frequency: u32,
|
||||
pub chains: u64,
|
||||
pub chain_len: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ParseManagerConfigErr;
|
||||
|
||||
impl Error for ParseManagerConfigErr {}
|
||||
|
||||
impl fmt::Display for ParseManagerConfigErr {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "parse manager config err")
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for ManagerConfig {
|
||||
type Err = ParseManagerConfigErr;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let splits: Vec<&str> = s.split(',').collect();
|
||||
|
||||
if let [name, frequency, chains, chain_len] = splits[..] {
|
||||
let name: String = name.parse().map_err(|_| ParseManagerConfigErr)?;
|
||||
let frequency: u32 = frequency.parse().map_err(|_| ParseManagerConfigErr)?;
|
||||
let chains: u64 = chains.parse().map_err(|_| ParseManagerConfigErr)?;
|
||||
let chain_len: u64 = chain_len.parse().map_err(|_| ParseManagerConfigErr)?;
|
||||
|
||||
Ok(ManagerConfig {
|
||||
name,
|
||||
chains,
|
||||
chain_len,
|
||||
frequency,
|
||||
})
|
||||
} else {
|
||||
Err(ParseManagerConfigErr)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,149 @@
|
|||
use std::{
|
||||
collections::HashMap,
|
||||
io,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use chrono::Utc;
|
||||
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>
|
||||
where
|
||||
T: Clone + Serialize + for<'de> Deserialize<'de> + std::fmt::Debug,
|
||||
{
|
||||
backup_dir: PathBuf,
|
||||
dirs: Vec<(PathBuf, PathBuf)>,
|
||||
default_metadata: T,
|
||||
managers: HashMap<String, Manager<T>>,
|
||||
}
|
||||
|
||||
impl<T> MetaManager<T>
|
||||
where
|
||||
T: Clone + Serialize + for<'de> Deserialize<'de> + std::fmt::Debug,
|
||||
{
|
||||
pub fn new<P: Into<PathBuf>>(
|
||||
backup_dir: P,
|
||||
dirs: Vec<(PathBuf, PathBuf)>,
|
||||
default_metadata: T,
|
||||
) -> Self {
|
||||
MetaManager {
|
||||
backup_dir: backup_dir.into(),
|
||||
dirs,
|
||||
default_metadata,
|
||||
managers: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a new manager to track, initializing it first.
|
||||
pub fn add(&mut self, config: &ManagerConfig) -> io::Result<()> {
|
||||
// Backup dir itself should exist, but we control its contents, so we can create
|
||||
// separate directories for each layer
|
||||
let path = self.backup_dir.join(&config.name);
|
||||
|
||||
// If the directory already exists, that's okay
|
||||
match std::fs::create_dir(&path) {
|
||||
Ok(()) => (),
|
||||
Err(e) => match e.kind() {
|
||||
io::ErrorKind::AlreadyExists => (),
|
||||
_ => return Err(e),
|
||||
},
|
||||
};
|
||||
|
||||
let mut manager = Manager::new(
|
||||
path,
|
||||
self.dirs.clone(),
|
||||
self.default_metadata.clone(),
|
||||
config.chain_len,
|
||||
config.chains,
|
||||
chrono::Duration::minutes(config.frequency.into()),
|
||||
);
|
||||
manager.load()?;
|
||||
self.managers.insert(config.name.clone(), manager);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Convenient wrapper for `add`.
|
||||
pub fn add_all(&mut self, configs: &Vec<ManagerConfig>) -> io::Result<()> {
|
||||
for config in configs {
|
||||
self.add(config)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Return the name of the next scheduled layer, if one or more managers are present.
|
||||
pub fn next_scheduled_layer(&self) -> Option<&str> {
|
||||
self.managers
|
||||
.iter()
|
||||
.min_by_key(|(_, m)| m.next_scheduled_time())
|
||||
.map(|(k, _)| k.as_str())
|
||||
}
|
||||
|
||||
/// Return the earliest scheduled time for the underlying managers.
|
||||
pub fn next_scheduled_time(&self) -> Option<chrono::DateTime<Utc>> {
|
||||
self.managers
|
||||
.values()
|
||||
.map(|m| m.next_scheduled_time())
|
||||
.min()
|
||||
}
|
||||
|
||||
/// Perform a backup cycle for the earliest scheduled manager.
|
||||
pub fn perform_backup_cycle(&mut self) -> io::Result<()> {
|
||||
if let Some(manager) = self
|
||||
.managers
|
||||
.values_mut()
|
||||
.min_by_key(|m| m.next_scheduled_time())
|
||||
{
|
||||
manager.create_backup()?;
|
||||
manager.remove_old_backups()
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a manual backup for a specific layer
|
||||
pub fn create_backup(&mut self, layer: &str) -> Option<io::Result<()>> {
|
||||
if let Some(manager) = self.managers.get_mut(layer) {
|
||||
let mut res = manager.create_backup();
|
||||
|
||||
if res.is_ok() {
|
||||
res = manager.remove_old_backups();
|
||||
}
|
||||
|
||||
Some(res)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Restore a backup for a specific layer
|
||||
pub fn restore_backup(
|
||||
&self,
|
||||
layer: &str,
|
||||
start_time: chrono::DateTime<Utc>,
|
||||
dirs: &Vec<(PathBuf, PathBuf)>,
|
||||
) -> Option<io::Result<()>> {
|
||||
self.managers
|
||||
.get(layer)
|
||||
.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
|
||||
}
|
||||
}
|
|
@ -0,0 +1,265 @@
|
|||
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;
|
||||
|
||||
/// Manages a single backup layer consisting of one or more chains of backups.
|
||||
pub struct Manager<T>
|
||||
where
|
||||
T: Clone + Serialize + for<'de> Deserialize<'de> + std::fmt::Debug,
|
||||
{
|
||||
backup_dir: PathBuf,
|
||||
dirs: Vec<(PathBuf, PathBuf)>,
|
||||
default_metadata: T,
|
||||
chain_len: u64,
|
||||
chains_to_keep: u64,
|
||||
frequency: chrono::Duration,
|
||||
chains: Vec<Vec<Backup<T>>>,
|
||||
}
|
||||
|
||||
impl<T> Manager<T>
|
||||
where
|
||||
T: Clone + Serialize + for<'de> Deserialize<'de> + std::fmt::Debug,
|
||||
{
|
||||
const METADATA_FILE: &str = "alex.json";
|
||||
|
||||
pub fn new<P: Into<PathBuf>>(
|
||||
backup_dir: P,
|
||||
dirs: Vec<(PathBuf, PathBuf)>,
|
||||
metadata: T,
|
||||
chain_len: u64,
|
||||
chains_to_keep: u64,
|
||||
frequency: chrono::Duration,
|
||||
) -> Self {
|
||||
Self {
|
||||
backup_dir: backup_dir.into(),
|
||||
dirs,
|
||||
default_metadata: metadata,
|
||||
chain_len,
|
||||
chains_to_keep,
|
||||
frequency,
|
||||
chains: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new backup, either full or incremental, depending on the state of the current
|
||||
/// chain.
|
||||
pub fn create_backup(&mut self) -> io::Result<()> {
|
||||
// We start a new chain if the current chain is complete, or if there isn't a first chain
|
||||
// yet
|
||||
if let Some(current_chain) = self.chains.last() {
|
||||
let current_chain_len: u64 = current_chain.len().try_into().unwrap();
|
||||
|
||||
if current_chain_len >= self.chain_len {
|
||||
self.chains.push(Vec::new());
|
||||
}
|
||||
} else {
|
||||
self.chains.push(Vec::new());
|
||||
}
|
||||
|
||||
let current_chain = self.chains.last_mut().unwrap();
|
||||
|
||||
let mut backup = if !current_chain.is_empty() {
|
||||
let previous_backup = current_chain.last().unwrap();
|
||||
let previous_state = State::from(current_chain.iter().map(|b| &b.delta));
|
||||
|
||||
Backup::create_from(
|
||||
previous_state,
|
||||
previous_backup.start_time,
|
||||
&self.backup_dir,
|
||||
&self.dirs,
|
||||
)?
|
||||
} else {
|
||||
Backup::create(&self.backup_dir, &self.dirs)?
|
||||
};
|
||||
|
||||
backup.set_metadata(self.default_metadata.clone());
|
||||
|
||||
current_chain.push(backup);
|
||||
|
||||
self.save()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Delete all backups associated with outdated chains, and forget those chains.
|
||||
pub fn remove_old_backups(&mut self) -> io::Result<()> {
|
||||
let chains_to_store: usize = self.chains_to_keep.try_into().unwrap();
|
||||
|
||||
if chains_to_store < self.chains.len() {
|
||||
let mut remove_count: usize = self.chains.len() - chains_to_store;
|
||||
|
||||
// We only count finished chains towards the list of stored chains
|
||||
let chain_len: usize = self.chain_len.try_into().unwrap();
|
||||
if self.chains.last().unwrap().len() < chain_len {
|
||||
remove_count -= 1;
|
||||
}
|
||||
|
||||
for chain in self.chains.drain(..remove_count) {
|
||||
for backup in chain {
|
||||
let path = Backup::path(&self.backup_dir, backup.start_time);
|
||||
std::fs::remove_file(path)?;
|
||||
}
|
||||
}
|
||||
|
||||
self.save()?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Write the in-memory state to disk.
|
||||
///
|
||||
/// 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 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(())
|
||||
}
|
||||
|
||||
/// Overwrite the in-memory state with the on-disk state.
|
||||
pub fn load(&mut self) -> io::Result<()> {
|
||||
let json_file = match File::open(self.backup_dir.join(Self::METADATA_FILE)) {
|
||||
Ok(f) => f,
|
||||
Err(e) => {
|
||||
// Don't error out if the file isn't there, it will be created when necessary
|
||||
if e.kind() == io::ErrorKind::NotFound {
|
||||
self.chains = Vec::new();
|
||||
|
||||
return Ok(());
|
||||
} else {
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
self.chains = serde_json::from_reader(json_file)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 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> {
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Restore the backup with the given start time by restoring its chain up to and including the
|
||||
/// backup, in order.
|
||||
pub fn restore_backup(
|
||||
&self,
|
||||
start_time: chrono::DateTime<Utc>,
|
||||
dirs: &Vec<(PathBuf, PathBuf)>,
|
||||
) -> io::Result<()> {
|
||||
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)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
/// 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
|
||||
pub fn chains(&self) -> &Vec<Vec<Backup<T>>> {
|
||||
&self.chains
|
||||
}
|
||||
}
|
|
@ -0,0 +1,149 @@
|
|||
use std::{
|
||||
collections::HashSet,
|
||||
ffi::OsString,
|
||||
fs::{self, DirEntry},
|
||||
io,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use chrono::{Local, Utc};
|
||||
|
||||
pub struct ReadDirRecursive {
|
||||
ignored: HashSet<OsString>,
|
||||
read_dir: fs::ReadDir,
|
||||
dir_stack: Vec<PathBuf>,
|
||||
files_only: bool,
|
||||
}
|
||||
|
||||
impl ReadDirRecursive {
|
||||
/// Start the iterator for a new directory
|
||||
pub fn start<P: AsRef<Path>>(path: P) -> io::Result<Self> {
|
||||
let path = path.as_ref();
|
||||
let read_dir = path.read_dir()?;
|
||||
|
||||
Ok(ReadDirRecursive {
|
||||
ignored: HashSet::new(),
|
||||
read_dir,
|
||||
dir_stack: Vec::new(),
|
||||
files_only: false,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn ignored<S: Into<OsString>>(mut self, s: S) -> Self {
|
||||
self.ignored.insert(s.into());
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
pub fn files(mut self) -> Self {
|
||||
self.files_only = true;
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Tries to populate the `read_dir` field with a new `ReadDir` instance to consume.
|
||||
fn next_read_dir(&mut self) -> io::Result<bool> {
|
||||
if let Some(path) = self.dir_stack.pop() {
|
||||
self.read_dir = path.read_dir()?;
|
||||
|
||||
Ok(true)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
/// Convenience method to add a new directory to the stack.
|
||||
fn push_entry(&mut self, entry: &io::Result<DirEntry>) {
|
||||
if let Ok(entry) = entry {
|
||||
if entry.path().is_dir() {
|
||||
self.dir_stack.push(entry.path());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Determine whether an entry should be returned by the iterator.
|
||||
fn should_return(&self, entry: &io::Result<DirEntry>) -> bool {
|
||||
if let Ok(entry) = entry {
|
||||
let mut res = !self.ignored.contains(&entry.file_name());
|
||||
|
||||
// Please just let me combine these already
|
||||
if self.files_only {
|
||||
if let Ok(file_type) = entry.file_type() {
|
||||
res = res && file_type.is_file();
|
||||
}
|
||||
// We couldn't determine if it's a file, so we don't return it
|
||||
else {
|
||||
res = false;
|
||||
}
|
||||
}
|
||||
|
||||
res
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for ReadDirRecursive {
|
||||
type Item = io::Result<DirEntry>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
loop {
|
||||
// First, we try to consume the current directory's items
|
||||
while let Some(entry) = self.read_dir.next() {
|
||||
self.push_entry(&entry);
|
||||
|
||||
if self.should_return(&entry) {
|
||||
return Some(entry);
|
||||
}
|
||||
}
|
||||
|
||||
// If we get an error while setting up a new directory, we return this, otherwise we
|
||||
// keep trying to consume the directories
|
||||
match self.next_read_dir() {
|
||||
Ok(true) => (),
|
||||
// There's no more directories to traverse, so the iterator is done
|
||||
Ok(false) => return None,
|
||||
Err(e) => return Some(Err(e)),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait PathExt {
|
||||
/// Confirm whether the file has not been modified since the given timestamp.
|
||||
///
|
||||
/// This function will only return true if it can determine with certainty that the file hasn't
|
||||
/// been modified.
|
||||
///
|
||||
/// # Args
|
||||
///
|
||||
/// * `timestamp` - Timestamp to compare modified time with
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// True if the file has not been modified for sure, false otherwise.
|
||||
fn not_modified_since(&self, timestamp: chrono::DateTime<Utc>) -> bool;
|
||||
|
||||
/// An extension of the `read_dir` command that runs through the entire underlying directory
|
||||
/// structure using breadth-first search
|
||||
fn read_dir_recursive(&self) -> io::Result<ReadDirRecursive>;
|
||||
}
|
||||
|
||||
impl PathExt for Path {
|
||||
fn not_modified_since(&self, timestamp: chrono::DateTime<Utc>) -> bool {
|
||||
self.metadata()
|
||||
.and_then(|m| m.modified())
|
||||
.map(|last_modified| {
|
||||
let t: chrono::DateTime<Utc> = last_modified.into();
|
||||
let t = t.with_timezone(&Local);
|
||||
|
||||
t < timestamp
|
||||
})
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn read_dir_recursive(&self) -> io::Result<ReadDirRecursive> {
|
||||
ReadDirRecursive::start(self)
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
123
src/main.rs
123
src/main.rs
|
@ -1,123 +0,0 @@
|
|||
mod server;
|
||||
|
||||
use clap::Parser;
|
||||
use server::ServerType;
|
||||
use std::io;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(author, version, about, long_about = None)]
|
||||
struct Cli {
|
||||
/// Type of server
|
||||
type_: ServerType,
|
||||
/// Version string for the server, e.g. 1.19.4-545
|
||||
#[arg(env = "ALEX_SERVER_VERSION")]
|
||||
server_version: String,
|
||||
|
||||
/// Server jar to execute
|
||||
#[arg(
|
||||
long,
|
||||
value_name = "JAR_PATH",
|
||||
default_value = "server.jar",
|
||||
env = "ALEX_JAR"
|
||||
)]
|
||||
jar: PathBuf,
|
||||
/// Directory where configs are stored, and where the server will run
|
||||
#[arg(
|
||||
long,
|
||||
value_name = "CONFIG_DIR",
|
||||
default_value = ".",
|
||||
env = "ALEX_CONFIG_DIR"
|
||||
)]
|
||||
config: PathBuf,
|
||||
/// Directory where world files will be saved
|
||||
#[arg(
|
||||
long,
|
||||
value_name = "WORLD_DIR",
|
||||
default_value = "../worlds",
|
||||
env = "ALEX_WORLD_DIR"
|
||||
)]
|
||||
world: PathBuf,
|
||||
/// Directory where backups will be stored
|
||||
#[arg(
|
||||
long,
|
||||
value_name = "BACKUP_DIR",
|
||||
default_value = "../backups",
|
||||
env = "ALEX_WORLD_DIR"
|
||||
)]
|
||||
backup: PathBuf,
|
||||
/// Java command to run the server jar with
|
||||
#[arg(long, value_name = "JAVA_CMD", default_value_t = String::from("java"), env = "ALEX_JAVA")]
|
||||
java: String,
|
||||
|
||||
/// XMS value in megabytes for the server instance
|
||||
#[arg(long, default_value_t = 1024, env = "ALEX_XMS")]
|
||||
xms: u64,
|
||||
/// XMX value in megabytes for the server instance
|
||||
#[arg(long, default_value_t = 2048, env = "ALEX_XMX")]
|
||||
xmx: u64,
|
||||
|
||||
/// How many backups to keep
|
||||
#[arg(short = 'n', long, default_value_t = 7, env = "ALEX_MAX_BACKUPS")]
|
||||
max_backups: u64,
|
||||
/// How frequently to perform a backup, in minutes; 0 to disable.
|
||||
#[arg(short = 't', long, default_value_t = 0, env = "ALEX_FREQUENCY")]
|
||||
frequency: u64,
|
||||
}
|
||||
|
||||
fn backups_thread(counter: Arc<Mutex<server::ServerProcess>>, frequency: u64) {
|
||||
loop {
|
||||
std::thread::sleep(std::time::Duration::from_secs(frequency * 60));
|
||||
|
||||
{
|
||||
let mut server = counter.lock().unwrap();
|
||||
|
||||
// We explicitely ignore the error here, as we don't want the thread to fail
|
||||
let _ = server.backup();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let cli = Cli::parse();
|
||||
|
||||
let cmd = server::ServerCommand::new(cli.type_, &cli.server_version)
|
||||
.java(&cli.java)
|
||||
.jar(cli.jar)
|
||||
.config(cli.config)
|
||||
.world(cli.world)
|
||||
.backup(cli.backup)
|
||||
.xms(cli.xms)
|
||||
.xmx(cli.xmx)
|
||||
.max_backups(cli.max_backups);
|
||||
let counter = Arc::new(Mutex::new(cmd.spawn().expect("Failed to start server.")));
|
||||
|
||||
if cli.frequency > 0 {
|
||||
let clone = Arc::clone(&counter);
|
||||
std::thread::spawn(move || backups_thread(clone, cli.frequency));
|
||||
}
|
||||
|
||||
let stdin = io::stdin();
|
||||
let input = &mut String::new();
|
||||
|
||||
loop {
|
||||
input.clear();
|
||||
|
||||
if stdin.read_line(input).is_err() {
|
||||
continue;
|
||||
};
|
||||
|
||||
{
|
||||
let mut server = counter.lock().unwrap();
|
||||
|
||||
if let Err(e) = server.send_command(input) {
|
||||
println!("{}", e);
|
||||
};
|
||||
}
|
||||
|
||||
if input.trim() == "stop" {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,137 +0,0 @@
|
|||
use crate::server::ServerProcess;
|
||||
use clap::ValueEnum;
|
||||
use std::fmt;
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::{Command, Stdio};
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
|
||||
pub enum ServerType {
|
||||
Paper,
|
||||
Forge,
|
||||
Vanilla,
|
||||
}
|
||||
|
||||
impl fmt::Display for ServerType {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let s = match self {
|
||||
ServerType::Paper => "PaperMC",
|
||||
ServerType::Forge => "Forge",
|
||||
ServerType::Vanilla => "Vanilla",
|
||||
};
|
||||
|
||||
write!(f, "{}", s)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ServerCommand {
|
||||
type_: ServerType,
|
||||
version: String,
|
||||
java: String,
|
||||
jar: PathBuf,
|
||||
config_dir: PathBuf,
|
||||
world_dir: PathBuf,
|
||||
backup_dir: PathBuf,
|
||||
xms: u64,
|
||||
xmx: u64,
|
||||
max_backups: u64,
|
||||
}
|
||||
|
||||
impl ServerCommand {
|
||||
pub fn new(type_: ServerType, version: &str) -> Self {
|
||||
ServerCommand {
|
||||
type_,
|
||||
version: String::from(version),
|
||||
java: String::from("java"),
|
||||
jar: PathBuf::from("server.jar"),
|
||||
config_dir: PathBuf::from("config"),
|
||||
world_dir: PathBuf::from("worlds"),
|
||||
backup_dir: PathBuf::from("backups"),
|
||||
xms: 1024,
|
||||
xmx: 2048,
|
||||
max_backups: 7,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn java(mut self, java: &str) -> Self {
|
||||
self.java = String::from(java);
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
pub fn jar<T: AsRef<Path>>(mut self, path: T) -> Self {
|
||||
self.jar = PathBuf::from(path.as_ref());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn config<T: AsRef<Path>>(mut self, path: T) -> Self {
|
||||
self.config_dir = PathBuf::from(path.as_ref());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn world<T: AsRef<Path>>(mut self, path: T) -> Self {
|
||||
self.world_dir = PathBuf::from(path.as_ref());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn backup<T: AsRef<Path>>(mut self, path: T) -> Self {
|
||||
self.backup_dir = PathBuf::from(path.as_ref());
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
pub fn xms(mut self, v: u64) -> Self {
|
||||
self.xms = v;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn xmx(mut self, v: u64) -> Self {
|
||||
self.xmx = v;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn max_backups(mut self, v: u64) -> Self {
|
||||
self.max_backups = v;
|
||||
self
|
||||
}
|
||||
|
||||
fn accept_eula(&self) -> std::io::Result<()> {
|
||||
let mut eula_path = self.config_dir.clone();
|
||||
eula_path.push("eula.txt");
|
||||
let mut eula_file = File::create(eula_path)?;
|
||||
eula_file.write_all(b"eula=true")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn spawn(self) -> std::io::Result<ServerProcess> {
|
||||
// To avoid any issues, we use absolute paths for everything when spawning the process
|
||||
let jar = self.jar.canonicalize()?;
|
||||
let config_dir = self.config_dir.canonicalize()?;
|
||||
let world_dir = self.world_dir.canonicalize()?;
|
||||
let backup_dir = self.backup_dir.canonicalize()?;
|
||||
|
||||
self.accept_eula()?;
|
||||
|
||||
let child = Command::new(&self.java)
|
||||
.current_dir(&config_dir)
|
||||
.arg("-jar")
|
||||
.arg(&jar)
|
||||
.arg("--universe")
|
||||
.arg(&world_dir)
|
||||
.arg("--nogui")
|
||||
.stdin(Stdio::piped())
|
||||
.spawn()?;
|
||||
|
||||
Ok(ServerProcess::new(
|
||||
self.type_,
|
||||
self.version,
|
||||
config_dir,
|
||||
world_dir,
|
||||
backup_dir,
|
||||
self.max_backups,
|
||||
child,
|
||||
))
|
||||
}
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
mod command;
|
||||
mod process;
|
||||
|
||||
pub use command::{ServerCommand, ServerType};
|
||||
pub use process::ServerProcess;
|
|
@ -1,159 +0,0 @@
|
|||
use crate::server::ServerType;
|
||||
use flate2::write::GzEncoder;
|
||||
use flate2::Compression;
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Child;
|
||||
|
||||
#[link(name = "c")]
|
||||
extern "C" {
|
||||
fn geteuid() -> u32;
|
||||
fn getegid() -> u32;
|
||||
}
|
||||
|
||||
pub struct ServerProcess {
|
||||
type_: ServerType,
|
||||
version: String,
|
||||
config_dir: PathBuf,
|
||||
world_dir: PathBuf,
|
||||
backup_dir: PathBuf,
|
||||
max_backups: u64,
|
||||
child: Child,
|
||||
}
|
||||
|
||||
impl ServerProcess {
|
||||
pub fn new(
|
||||
type_: ServerType,
|
||||
version: String,
|
||||
config_dir: PathBuf,
|
||||
world_dir: PathBuf,
|
||||
backup_dir: PathBuf,
|
||||
max_backups: u64,
|
||||
child: Child,
|
||||
) -> ServerProcess {
|
||||
ServerProcess {
|
||||
type_,
|
||||
version,
|
||||
config_dir,
|
||||
world_dir,
|
||||
backup_dir,
|
||||
max_backups,
|
||||
child,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn send_command(&mut self, cmd: &str) -> std::io::Result<()> {
|
||||
match cmd.trim() {
|
||||
"stop" | "exit" => self.stop()?,
|
||||
"backup" => self.backup()?,
|
||||
s => self.custom(s)?,
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn custom(&mut self, cmd: &str) -> std::io::Result<()> {
|
||||
let mut stdin = self.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<()> {
|
||||
self.custom("stop")?;
|
||||
self.child.wait()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Perform a backup by disabling the server's save feature and flushing its data, before
|
||||
/// creating an archive file.
|
||||
pub fn backup(&mut self) -> std::io::Result<()> {
|
||||
self.custom("say backing up server")?;
|
||||
|
||||
// Make sure the server isn't modifying the files during the backup
|
||||
self.custom("save-off")?;
|
||||
self.custom("save-all")?;
|
||||
|
||||
// TODO implement a better mechanism
|
||||
// We wait some time to (hopefully) ensure the save-all call has completed
|
||||
std::thread::sleep(std::time::Duration::from_secs(10));
|
||||
|
||||
let res = self.create_backup_archive();
|
||||
|
||||
if res.is_ok() {
|
||||
self.remove_old_backups()?;
|
||||
}
|
||||
|
||||
// The server's save feature needs to be enabled again even if the archive failed to create
|
||||
self.custom("save-on")?;
|
||||
|
||||
self.custom("say server backed up successfully")?;
|
||||
|
||||
res
|
||||
}
|
||||
|
||||
/// Create a new compressed backup archive of the server's data.
|
||||
fn create_backup_archive(&mut self) -> std::io::Result<()> {
|
||||
// Create a gzip-compressed tarball of the worlds folder
|
||||
let filename = format!(
|
||||
"{}",
|
||||
chrono::offset::Local::now().format("%Y-%m-%d_%H-%M-%S.tar.gz")
|
||||
);
|
||||
let path = self.backup_dir.join(filename);
|
||||
let tar_gz = std::fs::File::create(path)?;
|
||||
let enc = GzEncoder::new(tar_gz, Compression::default());
|
||||
let mut tar = tar::Builder::new(enc);
|
||||
|
||||
tar.append_dir_all("worlds", &self.world_dir)?;
|
||||
|
||||
// We don't store all files in the config, as this would include caches
|
||||
tar.append_path_with_name(
|
||||
self.config_dir.join("server.properties"),
|
||||
"config/server.properties",
|
||||
)?;
|
||||
|
||||
// We add a file to the backup describing for what version it was made
|
||||
let info = format!("{} {}", self.type_, self.version);
|
||||
let info_bytes = info.as_bytes();
|
||||
|
||||
let mut header = tar::Header::new_gnu();
|
||||
header.set_size(info_bytes.len().try_into().unwrap());
|
||||
header.set_mode(0o100644);
|
||||
unsafe {
|
||||
header.set_gid(getegid().into());
|
||||
header.set_uid(geteuid().into());
|
||||
}
|
||||
|
||||
tar.append_data(&mut header, "info.txt", info_bytes)?;
|
||||
|
||||
// tar.append_dir_all("config", &self.config_dir)?;
|
||||
|
||||
// Backup file gets finalized in the drop
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Remove the oldest backups
|
||||
fn remove_old_backups(&mut self) -> std::io::Result<()> {
|
||||
// The naming format used allows us to sort the backups by name and still get a sorting by
|
||||
// creation time
|
||||
let mut backups = std::fs::read_dir(&self.backup_dir)?
|
||||
.filter_map(|res| res.map(|e| e.path()).ok())
|
||||
.collect::<Vec<PathBuf>>();
|
||||
backups.sort();
|
||||
|
||||
let max_backups: usize = self.max_backups.try_into().unwrap();
|
||||
|
||||
if backups.len() > max_backups {
|
||||
let excess_backups = backups.len() - max_backups;
|
||||
|
||||
for backup in &backups[0..excess_backups] {
|
||||
std::fs::remove_file(backup)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue