From bcf798a5f972400bc8e2ce0a85a63ac763b1201f Mon Sep 17 00:00:00 2001 From: Nathan Rashleigh Date: Tue, 10 Feb 2026 10:46:45 +1100 Subject: [PATCH] fastdl --- .env | 4 ++++ docker-compose.yaml | 13 +++++++++-- etc/cfg/mapcycle.txt | 1 + etc/run.sh | 51 +++++++++++++++++++++++++++++++++++++++++++ src/fastdl/Dockerfile | 2 ++ src/fastdl/nginx.conf | 8 +++++++ src/mapsdl/common.ts | 31 ++++++++++++++++++-------- 7 files changed, 99 insertions(+), 11 deletions(-) create mode 100644 etc/cfg/mapcycle.txt create mode 100644 src/fastdl/Dockerfile create mode 100644 src/fastdl/nginx.conf diff --git a/.env b/.env index 5ac417a..cdffbd6 100644 --- a/.env +++ b/.env @@ -30,6 +30,10 @@ SRCDS_MAPCYCLE="mapcycle.txt" SRCDS_ADMINS="STEAM_1:1:12270329" +# ---------- +# fastdl +FASTDL_URL=https://fastdl.megastructure.surf + # ---------- # mariadb MARIADB_ROOT_PASSWORD="surfsup" diff --git a/docker-compose.yaml b/docker-compose.yaml index 20bb98b..642c60d 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -24,6 +24,7 @@ services: - ./etc:/home/steam/etc - ./data/cssds:/home/steam/cssds - tf2ds:/home/steam/tf2ds + - fastdl-maps:/fastdl/maps:ro db: image: mariadb:12 @@ -47,8 +48,8 @@ services: # Mount credentials (read-only) - ./src/mapsdl/credentials:/app/credentials:ro - # Mount maps directory (read-write) - - ./data/cssds/cstrike/maps:/maps + # Output bz2 files to shared FastDL volume + - fastdl-maps:/maps environment: - MAPS_DIR=/maps @@ -62,5 +63,13 @@ services: # Run once and exit restart: "no" + fastdl: + build: ./src/fastdl + ports: + - "8080:80" + volumes: + - fastdl-maps:/srv/maps:ro + volumes: tf2ds: + fastdl-maps: diff --git a/etc/cfg/mapcycle.txt b/etc/cfg/mapcycle.txt new file mode 100644 index 0000000..11cdfae --- /dev/null +++ b/etc/cfg/mapcycle.txt @@ -0,0 +1 @@ +surf_boreas diff --git a/etc/run.sh b/etc/run.sh index 636ec2a..b513279 100755 --- a/etc/run.sh +++ b/etc/run.sh @@ -451,6 +451,55 @@ EOF cfg() { cd cp etc/cfg/server.cfg cssds/cstrike/cfg/ + cp etc/cfg/mapcycle.txt cssds/cstrike/cfg/ + sed -i "s|sv_downloadurl .*|sv_downloadurl \"${FASTDL_URL:-}/cstrike/\"|" "$CSTRIKE/cfg/server.cfg" +} + +populate_maps() { + local BZ2_DIR="/fastdl/maps" + local MAPS_DIR="$CSTRIKE/maps" + local MAPCYCLE="$CSTRIKE/cfg/mapcycle.txt" + + echo "--------------------------------------------------------------" + echo "Populating maps from compressed archive" + echo "--------------------------------------------------------------" + + if [ ! -f "$MAPCYCLE" ]; then + echo "No mapcycle.txt found, skipping map population" + echo "--------------------------------------------------------------" + return + fi + + mkdir -p "$MAPS_DIR" + + local total=0 skipped=0 extracted=0 missing=0 + + while IFS= read -r line || [ -n "$line" ]; do + # skip empty lines and comments + line=$(echo "$line" | sed 's|//.*||' | xargs) + [ -z "$line" ] && continue + + total=$((total + 1)) + local bz2="$BZ2_DIR/${line}.bsp.bz2" + local bsp="$MAPS_DIR/${line}.bsp" + + if [ -f "$bsp" ]; then + skipped=$((skipped + 1)) + continue + fi + + if [ ! -f "$bz2" ]; then + missing=$((missing + 1)) + echo " [!] ${line}: not in fastdl volume" + continue + fi + + bunzip2 -c "$bz2" > "$bsp" + extracted=$((extracted + 1)) + done < "$MAPCYCLE" + + echo "Maps: $total in mapcycle, $extracted extracted, $skipped already present, $missing not available" + echo "--------------------------------------------------------------" } @@ -482,6 +531,8 @@ main() { # Zones will be available after server starts and creates tables import_zone_data + populate_maps + run_cssds } diff --git a/src/fastdl/Dockerfile b/src/fastdl/Dockerfile new file mode 100644 index 0000000..67fb077 --- /dev/null +++ b/src/fastdl/Dockerfile @@ -0,0 +1,2 @@ +FROM nginx:alpine +COPY nginx.conf /etc/nginx/conf.d/default.conf diff --git a/src/fastdl/nginx.conf b/src/fastdl/nginx.conf new file mode 100644 index 0000000..51a8a7b --- /dev/null +++ b/src/fastdl/nginx.conf @@ -0,0 +1,8 @@ +server { + listen 80; + root /srv; + location /cstrike/maps/ { + alias /srv/maps/; + autoindex off; + } +} diff --git a/src/mapsdl/common.ts b/src/mapsdl/common.ts index 620983d..7c6e9db 100644 --- a/src/mapsdl/common.ts +++ b/src/mapsdl/common.ts @@ -156,8 +156,8 @@ export async function getLocalMaps(mapsDir: string): Promise> { try { for await (const entry of Deno.readDir(mapsDir)) { - if (entry.isFile && entry.name.endsWith(".bsp")) { - const mapName = entry.name.replace(/\.bsp$/, ""); + if (entry.isFile && entry.name.endsWith(".bsp.bz2")) { + const mapName = entry.name.replace(/\.bsp\.bz2$/, ""); localMaps.add(mapName); } } @@ -317,29 +317,32 @@ export async function downloadAndExtractMaps( console.log(`[${completed}/${mapsToDownload.size}] Downloading ${mapName}...`); const archivePath = join(config.tempDir, gdriveFile.name); - const bspPath = join(config.mapsDir, `${mapName}.bsp`); + const tempBspPath = join(config.tempDir, `${mapName}.bsp`); + const bz2Path = join(config.mapsDir, `${mapName}.bsp.bz2`); await client.downloadFile(gdriveFile.id, archivePath); if (gdriveFile.name.endsWith(".rar")) { - await extractRAR(archivePath, mapName, config.mapsDir, config.tempDir); + await extractRAR(archivePath, mapName, config.tempDir, config.tempDir); await Deno.remove(archivePath); } else if (gdriveFile.name.endsWith(".bz2")) { - await decompressBZ2(archivePath, bspPath); + await decompressBZ2(archivePath, tempBspPath); await Deno.remove(archivePath); } else if (gdriveFile.name.endsWith(".zip")) { - await extractZIP(archivePath, mapName, config.mapsDir, config.tempDir); + await extractZIP(archivePath, mapName, config.tempDir, config.tempDir); await Deno.remove(archivePath); } else if (gdriveFile.name.endsWith(".bsp")) { - await moveFile(archivePath, bspPath); + await moveFile(archivePath, tempBspPath); } else { throw new Error(`Unknown archive format: ${gdriveFile.name}`); } - await verifyBSP(bspPath); + await verifyBSP(tempBspPath); + await compressBSP(tempBspPath, bz2Path); + await Deno.remove(tempBspPath); stats.success++; - console.log(` [OK] ${mapName} downloaded and extracted`); + console.log(` [OK] ${mapName} downloaded and compressed`); } catch (error) { stats.failed++; console.error(` [X] Failed to download ${mapName}: ${(error as Error).message}`); @@ -459,3 +462,13 @@ export async function verifyBSP(bspPath: string): Promise { file.close(); } } + +export async function compressBSP(bspPath: string, bz2Path: string): Promise { + const process = new Deno.Command("bzip2", { + args: ["-c", bspPath], + stdout: "piped", + }); + const { code, stdout } = await process.output(); + if (code !== 0) throw new Error(`bzip2 failed with exit code ${code}`); + await Deno.writeFile(bz2Path, stdout); +}