Initial
This commit is contained in:
81
web/Dockerfile
Normal file
81
web/Dockerfile
Normal file
@@ -0,0 +1,81 @@
|
||||
FROM debian:bookworm-slim
|
||||
|
||||
# Replace shell with bash so we can source files
|
||||
RUN rm /bin/sh && ln -s /bin/bash /bin/sh
|
||||
|
||||
# Set debconf to run non-interactively and agree to the SteamCMD EULA
|
||||
RUN echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections \
|
||||
&& echo steam steam/question select "I AGREE" | debconf-set-selections \
|
||||
&& echo steam steam/license note '' | debconf-set-selections \
|
||||
&& dpkg --add-architecture i386
|
||||
|
||||
# Add backports and contrib
|
||||
RUN sed -i /etc/apt/sources.list.d/debian.sources -e 's/Components: main/Components: main contrib non-free/g'
|
||||
|
||||
# Install _only_ the necessary packages
|
||||
RUN apt-get update && apt-get -y upgrade && apt-get -y install --no-install-recommends \
|
||||
binutils \
|
||||
curl \
|
||||
git \
|
||||
gwenhywfar-tools \
|
||||
jq \
|
||||
libxml2-utils \
|
||||
locales \
|
||||
nano \
|
||||
procps \
|
||||
wget \
|
||||
rename \
|
||||
steamcmd \
|
||||
xmlstarlet
|
||||
|
||||
# Set the locale
|
||||
RUN sed -i '/en_US.UTF-8/s/^# //g' /etc/locale.gen && locale-gen
|
||||
ENV LANG en_US.UTF-8
|
||||
ENV LANGUAGE en_US:en
|
||||
ENV LC_ALL en_US.UTF-8
|
||||
|
||||
# Steamcmd needs its path added, as it ends up in /usr/games.
|
||||
# Our server script is bind mounted in /files in docker-compose.
|
||||
ENV PATH /usr/games:/files/bin:/web/bin:${PATH}
|
||||
|
||||
# Install nodejs
|
||||
RUN mkdir /usr/local/nvm
|
||||
ENV NVM_DIR /usr/local/nvm
|
||||
ENV NODE_VERSION 20.12.2
|
||||
RUN echo $NODE_VERSION
|
||||
|
||||
# Install nvm with node and npm
|
||||
RUN wget -O - https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash \
|
||||
&& . $NVM_DIR/nvm.sh \
|
||||
&& nvm install $NODE_VERSION \
|
||||
&& nvm alias default $NODE_VERSION \
|
||||
&& nvm use default
|
||||
|
||||
ENV NODE_PATH $NVM_DIR/versions/node/v$NODE_VERSION/lib/node_modules
|
||||
ENV PATH $NVM_DIR/versions/node/v$NODE_VERSION/bin:$PATH
|
||||
|
||||
# Setup a non-privileged user
|
||||
ARG USER_ID
|
||||
|
||||
RUN groupadd -g ${USER_ID} user && \
|
||||
useradd -l -u ${USER_ID} -m -g user user && \
|
||||
mkdir -p /home/user /serverfiles/mpmissions /serverfiles/steamapps/workshop/content /web && \
|
||||
chown -R user:user /home/user /serverfiles /web
|
||||
|
||||
# Shut steamcmd up
|
||||
RUN cd /usr/lib/i386-linux-gnu && ln -s /web/bin/steamservice.so
|
||||
|
||||
# Add bercon https://github.com/WoozyMasta/bercon
|
||||
RUN wget https://github.com/WoozyMasta/bercon/releases/download/1.0.0/bercon \
|
||||
&& chmod +x bercon \
|
||||
&& mv bercon /usr/bin
|
||||
|
||||
# Use our non-privileged user
|
||||
USER user
|
||||
|
||||
# The dayzserver script expects a home directory to itself.
|
||||
WORKDIR /home/user
|
||||
|
||||
# Run the web server
|
||||
ENTRYPOINT ["entrypoint.sh"]
|
||||
CMD ["start.sh"]
|
||||
378
web/bin/dz
Normal file
378
web/bin/dz
Normal file
@@ -0,0 +1,378 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
source dz-common
|
||||
|
||||
# An array to store Workshop items. Each element contains the mod's ID, name, and state (active or not).
|
||||
WORKSHOP_DIR="/mods/${release_client_appid}"
|
||||
if [ ! -d ${WORKSHOP_DIR} ]
|
||||
then
|
||||
mkdir -p ${WORKSHOP_DIR}
|
||||
fi
|
||||
|
||||
workshoplist=""
|
||||
|
||||
# Functions
|
||||
|
||||
# Usage
|
||||
usage(){
|
||||
echo -e "
|
||||
${red}Bad option or arguments! ${yellow}${*}${default}
|
||||
|
||||
Usage: ${green}$(basename $0)${yellow} option [ arg1 [ arg2 ] ]
|
||||
|
||||
Options and arguments:
|
||||
|
||||
a|add id - Add a DayZ Workshop item by id. Added items become active by default
|
||||
i|install - Install the DayZ server files
|
||||
g|login - Login to Steam.
|
||||
m|modupdate - Update the mod files
|
||||
p|map id - Install a mod's mpmissions files by id. (Presumes template exists)
|
||||
r|remove id - Remove all files and directories of a Workshop item by id
|
||||
l|s|status - Shows Steam login status, if base files are installed, installed mods
|
||||
u|update - Update the server files
|
||||
x|xml id - Get and normalize XML files from a mod's template by id (Presumes template exists)
|
||||
${default}"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Manage the mod symlink
|
||||
symlink(){
|
||||
W=${1}
|
||||
ID=${2}
|
||||
NAME=${3}
|
||||
if [ ! -L "${SERVER_FILES}/@${NAME}" ] && [[ ${W} = 1 ]]
|
||||
then
|
||||
ln -sv ${WORKSHOP_DIR}/${ID} "${SERVER_FILES}/@${NAME}"
|
||||
elif [[ "${W}" = "0" ]]
|
||||
then
|
||||
rm -vf "${SERVER_FILES}/@${NAME}"
|
||||
fi
|
||||
}
|
||||
|
||||
installxml(){
|
||||
ID=${1}
|
||||
# Going to have to maintain a matrix of file names -> root node -> child node permutations
|
||||
for i in "CFGEVENTGROUPS:eventgroupdef:group" "CFGEVENTSPAWNS:eventposdef:event" "CFGSPAWNABLETYPES:spawnabletypes:type" "EVENTS:events:event" "TYPES:types:type"
|
||||
do
|
||||
var=$(echo ${i} | cut -d: -f1)
|
||||
CHECK=$(echo ${i} | cut -d: -f2)
|
||||
if [ -f "${WORKSHOP_DIR}/${ID}/${var,,}.xml" ]
|
||||
then
|
||||
echo "Normalizing ${WORKSHOP_DIR}/${ID}/${var,,}.xml..."
|
||||
cp ${WORKSHOP_DIR}/${ID}/${var,,}.xml /tmp/x
|
||||
# Quirks
|
||||
# Some cfgeventspanws.xml files have <events> instead of <eventposdef>. Let's just try to fix that first.
|
||||
if [[ ${var} = "CFGEVENTSPAWNS" ]]
|
||||
then
|
||||
if grep -q '<events>' /tmp/x
|
||||
then
|
||||
echo " - (Quirk) has <events> instead of <eventposdef>. fixing..."
|
||||
xmlstarlet ed -L -r "events" -v "eventposdef" /tmp/x
|
||||
fi
|
||||
fi
|
||||
if ! grep -q '<'${CHECK}'>' /tmp/x
|
||||
then
|
||||
echo " - has no root node <${CHECK}>. fixing..."
|
||||
echo '<'${CHECK}'>' > /tmp/y
|
||||
cat /tmp/x >> /tmp/y
|
||||
echo '</'${CHECK}'>' >> /tmp/y
|
||||
xmlstarlet fo /tmp/y > /tmp/x
|
||||
fi
|
||||
if ! grep -q '<?xml' /tmp/x
|
||||
then
|
||||
echo " - has no XML node, fixing..."
|
||||
xmlstarlet fo /tmp/x > /tmp/y
|
||||
mv /tmp/y /tmp/x
|
||||
fi
|
||||
xmllint --noout /tmp/x && (
|
||||
# Keep the normalized version in the /mods directory
|
||||
cp /tmp/x ${WORKSHOP_DIR}/${ID}/${var,,}.xml
|
||||
echo -e "${green}${WORKSHOP_DIR}/${ID}/${var,,}.xml passes XML lint test!${default}"
|
||||
) || (
|
||||
echo -e "${yellow}The final ${WORKSHOP_DIR}/${ID}/${var,,}.xml does not pass XML lint test! IT WAS NOT COPIED!${default}"
|
||||
)
|
||||
fi
|
||||
done
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Add a mod
|
||||
add(){
|
||||
if [ -d "${WORKSHOP_DIR}/${1}" ]
|
||||
then
|
||||
echo -e "${yellow}Warning: The mod directory ${WORKSHOP_DIR}/${1} already exists!${default}"
|
||||
MODNAME=$(get_mod_name ${1})
|
||||
fi
|
||||
if [ -L "${SERVER_FILES}/@${MODNAME}" ]
|
||||
then
|
||||
echo -e "${yellow}Warning: The mod symlink ${SERVER_FILES}/@${MODNAME} already exists!${default}"
|
||||
fi
|
||||
echo "Adding mod id ${1}"
|
||||
dologin
|
||||
${STEAMCMD} +force_install_dir ${SERVER_FILES} +login "${steamlogin}" +workshop_download_item "${release_client_appid}" "${1}" +quit
|
||||
# Make sure the install succeeded
|
||||
if [ ! -d "${WORKSHOP_DIR}/${1}" ]
|
||||
then
|
||||
echo -e "${red}Mod installation failed: The mod directory ${WORKSHOP_DIR}/${1} was not created!${default}"
|
||||
echo "Installation failed! See above (You probably need to use a real Steam login)"
|
||||
return
|
||||
fi
|
||||
# Get the name of the newly added mod
|
||||
MODNAME=$(get_mod_name ${1})
|
||||
symlink 1 ${1} "${MODNAME}"
|
||||
echo -e "Mod id ${1} - ${green}${MODNAME}${default} - added"
|
||||
xml ${ID}
|
||||
map ${ID}
|
||||
}
|
||||
|
||||
# Remove a mod
|
||||
remove(){
|
||||
DIR="${WORKSHOP_DIR}/${1:?}"
|
||||
if [ -d "${DIR}" ]
|
||||
then
|
||||
MODNAME=$(get_mod_name ${1})
|
||||
echo "Removing directory ${DIR}"
|
||||
rm -rf "${DIR}"
|
||||
else
|
||||
echo "Directory ${DIR} doesn't exist?"
|
||||
fi
|
||||
if [ -L "${SERVER_FILES}/@${MODNAME}" ]
|
||||
then
|
||||
echo "Removing symlink ${SERVER_FILES}/@${MODNAME}"
|
||||
rm -f "${SERVER_FILES}/@${MODNAME}"
|
||||
else
|
||||
echo "Symlink ${SERVER_FILES}/@${MODNAME} doesn't exist?"
|
||||
fi
|
||||
echo -e "Mod id ${1} - ${red}${MODNAME}${default} - removed"
|
||||
}
|
||||
|
||||
# Handle the Steam login information.
|
||||
login(){
|
||||
if [ -f "${STEAM_LOGIN}" ]
|
||||
then
|
||||
if prompt_yn "The steam login is already set. Reset it?"
|
||||
then
|
||||
rm -f "${STEAM_LOGIN}"
|
||||
else
|
||||
echo "Not reset."
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
if [ ! -f "${STEAM_LOGIN}" ]
|
||||
then
|
||||
echo "Setting up Steam credentials"
|
||||
echo -n "Steam Username (anonymous): "
|
||||
read steamlogin
|
||||
if [[ "${steamlogin}" = "" ]]
|
||||
then
|
||||
echo "Steam login set to 'anonymous'"
|
||||
steamlogin="anonymous"
|
||||
fi
|
||||
echo "steamlogin=${steamlogin}" > "${STEAM_LOGIN}"
|
||||
${STEAMCMD} +force_install_dir ${SERVER_FILES} +login "${steamlogin}" +quit
|
||||
fi
|
||||
}
|
||||
|
||||
# "Perform" the Steam login. This just sources the file with the Steam login name.
|
||||
dologin(){
|
||||
if [ -f "${STEAM_LOGIN}" ]
|
||||
then
|
||||
source "${STEAM_LOGIN}"
|
||||
else
|
||||
echo "No cached Steam credentials. Please configure this now: "
|
||||
login
|
||||
fi
|
||||
}
|
||||
|
||||
# Perform the installation of the server files.
|
||||
install(){
|
||||
if [ ! -f "${SERVER_INSTALL_FILE}" ] || [[ ${1} = "force" ]]
|
||||
then
|
||||
printf "[ ${yellow}DayZ${default} ] Downloading DayZ Server-Files!\n"
|
||||
dologin
|
||||
${STEAMCMD} +force_install_dir ${SERVER_FILES} +login "${steamlogin}" +app_update "${release_server_appid}" validate +quit
|
||||
# This installs the mpmissions for charnarusplus and enoch (AKA Livonia) from github. The game once allowed the full server
|
||||
# to be downloaded, but now you get the server without any mpmissions. This is a workaround.
|
||||
echo "Installing mpmissions for ChernarusPlus and Livonia from github..."
|
||||
map default
|
||||
else
|
||||
printf "[ ${lightblue}DayZ${default} ] The server is already installed.\n"
|
||||
fi
|
||||
}
|
||||
|
||||
# Update the server files.
|
||||
update(){
|
||||
dologin
|
||||
appmanifestfile=${SERVER_FILES}/steamapps/appmanifest_"${release_server_appid}".acf
|
||||
printf "[ ... ] Checking for update:"
|
||||
# gets currentbuild
|
||||
currentbuild=$(grep buildid "${appmanifestfile}" | tr '[:blank:]"' ' ' | tr -s ' ' | cut -d \ -f3)
|
||||
# Removes appinfo.vdf as a fix for not always getting up to date version info from SteamCMD
|
||||
if [ -f "${HOME}/Steam/appcache/appinfo.vdf" ]
|
||||
then
|
||||
rm -f "${HOME}/Steam/appcache/appinfo.vdf"
|
||||
fi
|
||||
# check for new build
|
||||
availablebuild=$(${STEAMCMD} +login "${steamlogin}" +app_info_update 1 +app_info_print "${release_server_appid}" +quit | \
|
||||
sed -n '/branch/,$p' | grep -m 1 buildid | tr -cd '[:digit:]')
|
||||
if [ -z "${availablebuild}" ]
|
||||
then
|
||||
printf "\r[ ${red}FAIL${default} ] Checking for update:\n"
|
||||
printf "\r[ ${red}FAIL${default} ] Checking for update:: Not returning version info\n"
|
||||
exit
|
||||
else
|
||||
printf "\r[ ${green}OK${default} ] Checking for update:"
|
||||
fi
|
||||
# compare builds
|
||||
if [ "${currentbuild}" != "${availablebuild}" ] || [[ ${1} = "force" ]]
|
||||
then
|
||||
printf "\r[ ${green}OK${default} ] Checking for update:: Update available\n"
|
||||
printf "Update available:\n"
|
||||
printf "\tCurrent build: ${red}${currentbuild}${default}\n"
|
||||
printf "\tAvailable build: ${green}${availablebuild}${default}\n"
|
||||
printf "\thttps://steamdb.info/app/${release_server_appid}/\n"
|
||||
printf "\nApplying update"
|
||||
# run update
|
||||
dologin
|
||||
${STEAMCMD} +force_install_dir ${SERVER_FILES} +login "${steamlogin}" +app_update "${release_server_appid}" validate +quit
|
||||
modupdate
|
||||
else
|
||||
printf "\r[ ${green}OK${default} ] Checking for update:: No update available\n"
|
||||
printf "\nNo update available:\n"
|
||||
printf "\tCurrent version: ${green}${currentbuild}${default}\n"
|
||||
printf "\tAvailable version: ${green}${availablebuild}${default}\n"
|
||||
printf "\thttps://steamdb.info/app/${release_server_appid}/\n\n"
|
||||
fi
|
||||
}
|
||||
|
||||
# Update mods
|
||||
modupdate(){
|
||||
echo "Updating mods..."
|
||||
dologin
|
||||
get_mods
|
||||
${STEAMCMD} +force_install_dir ${SERVER_FILES} +login "${steamlogin}" ${workshoplist} +quit
|
||||
# Updated files come in with mixed cases. Fix that.
|
||||
echo "done"
|
||||
echo
|
||||
}
|
||||
|
||||
# Display the status of everything
|
||||
status(){
|
||||
INSTALLED="${NO}"
|
||||
LOGGED_IN="${NO}"
|
||||
|
||||
# DayZ Server files installation
|
||||
if [ -f "${SERVER_INSTALL_FILE}" ]
|
||||
then
|
||||
INSTALLED="${YES}"
|
||||
if [[ ${release_client_appid} = "221100" ]]
|
||||
then
|
||||
RELEASE="Stable"
|
||||
else
|
||||
RELEASE="Experimental"
|
||||
fi
|
||||
VERSION="$(strings /serverfiles/DayZServer | grep -P "DayZ \d\.\d+\.\d+" | cut -c6-) - ${RELEASE}"
|
||||
fi
|
||||
# Logged into Steam
|
||||
if [ -f "${STEAM_LOGIN}" ]
|
||||
then
|
||||
LOGGED_IN="${YES}"
|
||||
if grep -q anonymous "${STEAM_LOGIN}"
|
||||
then
|
||||
ANONYMOUS="${yellow}(as anonymous)${default}"
|
||||
else
|
||||
ANONYMOUS="${green}(not anonymous)${default}"
|
||||
fi
|
||||
fi
|
||||
echo -ne "
|
||||
Logged in to Steam: ${LOGGED_IN} ${ANONYMOUS}
|
||||
Server files installed: ${INSTALLED}"
|
||||
if [[ "${INSTALLED}" = "${NO}" ]]
|
||||
then
|
||||
echo
|
||||
echo
|
||||
exit 0
|
||||
fi
|
||||
# Version of DayZ Server files
|
||||
echo -ne "
|
||||
Version: ${VERSION}"
|
||||
|
||||
# Mods
|
||||
echo -ne "
|
||||
Mods: "
|
||||
MODS=$(list)
|
||||
if [[ ${MODS} == "" ]]
|
||||
then
|
||||
echo -ne "${red}none${default}"
|
||||
fi
|
||||
echo -e "${MODS}"
|
||||
}
|
||||
|
||||
map(){
|
||||
# Install map mpmissions for mods that have them. Presumes a map.env was created for the mod, with the required metadata (git URL, etc.)
|
||||
TERM="map"
|
||||
if [[ "${1}" =~ ^[0-9]+$ ]]
|
||||
then
|
||||
TERM="mod id"
|
||||
fi
|
||||
if [ -f "${FILES}/mods/${1}/map.env" ]
|
||||
then
|
||||
echo "Installing mpmissions files for ${TERM} ${1}..."
|
||||
source ${FILES}/mods/${1}/map.env
|
||||
${FILES}/bin/map.sh ${1} install
|
||||
fi
|
||||
}
|
||||
|
||||
mod_install(){
|
||||
if [ -f ${FILES}/mods/${1}/${2}.sh ]
|
||||
then
|
||||
echo "An ${2}.sh was found for mod id ${1}. Running..."
|
||||
${FILES}/mods/${1}/${2}.sh
|
||||
fi
|
||||
# A generic map install script. Presumes a git repo as the source
|
||||
}
|
||||
|
||||
# "Manage" XML files.
|
||||
xml(){
|
||||
/files/bin/xml.sh ${1}
|
||||
installxml ${1}
|
||||
}
|
||||
|
||||
# Capture the first argument and shift it off so we can pass $@ to every function
|
||||
C=${1}
|
||||
shift || {
|
||||
usage
|
||||
}
|
||||
|
||||
case "${C}" in
|
||||
a|add)
|
||||
add "${@}"
|
||||
;;
|
||||
i|install)
|
||||
install "${@}"
|
||||
;;
|
||||
g|login)
|
||||
login "${@}"
|
||||
;;
|
||||
m|modupdate)
|
||||
modupdate "${@}"
|
||||
;;
|
||||
r|remove)
|
||||
remove "${@}"
|
||||
;;
|
||||
l|s|status)
|
||||
status "${@}"
|
||||
;;
|
||||
p|map)
|
||||
map "${@}"
|
||||
;;
|
||||
u|update)
|
||||
update "${@}"
|
||||
;;
|
||||
x|xml)
|
||||
xml "${@}"
|
||||
;;
|
||||
*)
|
||||
usage "$*"
|
||||
;;
|
||||
esac
|
||||
3
web/bin/entrypoint.sh
Normal file
3
web/bin/entrypoint.sh
Normal file
@@ -0,0 +1,3 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
exec "$@"
|
||||
27
web/bin/start.sh
Normal file
27
web/bin/start.sh
Normal file
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Set PS1 so we know we're in the container
|
||||
if ! echo .bashrc | grep -q "dz-web"
|
||||
then
|
||||
echo "Adding PS1 to .bashrc..."
|
||||
cat > .bashrc <<EOF
|
||||
alias ls='ls --color'
|
||||
export PS1="${debian_chroot:+($debian_chroot)}\u@dz-web:\w\$ "
|
||||
unset DEVELOPMENT
|
||||
EOF
|
||||
fi
|
||||
|
||||
# Shut steamcmd up
|
||||
if ! [ -d ${HOME}/.steam ]
|
||||
then
|
||||
mkdir -p ${HOME}/.steam
|
||||
fi
|
||||
|
||||
cd /web
|
||||
npm i
|
||||
export DEBUG='express:*'
|
||||
npx nodemon web.js &
|
||||
|
||||
cd docroot
|
||||
npm i
|
||||
exec npm run dev
|
||||
BIN
web/bin/steamservice.so
Normal file
BIN
web/bin/steamservice.so
Normal file
Binary file not shown.
13
web/docroot/index.html
Normal file
13
web/docroot/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>DayZ Docker Server</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
1365
web/docroot/package-lock.json
generated
Normal file
1365
web/docroot/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
22
web/docroot/package.json
Normal file
22
web/docroot/package.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "docroot",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite --host",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"@vueuse/core": "^10.1.2",
|
||||
"bootstrap": "^5.3.0",
|
||||
"bootstrap-icons": "^1.10.5",
|
||||
"pinia": "^2.1.3",
|
||||
"vue": "^3.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^4.2.3",
|
||||
"vite": "^4.3.5"
|
||||
}
|
||||
}
|
||||
BIN
web/docroot/public/favicon.ico
Normal file
BIN
web/docroot/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 81 KiB |
17
web/docroot/src/App.vue
Normal file
17
web/docroot/src/App.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<script setup>
|
||||
import Body from '@/components/Body.vue'
|
||||
import Error from '@/components/Error.vue'
|
||||
import Header from '@/components/Header.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Suspense>
|
||||
<main>
|
||||
<Error />
|
||||
<div class="container-fluid min-vh-100 d-flex flex-column bg-light">
|
||||
<Header />
|
||||
<Body />
|
||||
</div>
|
||||
</main>
|
||||
</Suspense>
|
||||
</template>
|
||||
9
web/docroot/src/components/Body.vue
Normal file
9
web/docroot/src/components/Body.vue
Normal file
@@ -0,0 +1,9 @@
|
||||
<script setup>
|
||||
import Mods from '@/components/Mods.vue'
|
||||
import SearchResults from "@/components/SearchResults.vue";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SearchResults />
|
||||
<Mods />
|
||||
</template>
|
||||
38
web/docroot/src/components/Error.vue
Normal file
38
web/docroot/src/components/Error.vue
Normal file
@@ -0,0 +1,38 @@
|
||||
<script setup>
|
||||
import { onMounted } from 'vue'
|
||||
import { Modal } from 'bootstrap'
|
||||
import { useAppStore } from '@/stores/app.js'
|
||||
const store = useAppStore()
|
||||
let modal = {}
|
||||
onMounted(() => {
|
||||
modal = new Modal('#errorModal', {})
|
||||
// modal.show()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="modal"
|
||||
id="errorModal"
|
||||
data-bs-backdrop="static"
|
||||
data-bs-keyboard="false"
|
||||
tabindex="-1"
|
||||
aria-labelledby="errorModalLabel"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h1 class="modal-title fs-5" id="errorModalLabel">Error</h1>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
{{ store.errorText }}
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
40
web/docroot/src/components/Header.vue
Normal file
40
web/docroot/src/components/Header.vue
Normal file
@@ -0,0 +1,40 @@
|
||||
<script setup>
|
||||
import Search from '@/components/Search.vue'
|
||||
import Status from '@/components/Status.vue'
|
||||
import Servers from '@/components/Servers.vue'
|
||||
import { useFetch } from '@vueuse/core'
|
||||
import { useAppStore } from '@/stores/app.js'
|
||||
const store = useAppStore()
|
||||
import { config } from '@/config'
|
||||
const { error, data } = await useFetch(config.baseUrl + '/status').get().json()
|
||||
const set = (w, e) => {
|
||||
store.section = w
|
||||
const active = Array.from(document.getElementsByClassName('active'))
|
||||
active.forEach((a) => a.classList.remove('active'))
|
||||
e.target.classList.add('active')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="data" class="row">
|
||||
<div class="col-3 text-center">
|
||||
<h1>DayZ Docker Server</h1>
|
||||
</div>
|
||||
<div class="col-5">
|
||||
<button
|
||||
@click="installbase"
|
||||
:class="'btn btn-sm ' + (data.installed ? 'btn-danger' : 'btn-success')"
|
||||
>
|
||||
Install Server Files
|
||||
</button>
|
||||
<button @click="updatebase" class="btn btn-sm btn-outline-success">Update Server Files</button>
|
||||
<button @click="updatemods" class="btn btn-sm btn-outline-success">Update Mods</button>
|
||||
<button type="button" @click="set('servers', $event)" class="btn btn-sm btn-outline-primary">Servers</button>
|
||||
<button type="button" @click="set('mods', $event)" class="btn btn-sm btn-outline-primary active" data-bs-toggle="button">Mods</button>
|
||||
<button type="button" @click="set('search', $event)" class="btn btn-sm btn-outline-primary">Search</button>
|
||||
</div>
|
||||
<Search />
|
||||
<Status :status="data" />
|
||||
<Servers />
|
||||
</div>
|
||||
</template>
|
||||
47
web/docroot/src/components/Modinfo.vue
Normal file
47
web/docroot/src/components/Modinfo.vue
Normal file
@@ -0,0 +1,47 @@
|
||||
<script setup>
|
||||
import { useFetch } from "@vueuse/core"
|
||||
import XmlFile from '@/components/XmlFile.vue'
|
||||
import { useAppStore } from '@/stores/app.js'
|
||||
const store = useAppStore()
|
||||
import { config } from '@/config'
|
||||
const { data, error } = useFetch(() => config.baseUrl + `/mod/${store.modId}`, {
|
||||
immediate: false,
|
||||
refetch: true
|
||||
}).get().json()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="col-md-9 border">
|
||||
<div v-if="error" class="d-flex">Error: {{ error }}</div>
|
||||
<div v-else-if="data" class="d-flex">
|
||||
<div>
|
||||
<div>
|
||||
<strong>{{ data.name }}</strong>
|
||||
</div>
|
||||
<div>
|
||||
ID: {{ data.id }}
|
||||
</div>
|
||||
<div>
|
||||
Size: {{ data.size.toLocaleString("en-US") }}
|
||||
</div>
|
||||
<div v-if="data.customXML.length > 0">
|
||||
Custom XML files:
|
||||
<ul>
|
||||
<li v-for="info in data.customXML">
|
||||
<a
|
||||
:class="'simulink xmlfile ' + (store.modFile === info.name ? 'active' : '')"
|
||||
@click="store.modFile=info.name"
|
||||
>
|
||||
{{ info.name }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-1"></div>
|
||||
<div>
|
||||
<XmlFile />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
55
web/docroot/src/components/Mods.vue
Normal file
55
web/docroot/src/components/Mods.vue
Normal file
@@ -0,0 +1,55 @@
|
||||
<script setup>
|
||||
import { config } from '@/config'
|
||||
import { useFetch } from '@vueuse/core'
|
||||
import { useAppStore } from '@/stores/app.js'
|
||||
import ModInfo from '@/components/Modinfo.vue'
|
||||
const store = useAppStore()
|
||||
const { data, error } = useFetch(config.baseUrl + '/mods', {
|
||||
afterFetch(ctx) {
|
||||
store.mods = ctx.data.mods
|
||||
return ctx
|
||||
}
|
||||
}).get().json()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="row flex-grow-1" v-if="store.section === 'mods'">
|
||||
<div v-if="error" class="row text-danger">
|
||||
{{ error }}
|
||||
</div>
|
||||
<div class="col-md-3 border" v-if="data">
|
||||
<div>
|
||||
<h4 class="text-center">Installed Mods</h4>
|
||||
<table>
|
||||
<tr>
|
||||
<th>Steam Link</th>
|
||||
<th>Mod Name</th>
|
||||
</tr>
|
||||
<template
|
||||
v-for="mod in data.mods.sort( (a,b) => { return a.name.localeCompare(b.name) } )"
|
||||
>
|
||||
<tr>
|
||||
<td>
|
||||
<a
|
||||
target="_blank"
|
||||
:href="config.steamUrl + mod.id"
|
||||
>
|
||||
{{ mod.id }}
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a
|
||||
:class="'simulink' + (store.modId === parseInt(mod.id) ? ' active' : '')"
|
||||
@click="store.modFile='';store.modId=parseInt(mod.id)"
|
||||
>
|
||||
{{ mod.name }}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<ModInfo />
|
||||
</div>
|
||||
</template>
|
||||
12
web/docroot/src/components/Search.vue
Normal file
12
web/docroot/src/components/Search.vue
Normal file
@@ -0,0 +1,12 @@
|
||||
<script setup>
|
||||
import { useAppStore } from '@/stores/app.js'
|
||||
const store = useAppStore()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="col form-control-lg text-center">
|
||||
<form @submit.prevent="(e) => {store.searchText=e.target.search.value; store.section='search'}">
|
||||
<input name="search" placeholder="Search mods..." autofocus>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
77
web/docroot/src/components/SearchResults.vue
Normal file
77
web/docroot/src/components/SearchResults.vue
Normal file
@@ -0,0 +1,77 @@
|
||||
<script setup>
|
||||
import { config } from '@/config'
|
||||
import { BKMG } from '@/util'
|
||||
import { useFetch} from '@vueuse/core'
|
||||
import { useAppStore } from '@/stores/app.js'
|
||||
const store = useAppStore()
|
||||
const { data: searchResults, error, isFetching } = useFetch(() => config.baseUrl + `/search/${store.searchText}`, {
|
||||
immediate: false,
|
||||
refetch: true,
|
||||
afterFetch(response) {
|
||||
// const sortField = "time_updated"
|
||||
const sortField = "lifetime_subscriptions"
|
||||
response.data.response.publishedfiledetails.sort((a, b) =>
|
||||
a[sortField] < b[sortField] ? 1 : -1
|
||||
)
|
||||
return response
|
||||
}
|
||||
}).get().json()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="store.section === 'search'">
|
||||
<div v-if="error" class="row text-danger">
|
||||
{{ error }}
|
||||
</div>
|
||||
<div v-if="store.searchText === ''">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-4">
|
||||
<h2>Search for something...</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="isFetching" class="row justify-content-center">
|
||||
<div class="col-1 text-end">
|
||||
<div class="spinner-border" role="status"></div>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<h2>Searching for <strong>"{{ store.searchText }}"...</strong></h2>
|
||||
</div>
|
||||
</div>
|
||||
<template v-if="searchResults && ! isFetching">
|
||||
<div class="text-center">
|
||||
<h2>{{ searchResults.response.total }} results for <strong>"{{ store.searchText }}"</strong></h2>
|
||||
</div>
|
||||
<div class="d-flex">
|
||||
<table>
|
||||
<tr>
|
||||
<th>Steam Link</th>
|
||||
<th>Title</th>
|
||||
<th>Size</th>
|
||||
<th>Last Updated</th>
|
||||
<th>Subscriptions</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
<tr v-for="result in searchResults.response.publishedfiledetails">
|
||||
<td>
|
||||
<a
|
||||
target="_blank"
|
||||
:href="config.steamUrl + result.publishedfileid"
|
||||
>
|
||||
<img :alt="result.short_description" data-bs-toggle="tooltip" data-bs-placement="left" :title="result.short_description" width="160" height="90" :src="result.preview_url">
|
||||
</a>
|
||||
</td>
|
||||
<td>{{ result.title }}</td>
|
||||
<td>{{ BKMG(result.file_size) }}</td>
|
||||
<td>{{ new Date(result.time_updated * 1000).toLocaleDateString("en-us") }}</td>
|
||||
<td>{{ result.lifetime_subscriptions }}</td>
|
||||
<td>
|
||||
<button v-if="store.mods.find(o => o.id === result.publishedfileid)" @click="removeMod(result.publishedfileid)" type="button" class="btn btn-danger">Remove</button>
|
||||
<button v-else @click="installMod(result.publishedfileid)" type="button" class="btn btn-success">Install</button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
20
web/docroot/src/components/Servers.vue
Normal file
20
web/docroot/src/components/Servers.vue
Normal file
@@ -0,0 +1,20 @@
|
||||
<script setup>
|
||||
import { useAppStore } from '@/stores/app.js'
|
||||
const store = useAppStore()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="row" v-if="store.section === 'servers'">
|
||||
<div class="col text-center">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h2>Servers</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row flex-grow-1">
|
||||
<div class="col-6 justify-content-end">Running</div>
|
||||
<div class="col-6">Stopped</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
17
web/docroot/src/components/Status.vue
Normal file
17
web/docroot/src/components/Status.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<script setup>
|
||||
const { status } = defineProps(['status'])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="col">
|
||||
<div>
|
||||
Server files installed:
|
||||
<span v-if="status.installed" class="bi bi-check h2 text-success"></span>
|
||||
<span v-else class="bi bi-x h2 danger text-danger"></span>
|
||||
</div>
|
||||
<div v-if="status.version">
|
||||
Version: <span class="text-success fw-bold">{{ status.version }}</span>
|
||||
<span class="text-success fw-bold">({{ status.appid }})</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
28
web/docroot/src/components/XmlFile.vue
Normal file
28
web/docroot/src/components/XmlFile.vue
Normal file
@@ -0,0 +1,28 @@
|
||||
<script setup>
|
||||
import { useFetch } from '@vueuse/core'
|
||||
import { config } from '@/config'
|
||||
import { useAppStore } from '@/stores/app.js'
|
||||
import XmlTree from '@/components/XmlTree.vue'
|
||||
const store = useAppStore()
|
||||
const { data, error } = await useFetch(() => config.baseUrl + `/mod/${store.modId}/${store.modFile}`, {
|
||||
immediate: false,
|
||||
refetch: true,
|
||||
afterFetch(response) {
|
||||
const parser = new DOMParser()
|
||||
try {
|
||||
response.data = parser.parseFromString(response.data, "text/xml").documentElement
|
||||
} catch(e) {
|
||||
console.error(e)
|
||||
response.error = e
|
||||
}
|
||||
return response
|
||||
}
|
||||
}).get()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="error">{{ error }}</div>
|
||||
<div v-else-if="data">
|
||||
<XmlTree :element="data" :depth="0" />
|
||||
</div>
|
||||
</template>
|
||||
49
web/docroot/src/components/XmlTree.vue
Normal file
49
web/docroot/src/components/XmlTree.vue
Normal file
@@ -0,0 +1,49 @@
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
depth: Number,
|
||||
element: [Element, Text],
|
||||
})
|
||||
function collapse(e) {
|
||||
console.log(e)
|
||||
// e.children?.forEach(x => x.classList?.add("d-none"))
|
||||
}
|
||||
function children(e) {
|
||||
let children = []
|
||||
let node = e.firstChild
|
||||
while (node) {
|
||||
children.push(node)
|
||||
node = node.nextSibling
|
||||
}
|
||||
return children
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="props.element.nodeType === 1" :style="'padding-left: ' + (props.depth * 10) + 'px'">
|
||||
<span class="d-flex">
|
||||
<span
|
||||
v-if="props.element.children.length > 0"
|
||||
class="bi-dash simulink text-center"
|
||||
@click="collapse"
|
||||
/>
|
||||
<span><{{props.element.nodeName}}</span>
|
||||
<span v-if="props.element.hasAttributes()" v-for="attribute in props.element.attributes">
|
||||
<span> {{attribute.name}}</span>
|
||||
<span>=</span>
|
||||
<span>"{{attribute.value}}"</span>
|
||||
</span>
|
||||
<span v-if="props.element.children.length === 0"> /</span>
|
||||
<span>></span>
|
||||
</span>
|
||||
<span v-for="child in children(props.element)">
|
||||
<XmlTree v-if="child.nodeType !== 8" :element="child" :depth="props.depth + 1" />
|
||||
</span>
|
||||
<span
|
||||
v-if="props.element.nodeType === 1"
|
||||
style="padding-left: -10px"
|
||||
>
|
||||
<span></{{props.element.nodeName}}></span>
|
||||
</span>
|
||||
</div>
|
||||
<span v-if="props.element.nodeType === 3">{{props.element.data.trim()}}</span>
|
||||
</template>
|
||||
6
web/docroot/src/config.js
Normal file
6
web/docroot/src/config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
const config = {
|
||||
baseUrl: window.location.protocol + '//' + window.location.hostname + ':8000',
|
||||
steamUrl: 'https://steamcommunity.com/sharedfiles/filedetails/?id='
|
||||
}
|
||||
|
||||
export { config }
|
||||
21
web/docroot/src/css/index.css
Normal file
21
web/docroot/src/css/index.css
Normal file
@@ -0,0 +1,21 @@
|
||||
button {
|
||||
padding: 5px;
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
th, td {
|
||||
padding-right: 10px
|
||||
}
|
||||
|
||||
.active {
|
||||
background-color: cyan;
|
||||
}
|
||||
|
||||
.simulink {
|
||||
cursor: pointer;
|
||||
text-underline: blue;
|
||||
}
|
||||
|
||||
.simulink:hover {
|
||||
background-color: green;
|
||||
}
|
||||
25
web/docroot/src/main.js
Normal file
25
web/docroot/src/main.js
Normal file
@@ -0,0 +1,25 @@
|
||||
import 'bootstrap/dist/css/bootstrap.min.css'
|
||||
import 'bootstrap'
|
||||
import 'bootstrap-icons/font/bootstrap-icons.css'
|
||||
import './css/index.css'
|
||||
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import App from './App.vue'
|
||||
import {useAppStore} from "@/stores/app";
|
||||
|
||||
// Create an instance of our Vue app
|
||||
const app = createApp(App)
|
||||
|
||||
// Add the store
|
||||
app.use(createPinia())
|
||||
|
||||
// A global error handler
|
||||
app.config.errorHandler = (err, instance, info) => {
|
||||
const store = useAppStore()
|
||||
store.errorText = err.message
|
||||
console.error('GLOBAL ERROR HANDLER! ', err, instance, info)
|
||||
}
|
||||
|
||||
// Mount it
|
||||
app.mount('#app')
|
||||
12
web/docroot/src/stores/app.js
Normal file
12
web/docroot/src/stores/app.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const useAppStore = defineStore('app', {
|
||||
state: () => ({
|
||||
errorText: '',
|
||||
modId: 0,
|
||||
modFile: '',
|
||||
mods: [],
|
||||
searchText: '',
|
||||
section: 'mods',
|
||||
})
|
||||
})
|
||||
11
web/docroot/src/util.js
Normal file
11
web/docroot/src/util.js
Normal file
@@ -0,0 +1,11 @@
|
||||
const units = ['bytes', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']
|
||||
|
||||
const BKMG = (val) => {
|
||||
let l = 0, n = parseInt(val, 10) || 0
|
||||
while(n >= 1024 && ++l){
|
||||
n = n/1024
|
||||
}
|
||||
return(n.toFixed(n < 10 && l > 0 ? 1 : 0) + ' ' + units[l])
|
||||
}
|
||||
|
||||
export { BKMG }
|
||||
17
web/docroot/vite.config.js
Normal file
17
web/docroot/vite.config.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import { fileURLToPath, URL } from 'node:url'
|
||||
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||
}
|
||||
},
|
||||
server: {
|
||||
port: 8001
|
||||
}
|
||||
})
|
||||
1658
web/package-lock.json
generated
Normal file
1658
web/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
19
web/package.json
Normal file
19
web/package.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "web",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"express": "^4.18.2"
|
||||
},
|
||||
"type": "module",
|
||||
"devDependencies": {
|
||||
"nodemon": "^2.0.22"
|
||||
}
|
||||
}
|
||||
BIN
web/root/favicon.png
Normal file
BIN
web/root/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 81 KiB |
22
web/root/index.css
Normal file
22
web/root/index.css
Normal file
@@ -0,0 +1,22 @@
|
||||
button {
|
||||
padding: 5px;
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
th, td {
|
||||
padding-right: 10px
|
||||
}
|
||||
|
||||
|
||||
.selected {
|
||||
background-color: cyan;
|
||||
}
|
||||
|
||||
.simulink {
|
||||
cursor: pointer;
|
||||
text-underline: blue;
|
||||
}
|
||||
|
||||
.simulink:hover {
|
||||
background-color: cyan;
|
||||
}
|
||||
23
web/root/index.html
Normal file
23
web/root/index.html
Normal file
@@ -0,0 +1,23 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>DayZ Docker Server</title>
|
||||
<link rel="icon" type="image/x-icon" href="/favicon.png">
|
||||
<!-- Bootstrap -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM" crossorigin="anonymous">
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-geWF76RCwLtnZ8qwWowPQNguL3RmwHVBC9FhGdlKrxdiJJigb/j/68SIy3Te4Bkz" crossorigin="anonymous"></script>
|
||||
<!-- Bootstrap Icons -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.3.0/font/bootstrap-icons.css">
|
||||
<!-- Our CSS -->
|
||||
<link rel="stylesheet" href="/index.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module">
|
||||
import { createApp } from 'https://unpkg.com/vue@3/dist/vue.esm-browser.js'
|
||||
import app from '/index.js'
|
||||
createApp(app).mount('#app')
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
313
web/root/index.js
Normal file
313
web/root/index.js
Normal file
@@ -0,0 +1,313 @@
|
||||
const template = `
|
||||
<div
|
||||
class="modal"
|
||||
id="errorModal"
|
||||
data-bs-backdrop="static"
|
||||
data-bs-keyboard="false"
|
||||
tabindex="-1"
|
||||
aria-labelledby="errorModalLabel"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h1 class="modal-title fs-5" id="errorModalLabel">Error</h1>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
{{ fetchError }}
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container-fluid min-vh-100 d-flex flex-column bg-light">
|
||||
<div class="row">
|
||||
<div class="col-3 text-center">
|
||||
<h1>DayZ Docker Server</h1>
|
||||
</div>
|
||||
<div class="col-5">
|
||||
<button
|
||||
@click="installbase"
|
||||
:class="'btn ' + (installed ? 'btn-danger' : 'btn-success')"
|
||||
>
|
||||
Install Server Files
|
||||
</button>
|
||||
<button @click="updatebase" class="btn btn-success">Update Server Files</button>
|
||||
<button @click="updatemods" class="btn btn-success">Update Mods</button>
|
||||
<button @click="servers" class="btn btn-primary">Servers</button>
|
||||
<button @click="listmods" class="btn btn-primary">Mods</button>
|
||||
</div>
|
||||
<div class="col form-control-lg text-center">
|
||||
<form @submit="handleSubmit">
|
||||
<input name="search" placeholder="Search mods..." autofocus>
|
||||
</form>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div>
|
||||
Server files installed:
|
||||
<span class="bi bi-check h2 text-success" v-if="installed"></span>
|
||||
<span class="bi bi-x h2 danger text-danger" v-else></span>
|
||||
</div>
|
||||
<div v-if="version != ''">
|
||||
Version: <span class="text-success font-weight-bold">{{ version }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row flex-grow-1">
|
||||
<div class="col-md-3 border">
|
||||
<div>
|
||||
<h4 class="text-center">Installed Mods</h4>
|
||||
<table>
|
||||
<tr>
|
||||
<th>Steam Link</th>
|
||||
<th>Mod Info</th>
|
||||
</tr>
|
||||
<template
|
||||
v-for="mod in mods"
|
||||
>
|
||||
<tr>
|
||||
<td>
|
||||
<a
|
||||
target="_blank"
|
||||
:href="steamURL + mod.id"
|
||||
>
|
||||
{{ mod.id }}
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a class="simulink" @click="getModInfo(mod.id)">{{ mod.name }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-9 border">
|
||||
<div class="d-flex" v-if="modInfo != ''">
|
||||
<div>
|
||||
<div>
|
||||
<strong>{{ modInfo.name }}</strong>
|
||||
</div>
|
||||
<div>
|
||||
ID: {{ modInfo.id }}
|
||||
</div>
|
||||
<div>
|
||||
Size: {{ modInfo.size.toLocaleString("en-US") }}
|
||||
</div>
|
||||
<div v-if="modInfo.customXML.length > 0">
|
||||
Custom XML files:
|
||||
<ul>
|
||||
<li v-for="info in modInfo.customXML">
|
||||
<a
|
||||
:class="'simulink xmlfile ' + info.name"
|
||||
@click="getXMLInfo(modInfo.id,info.name)"
|
||||
>
|
||||
{{ info.name }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-1"></div>
|
||||
<div>
|
||||
<xmltree v-if="XMLInfo != ''" :xmlData="XMLInfo" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="searchResults != ''" class="d-flex">
|
||||
<table>
|
||||
<tr>
|
||||
<th>Steam Link</th>
|
||||
<th>Title</th>
|
||||
<th>Size</th>
|
||||
<th>Last Updated</th>
|
||||
<th>Subscriptions</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
<tr v-for="result in searchResults">
|
||||
<td>
|
||||
<a
|
||||
target="_blank"
|
||||
:href="steamURL + result.publishedfileid"
|
||||
>
|
||||
<img :alt="result.short_description" data-bs-toggle="tooltip" data-bs-placement="left" :title="result.short_description" width="160" height="90" :src="result.preview_url">
|
||||
</a>
|
||||
</td>
|
||||
<td>{{ result.title }}</td>
|
||||
<td>{{ BKMG(result.file_size) }}</td>
|
||||
<td>{{ new Date(result.time_updated * 1000).toLocaleDateString("en-us") }}</td>
|
||||
<td>{{ result.lifetime_subscriptions }}</td>
|
||||
<td>
|
||||
<button v-if="mods.find(o => o.id == result.publishedfileid)" @click="removeMod(result.publishedfileid)" type="button" class="btn btn-danger">Remove</button>
|
||||
<button v-else @click="installMod(result.publishedfileid)" type="button" class="btn btn-success">Install</button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
|
||||
import xmltree from "/xmltree.js"
|
||||
|
||||
const fetcher = (args) => {
|
||||
fetch(args.url)
|
||||
.then(response => (
|
||||
args.type === "json" ? response.json() : response.text()
|
||||
))
|
||||
.then(response => args.callback(response))
|
||||
.catch((error) => {
|
||||
console.error(error)
|
||||
this.fetchError = error.message
|
||||
})
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'DazDockerServer',
|
||||
template: template,
|
||||
components: {
|
||||
xmltree
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
fetchError: "",
|
||||
installed: false,
|
||||
mods: [],
|
||||
modInfo: "",
|
||||
searchResults: [],
|
||||
steamURL: 'https://steamcommunity.com/sharedfiles/filedetails/?id=',
|
||||
version: "Unknown",
|
||||
XMLFile: "",
|
||||
XMLInfo: "",
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getModInfo(modId) {
|
||||
fetcher ({
|
||||
url: '/mod/' + modId,
|
||||
type: "json",
|
||||
callback: (response) => {
|
||||
this.modInfo = response
|
||||
this.XMLInfo = ""
|
||||
this.searchResults = ""
|
||||
}
|
||||
})
|
||||
},
|
||||
getXMLInfo(modId, file) {
|
||||
for (const e of document.getElementsByClassName("selected")) e.classList.remove("selected")
|
||||
fetch('/mod/' + modId + '/' + file)
|
||||
.then(response => response.text())
|
||||
.then(response => {
|
||||
this.XMLFile = file
|
||||
this.XMLInfo = response
|
||||
for (const e of document.getElementsByClassName(file)) e.classList.add("selected")
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error)
|
||||
this.fetchError = error.message
|
||||
})
|
||||
},
|
||||
handleSubmit(e) {
|
||||
e.preventDefault()
|
||||
fetch('/search/' + e.target.search.value)
|
||||
.then(response => response.json())
|
||||
.then(response => {
|
||||
this.modInfo = ""
|
||||
this.XMLInfo = ""
|
||||
// const sortField = "time_updated"
|
||||
const sortField = "lifetime_subscriptions"
|
||||
response.response.publishedfiledetails.sort((a, b) =>
|
||||
a[sortField] < b[sortField] ? 1 : -1
|
||||
)
|
||||
this.searchResults = response.response.publishedfiledetails
|
||||
})
|
||||
.then(() => {
|
||||
// Enable all tooltips
|
||||
const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'))
|
||||
tooltipTriggerList.map(function (tooltipTriggerEl) {
|
||||
return new bootstrap.Tooltip(tooltipTriggerEl)
|
||||
})
|
||||
// Enable all alerts
|
||||
$('.alert').alert()
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error)
|
||||
this.fetchError = error.message
|
||||
})
|
||||
},
|
||||
installMod(modId) {
|
||||
fetch('/install/' + modId)
|
||||
.then(response => response.text())
|
||||
.then(response => {
|
||||
console.log(response)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error)
|
||||
this.fetchError = error.message
|
||||
})
|
||||
},
|
||||
removeMod(modId) {
|
||||
fetch('/remove/' + modId)
|
||||
.then(response => response.text())
|
||||
.then(response => {
|
||||
console.log(response)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error)
|
||||
this.fetchError = error.message
|
||||
})
|
||||
},
|
||||
BKMG(val) {
|
||||
const units = ['bytes', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']
|
||||
let l = 0, n = parseInt(val, 10) || 0
|
||||
while(n >= 1024 && ++l){
|
||||
n = n/1024
|
||||
}
|
||||
return(n.toFixed(n < 10 && l > 0 ? 1 : 0) + ' ' + units[l])
|
||||
},
|
||||
installbase() {
|
||||
console.log("Install base files")
|
||||
},
|
||||
servers() {
|
||||
console.log("List servers")
|
||||
},
|
||||
listmods() {
|
||||
console.log("List mods")
|
||||
},
|
||||
updatebase() {
|
||||
console.log("Update base files")
|
||||
},
|
||||
updatemods() {
|
||||
console.log("Update mod files")
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
// Get the data
|
||||
fetch('/status')
|
||||
.then(response => response.json())
|
||||
.then(response => {
|
||||
this.installed = response.installed
|
||||
this.version = response.version
|
||||
this.mods = response.mods
|
||||
if(response.error) {
|
||||
this.fetchError = response.error
|
||||
// Since it's a modal, we have to manually show it...?
|
||||
const modal = new bootstrap.Modal(document.getElementById('errorModal'))
|
||||
modal.show()
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error)
|
||||
this.fetchError = error.message
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
{ "result": 1, "publishedfileid": "2489240546", "creator": "76561199068873691", "creator_appid": 221100, "consumer_appid": 221100, "consumer_shortcutid": 0, "filename": "", "file_size": "276817803", "preview_file_size": "27678", "preview_url": "https://steamuserimages-a.akamaihd.net/ugc/2011465736408144669/A7137390FBB9F4F94E0BFE5389932F6DE7AB7B87/", "url": "", "hcontent_file": "4050838808220661564", "hcontent_preview": "2011465736408144669", "title": "LastDayZ_Helis", "short_description": "The author of the helicopter mod https://sibnic.info on the site you can download the latest version of free helicopters, If you need help with installation, go to discord https://sibnic.info/discord", "time_created": 1621186063, "time_updated": 1684985831, "visibility": 0, "flags": 5632, "workshop_file": false, "workshop_accepted": false, "show_subscribe_all": false, "num_comments_public": 0, "banned": false, "ban_reason": "", "banner": "76561197960265728", "can_be_deleted": true, "app_name": "DayZ", "file_type": 0, "can_subscribe": true, "subscriptions": 7935, "favorited": 3, "followers": 0, "lifetime_subscriptions": 22759, "lifetime_favorited": 5, "lifetime_followers": 0, "lifetime_playtime": "0", "lifetime_playtime_sessions": "0", "views": 535, "num_children": 0, "num_reports": 0, "tags": [ { "tag": "Animation", "display_name": "Animation" }, { "tag": "Environment", "display_name": "Environment" }, { "tag": "Sound", "display_name": "Sound" }, { "tag": "Vehicle", "display_name": "Vehicle" }, { "tag": "Mod", "display_name": "Mod" } ], "language": 0, "maybe_inappropriate_sex": false, "maybe_inappropriate_violence": false, "revision_change_number": "14", "revision": 1, "ban_text_check_result": 5 }
|
||||
|
||||
*/
|
||||
103
web/root/xmltree.js
Normal file
103
web/root/xmltree.js
Normal file
@@ -0,0 +1,103 @@
|
||||
const template = `
|
||||
<div
|
||||
v-if="elem.nodeType === 1 && isText"
|
||||
:style="'padding-left: ' + (depth * 10) + 'px'"
|
||||
@click="collapse"
|
||||
>
|
||||
<span class="xml-tree-tag"><{{elem.nodeName}}</span>
|
||||
<span v-if="elem.hasAttributes()" v-for="attribute in elem.attributes">
|
||||
<span class="xml-tree-attr"> {{attribute.name}}</span>
|
||||
<span>=</span>
|
||||
<span class="xml-tree-attr">"{{attribute.value}}"</span>
|
||||
</span>
|
||||
<span class="xml-tree-tag">></span>
|
||||
<span>{{this.children[0].data.trim()}}</span>
|
||||
<span class="xml-tree-tag"></{{elem.nodeName}}></span>
|
||||
</div>
|
||||
<div v-else :style="'padding-left: ' + (depth * 10) + 'px'">
|
||||
<span v-if="elem.nodeType === 1" class="d-flex">
|
||||
<span
|
||||
v-if="elem.children.length > 0"
|
||||
class="bi-dash simulink text-center"
|
||||
@click="collapse"
|
||||
/>
|
||||
<span v-else></span>
|
||||
<span class="xml-tree-tag"><{{elem.nodeName}}</span>
|
||||
<span v-if="elem.hasAttributes()" v-for="attribute in elem.attributes">
|
||||
<span class="xml-tree-attr"> {{attribute.name}}</span>
|
||||
<span>=</span>
|
||||
<span class="xml-tree-attr">"{{attribute.value}}"</span>
|
||||
</span>
|
||||
<span v-if="elem.children.length === 0" class="xml-tree-tag"> /></span>
|
||||
<span v-else class="xml-tree-tag">></span>
|
||||
</span>
|
||||
<span v-if="elem.nodeType === 3">{{elem.data.trim()}}</span>
|
||||
<div v-for="child in children">
|
||||
<xmltree v-if="child.nodeType !== 8" :element="child" :d="depth" />
|
||||
</div>
|
||||
<span
|
||||
v-if="elem.nodeType === 1 && elem.children.length > 0"
|
||||
style="padding-left: -10px"
|
||||
>
|
||||
<span style="padding-left: 20px" class="xml-tree-tag"></{{elem.nodeName}}></span>
|
||||
</span>
|
||||
</div>
|
||||
`
|
||||
|
||||
export default {
|
||||
name: "xmltree",
|
||||
props: {
|
||||
d: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
element: {
|
||||
type: [Element, Text],
|
||||
default: undefined
|
||||
},
|
||||
xmlData: String
|
||||
},
|
||||
template: template,
|
||||
data() {
|
||||
return {
|
||||
depth: 1
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
collapse() {
|
||||
this.children.forEach(x => x.classList?.add("d-none"))
|
||||
},
|
||||
log(message) {
|
||||
console.log(message)
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
elem() {
|
||||
this.depth = parseInt(this.d) + 1
|
||||
if (this.element) {
|
||||
return this.element
|
||||
} else {
|
||||
const parser = new DOMParser()
|
||||
const xmlDoc = parser.parseFromString(this.xmlData, "text/xml")
|
||||
return xmlDoc.documentElement
|
||||
}
|
||||
},
|
||||
children() {
|
||||
let children = []
|
||||
let node = this.elem.firstChild
|
||||
while (node) {
|
||||
children.push(node)
|
||||
node = node.nextSibling
|
||||
}
|
||||
return children
|
||||
},
|
||||
isText() {
|
||||
if (this.children.length === 1) {
|
||||
if (this.children[0].nodeType === 3) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
299
web/web.js
Normal file
299
web/web.js
Normal file
@@ -0,0 +1,299 @@
|
||||
/*
|
||||
A DayZ Linux server provisioning system.
|
||||
|
||||
This is the web UI for provisioning a DayZ server running under Linux.
|
||||
It manages the main container that installs and maintains the base DayZ server files
|
||||
along with all mod base files. The goal being to keep all of these centralized and consistent,
|
||||
but to also make them available for the creation of server containers.
|
||||
*/
|
||||
import express from 'express'
|
||||
import path from 'path'
|
||||
import fs from 'fs'
|
||||
import https from 'https'
|
||||
import { spawn } from 'child_process'
|
||||
|
||||
const app = express()
|
||||
|
||||
app.use((req, res, next) => {
|
||||
res.append('Access-Control-Allow-Origin', ['*'])
|
||||
res.append('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE')
|
||||
res.append('Access-Control-Allow-Headers', 'Content-Type')
|
||||
next()
|
||||
})
|
||||
|
||||
/*
|
||||
The DayZ server Steam app ID. USE ONE OR THE OTHER!!
|
||||
|
||||
Presumably once the Linux server is officially released, the binaries will come from this ID.
|
||||
Meanwhile, if we have a release-compatible binary, the base files must be installed from this id,
|
||||
even if the server binary and required shared objects don't come from it. (They'd come from...elsewhere...)
|
||||
*/
|
||||
//const server_appid = "223350"
|
||||
|
||||
/*
|
||||
Without a release binary, we must use the experimental server app ID.
|
||||
*/
|
||||
const server_appid = "1042420"
|
||||
|
||||
/*
|
||||
DayZ release client Steam app ID. This is for mods, as only the release client has them.
|
||||
*/
|
||||
const client_appid = "221100"
|
||||
|
||||
/*
|
||||
Denote if it's release or experimental
|
||||
*/
|
||||
const versions = {
|
||||
"1042420": "Experimental",
|
||||
"223350": "Release"
|
||||
}
|
||||
const appid_version = versions[server_appid]
|
||||
|
||||
/*
|
||||
Base file locations
|
||||
*/
|
||||
const modDir = "/mods"
|
||||
const serverFiles = "/serverfiles"
|
||||
|
||||
/*
|
||||
File path delimiter
|
||||
*/
|
||||
const d = '/'
|
||||
|
||||
/*
|
||||
XML config files the system can handle. These are retrieved from values in templates located in /files/mods/:modId
|
||||
*/
|
||||
const configFiles = [
|
||||
'cfgeventspawns.xml',
|
||||
'cfgspawnabletypes.xml',
|
||||
'events.xml',
|
||||
'types.xml',
|
||||
]
|
||||
|
||||
// From https://helpthedeadreturn.wordpress.com/2019/07/17/dayz-sa-mission-file
|
||||
const allConfigFiles = {
|
||||
"db": [ // global server config and core loot economy files
|
||||
"events.xml", // dynamic events
|
||||
"globals.xml", // global settings
|
||||
"messages.xml", // server broadcast messages and shutdown
|
||||
"types.xml" // loot table
|
||||
],
|
||||
"env": [ // coordinates, static and dynamic spawns for each entity
|
||||
"cattle_territories.xml",
|
||||
"domestic_animals_territories.xml",
|
||||
"hare_territories.xml",
|
||||
"hen_territories.xml",
|
||||
"pig_territories.xml",
|
||||
"red_deer_territories.xml",
|
||||
"roe_deer_territories.xml",
|
||||
"sheep_goat_territories.xml",
|
||||
"wild_boar_territories.xml",
|
||||
"wolf_territories.xml",
|
||||
"zombie_territories.xml"
|
||||
],
|
||||
"root": [
|
||||
"cfgeconomycore.xml", // loot economy core settings and extensions
|
||||
"cfgeffectarea.json", // static contaminated area coordinates and other properties
|
||||
"cfgenvironment.xml", // includes env\* files and parameters
|
||||
"cfgeventgroups.xml", // definitions of groups of objects that spawn together in a dynamic event
|
||||
"cfgeventspawns.xml", // coordinates where events may occur
|
||||
"cfggameplay.json", // gameplay configuration settings.
|
||||
"cfgIgnoreList.xml", // list of items that won’t be loaded from the storage
|
||||
"cfglimitsdefinition.xml", // list of valid categories, tags, usageflags and valueflags
|
||||
"cfglimitsdefinitionuser.xml", // shortcut groups of usageflags and valueflags
|
||||
"cfgplayerspawnpoints.xml", // new character spawn points
|
||||
"cfgrandompresets.xml", // collection of groups of items
|
||||
"cfgspawnabletypes.xml", // loot categorization (ie hoarder) as well as set of items that spawn as cargo or as attachment on weapons, vehicles or infected.
|
||||
"cfgundergroundtriggers.json", // used for triggering light and sounds in the Livonia bunker, not used for Chernarus
|
||||
"cfgweather.xml", // weather configuration
|
||||
"init.c", // mission startup file (PC only)
|
||||
"map*.xml",
|
||||
"mapgroupproto.xml", // structures, tags, maxloot and lootpoints
|
||||
"mapgrouppos.xml" // all valid lootpoints
|
||||
]
|
||||
}
|
||||
|
||||
const config = {
|
||||
installFile: serverFiles + "/DayZServer",
|
||||
modDir: modDir + "/" + client_appid,
|
||||
port: 8000,
|
||||
steamAPIKey: process.env["STEAMAPIKEY"]
|
||||
}
|
||||
|
||||
const getVersion = (installed) => {
|
||||
if(installed) {
|
||||
return "1.22.bogus"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
const getDirSize = (dirPath) => {
|
||||
let size = 0
|
||||
if (! fs.existsSync(dirPath)) return size
|
||||
const files = fs.readdirSync(dirPath)
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const filePath = path.join(dirPath, files[i])
|
||||
const stats = fs.statSync(filePath)
|
||||
if (stats.isFile()) {
|
||||
size += stats.size
|
||||
} else if (stats.isDirectory()) {
|
||||
size += getDirSize(filePath)
|
||||
}
|
||||
}
|
||||
return size
|
||||
}
|
||||
|
||||
const getCustomXML = (modId) => {
|
||||
const ret = []
|
||||
if (! fs.existsSync(config.modDir)) return ret
|
||||
for(const file of configFiles) {
|
||||
if (fs.existsSync(config.modDir + d + modId + d + file)) {
|
||||
ret.push({name:file})
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
const getModNameById = (id) => {
|
||||
const files = fs.readdirSync(serverFiles, {encoding: 'utf8', withFileTypes: true})
|
||||
for (const file of files) {
|
||||
if (file.isSymbolicLink()) {
|
||||
const sym = fs.readlinkSync(serverFiles + d + file.name)
|
||||
if(sym.indexOf(id) > -1) return file.name
|
||||
}
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
const getMods = () => {
|
||||
const mods = []
|
||||
if (! fs.existsSync(config.modDir)) return mods
|
||||
fs.readdirSync(config.modDir).forEach(file => {
|
||||
const name = getModNameById(file)
|
||||
mods.push({name:name,id:file})
|
||||
})
|
||||
return mods
|
||||
}
|
||||
|
||||
const login = () => {
|
||||
const args = "+force_install_dir " + serverFiles + " +login '" + config.steamLogin + "' +quit"
|
||||
steamcmd(args)
|
||||
}
|
||||
|
||||
const steamcmd = (args) => {
|
||||
const proc = spawn('steamcmd ' + args)
|
||||
proc.stdout.on('data', (data) => {
|
||||
res.write(data)
|
||||
})
|
||||
proc.stderr.on('data', (data) => {
|
||||
res.write(data)
|
||||
})
|
||||
proc.on('error', (error) => {
|
||||
res.write(error)
|
||||
})
|
||||
proc.on('close', (error) => {
|
||||
if(error) res.write(error)
|
||||
res.end()
|
||||
})
|
||||
}
|
||||
|
||||
app.use(express.static('root'))
|
||||
|
||||
// Get mod metadata by ID
|
||||
app.get('/mod/:modId', (req, res) => {
|
||||
const modId = req.params["modId"]
|
||||
const modDir = config.modDir + d + modId
|
||||
const customXML = getCustomXML(modId)
|
||||
const ret = {
|
||||
id: modId,
|
||||
name: getModNameById(modId),
|
||||
size: getDirSize(modDir),
|
||||
customXML: customXML
|
||||
}
|
||||
res.send(ret)
|
||||
})
|
||||
|
||||
// Get a mod's XML file
|
||||
app.get('/mod/:modId/:file', (req, res) => {
|
||||
const modId = req.params["modId"]
|
||||
const file = req.params["file"]
|
||||
if (fs.existsSync(config.modDir + d + modId + d + file)) {
|
||||
const contents = fs.readFileSync(config.modDir + d + modId + d + file)
|
||||
res.set('Content-type', 'application/xml')
|
||||
res.send(contents)
|
||||
}
|
||||
})
|
||||
|
||||
// Search for a mod
|
||||
app.get(('/search/:searchString'), (req, res) => {
|
||||
const searchString = req.params["searchString"]
|
||||
const url = "https://api.steampowered.com/IPublishedFileService/QueryFiles/v1/?numperpage=1000&appid=221100&return_short_description=true&strip_description_bbcode=true&key=" + config.steamAPIKey + "&search_text=" + searchString
|
||||
https.get(url, resp => {
|
||||
let data = '';
|
||||
resp.on('data', chunk => {
|
||||
data += chunk;
|
||||
});
|
||||
resp.on('end', () => {
|
||||
res.send(JSON.parse(data))
|
||||
})
|
||||
}).on('error', err => {
|
||||
console.log(err.message)
|
||||
})
|
||||
})
|
||||
|
||||
// Install a mod
|
||||
app.get(('/install/:modId'), (req, res) => {
|
||||
const modId = req.params["modId"]
|
||||
// Shell out to steamcmd, monitor the process, and display the output as it runs
|
||||
res.send(modId + " was installed")
|
||||
})
|
||||
|
||||
// Remove a mod
|
||||
app.get(('/remove/:modId'), (req, res) => {
|
||||
const modId = req.params["modId"]
|
||||
// Shell out to steamcmd, monitor the process, and display the output as it runs
|
||||
res.send(modId + " was removed")
|
||||
})
|
||||
|
||||
// Update base files
|
||||
app.get('/updatebase', (req, res) => {
|
||||
login()
|
||||
res.send("Base files were updates")
|
||||
})
|
||||
|
||||
// Update mods
|
||||
app.get('/updatemods', (req, res) => {
|
||||
res.send("Mod files were updates")
|
||||
})
|
||||
|
||||
/*
|
||||
Get the status of things:
|
||||
If the base files are installed, the version of the server, the appid (If release or experimental)
|
||||
*/
|
||||
app.get('/status', (req, res) => {
|
||||
// FIXME Async/await this stuff...
|
||||
const installed = fs.existsSync(config.installFile)
|
||||
const version = getVersion(installed)
|
||||
const ret = {
|
||||
"appid": appid_version,
|
||||
"installed": installed,
|
||||
"version": version
|
||||
}
|
||||
ret.error = "This is a test error from the back end"
|
||||
res.send(ret)
|
||||
})
|
||||
|
||||
/*
|
||||
Get all mod metadata
|
||||
*/
|
||||
app.get('/mods', (req, res) => {
|
||||
const mods = getMods()
|
||||
const ret = {
|
||||
"mods": mods
|
||||
}
|
||||
res.send(ret)
|
||||
})
|
||||
|
||||
app.listen(config.port, () => {
|
||||
console.log(`Listening on port ${config.port}`)
|
||||
})
|
||||
Reference in New Issue
Block a user