FROM eclipse-temurin:17-jre-jammy USER root RUN apt-get update && \ apt-get install -y --no-install-recommends \ bash \ curl \ jq \ ca-certificates \ coreutils \ findutils \ python3 && \ rm -rf /var/lib/apt/lists/* # ── Bake Vineflower into the image at build time ────────────────────────────── RUN curl -fL \ -o /app/vineflower.jar \ https://github.com/Vineflower/vineflower/releases/download/1.11.2/vineflower-1.11.2.jar && \ java -jar /app/vineflower.jar --help > /dev/null 2>&1 || true WORKDIR /app EXPOSE 7860 CMD ["bash", "-lc", "\ set -u; \ BASE=/data/minecraft; \ TMP=/tmp/mc-sync; \ MANIFEST_URL=https://piston-meta.mojang.com/mc/game/version_manifest_v2.json; \ MANIFEST_PATH=$BASE/version_manifest_v2.json; \ LOCKDIR=$BASE/.lock; \ LOG=$BASE/download.log; \ PROGRESS=$BASE/progress.txt; \ JOBS=${JOBS:-8}; \ DECOMPILE_JOBS=${DECOMPILE_JOBS:-4}; \ VINEFLOWER=/app/vineflower.jar; \ mkdir -p $BASE/server $BASE/client $TMP; \ printf 'starting\\n' > $PROGRESS; \ \ cat > /app/web.py <<'PY'\n\ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer\n\ import os, html\n\ BASE='/data/minecraft'\n\ class H(BaseHTTPRequestHandler):\n\ def do_GET(self):\n\ self.send_response(200)\n\ self.send_header('Content-Type','text/html; charset=utf-8')\n\ self.end_headers()\n\ prog='starting'\n\ log=''\n\ try:\n\ with open(os.path.join(BASE,'progress.txt'),'r',encoding='utf-8',errors='ignore') as f: prog=f.read()\n\ except: pass\n\ try:\n\ with open(os.path.join(BASE,'download.log'),'r',encoding='utf-8',errors='ignore') as f: log=''.join(f.readlines()[-200:])\n\ except: pass\n\ self.wfile.write(f'''Minecraft Sync

Minecraft Official Artifact Sync + Decompile

Progress

{html.escape(prog)}

Recent Logs

{html.escape(log)}
'''.encode())\n\ def log_message(self,*a):\n\ return\n\ ThreadingHTTPServer(('0.0.0.0',7860),H).serve_forever()\n\ PY\n\ python3 /app/web.py & \ \ log(){ printf '[%s] %s\\n' \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\" \"$*\" | tee -a $LOG; printf '%s\\n' \"$*\" > $PROGRESS; }; \ cleanup_tmp(){ rm -rf $TMP/*; }; \ acquire_lock(){ if mkdir $LOCKDIR 2>/dev/null; then trap 'rm -rf \"$LOCKDIR\"; rm -rf \"$TMP\"/*' EXIT INT TERM; return 0; fi; log 'Another sync appears to be running'; exit 1; }; \ download_atomic(){ \ url=\"$1\"; dest=\"$2\"; expected_sha1=\"${3:-}\"; \ mkdir -p \"$(dirname \"$dest\")\"; \ part=\"${dest}.part\"; \ rm -f \"$part\"; \ if ! curl -fL --retry 8 --retry-delay 2 --connect-timeout 20 --max-time 3600 -A 'Mozilla/5.0' -o \"$part\" \"$url\"; then rm -f \"$part\"; return 1; fi; \ if [ ! -s \"$part\" ]; then rm -f \"$part\"; return 1; fi; \ if [ -n \"$expected_sha1\" ]; then \ actual_sha1=\"$(sha1sum \"$part\" | awk '{print $1}')\"; \ if [ \"$actual_sha1\" != \"$expected_sha1\" ]; then \ log \"SHA1 mismatch for $dest expected=$expected_sha1 actual=$actual_sha1\"; \ rm -f \"$part\"; return 1; \ fi; \ fi; \ mv -f \"$part\" \"$dest\"; return 0; \ }; \ is_valid_existing(){ \ file=\"$1\"; expected_sha1=\"${2:-}\"; \ [ -s \"$file\" ] || return 1; \ if [ -n \"$expected_sha1\" ]; then \ actual_sha1=\"$(sha1sum \"$file\" | awk '{print $1}')\"; \ [ \"$actual_sha1\" = \"$expected_sha1\" ] || return 1; \ fi; \ return 0; \ }; \ \ # ── Download worker ──────────────────────────────────────────────────────────── cat > /app/worker.sh <<'SH'\n\ #!/bin/bash\n\ set -u\n\ BASE=\"$1\"\n\ TMP=\"$2\"\n\ LOG=\"$3\"\n\ PROGRESS=\"$4\"\n\ ITEM=\"$5\"\n\ log(){ printf '[%s] %s\\n' \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\" \"$*\" | tee -a \"$LOG\"; printf '%s\\n' \"$*\" > \"$PROGRESS\"; }\n\ download_atomic(){\n\ url=\"$1\"; dest=\"$2\"; expected_sha1=\"${3:-}\"\n\ mkdir -p \"$(dirname \"$dest\")\"\n\ part=\"${dest}.part\"\n\ rm -f \"$part\"\n\ if ! curl -fL --retry 8 --retry-delay 2 --connect-timeout 20 --max-time 3600 -A 'Mozilla/5.0' -o \"$part\" \"$url\"; then rm -f \"$part\"; return 1; fi\n\ if [ ! -s \"$part\" ]; then rm -f \"$part\"; return 1; fi\n\ if [ -n \"$expected_sha1\" ]; then\n\ actual_sha1=\"$(sha1sum \"$part\" | awk '{print $1}')\"\n\ [ \"$actual_sha1\" = \"$expected_sha1\" ] || { log \"SHA1 mismatch for $dest\"; rm -f \"$part\"; return 1; }\n\ fi\n\ mv -f \"$part\" \"$dest\"\n\ }\n\ is_valid_existing(){\n\ file=\"$1\"; expected_sha1=\"${2:-}\"\n\ [ -s \"$file\" ] || return 1\n\ if [ -n \"$expected_sha1\" ]; then\n\ actual_sha1=\"$(sha1sum \"$file\" | awk '{print $1}')\"\n\ [ \"$actual_sha1\" = \"$expected_sha1\" ] || return 1\n\ fi\n\ return 0\n\ }\n\ id=\"$(printf '%s' \"$ITEM\" | cut -d'|' -f1)\"\n\ meta_url=\"$(printf '%s' \"$ITEM\" | cut -d'|' -f2-)\"\n\ tmp_json=\"$TMP/${id}.json\"\n\ log \"$id fetching version metadata\"\n\ download_atomic \"$meta_url\" \"$tmp_json\" '' || { log \"$id failed metadata\"; exit 0; }\n\ for side in client server; do\n\ dl_url=\"$(jq -r \".downloads.${side}.url // empty\" \"$tmp_json\")\"\n\ dl_sha1=\"$(jq -r \".downloads.${side}.sha1 // empty\" \"$tmp_json\")\"\n\ [ -n \"$dl_url\" ] || { log \"$id [$side] no official download entry, skipping\"; continue; }\n\ dir=\"$BASE/$side/$id\"\n\ json_dest=\"$dir/$id.json\"\n\ jar_dest=\"$dir/${side}.jar\"\n\ mkdir -p \"$dir\"\n\ cp -f \"$tmp_json\" \"$json_dest\"\n\ if is_valid_existing \"$jar_dest\" \"$dl_sha1\"; then log \"$id [$side] already complete, skipping\"; continue; fi\n\ [ ! -e \"$jar_dest\" ] || rm -f \"$jar_dest\"\n\ log \"$id [$side] downloading jar\"\n\ if download_atomic \"$dl_url\" \"$jar_dest\" \"$dl_sha1\"; then log \"$id [$side] download complete\"; else rm -f \"$jar_dest\"; log \"$id [$side] download failed\"; fi\n\ done\n\ SH\n\ chmod +x /app/worker.sh; \ \ # ── Decompile worker ─────────────────────────────────────────────────────────── # Layout per version/side: # /data/minecraft/{client|server}/{id}/ # {id}.json <- version metadata # {client|server}.jar # src/ <- vineflower output (java source tree) # .decompiled <- stamp: written only after a clean run cat > /app/decompile_worker.sh <<'SH'\n\ #!/bin/bash\n\ set -u\n\ LOG=\"$1\"\n\ PROGRESS=\"$2\"\n\ JAR=\"$3\"\n\ log(){ printf '[%s] %s\\n' \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\" \"$*\" | tee -a \"$LOG\"; printf '%s\\n' \"$*\" > \"$PROGRESS\"; }\n\ \n\ dir=\"$(dirname \"$JAR\")\"\n\ side=\"$(basename \"$JAR\" .jar)\"\n\ id=\"$(basename \"$dir\")\"\n\ label=\"$id [$side]\"\n\ src_dir=\"$dir/src\"\n\ stamp=\"$dir/.decompiled\"\n\ \n\ # Skip if already successfully decompiled\n\ if [ -f \"$stamp\" ] && [ -d \"$src_dir\" ] && [ -n \"$(ls -A \"$src_dir\" 2>/dev/null)\" ]; then\n\ log \"$label decompile already done, skipping\"\n\ exit 0\n\ fi\n\ \n\ log \"$label starting decompile -> $src_dir\"\n\ rm -rf \"$src_dir\"\n\ mkdir -p \"$src_dir\"\n\ \n\ if java -jar /app/vineflower.jar \\\n\ --folder \\\n\ --log-level=WARN \\\n\ --thread-count=2 \\\n\ --decompile-generics=1 \\\n\ --decompile-enums=1 \\\n\ --decompile-inner=1 \\\n\ --use-lvt-names=1 \\\n\ --use-method-parameters=1 \\\n\ --override-annotation=1 \\\n\ --pattern-matching=1 \\\n\ --remove-synthetic=0 \\\n\ --remove-bridge=0 \\\n\ \"$JAR\" \"$src_dir\" >> \"$LOG\" 2>&1; then\n\ touch \"$stamp\"\n\ log \"$label decompile complete\"\n\ else\n\ log \"$label decompile FAILED (partial output left in $src_dir)\"\n\ exit 1\n\ fi\n\ SH\n\ chmod +x /app/decompile_worker.sh; \ \ # ── Main flow ────────────────────────────────────────────────────────────────── acquire_lock; \ \ log 'Starting Minecraft artifact sync'; \ log 'Downloading global version manifest'; \ if ! download_atomic \"$MANIFEST_URL\" \"$MANIFEST_PATH\" ''; then log 'Failed to download manifest'; exit 1; fi; \ TOTAL=\"$(jq '.versions | length' \"$MANIFEST_PATH\")\"; \ log \"Manifest contains $TOTAL versions\"; \ \ log \"Phase 1/2 — downloading all jars (parallel jobs=$JOBS)\"; \ jq -r '.versions[] | \"\\(.id)|\\(.url)\"' \"$MANIFEST_PATH\" \ | xargs -I{} -P \"$JOBS\" bash /app/worker.sh \"$BASE\" \"$TMP\" \"$LOG\" \"$PROGRESS\" '{}'; \ \ log 'Phase 1/2 complete — all jars downloaded'; \ \ log \"Phase 2/2 — decompiling all jars with Vineflower (parallel jobs=$DECOMPILE_JOBS, 2 threads each)\"; \ find \"$BASE/client\" \"$BASE/server\" \ -mindepth 2 -maxdepth 2 \ -name '*.jar' \ ! -name '*.part' \ | sort \ | xargs -I{} -P \"$DECOMPILE_JOBS\" bash /app/decompile_worker.sh \"$LOG\" \"$PROGRESS\" '{}'; \ \ log 'Phase 2/2 complete — all decompilation finished'; \ log 'Sync + decompile complete'; \ wait \ "]