release: yet another Nix Minecraft Module
This commit is contained in:
parent
9fdb947c2a
commit
839bb7de43
13 changed files with 1621 additions and 1 deletions
119
README.md
119
README.md
|
|
@ -1 +1,118 @@
|
|||
# Nyx-Minecraft
|
||||
# Yet another Nix Minecraft module
|
||||
|
||||
This NixOS module extends [Infinidoge/nix-minecraft](https://github.com/Infinidoge/nix-minecraft) with additional features for managing multiple Minecraft servers declaratively.
|
||||
|
||||
## Features
|
||||
|
||||
- Declarative configuration of multiple Minecraft servers
|
||||
|
||||
- Control over memory, operators, whitelist, symlinks, and server properties
|
||||
|
||||
- Automatic **systemd services** and **timers** for scheduled jobs
|
||||
|
||||
- Prebuilt helper scripts for administration:
|
||||
|
||||
- `rcon`
|
||||
- `query`
|
||||
- `backup`
|
||||
- `say`
|
||||
- `backup-routine`
|
||||
|
||||
- Support for:
|
||||
|
||||
- Backups
|
||||
- Logging
|
||||
- Scheduled maintenance tasks
|
||||
|
||||
## Helper Scripts
|
||||
|
||||
For each server defined, helper scripts are generated into your system `$PATH`.
|
||||
They follow the naming convention:
|
||||
|
||||
```bash
|
||||
minecraft-<SERVERNAME>-<TASK>
|
||||
# Example:
|
||||
minecraft-myserver-say "yellow" "hello"
|
||||
```
|
||||
|
||||
You can inspect the generated scripts under: `minecraft/Scripts`
|
||||
|
||||
## Configuration Options
|
||||
|
||||
```nix
|
||||
nyx-minecraft.service = {
|
||||
enable = true; # Enable the enhanced servers (boolean)
|
||||
eula = true; # I can't accept this for you
|
||||
user = "minecraft"; # System user that owns/runs servers - DON'T CHANGE THIS
|
||||
group = "minecraft"; # System group that owns/runs servers - DON'T CHANGE THIS
|
||||
dataDir = "/srv/minecraft"; # Directory for Minecraft server data
|
||||
|
||||
servers = {
|
||||
myserver = {
|
||||
enable = true;
|
||||
memory.min = "2G"; # JVM minimum memory
|
||||
memory.max = "4G"; # JVM maximum memory
|
||||
|
||||
# This will be directly exposed to [Nix-Minecraft](https://github.com/Infinidoge/nix-minecraft/tree/master?tab=readme-ov-file#packages)
|
||||
# Check their documentation for available options.
|
||||
package = pkgs.minecraftServers.vanilla-1_20_4;
|
||||
|
||||
autoStart = true; # Start server on boot
|
||||
|
||||
whitelist = { # Declarative whitelist (UUIDs per user)
|
||||
alice = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee";
|
||||
bob = "ffffffff-1111-2222-3333-444444444444";
|
||||
};
|
||||
|
||||
operators = { # Declarative operator list
|
||||
alice = {
|
||||
uuid = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee";
|
||||
level = 4;
|
||||
};
|
||||
};
|
||||
|
||||
properties = { # Declarative `server.properties`
|
||||
serverPort = 25565;
|
||||
motd = "Welcome to my NixOS server!";
|
||||
maxPlayers = 10;
|
||||
};
|
||||
|
||||
schedules.backup = { # Systemd timer + service job
|
||||
timer = "daily";
|
||||
code = "minecraft-myserver-backup";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
For more details, refer to the [official nix-minecraft documentation](https://github.com/Infinidoge/nix-minecraft), since most arguments are passed through.
|
||||
|
||||
### Server Options
|
||||
|
||||
- **memory.min / memory.max** — JVM memory allocation (e.g. `"2G"`)
|
||||
- **package** — Minecraft server package (default: `pkgs.minecraft-server`)
|
||||
- **autoStart** — Start on boot (`true` / `false`)
|
||||
- **whitelist** — Declarative whitelist keyed by username with UUID
|
||||
- **operators** — Operator list with permission levels (`0–4`)
|
||||
- **symlinks** — Files/packages symlinked into server data directory
|
||||
- **properties** — Declarative `server.properties` values (ports, motd, difficulty, etc.)
|
||||
- **schedules** — Declarative scheduled jobs (with systemd timers and services)
|
||||
|
||||
## Examples
|
||||
|
||||
See the examples in:
|
||||
|
||||
- `./example/full`
|
||||
- `./example/small`
|
||||
|
||||
## Warnings
|
||||
|
||||
- **Use at your own risk** — verify backups and test schedules before relying on them
|
||||
- Pin a known working version for stable usage
|
||||
- Perform **manual backups before updates**
|
||||
|
||||
## Contributing
|
||||
|
||||
I am happy to help with issues and welcome pull requests for improvements.
|
||||
Since I use this module personally, you can expect frequent updates.
|
||||
|
|
|
|||
112
example/full/configuration.nix
Normal file
112
example/full/configuration.nix
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
{
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
|
||||
imports = [
|
||||
|
||||
# ... your imports
|
||||
inputs.nyx-minecraft.nixosModules.minecraft-servers
|
||||
];
|
||||
|
||||
nyx-minecraft.service = {
|
||||
enable = true;
|
||||
eula = true;
|
||||
# user # don't change this
|
||||
# group # don't change this
|
||||
dataDir = "srv/minecraft";
|
||||
|
||||
servers = {
|
||||
testingServer = {
|
||||
enable = true;
|
||||
|
||||
memory = {
|
||||
min = "2G";
|
||||
max = "4G";
|
||||
};
|
||||
|
||||
package = pkgs.minecraftServers.vanilla-1_20_4;
|
||||
autoStart = true;
|
||||
|
||||
whitelist = {
|
||||
player1 = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee";
|
||||
player2 = "ffffffff-1111-2222-3333-444444444444";
|
||||
};
|
||||
|
||||
operators = {
|
||||
admin = {
|
||||
uuid = "99999999-aaaa-bbbb-cccc-dddddddddddd";
|
||||
level = 4;
|
||||
bypassesPlayerLimit = true;
|
||||
};
|
||||
mod = {
|
||||
uuid = "88888888-aaaa-bbbb-cccc-eeeeeeeeeeee";
|
||||
level = 2;
|
||||
bypassesPlayerLimit = false;
|
||||
};
|
||||
};
|
||||
|
||||
properties = {
|
||||
serverPort = 25565;
|
||||
difficulty = 2;
|
||||
gamemode = 0;
|
||||
maxPlayers = 20;
|
||||
motd = "Welcome to the testingServer!";
|
||||
rconPassword = "superSecret123";
|
||||
hardcore = false;
|
||||
levelSeed = "8675309";
|
||||
};
|
||||
|
||||
schedules = {
|
||||
# note schedule can be enabled without the server being enabled
|
||||
backup-hourly = {
|
||||
enable = true;
|
||||
# this is using systemD timers check the official Documentation
|
||||
timer = "hourly";
|
||||
code = ''
|
||||
minecraft-testingServer-backup-routine \
|
||||
--sleep 16 \
|
||||
--destination /srv/minecraft/backups/testingServer/hourly \
|
||||
--pure
|
||||
'';
|
||||
};
|
||||
backup-daily = {
|
||||
enable = true;
|
||||
timer = "daily";
|
||||
code = ''
|
||||
minecraft-testingServer-backup-routine \
|
||||
--sleep 60 \
|
||||
--destination /srv/minecraft/backups/testingServer/daily \
|
||||
--format zip
|
||||
'';
|
||||
};
|
||||
backup-weekly = {
|
||||
enable = true;
|
||||
timer = "weekly";
|
||||
code = ''
|
||||
minecraft-testingServer-backup-routine \
|
||||
--sleep 600 \
|
||||
--full \
|
||||
--destination /srv/minecraft/backups/testingServer/weekly \
|
||||
--format zip
|
||||
'';
|
||||
};
|
||||
backup-monthly = {
|
||||
enable = true;
|
||||
timer = "monthly";
|
||||
code = ''
|
||||
minecraft-testingServer-backup-routine \
|
||||
--sleep 960 \
|
||||
--full \
|
||||
--destination /srv/minecraft/backups/testingServer/monthly \
|
||||
--format zip
|
||||
'';
|
||||
};
|
||||
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
34
example/full/flake.nix
Normal file
34
example/full/flake.nix
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
{
|
||||
description = "EXAMPLE - Flake";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
|
||||
# your own imports
|
||||
nyx-minecraft.url = "github:Peritia-System/Nyx-Minecraft";
|
||||
nyx-minecraft.inputs.nixpkgs.follows = "nixpkgs";
|
||||
|
||||
};
|
||||
|
||||
outputs = inputs @ {
|
||||
self,
|
||||
nixpkgs,
|
||||
nix-minecraft,
|
||||
...
|
||||
}: {
|
||||
nixosConfigurations = {
|
||||
yourSystem = nixpkgs.lib.nixosSystem {
|
||||
system = "x86_64-linux";
|
||||
|
||||
specialArgs = {
|
||||
inherit inputs self;
|
||||
host = "yourSystem";
|
||||
};
|
||||
|
||||
modules = [
|
||||
nixos95.nixosModules.default
|
||||
|
||||
./configuration.nix
|
||||
];
|
||||
};
|
||||
};
|
||||
}
|
||||
58
example/small/configuration.nix
Normal file
58
example/small/configuration.nix
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
{
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
|
||||
imports = [
|
||||
|
||||
# ... your imports
|
||||
inputs.nyx-minecraft.nixosModules.minecraft-servers
|
||||
];
|
||||
|
||||
nyx-minecraft.service = {
|
||||
enable = true;
|
||||
eula = true;
|
||||
# user # don't change this
|
||||
# group # don't change this
|
||||
dataDir = "srv/minecraft";
|
||||
|
||||
servers = {
|
||||
testingServer = {
|
||||
enable = true;
|
||||
|
||||
memory = {
|
||||
min = "2G";
|
||||
max = "4G";
|
||||
};
|
||||
|
||||
package = pkgs.minecraftServers.vanilla-1_20_4;
|
||||
|
||||
# leaving whitelis out just deactivates whitelist
|
||||
# leaving Operators out will just not set any operator
|
||||
|
||||
# Leaving out a property will just set the default
|
||||
properties = {
|
||||
serverPort = 25565;
|
||||
# note you don't need to set query or rcon port
|
||||
# since they will be set 200 and 100 above the Serverport
|
||||
};
|
||||
# you can leave them out than but here a simple example
|
||||
schedules = {
|
||||
# Hourly world-only, pure rsync, no restart
|
||||
greeting-hourly = {
|
||||
enable = true;
|
||||
# note schedule can be enabled without the server being enabled
|
||||
timer = "hourly";
|
||||
code = ''
|
||||
minecraft-testingServer-say "yellow" "hello"
|
||||
# now once an hour it will greet everyone in the server
|
||||
'';
|
||||
};
|
||||
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
34
example/small/flake.nix
Normal file
34
example/small/flake.nix
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
{
|
||||
description = "EXAMPLE - Flake";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
|
||||
# your own imports
|
||||
nyx-minecraft.url = "github:Peritia-System/Nyx-Minecraft";
|
||||
nyx-minecraft.inputs.nixpkgs.follows = "nixpkgs";
|
||||
|
||||
};
|
||||
|
||||
outputs = inputs @ {
|
||||
self,
|
||||
nixpkgs,
|
||||
nix-minecraft,
|
||||
...
|
||||
}: {
|
||||
nixosConfigurations = {
|
||||
yourSystem = nixpkgs.lib.nixosSystem {
|
||||
system = "x86_64-linux";
|
||||
|
||||
specialArgs = {
|
||||
inherit inputs self;
|
||||
host = "yourSystem";
|
||||
};
|
||||
|
||||
modules = [
|
||||
nixos95.nixosModules.default
|
||||
|
||||
./configuration.nix
|
||||
];
|
||||
};
|
||||
};
|
||||
}
|
||||
23
flake.nix
Normal file
23
flake.nix
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
description = "Nyx-Modules";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
|
||||
nix-minecraft.url = "github:Infinidoge/nix-minecraft";
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs, nix-minecraft, ... }: {
|
||||
nixosModules.minecraft-servers = {
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}: {
|
||||
imports = [
|
||||
./minecraft
|
||||
nix-minecraft.nixosModules.minecraft-servers
|
||||
];
|
||||
nixpkgs.overlays = [nix-minecraft.overlay];
|
||||
};
|
||||
};
|
||||
}
|
||||
420
minecraft/Scripts/minecraft-template-backup-routine.sh
Normal file
420
minecraft/Scripts/minecraft-template-backup-routine.sh
Normal file
|
|
@ -0,0 +1,420 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Injected by Nix
|
||||
RSYNC_BIN="@RSYNC_BIN@"
|
||||
DATA_DIR="@DATA_DIR@"
|
||||
MCSTATUS_BIN="@MCSTATUS_BIN@"
|
||||
MCRCON_BIN="@MCRCON_BIN@"
|
||||
AWK_BIN="@AWK_BIN@"
|
||||
QUERY_PORT="@QUERY_PORT@"
|
||||
RCON_PORT="@RCON_PORT@"
|
||||
RCON_PASSWORD="@RCON_PASSWORD@"
|
||||
SERVER_NAME="@SERVER_NAME@"
|
||||
TAR_BIN="@TAR_BIN@"
|
||||
ZIP_BIN="@ZIP_BIN@"
|
||||
UNZIP_BIN="@UNZIP_BIN@"
|
||||
GZIP_BIN="@GZIP_BIN@"
|
||||
ZSTD_BIN="@ZSTD_BIN@"
|
||||
PV_BIN="@PV_BIN@"
|
||||
DU_BIN="@DU_BIN@"
|
||||
BZIP2_BIN="@BZIP2_BIN@"
|
||||
XZ_BIN="@XZ_BIN@"
|
||||
|
||||
# Convenience wrappers
|
||||
rsync_cmd="$RSYNC_BIN"
|
||||
awk_cmd="$AWK_BIN"
|
||||
mcstatus_cmd="$MCSTATUS_BIN 127.0.0.1:${QUERY_PORT}"
|
||||
mcrcon_cmd="$MCRCON_BIN -H 127.0.0.1 -P ${RCON_PORT} -p ${RCON_PASSWORD}"
|
||||
tar_cmd="$TAR_BIN"
|
||||
zip_cmd="$ZIP_BIN"
|
||||
unzip_cmd="$UNZIP_BIN"
|
||||
gzip_cmd="$GZIP_BIN"
|
||||
zstd_cmd="$ZSTD_BIN"
|
||||
pv_cmd="$PV_BIN"
|
||||
du_cmd="$DU_BIN"
|
||||
bzip2_cmd="$BZIP2_BIN"
|
||||
xz_cmd="$XZ_BIN"
|
||||
|
||||
# PATH extension
|
||||
# (only figured that out later if you add it here it can actually just use the bin)
|
||||
# So you can easily just switch out the "*_cmd" with the "normal" name
|
||||
export PATH="$(dirname "$GZIP_BIN")":"$(dirname "$ZSTD_BIN")":"$(dirname "$PV_BIN")":"$(dirname "$DU_BIN")":"$(dirname "$BZIP2_BIN")":"$(dirname "$XZ_BIN")":"$PATH"
|
||||
|
||||
# Defaults
|
||||
REBOOT=false
|
||||
SLEEP_TIME=0
|
||||
FULL=false
|
||||
DESTINATION="/srv/minecraft/backups/unknown"
|
||||
PURE=false
|
||||
FORMAT="tar"
|
||||
COMPRESSION="gzip"
|
||||
|
||||
# Usage
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: $0 [--reboot] [--sleep <seconds>] [--full] [--destination <path>] [--pure]
|
||||
[--format <tar|zip>] [--compression <gzip|bzip2|xz|zstd>]
|
||||
|
||||
--reboot Stop server before backup and start afterwards [DOES NOT WORK]
|
||||
--sleep N Wait N seconds with countdown announcements
|
||||
--full Backup entire server directory (default: world only)
|
||||
--destination X Backup target directory (default: /srv/minecraft/backups/unknown)
|
||||
--pure Use rsync to copy files (no compression, symlinks resolved)
|
||||
--format X Archive format: tar or zip (ignored if --pure)
|
||||
--compression X Compression for tar (default: gzip)
|
||||
EOF
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Argument parsing
|
||||
echo "[DEBUG] Parsing command-line arguments..."
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--reboot) echo "[DEBUG] Flag: --reboot"; REBOOT=true; shift ;;
|
||||
--sleep) echo "[DEBUG] Flag: --sleep $2"; SLEEP_TIME="$2"; shift 2 ;;
|
||||
--full) echo "[DEBUG] Flag: --full"; FULL=true; shift ;;
|
||||
--destination) echo "[DEBUG] Flag: --destination $2"; DESTINATION="$2"; shift 2 ;;
|
||||
--pure) echo "[DEBUG] Flag: --pure"; PURE=true; shift ;;
|
||||
--format) echo "[DEBUG] Flag: --format $2"; FORMAT="$2"; shift 2 ;;
|
||||
--compression) echo "[DEBUG] Flag: --compression $2"; COMPRESSION="$2"; shift 2 ;;
|
||||
--help) usage ;;
|
||||
*) echo "[ERROR] Unknown option: $1"; usage ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Restart if rebooting
|
||||
if [[ "$REBOOT" == true ]]; then
|
||||
echo "[DEBUG] Restarting server does not work"
|
||||
echo "[DEBUG] The sudo can't be enabled due to it not being used by the same path as for the systemd"
|
||||
echo "[DEBUG] and just using systemctl won't work either due to it not having the rights to stop it"
|
||||
echo "[DEBUG] if you fix this pls make a PR"
|
||||
|
||||
fi
|
||||
|
||||
|
||||
|
||||
# Helpers
|
||||
|
||||
say_with_color() {
|
||||
local color="$1"
|
||||
shift
|
||||
local message="$*"
|
||||
local code
|
||||
|
||||
case "$color" in
|
||||
black) code="§0" ;;
|
||||
dark_blue) code="§1" ;;
|
||||
dark_green) code="§2" ;;
|
||||
dark_aqua) code="§3" ;;
|
||||
dark_red) code="§4" ;;
|
||||
dark_purple) code="§5" ;;
|
||||
gold) code="§6" ;;
|
||||
gray) code="§7" ;;
|
||||
dark_gray) code="§8" ;;
|
||||
blue) code="§9" ;;
|
||||
green) code="§a" ;;
|
||||
aqua) code="§b" ;;
|
||||
red) code="§c" ;;
|
||||
light_purple|pink) code="§d" ;;
|
||||
yellow) code="§e" ;;
|
||||
white) code="§f" ;;
|
||||
obfuscated) code="§k" ;;
|
||||
bold) code="§l" ;;
|
||||
strikethrough) code="§m" ;;
|
||||
underline) code="§n" ;;
|
||||
italic) code="§o" ;;
|
||||
reset) code="§r" ;;
|
||||
*) code="" ;;
|
||||
esac
|
||||
|
||||
local full_message="${code}${message}§r"
|
||||
# echo "[DEBUG] Sending RCON say: $full_message"
|
||||
$mcrcon_cmd "say $full_message"
|
||||
}
|
||||
|
||||
say() {
|
||||
echo "[INFO] $1"
|
||||
say_with_color yellow "$1"
|
||||
}
|
||||
|
||||
|
||||
|
||||
countdown() {
|
||||
local seconds="$1"
|
||||
|
||||
#echo "[DEBUG] Starting countdown of $seconds seconds..."
|
||||
say "Backup will start in $seconds seconds"
|
||||
|
||||
while [ "$seconds" -gt 0 ]; do
|
||||
#echo " $seconds"
|
||||
|
||||
# Logic for when to speak updates
|
||||
if [ "$seconds" -le 15 ]; then
|
||||
say "$seconds"
|
||||
elif [ "$seconds" -le 60 ] && (( seconds % 10 == 0 )); then
|
||||
say "$seconds seconds remaining"
|
||||
elif [ "$seconds" -le 120 ] && (( seconds % 30 == 0 )); then
|
||||
say "$seconds seconds remaining"
|
||||
elif [ "$seconds" -le 300 ] && (( seconds % 60 == 0 )); then
|
||||
say "$seconds seconds remaining"
|
||||
fi
|
||||
|
||||
sleep 1
|
||||
((seconds--))
|
||||
done
|
||||
|
||||
echo
|
||||
say "Countdown finished. Starting backup now."
|
||||
}
|
||||
|
||||
|
||||
do_backup() {
|
||||
#echo "[DEBUG] Entering do_backup with args: $*"
|
||||
local source=""
|
||||
local destination=""
|
||||
local compression="gzip"
|
||||
local format="tar"
|
||||
local pure=false
|
||||
|
||||
# parse args
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--source) source="$2"; shift 2 ;;
|
||||
--destination) destination="$2"; shift 2 ;;
|
||||
--compression) compression="$2"; shift 2 ;;
|
||||
--format) format="$2"; shift 2 ;;
|
||||
--pure) pure=true; shift ;;
|
||||
|
||||
*) echo "[ERROR] Unknown option to do_backup: $1"; return 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
|
||||
if [[ -z "$source" || -z "$destination" ]]; then
|
||||
echo "[ERROR] Missing --source or --destination"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local timestamp="$(date +%Y%m%d-%H%M%S)"
|
||||
local full_source="$DATA_DIR/$source"
|
||||
local basename="$(basename "$source")"
|
||||
local archive=""
|
||||
local ext=""
|
||||
|
||||
|
||||
if [[ ! -d "$full_source" ]]; then
|
||||
echo "[ERROR] Source directory not found: $full_source"
|
||||
return 1
|
||||
fi
|
||||
|
||||
mkdir -p "$destination"
|
||||
|
||||
if [[ "$pure" == true ]]; then
|
||||
local target_dir="$destination/${basename}-${timestamp}"
|
||||
echo "[INFO] Performing pure rsync backup to $target_dir"
|
||||
echo "#####"
|
||||
|
||||
local last_percentage=-1
|
||||
|
||||
"$rsync_cmd" -rptgoDL --delete --info=progress2 --stats \
|
||||
"$full_source/" "$target_dir/" 2>&1 | \
|
||||
while IFS= read -r -d $'\r' chunk; do
|
||||
# Extract percentage
|
||||
if [[ $chunk =~ ([0-9]{1,3})% ]]; then
|
||||
current_percentage=${BASH_REMATCH[1]}
|
||||
# Print only if percentage changed
|
||||
if [[ $current_percentage -ne $last_percentage ]]; then
|
||||
echo -e "Progress: ${current_percentage}%\r" | say "$@"
|
||||
last_percentage=$current_percentage
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
|
||||
return 0
|
||||
fi
|
||||
|
||||
#echo "[DEBUG] Using archive mode: format=$format compression=$compression"
|
||||
# Archive/compression backup
|
||||
|
||||
case "$format" in
|
||||
tar)
|
||||
# Map compression → extension and tar invocation (using only *_cmd vars)
|
||||
case "$compression" in
|
||||
none)
|
||||
ext="tar"
|
||||
tar_create=( "$tar_cmd" -cvf )
|
||||
;;
|
||||
gzip)
|
||||
ext="tar.gz"
|
||||
tar_create=( "$tar_cmd" --use-compress-program="$gzip_cmd" -cvf )
|
||||
;;
|
||||
bzip2)
|
||||
ext="tar.bz2"
|
||||
tar_create=( "$tar_cmd" --use-compress-program="$bzip2_cmd" -cvf )
|
||||
;;
|
||||
xz)
|
||||
ext="tar.xz"
|
||||
tar_create=( "$tar_cmd" --use-compress-program="$xz_cmd" -cvf )
|
||||
;;
|
||||
zstd)
|
||||
ext="tar.zst"
|
||||
tar_create=( "$tar_cmd" --use-compress-program="$zstd_cmd" -cvf )
|
||||
;;
|
||||
*)
|
||||
echo "[ERROR] Unsupported tar compression: $compression" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
archive="$destination/${basename}-${timestamp}.${ext}"
|
||||
sources=( $source )
|
||||
for s in "${sources[@]}"; do
|
||||
if [[ ! -e "$DATA_DIR/$s" ]]; then
|
||||
echo "[ERROR] Source not found under DATA_DIR: $DATA_DIR/$s" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
# Count total items for progress (files + dirs)
|
||||
total_items=0
|
||||
for s in "${sources[@]}"; do
|
||||
count=$(find "$DATA_DIR/$s" | wc -l)
|
||||
total_items=$(( total_items + count ))
|
||||
done
|
||||
current=0
|
||||
|
||||
echo "[INFO] Creating $archive"
|
||||
|
||||
last_percent=-1
|
||||
last_info_time=$(date +%s)
|
||||
# seconds between info logs
|
||||
interval=2
|
||||
|
||||
(
|
||||
"${tar_create[@]}" "$archive" -C "$DATA_DIR" "${sources[@]}"
|
||||
) 2>&1 | while read -r line; do
|
||||
if [[ -n "$line" ]]; then
|
||||
current=$(( current + 1 ))
|
||||
if (( total_items > 0 )); then
|
||||
percent=$(( current * 100 / total_items ))
|
||||
|
||||
# echo full percent:
|
||||
#echo "[DEBUG] Progress: ${percent}%"
|
||||
now=$(date +%s)
|
||||
if (( percent != last_percent )) && (( now - last_info_time >= interval )); then
|
||||
#echo "[INFO] Progress: ${percent}%"
|
||||
say "Progress: ${percent}%"
|
||||
last_percent=$percent
|
||||
last_info_time=$now
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
# Ensure 100% gets printed once at the end
|
||||
if (( last_percent < 100 )); then
|
||||
#echo "[INFO] Progress: 100%"
|
||||
say "Progress: 100%"
|
||||
|
||||
fi
|
||||
|
||||
echo "[INFO] Tar archive created: $archive"
|
||||
;;
|
||||
|
||||
zip)
|
||||
ext="zip"
|
||||
archive="$destination/${basename}-${timestamp}.${ext}"
|
||||
echo "[INFO] Creating zip archive $archive"
|
||||
|
||||
# Count both files and directories
|
||||
total_items=$(find "$DATA_DIR/$source" | wc -l)
|
||||
current=0
|
||||
|
||||
last_percent=-1
|
||||
last_info_time=$(date +%s)
|
||||
interval=2 # seconds between info logs
|
||||
|
||||
(
|
||||
cd "$DATA_DIR"
|
||||
"$zip_cmd" -r "$archive" "$source"
|
||||
) 2>&1 | while read -r line; do
|
||||
if [[ $line =~ adding: ]]; then
|
||||
current=$((current+1))
|
||||
if (( total_items > 0 )); then
|
||||
percent=$(( current * 100 / total_items ))
|
||||
|
||||
#echo "[DEBUG] Progress: ${percent}%"
|
||||
|
||||
now=$(date +%s)
|
||||
if (( percent != last_percent )) && (( now - last_info_time >= interval )); then
|
||||
#echo "[INFO] Progress: ${percent}%"
|
||||
say "Progress: ${percent}%"
|
||||
last_percent=$percent
|
||||
last_info_time=$now
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
# Ensure 100% gets printed once at the end
|
||||
if (( last_percent < 100 )); then
|
||||
#echo "[INFO] Progress: 100%"
|
||||
say "Progress: 100%"
|
||||
fi
|
||||
|
||||
echo "[INFO] Zip archive created: $archive"
|
||||
;;
|
||||
|
||||
*)
|
||||
echo "[ERROR] Unsupported format: $format"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "[INFO] Backup completed: $archive"
|
||||
return 0
|
||||
|
||||
}
|
||||
|
||||
|
||||
# MAIN
|
||||
|
||||
#echo "[DEBUG] FULL=$FULL"
|
||||
|
||||
if [[ "$FULL" == true ]]; then
|
||||
BACKUP_SOURCE="${SERVER_NAME}"
|
||||
BACKUP_MODE="full server directory"
|
||||
DESTINATION="${DESTINATION}/Full"
|
||||
else
|
||||
BACKUP_SOURCE="${SERVER_NAME}/world"
|
||||
BACKUP_MODE="world folder only"
|
||||
DESTINATION="${DESTINATION}/World"
|
||||
fi
|
||||
|
||||
say "Backup for ($BACKUP_MODE) initiated"
|
||||
|
||||
# Pre-backup wait
|
||||
if (( SLEEP_TIME > 0 )); then
|
||||
countdown "$SLEEP_TIME"
|
||||
fi
|
||||
|
||||
|
||||
mkdir -p "$DESTINATION"
|
||||
|
||||
echo "[INFO] Running backup of $BACKUP_MODE to $DESTINATION..."
|
||||
if do_backup \
|
||||
--source "$BACKUP_SOURCE" \
|
||||
--destination "$DESTINATION" \
|
||||
$([[ "$PURE" == true ]] && echo "--pure") \
|
||||
--compression "$COMPRESSION" \
|
||||
--format "$FORMAT"; then
|
||||
echo "[INFO] Backup finished successfully."
|
||||
else
|
||||
echo "[ERROR] Backup failed!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
say "Backup ($BACKUP_MODE) completed successfully."
|
||||
134
minecraft/Scripts/minecraft-template-backup.sh
Normal file
134
minecraft/Scripts/minecraft-template-backup.sh
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Injected by Nix
|
||||
RSYNC_BIN="@RSYNC_BIN@"
|
||||
DATA_DIR="@DATA_DIR@"
|
||||
MCSTATUS_BIN="@MCSTATUS_BIN@"
|
||||
MCRCON_BIN="@MCRCON_BIN@"
|
||||
AWK_BIN="@AWK_BIN@"
|
||||
QUERY_PORT="@QUERY_PORT@"
|
||||
RCON_PORT="@RCON_PORT@"
|
||||
RCON_PASSWORD="@RCON_PASSWORD@"
|
||||
SERVER_NAME="@SERVER_NAME@"
|
||||
TAR_BIN="@TAR_BIN@"
|
||||
ZIP_BIN="@ZIP_BIN@"
|
||||
UNZIP_BIN="@UNZIP_BIN@"
|
||||
GZIP_BIN="@GZIP_BIN@"
|
||||
ZSTD_BIN="@ZSTD_BIN@"
|
||||
PV_BIN="@PV_BIN@"
|
||||
DU_BIN="@DU_BIN@"
|
||||
BZIP2_BIN="@BZIP2_BIN@"
|
||||
XZ_BIN="@XZ_BIN@"
|
||||
|
||||
# Convenience wrappers
|
||||
rsync_cmd="$RSYNC_BIN"
|
||||
awk_cmd="$AWK_BIN"
|
||||
mcstatus_cmd="$MCSTATUS_BIN 127.0.0.1:${QUERY_PORT}"
|
||||
mcrcon_cmd="$MCRCON_BIN -H 127.0.0.1 -P ${RCON_PORT} -p ${RCON_PASSWORD}"
|
||||
tar_cmd="$TAR_BIN"
|
||||
zip_cmd="$ZIP_BIN"
|
||||
unzip_cmd="$UNZIP_BIN"
|
||||
gzip_cmd="$GZIP_BIN"
|
||||
zstd_cmd="$ZSTD_BIN"
|
||||
pv_cmd="$PV_BIN"
|
||||
du_cmd="$DU_BIN"
|
||||
bzip2_cmd="$BZIP2_BIN"
|
||||
xz_cmd="$XZ_BIN"
|
||||
|
||||
# PATH extension
|
||||
# (only figured that out later if you add it here it can actually just use the bin)
|
||||
# So you can easily just switch out the "*_cmd" with the "normal" name
|
||||
# export PATH="$(dirname "$GZIP_BIN")":"$(dirname "$ZSTD_BIN")":"$(dirname "$PV_BIN")":"$(dirname "$DU_BIN")":"$(dirname "$BZIP2_BIN")":"$(dirname "$XZ_BIN")":"$PATH"
|
||||
|
||||
|
||||
|
||||
# Defaults
|
||||
SOURCE=""
|
||||
DESTINATION=""
|
||||
COMPRESSION="gzip"
|
||||
FORMAT="tar"
|
||||
PURE=false
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: $0 --source <subfolder> --destination <path>
|
||||
[--compression <gzip|bzip2|xz|zstd>] [--format <tar|zip>] [--pure]
|
||||
|
||||
Options:
|
||||
--source Subfolder under \$DATA_DIR to back up (required)
|
||||
--destination Backup destination path (required)
|
||||
--compression Compression method for tar archives (default: gzip)
|
||||
--format Archive format: tar or zip (default: tar)
|
||||
--pure Perform plain rsync copy without compression
|
||||
--help Show this help
|
||||
EOF
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Parse arguments
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--source) SOURCE="$2"; shift 2;;
|
||||
--destination) DESTINATION="$2"; shift 2;;
|
||||
--compression) COMPRESSION="$2"; shift 2;;
|
||||
--format) FORMAT="$2"; shift 2;;
|
||||
--pure) PURE=true; shift 1;;
|
||||
--help) usage;;
|
||||
*) echo "Unknown option: $1"; usage;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Validation
|
||||
if [[ -z "$SOURCE" || -z "$DESTINATION" ]]; then
|
||||
echo "Error: --source and --destination are required."
|
||||
usage
|
||||
fi
|
||||
|
||||
FULL_SOURCE="$DATA_DIR/$SOURCE"
|
||||
|
||||
if [[ ! -d "$FULL_SOURCE" ]]; then
|
||||
echo "Error: Source directory '$FULL_SOURCE' does not exist."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$DESTINATION"
|
||||
|
||||
TIMESTAMP="$(date +%Y%m%d-%H%M%S)"
|
||||
BASENAME="$(basename "$SOURCE")"
|
||||
|
||||
# Pure rsync backup
|
||||
if [[ "$PURE" == true ]]; then
|
||||
TARGET_DIR="$DESTINATION/${BASENAME}-${TIMESTAMP}"
|
||||
echo "Performing pure rsync backup to $TARGET_DIR"
|
||||
"$rsync_cmd" -rptgoDL --delete "$FULL_SOURCE/" "$TARGET_DIR/"
|
||||
echo "Backup completed (pure): $TARGET_DIR"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Archive/compression backup
|
||||
case "$FORMAT" in
|
||||
tar)
|
||||
case "$COMPRESSION" in
|
||||
gzip) EXT="tar.gz"; TAR_ARGS="-czf";;
|
||||
bzip2) EXT="tar.bz2"; TAR_ARGS="-cjf";;
|
||||
xz) EXT="tar.xz"; TAR_ARGS="-cJf";;
|
||||
zstd) EXT="tar.zst"; TAR_ARGS="--zstd -cf";;
|
||||
*) echo "Unsupported compression for tar: $COMPRESSION"; exit 1;;
|
||||
esac
|
||||
ARCHIVE="$DESTINATION/${BASENAME}-${TIMESTAMP}.${EXT}"
|
||||
"$tar_cmd" -C "$DATA_DIR" $TAR_ARGS "$ARCHIVE" "$SOURCE"
|
||||
;;
|
||||
zip)
|
||||
EXT="zip"
|
||||
ARCHIVE="$DESTINATION/${BASENAME}-${TIMESTAMP}.${EXT}"
|
||||
(cd "$DATA_DIR" && "$zip_cmd" -r "$ARCHIVE" "$SOURCE")
|
||||
;;
|
||||
*)
|
||||
echo "Unsupported format: $FORMAT"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
|
||||
echo "Backup completed: $ARCHIVE"
|
||||
47
minecraft/Scripts/minecraft-template-query.sh
Normal file
47
minecraft/Scripts/minecraft-template-query.sh
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Injected by Nix
|
||||
RSYNC_BIN="@RSYNC_BIN@"
|
||||
DATA_DIR="@DATA_DIR@"
|
||||
MCSTATUS_BIN="@MCSTATUS_BIN@"
|
||||
MCRCON_BIN="@MCRCON_BIN@"
|
||||
AWK_BIN="@AWK_BIN@"
|
||||
QUERY_PORT="@QUERY_PORT@"
|
||||
RCON_PORT="@RCON_PORT@"
|
||||
RCON_PASSWORD="@RCON_PASSWORD@"
|
||||
SERVER_NAME="@SERVER_NAME@"
|
||||
TAR_BIN="@TAR_BIN@"
|
||||
ZIP_BIN="@ZIP_BIN@"
|
||||
UNZIP_BIN="@UNZIP_BIN@"
|
||||
GZIP_BIN="@GZIP_BIN@"
|
||||
ZSTD_BIN="@ZSTD_BIN@"
|
||||
PV_BIN="@PV_BIN@"
|
||||
DU_BIN="@DU_BIN@"
|
||||
BZIP2_BIN="@BZIP2_BIN@"
|
||||
XZ_BIN="@XZ_BIN@"
|
||||
|
||||
# Convenience wrappers
|
||||
rsync_cmd="$RSYNC_BIN"
|
||||
awk_cmd="$AWK_BIN"
|
||||
mcstatus_cmd="$MCSTATUS_BIN 127.0.0.1:${QUERY_PORT}"
|
||||
mcrcon_cmd="$MCRCON_BIN -H 127.0.0.1 -P ${RCON_PORT} -p ${RCON_PASSWORD}"
|
||||
tar_cmd="$TAR_BIN"
|
||||
zip_cmd="$ZIP_BIN"
|
||||
unzip_cmd="$UNZIP_BIN"
|
||||
gzip_cmd="$GZIP_BIN"
|
||||
zstd_cmd="$ZSTD_BIN"
|
||||
pv_cmd="$PV_BIN"
|
||||
du_cmd="$DU_BIN"
|
||||
bzip2_cmd="$BZIP2_BIN"
|
||||
xz_cmd="$XZ_BIN"
|
||||
|
||||
# PATH extension
|
||||
# (only figured that out later if you add it here it can actually just use the bin)
|
||||
# So you can easily just switch out the "*_cmd" with the "normal" name
|
||||
# export PATH="$(dirname "$GZIP_BIN")":"$(dirname "$ZSTD_BIN")":"$(dirname "$PV_BIN")":"$(dirname "$DU_BIN")":"$(dirname "$BZIP2_BIN")":"$(dirname "$XZ_BIN")":"$PATH"
|
||||
|
||||
|
||||
|
||||
# Query the server
|
||||
exec $mcstatus_cmd query
|
||||
47
minecraft/Scripts/minecraft-template-rcon.sh
Normal file
47
minecraft/Scripts/minecraft-template-rcon.sh
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Injected by Nix
|
||||
RSYNC_BIN="@RSYNC_BIN@"
|
||||
DATA_DIR="@DATA_DIR@"
|
||||
MCSTATUS_BIN="@MCSTATUS_BIN@"
|
||||
MCRCON_BIN="@MCRCON_BIN@"
|
||||
AWK_BIN="@AWK_BIN@"
|
||||
QUERY_PORT="@QUERY_PORT@"
|
||||
RCON_PORT="@RCON_PORT@"
|
||||
RCON_PASSWORD="@RCON_PASSWORD@"
|
||||
SERVER_NAME="@SERVER_NAME@"
|
||||
TAR_BIN="@TAR_BIN@"
|
||||
ZIP_BIN="@ZIP_BIN@"
|
||||
UNZIP_BIN="@UNZIP_BIN@"
|
||||
GZIP_BIN="@GZIP_BIN@"
|
||||
ZSTD_BIN="@ZSTD_BIN@"
|
||||
PV_BIN="@PV_BIN@"
|
||||
DU_BIN="@DU_BIN@"
|
||||
BZIP2_BIN="@BZIP2_BIN@"
|
||||
XZ_BIN="@XZ_BIN@"
|
||||
|
||||
# Convenience wrappers
|
||||
rsync_cmd="$RSYNC_BIN"
|
||||
awk_cmd="$AWK_BIN"
|
||||
mcstatus_cmd="$MCSTATUS_BIN 127.0.0.1:${QUERY_PORT}"
|
||||
mcrcon_cmd="$MCRCON_BIN -H 127.0.0.1 -P ${RCON_PORT} -p ${RCON_PASSWORD}"
|
||||
tar_cmd="$TAR_BIN"
|
||||
zip_cmd="$ZIP_BIN"
|
||||
unzip_cmd="$UNZIP_BIN"
|
||||
gzip_cmd="$GZIP_BIN"
|
||||
zstd_cmd="$ZSTD_BIN"
|
||||
pv_cmd="$PV_BIN"
|
||||
du_cmd="$DU_BIN"
|
||||
bzip2_cmd="$BZIP2_BIN"
|
||||
xz_cmd="$XZ_BIN"
|
||||
|
||||
# PATH extension
|
||||
# (only figured that out later if you add it here it can actually just use the bin)
|
||||
# So you can easily just switch out the "*_cmd" with the "normal" name
|
||||
# export PATH="$(dirname "$GZIP_BIN")":"$(dirname "$ZSTD_BIN")":"$(dirname "$PV_BIN")":"$(dirname "$DU_BIN")":"$(dirname "$BZIP2_BIN")":"$(dirname "$XZ_BIN")":"$PATH"
|
||||
|
||||
|
||||
|
||||
# Pass arguments directly to mcrcon
|
||||
exec $mcrcon_cmd "$@"
|
||||
109
minecraft/Scripts/minecraft-template-say.sh
Normal file
109
minecraft/Scripts/minecraft-template-say.sh
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Injected by Nix
|
||||
RSYNC_BIN="@RSYNC_BIN@"
|
||||
DATA_DIR="@DATA_DIR@"
|
||||
MCSTATUS_BIN="@MCSTATUS_BIN@"
|
||||
MCRCON_BIN="@MCRCON_BIN@"
|
||||
AWK_BIN="@AWK_BIN@"
|
||||
QUERY_PORT="@QUERY_PORT@"
|
||||
RCON_PORT="@RCON_PORT@"
|
||||
RCON_PASSWORD="@RCON_PASSWORD@"
|
||||
SERVER_NAME="@SERVER_NAME@"
|
||||
TAR_BIN="@TAR_BIN@"
|
||||
ZIP_BIN="@ZIP_BIN@"
|
||||
UNZIP_BIN="@UNZIP_BIN@"
|
||||
GZIP_BIN="@GZIP_BIN@"
|
||||
ZSTD_BIN="@ZSTD_BIN@"
|
||||
PV_BIN="@PV_BIN@"
|
||||
DU_BIN="@DU_BIN@"
|
||||
BZIP2_BIN="@BZIP2_BIN@"
|
||||
XZ_BIN="@XZ_BIN@"
|
||||
|
||||
# Convenience wrappers
|
||||
rsync_cmd="$RSYNC_BIN"
|
||||
awk_cmd="$AWK_BIN"
|
||||
mcstatus_cmd="$MCSTATUS_BIN 127.0.0.1:${QUERY_PORT}"
|
||||
mcrcon_cmd="$MCRCON_BIN -H 127.0.0.1 -P ${RCON_PORT} -p ${RCON_PASSWORD}"
|
||||
tar_cmd="$TAR_BIN"
|
||||
zip_cmd="$ZIP_BIN"
|
||||
unzip_cmd="$UNZIP_BIN"
|
||||
gzip_cmd="$GZIP_BIN"
|
||||
zstd_cmd="$ZSTD_BIN"
|
||||
pv_cmd="$PV_BIN"
|
||||
du_cmd="$DU_BIN"
|
||||
bzip2_cmd="$BZIP2_BIN"
|
||||
xz_cmd="$XZ_BIN"
|
||||
|
||||
# PATH extension
|
||||
# (only figured that out later if you add it here it can actually just use the bin)
|
||||
# So you can easily just switch out the "*_cmd" with the "normal" name
|
||||
# export PATH="$(dirname "$GZIP_BIN")":"$(dirname "$ZSTD_BIN")":"$(dirname "$PV_BIN")":"$(dirname "$DU_BIN")":"$(dirname "$BZIP2_BIN")":"$(dirname "$XZ_BIN")":"$PATH"
|
||||
|
||||
|
||||
|
||||
# Argument parsing
|
||||
if [[ $# -lt 2 ]]; then
|
||||
echo "Usage: $0 <color|format> <message...>"
|
||||
echo "Example: $0 red 'Server restarting soon!'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
CODE_NAME="$1"
|
||||
shift
|
||||
MESSAGE="$*"
|
||||
|
||||
# Map color/format names to Minecraft § codes
|
||||
case "$CODE_NAME" in
|
||||
# Colors
|
||||
black) CODE="§0" ;;
|
||||
dark_blue) CODE="§1" ;;
|
||||
dark_green) CODE="§2" ;;
|
||||
dark_aqua) CODE="§3" ;;
|
||||
dark_red) CODE="§4" ;;
|
||||
dark_purple) CODE="§5" ;;
|
||||
gold) CODE="§6" ;;
|
||||
gray) CODE="§7" ;;
|
||||
dark_gray) CODE="§8" ;;
|
||||
blue) CODE="§9" ;;
|
||||
green) CODE="§a" ;;
|
||||
aqua) CODE="§b" ;;
|
||||
red) CODE="§c" ;;
|
||||
light_purple|pink) CODE="§d" ;;
|
||||
yellow) CODE="§e" ;;
|
||||
white) CODE="§f" ;;
|
||||
|
||||
# Bedrock-only extras
|
||||
minecoin_gold) CODE="§g" ;;
|
||||
material_quartz) CODE="§h" ;;
|
||||
material_iron) CODE="§i" ;;
|
||||
material_netherite) CODE="§j" ;;
|
||||
material_redstone) CODE="§m" ;;
|
||||
material_copper) CODE="§n" ;;
|
||||
material_gold) CODE="§p" ;;
|
||||
material_emerald) CODE="§q" ;;
|
||||
material_diamond) CODE="§s" ;;
|
||||
material_lapis) CODE="§t" ;;
|
||||
material_amethyst) CODE="§u" ;;
|
||||
|
||||
# Formatting
|
||||
obfuscated) CODE="§k" ;;
|
||||
bold) CODE="§l" ;;
|
||||
strikethrough) CODE="§m" ;;
|
||||
underline) CODE="§n" ;;
|
||||
italic) CODE="§o" ;;
|
||||
reset) CODE="§r" ;;
|
||||
|
||||
*)
|
||||
echo "Unknown code: $CODE_NAME"
|
||||
echo "Available colors: black, dark_blue, dark_green, dark_aqua, dark_red, dark_purple, gold, gray, dark_gray, blue, green, aqua, red, light_purple, yellow, white"
|
||||
echo "Formats: obfuscated, bold, strikethrough, underline, italic, reset"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
FULL_MESSAGE="${CODE}${MESSAGE}§r"
|
||||
|
||||
# Send via RCON
|
||||
exec $mcrcon_cmd "say $FULL_MESSAGE"
|
||||
11
minecraft/default.nix
Normal file
11
minecraft/default.nix
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
inputs,
|
||||
...
|
||||
}: {
|
||||
imports = [
|
||||
./minecraft.nix
|
||||
];
|
||||
}
|
||||
474
minecraft/minecraft.nix
Normal file
474
minecraft/minecraft.nix
Normal file
|
|
@ -0,0 +1,474 @@
|
|||
# Enhanced Minecraft Server Module
|
||||
#
|
||||
# This module provides:
|
||||
# - A wrapper around the Infinidoge/nix-minecraft module
|
||||
# - Declarative configuration for multiple servers, including memory, operators, whitelist, and symlinks
|
||||
# - Automatic systemd service and timer generation for scheduled jobs
|
||||
# - Prebuilt helper scripts (rcon, query, backup, say, backup-routine) for server administration
|
||||
# - Support for backups, logging, and scheduled maintenance tasks
|
||||
#
|
||||
# Configuration Options:
|
||||
# enable – Enable the enhanced Minecraft servers (boolean)
|
||||
# user – System user that owns and runs the servers (string) - DON'T CHANGE THIS
|
||||
# group – System group that owns and runs the servers (string) - DON'T CHANGE THIS
|
||||
# dataDir – Directory to store Minecraft server data (path)
|
||||
# servers – Attribute set of servers, keyed by name. Each server can define:
|
||||
# memory.min / memory.max – JVM memory allocation (strings, e.g. "2G")
|
||||
# package – Minecraft server package to use (package)
|
||||
# autoStart – Start server at boot (boolean)
|
||||
# whitelist – Declarative whitelist (UUIDs per user)
|
||||
# operators – Declarative operator list with permission levels
|
||||
# symlinks – Files or packages symlinked into the server data directory
|
||||
# properties – Declarative `server.properties` values (ports, motd, difficulty, etc.)
|
||||
# schedules – Declarative scheduled jobs with systemd timers and services
|
||||
#
|
||||
#
|
||||
# Info:
|
||||
# I am happy to help if you have Issues and am happy to see a PR for any change
|
||||
# I ll use it personally too so you can expect frequent updates
|
||||
#
|
||||
#
|
||||
# ToDo:
|
||||
# Schedule to restart a server using systemD. I have not figured that out yet.
|
||||
# what i do know is that `sudo` does not work neither does it work if you just tell the server to restart.
|
||||
#
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
with lib; let
|
||||
cfg = config.nyx-minecraft.service;
|
||||
minecraftUUID = types.strMatching "[0-9a-fA-F-]{36}";
|
||||
|
||||
# Setup the Scripts for the server
|
||||
scriptDir = ./Scripts;
|
||||
mkScript = serverName: serverCfg: scriptType: let
|
||||
templateFile = scriptDir + "/minecraft-template-${scriptType}.sh";
|
||||
rawText = builtins.readFile templateFile;
|
||||
scriptText =
|
||||
builtins.replaceStrings
|
||||
[
|
||||
"@DATA_DIR@"
|
||||
"@RSYNC_BIN@"
|
||||
"@MCSTATUS_BIN@"
|
||||
"@MCRCON_BIN@"
|
||||
"@AWK_BIN@"
|
||||
"@QUERY_PORT@"
|
||||
"@RCON_PORT@"
|
||||
"@RCON_PASSWORD@"
|
||||
"@SERVER_NAME@"
|
||||
"@TAR_BIN@"
|
||||
"@ZIP_BIN@"
|
||||
"@UNZIP_BIN@"
|
||||
"@GZIP_BIN@"
|
||||
"@ZSTD_BIN@"
|
||||
"@PV_BIN@"
|
||||
"@DU_BIN@"
|
||||
"@BZIP2_BIN@"
|
||||
"@XZ_BIN@"
|
||||
]
|
||||
# If you add anything here make sure to add it at the systemd service too
|
||||
[
|
||||
cfg.dataDir
|
||||
"${pkgs.rsync}/bin/rsync"
|
||||
"${pkgs.mcstatus}/bin/mcstatus"
|
||||
"${pkgs.mcrcon}/bin/mcrcon"
|
||||
"${pkgs.gawk}/bin/awk"
|
||||
(toString (serverCfg.properties.serverPort + 200))
|
||||
(toString (serverCfg.properties.serverPort + 100))
|
||||
serverCfg.properties.rconPassword
|
||||
serverName
|
||||
"${pkgs.gnutar}/bin/tar"
|
||||
"${pkgs.zip}/bin/zip"
|
||||
"${pkgs.unzip}/bin/unzip"
|
||||
"${pkgs.gzip}/bin/gzip"
|
||||
"${pkgs.zstd}/bin/zstd"
|
||||
"${pkgs.pv}/bin/pv"
|
||||
"${pkgs.coreutils}/bin/du"
|
||||
"${pkgs.bzip2}/bin/bzip2"
|
||||
"${pkgs.xz}/bin/xz"
|
||||
]
|
||||
rawText;
|
||||
in
|
||||
pkgs.writeShellScriptBin "minecraft-${serverName}-${scriptType}" scriptText;
|
||||
in {
|
||||
# Note most of the options get directly exposed
|
||||
# to nix-minecraft which makes it almost an
|
||||
# Drop in replacement
|
||||
options.nyx-minecraft.service = {
|
||||
enable = mkEnableOption "Enable enhanced Minecraft servers with backup, logging, and admin helpers.";
|
||||
|
||||
eula = mkEnableOption ''
|
||||
Whether you agree to
|
||||
<link xlink:href="https://account.mojang.com/documents/minecraft_eula">
|
||||
Mojang's EULA</link>. This option must be set to
|
||||
<literal>true</literal> to run Minecraft server.
|
||||
'';
|
||||
|
||||
user = mkOption {
|
||||
type = types.str;
|
||||
default = "minecraft";
|
||||
description = ''
|
||||
Name of the user to create and run servers under.
|
||||
It is recommended to leave this as the default, as it is
|
||||
the same user as <option>services.minecraft-server</option>.
|
||||
'';
|
||||
internal = true;
|
||||
visible = false;
|
||||
};
|
||||
|
||||
group = mkOption {
|
||||
type = types.str;
|
||||
default = "minecraft";
|
||||
description = ''
|
||||
Name of the group to create and run servers under.
|
||||
In order to modify the server files your user must be a part of this
|
||||
group. If you are using the tmux management system (the default), you also need to be a part of this group to attach to the tmux socket.
|
||||
It is recommended to leave this as the default, as it is
|
||||
the same group as <option>services.minecraft-server</option>.
|
||||
'';
|
||||
};
|
||||
|
||||
dataDir = mkOption {
|
||||
type = types.path;
|
||||
default = "/srv/minecraft";
|
||||
description = ''
|
||||
Directory to store the Minecraft servers.
|
||||
Each server will be under a subdirectory named after
|
||||
the server name in this directory, such as <literal>/srv/minecraft/servername</literal>. '';
|
||||
};
|
||||
|
||||
servers = mkOption {
|
||||
type = types.attrsOf (types.submodule ({name, ...}: {
|
||||
options = {
|
||||
enable = mkEnableOption "Enable this Server";
|
||||
|
||||
memory = mkOption {
|
||||
type = types.submodule {
|
||||
options = {
|
||||
min = mkOption {
|
||||
type = types.str;
|
||||
default = "2G";
|
||||
description = "Min JVM memory.";
|
||||
};
|
||||
max = mkOption {
|
||||
type = types.str;
|
||||
default = "2G";
|
||||
description = "Max JVM memory.";
|
||||
};
|
||||
};
|
||||
};
|
||||
default = {
|
||||
min = "2G";
|
||||
max = "2G";
|
||||
};
|
||||
description = "JVM memory settings for this server.";
|
||||
};
|
||||
|
||||
package = mkOption {
|
||||
description = "The Minecraft server package to use.";
|
||||
type = types.package;
|
||||
default = pkgs.minecraft-server;
|
||||
defaultText = literalExpression "pkgs.minecraft-server";
|
||||
example = "pkgs.minecraftServers.vanilla-1_18_2";
|
||||
};
|
||||
|
||||
autoStart = mkOption {
|
||||
type = types.bool;
|
||||
default = true;
|
||||
description = ''
|
||||
Whether to start this server on boot.
|
||||
If set to <literal>false</literal>, it can still be started with
|
||||
<literal>systemctl start minecraft-server-servername</literal>.
|
||||
Requires the server to be enabled.
|
||||
'';
|
||||
};
|
||||
|
||||
whitelist = mkOption {
|
||||
type = types.attrsOf minecraftUUID;
|
||||
default = {};
|
||||
description = ''
|
||||
Whitelisted players, only has an effect when
|
||||
enabled via <option>services.minecraft-servers.<name>.serverProperties</option>
|
||||
by setting <literal>white-list</literal> to <literal>true</literal>.
|
||||
|
||||
To use a non-declarative whitelist, enable the whitelist and don't fill in this value.
|
||||
As long as it is empty, no whitelist file is generated.
|
||||
'';
|
||||
example = literalExpression ''
|
||||
{
|
||||
username1 = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx";
|
||||
username2 = "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy";
|
||||
}
|
||||
'';
|
||||
};
|
||||
|
||||
symlinks = mkOption {
|
||||
type = types.attrsOf (types.either types.path types.package);
|
||||
default = {};
|
||||
description = ''
|
||||
Things to symlink into this server's data directory.
|
||||
Can be used to declaratively manage arbitrary files (e.g., mods, configs).
|
||||
'';
|
||||
};
|
||||
|
||||
operators = mkOption {
|
||||
type = types.attrsOf (
|
||||
types.coercedTo minecraftUUID (v: {uuid = v;}) (
|
||||
types.submodule {
|
||||
options = {
|
||||
uuid = mkOption {
|
||||
type = minecraftUUID;
|
||||
description = "The operator's UUID";
|
||||
example = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx";
|
||||
};
|
||||
level = mkOption {
|
||||
type = types.ints.between 0 4;
|
||||
description = "The operator's permission level";
|
||||
default = 4;
|
||||
};
|
||||
bypassesPlayerLimit = mkOption {
|
||||
type = types.bool;
|
||||
description = "If true, the operator can join the server even if the player limit has been reached";
|
||||
default = false;
|
||||
};
|
||||
};
|
||||
}
|
||||
)
|
||||
);
|
||||
default = {};
|
||||
description = "Operators with permission levels.";
|
||||
};
|
||||
|
||||
properties = mkOption {
|
||||
type = types.submodule {
|
||||
options = {
|
||||
serverPort = mkOption {
|
||||
type = types.int;
|
||||
default = 25565;
|
||||
description = "Server Port";
|
||||
};
|
||||
difficulty = mkOption {
|
||||
type = types.int;
|
||||
default = 2;
|
||||
description = "Difficulty in numbers: 0=Peaceful, 1=Easy, 2=Normal, 3=Hard";
|
||||
};
|
||||
gamemode = mkOption {
|
||||
type = types.int;
|
||||
default = 0;
|
||||
description = "Gamemode: 0=Survival, 1=Creative";
|
||||
};
|
||||
maxPlayers = mkOption {
|
||||
type = types.int;
|
||||
default = 5;
|
||||
description = "How many players can join the server";
|
||||
};
|
||||
motd = mkOption {
|
||||
type = types.str;
|
||||
default = "NixOS Minecraft server!";
|
||||
description = "Message displayed when selecting the server";
|
||||
};
|
||||
rconPassword = mkOption {
|
||||
type = types.str;
|
||||
default = "superSecret";
|
||||
description = "Password for Rcon";
|
||||
};
|
||||
hardcore = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = "Enable Hardcore mode";
|
||||
};
|
||||
levelSeed = mkOption {
|
||||
type = types.str;
|
||||
default = "42";
|
||||
description = "World seed (default is the answer to the universe)";
|
||||
};
|
||||
};
|
||||
};
|
||||
default = {};
|
||||
description = "Declarative Minecraft server.properties values.";
|
||||
};
|
||||
|
||||
schedules = mkOption {
|
||||
type = types.attrsOf (types.submodule ({name, ...}: {
|
||||
options = {
|
||||
enable = mkOption {
|
||||
type = types.bool;
|
||||
default = true;
|
||||
description = "Whether this schedule is active.";
|
||||
};
|
||||
|
||||
timer = mkOption {
|
||||
type = types.str;
|
||||
example = "hourly";
|
||||
description = "Systemd timer unit specifier (e.g., hourly, daily, weekly).";
|
||||
};
|
||||
|
||||
code = mkOption {
|
||||
type = types.lines;
|
||||
description = "Shell code to execute when the schedule fires.";
|
||||
};
|
||||
|
||||
# Not properly enough tested
|
||||
#customEnviorment = mkOption {
|
||||
# type = types.lines;
|
||||
# description = "to expose binaries or other things to the SystemD service";
|
||||
# example = "ZSTD_BIN=${pkgs.zstd}/bin/zstd";
|
||||
#};
|
||||
};
|
||||
}));
|
||||
default = {};
|
||||
description = "Scheduled jobs for this Minecraft server.";
|
||||
};
|
||||
};
|
||||
}));
|
||||
default = {};
|
||||
description = "Servers to run under the enhanced Minecraft service.";
|
||||
};
|
||||
};
|
||||
|
||||
# Wrapper for nix-minecraft
|
||||
config = mkIf cfg.enable {
|
||||
services.minecraft-servers = {
|
||||
enable = true;
|
||||
eula = cfg.eula;
|
||||
openFirewall = true;
|
||||
user = cfg.user;
|
||||
group = cfg.group;
|
||||
dataDir = cfg.dataDir;
|
||||
|
||||
servers =
|
||||
lib.mapAttrs (serverName: serverCfg: {
|
||||
enable = serverCfg.enable;
|
||||
package = serverCfg.package;
|
||||
jvmOpts = "-Xmx${serverCfg.memory.max} -Xms${serverCfg.memory.min}";
|
||||
autoStart = serverCfg.autoStart;
|
||||
|
||||
symlinks = serverCfg.symlinks;
|
||||
whitelist = serverCfg.whitelist;
|
||||
operators = serverCfg.operators;
|
||||
|
||||
serverProperties = {
|
||||
enable-rcon = true;
|
||||
enable-command-block = true;
|
||||
allow-flight = true;
|
||||
enable-query = true;
|
||||
server-port = serverCfg.properties.serverPort;
|
||||
difficulty = serverCfg.properties.difficulty;
|
||||
gamemode = serverCfg.properties.gamemode;
|
||||
max-players = serverCfg.properties.maxPlayers;
|
||||
motd = serverCfg.properties.motd;
|
||||
"rcon.password" = serverCfg.properties.rconPassword;
|
||||
"rcon.port" = serverCfg.properties.serverPort + 100;
|
||||
"query.port" = serverCfg.properties.serverPort + 200;
|
||||
|
||||
hardcore = serverCfg.properties.hardcore;
|
||||
level-seed = serverCfg.properties.levelSeed;
|
||||
};
|
||||
})
|
||||
cfg.servers;
|
||||
};
|
||||
|
||||
# Schedule logic
|
||||
systemd.services = lib.mkMerge (
|
||||
lib.mapAttrsToList (
|
||||
serverName: serverCfg:
|
||||
lib.mapAttrs' (scheduleName: scheduleCfg: let
|
||||
# yes this will be building the scripts twice but thsi
|
||||
# way the path is accessible by the SystemD service
|
||||
rconBin = mkScript serverName serverCfg "rcon";
|
||||
queryBin = mkScript serverName serverCfg "query";
|
||||
backupBin = mkScript serverName serverCfg "backup";
|
||||
sayBin = mkScript serverName serverCfg "say";
|
||||
routineBin = mkScript serverName serverCfg "backup-routine";
|
||||
in {
|
||||
name = "minecraft-${serverName}-${scheduleName}";
|
||||
value = {
|
||||
description = "Minecraft ${serverName} scheduled job: ${scheduleName}";
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
User = cfg.user;
|
||||
Group = cfg.group;
|
||||
Environment = [
|
||||
"RCON_BIN=${rconBin}/bin/minecraft-${serverName}-rcon"
|
||||
"QUERY_BIN=${queryBin}/bin/minecraft-${serverName}-query"
|
||||
"BACKUP_BIN=${backupBin}/bin/minecraft-${serverName}-backup"
|
||||
"SAY_BIN=${sayBin}/bin/minecraft-${serverName}-say"
|
||||
"ROUTINE_BIN=${routineBin}/bin/minecraft-${serverName}-backup-routine"
|
||||
"ZSTD_BIN=${pkgs.zstd}/bin/zstd"
|
||||
# add more bin here
|
||||
|
||||
|
||||
|
||||
# Not properly enough tested
|
||||
#
|
||||
#scheduleCfg.customEnviorment
|
||||
];
|
||||
ExecStart = pkgs.writeShellScript "minecraft-${serverName}-${scheduleName}.sh" ''
|
||||
#!/usr/bin/env bash
|
||||
echo "hi — available helpers:"
|
||||
echo "if you want any custom scripts or"
|
||||
echo "packages for your script you need to ExecStart"
|
||||
echo " $RCON_BIN"
|
||||
echo " $QUERY_BIN"
|
||||
echo " $BACKUP_BIN"
|
||||
echo " $SAY_BIN"
|
||||
echo " $ROUTINE_BIN"
|
||||
|
||||
# this is so it can use the scripts the same way you would run them:
|
||||
minecraft-${serverName}-query() { $QUERY_BIN "$@"; }
|
||||
minecraft-${serverName}-rcon() { $RCON_BIN "$@"; }
|
||||
minecraft-${serverName}-backup() { $BACKUP_BIN "$@"; }
|
||||
minecraft-${serverName}-say() { $SAY_BIN "$@"; }
|
||||
minecraft-${serverName}-backup-routine() { $ROUTINE_BIN "$@"; }
|
||||
minecraft-${serverName}-query
|
||||
|
||||
# her your code will go:
|
||||
${scheduleCfg.code}
|
||||
|
||||
'';
|
||||
};
|
||||
|
||||
# If you have this on:
|
||||
# wantedBy = [ "multi-user.target" ];
|
||||
# it will run the service on each rebuild.
|
||||
# i dont want that you can enable it if you like
|
||||
};
|
||||
}) serverCfg.schedules
|
||||
)
|
||||
cfg.servers
|
||||
);
|
||||
|
||||
# the timers to actually run the SystemD service
|
||||
systemd.timers = lib.mkMerge (
|
||||
lib.mapAttrsToList (
|
||||
serverName: serverCfg:
|
||||
lib.mapAttrs' (scheduleName: scheduleCfg: {
|
||||
name = "minecraft-${serverName}-${scheduleName}";
|
||||
value = {
|
||||
description = "Timer for Minecraft ${serverName} schedule ${scheduleName}";
|
||||
wantedBy = ["timers.target"];
|
||||
timerConfig.OnCalendar = scheduleCfg.timer;
|
||||
};
|
||||
})
|
||||
serverCfg.schedules
|
||||
)
|
||||
cfg.servers
|
||||
);
|
||||
|
||||
# this is building the scripts for the user
|
||||
# Those are the prewritten scripts from the ./Script dir
|
||||
environment.systemPackages = lib.flatten (
|
||||
lib.mapAttrsToList (serverName: serverCfg: [
|
||||
(mkScript serverName serverCfg "rcon")
|
||||
(mkScript serverName serverCfg "query")
|
||||
(mkScript serverName serverCfg "backup")
|
||||
(mkScript serverName serverCfg "say")
|
||||
(mkScript serverName serverCfg "backup-routine")
|
||||
])
|
||||
cfg.servers
|
||||
);
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue