diff --git a/tooling/mirror-mods.sh b/tooling/mirror-mods.sh new file mode 100755 index 0000000..b2c2f2f --- /dev/null +++ b/tooling/mirror-mods.sh @@ -0,0 +1,136 @@ +#!/usr/bin/env bash +# mirror-mods.sh — vendor mod jars locally + rewrite packwiz download URLs. +# +# For offline (air-gapped) LAN play, packwiz .pw.toml files point at the +# Modrinth/CF CDN. This script downloads each jar into pack/mods-files/, +# rewrites the [download] url to the local Caddy mirror, then runs +# `packwiz refresh` so index.toml/hashes match the rewritten state. +# +# Cautious by design: halts on download failure or hash mismatch, idempotent +# (skips already-vendored valid jars), and no-ops gracefully with zero mods. +# pack/mods-files/ is gitignored — only the .pw.toml metadata is tracked. +set -euo pipefail + +cd "$(dirname "$0")/.." # repo root + +# --- env ------------------------------------------------------------------- +[ -f .env ] && { set -a; . ./.env; set +a; } +: "${BASE_DOMAIN:?BASE_DOMAIN unset (set in .env)}" + +# --- packwiz binary -------------------------------------------------------- +PACKWIZ="/home/oier/go/bin/packwiz" +[ -x "$PACKWIZ" ] || PACKWIZ="$(command -v packwiz || true)" +[ -n "$PACKWIZ" ] || { echo "error: packwiz not found (looked at /home/oier/go/bin/packwiz and PATH)" >&2; exit 1; } + +PACK_DIR="pack" +MODS_META_DIR="$PACK_DIR/mods" +MODS_FILES_DIR="$PACK_DIR/mods-files" +MIRROR_BASE="http://packwiz.${BASE_DOMAIN}/mods-files" + +[ -f "$PACK_DIR/pack.toml" ] || { echo "error: $PACK_DIR/pack.toml not found — run packwiz init first" >&2; exit 1; } + +# --- collect .pw.toml files ------------------------------------------------ +shopt -s nullglob +metas=("$MODS_META_DIR"/*.pw.toml) +shopt -u nullglob + +if [ "${#metas[@]}" -eq 0 ]; then + echo "no mods in $MODS_META_DIR/ — nothing to mirror (no-op)." + exit 0 +fi + +mkdir -p "$MODS_FILES_DIR" + +# --- helpers --------------------------------------------------------------- +# Read a top-level scalar TOML key (filename) from a .pw.toml. +toml_top_value() { # $1=file $2=key + sed -n "s/^[[:space:]]*$2[[:space:]]*=[[:space:]]*\"\(.*\)\"[[:space:]]*\$/\1/p" "$1" | head -n1 +} + +# Read a key from the [download] table only. +toml_download_value() { # $1=file $2=key + awk -v key="$2" ' + /^\[/ { in_dl = ($0 ~ /^\[download\]/) ; next } + in_dl { + line=$0 + sub(/^[ \t]*/, "", line) + if (line ~ "^" key "[ \t]*=") { + sub("^" key "[ \t]*=[ \t]*", "", line) + gsub(/^"|"[ \t]*$/, "", line) + print line + exit + } + } + ' "$1" +} + +sha256_of() { sha256sum "$1" | awk '{print $1}'; } + +changed=0 +echo "mirroring ${#metas[@]} mod(s) → $MIRROR_BASE" + +for meta in "${metas[@]}"; do + filename="$(toml_top_value "$meta" filename)" + url="$(toml_download_value "$meta" url)" + hash_format="$(toml_download_value "$meta" hash-format)" + want_hash="$(toml_download_value "$meta" hash)" + + [ -n "$filename" ] || { echo "error: no filename in $meta" >&2; exit 1; } + [ -n "$url" ] || { echo "error: no [download] url in $meta" >&2; exit 1; } + + dest="$MODS_FILES_DIR/$filename" + already_local="$MIRROR_BASE/$filename" + + # If the .pw.toml already points at the mirror and the jar exists & verifies, + # skip (idempotent re-run). + if [ "$url" = "$already_local" ] && [ -f "$dest" ]; then + if [ "${hash_format:-sha256}" = "sha256" ] && [ -n "$want_hash" ]; then + if [ "$(sha256_of "$dest")" = "$want_hash" ]; then + echo " skip $filename (already vendored, hash ok)" + continue + fi + echo " re-fetch $filename (local hash mismatch)" + else + echo " skip $filename (already vendored)" + continue + fi + fi + + # Download (from current url — CDN on first run). Use a temp file so a failed + # download never leaves a partial jar behind. + echo " fetch $filename" + tmp="$dest.partial" + if ! curl -fSL --retry 3 -o "$tmp" "$url"; then + rm -f "$tmp" + echo "error: download failed for $filename ($url)" >&2 + exit 1 + fi + + # Verify against the recorded hash before trusting the file. + if [ "${hash_format:-sha256}" = "sha256" ] && [ -n "$want_hash" ]; then + got_hash="$(sha256_of "$tmp")" + if [ "$got_hash" != "$want_hash" ]; then + rm -f "$tmp" + echo "error: hash mismatch for $filename" >&2 + echo " expected: $want_hash" >&2 + echo " got: $got_hash" >&2 + exit 1 + fi + fi + mv -f "$tmp" "$dest" + + # Rewrite the [download] url to the local mirror if not already. + if [ "$url" != "$already_local" ]; then + esc_old="$(printf '%s' "$url" | sed -e 's/[\/&]/\\&/g')" + esc_new="$(printf '%s' "$already_local" | sed -e 's/[\/&]/\\&/g')" + sed -i "s/^\([[:space:]]*url[[:space:]]*=[[:space:]]*\)\"$esc_old\"/\1\"$esc_new\"/" "$meta" + echo " url → $already_local" + changed=1 + fi +done + +# --- refresh so index.toml + hashes reflect the rewritten metadata --------- +echo "running packwiz refresh ..." +( cd "$PACK_DIR" && "$PACKWIZ" refresh ) + +echo "done. mirrored ${#metas[@]} mod(s)$( [ "$changed" -eq 1 ] && echo ', URLs rewritten to local mirror' )."