Implement much missing stuff:

Login. Still WIP, but works.
Install server files. Once logged in, this works.
Add support for experimental in both web and scripts. WIP.
Continuous streaming text web interface for long-running server processes.
Large refactor of the config file. Use arrays instead of delimited strings.
Large refactor of the express server to support the above.
Add Steam API key to env for when we do mod searches.
Customize the bash shell of the web container should we exec into it.
Ignore the env file.
This commit is contained in:
Daniel Ceregatti 2024-08-26 09:13:30 -07:00
parent 20700cdc26
commit 569e45c93c
13 changed files with 269 additions and 199 deletions

1
.gitignore vendored
View file

@ -1,6 +1,7 @@
.idea .idea
*.iml *.iml
.env* .env*
env.json
node_modules/ node_modules/
web/client/bin web/client/bin
web/client/obj web/client/obj

View file

@ -12,6 +12,7 @@ volumes:
serverfiles_experimental: serverfiles_experimental:
# Upstream mission files # Upstream mission files
mpmissions: mpmissions:
mpmissions_experimental:
services: services:
@ -27,6 +28,7 @@ services:
- serverfiles:/serverfiles - serverfiles:/serverfiles
- serverfiles_experimental:/serverfiles_experimental - serverfiles_experimental:/serverfiles_experimental
- mpmissions:/serverfiles/mpmissions - mpmissions:/serverfiles/mpmissions
- mpmissions_experimental:/serverfiles_experimental/mpmissions
- mods:/serverfiles/steamapps/workshop/content - mods:/serverfiles/steamapps/workshop/content
- mods:/mods - mods:/mods
- ./files:/files - ./files:/files

View file

@ -29,14 +29,14 @@ export yellow="\e[93m"
export lightblue="\e[94m" export lightblue="\e[94m"
export blue="\e[34m" export blue="\e[34m"
export magenta="\e[35m" export magenta="\e[35m"
export cyan="\e[36m" export cyan="\e[36m"
# DayZ release server Steam app ID. # DayZ release server Steam app ID.
# Now that the Linux server is released, the binaries will come from this ID. # Now that the Linux server is released, the binaries will come from this ID.
export release_server_appid=223350 export release_server_appid=223350
# Leaving the experimental server appid here to allow for the use of the experimental server. # Leaving the experimental server appid here to allow for the use of the experimental server.
#export release_server_appid=1042420 export experimental_server_appid=1042420
# DayZ release client SteamID. This is for mods, as only the release client has them. # DayZ release client SteamID. This is for mods, as only the release client has them.
export release_client_appid=221100 export release_client_appid=221100
@ -47,6 +47,7 @@ export SERVER_PROFILE="/profiles"
# Common container base directories # Common container base directories
export FILES="/files" export FILES="/files"
export SERVER_FILES="/serverfiles" export SERVER_FILES="/serverfiles"
export SERVER_FILES_EXPERIMENTAL="/serverfiles_experimental"
# Used to check if dayZ is installed # Used to check if dayZ is installed
export SERVER_INSTALL_FILE="${SERVER_FILES}/DayZServer" export SERVER_INSTALL_FILE="${SERVER_FILES}/DayZServer"

View file

@ -131,8 +131,10 @@ ARG USER_ID
RUN groupadd -g ${USER_ID} user && \ RUN groupadd -g ${USER_ID} user && \
useradd -l -u ${USER_ID} -m -g user user && \ useradd -l -u ${USER_ID} -m -g user user && \
mkdir -p /home/user /serverfiles/mpmissions /serverfiles/steamapps/workshop/content /web && \ mkdir -p /home/user \
chown -R user:user /home/user /serverfiles /web /serverfiles/mpmissions /serverfiles/steamapps/workshop/content \
/serverfiles_experimental/mpmissions /serverfiles_experimental/steamapps/workshop/content /web && \
chown -R user:user /home/user /serverfiles /serverfiles_experimental /web
# Use our non-privileged user # Use our non-privileged user
USER user USER user

View file

@ -180,7 +180,8 @@ login(){
dologin(){ dologin(){
if [ -f "${STEAM_LOGIN}" ] if [ -f "${STEAM_LOGIN}" ]
then then
source "${STEAM_LOGIN}" echo "Logging in to Steam"
steamlogin=$(cat ${STEAM_LOGIN})
else else
echo "No cached Steam credentials. Please configure this now: " echo "No cached Steam credentials. Please configure this now: "
login login
@ -189,9 +190,16 @@ dologin(){
# Perform the installation of the server files. # Perform the installation of the server files.
install(){ install(){
if [ ! -f "${SERVER_INSTALL_FILE}" ] || [[ ${1} = "force" ]] WHICH=${1}
if [[ ${WHICH} = "experimental" ]]
then then
printf "[ ${yellow}DayZ${default} ] Downloading DayZ Server-Files!\n" SERVER_FILES=${SERVER_FILES_EXPERIMENTAL}
SERVER_INSTALL_FILE="${SERVER_FILES}/DayZServer"
release_server_appid=${experimental_server_appid}
fi
if [ ! -f "${SERVER_INSTALL_FILE}" ] || [[ ${2} = "force" ]]
then
printf "[ ${yellow}DayZ${default} ] Downloading ${WHICH} DayZ Server-Files!\n"
dologin dologin
${STEAMCMD} +force_install_dir ${SERVER_FILES} +login "${steamlogin}" +app_update "${release_server_appid}" validate +quit ${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 # This installs the mpmissions for charnarusplus and enoch (AKA Livonia) from github. The game once allowed the full server
@ -199,7 +207,7 @@ install(){
echo "Installing mpmissions for ChernarusPlus and Livonia from github..." echo "Installing mpmissions for ChernarusPlus and Livonia from github..."
map default map default
else else
printf "[ ${lightblue}DayZ${default} ] The server is already installed.\n" printf "[ ${lightblue}DayZ${default} ] The ${WHICH} server is already installed.\n"
fi fi
} }
@ -341,8 +349,6 @@ xml(){
installxml ${1} installxml ${1}
} }
# Capture the first argument and shift it off so we can pass $@ to every function
C=${1}
shift || { shift || {
usage usage
} }

View file

@ -7,16 +7,13 @@ trap '
' SIGINT SIGTERM ' SIGINT SIGTERM
# Set PS1 so we know we're in the container # Set PS1 so we know we're in the container
if ! grep -q "dz-web" .bashrc echo "Adding PS1 to .bashrc..."
then cat > .bashrc <<EOF
echo "Adding PS1 to .bashrc..."
cat >> .bashrc <<EOF
alias ls='ls --color' alias ls='ls --color'
export PS1='${debian_chroot:+($debian_chroot)}\[\033[01;32m\]\u@\h\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]\$ ' export PS1='${debian_chroot:+($debian_chroot)}\[\033[01;35m\]\u@\h\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]\$ '
unset DEVELOPMENT unset DEVELOPMENT
export PATH=${PATH}:/usr/local/dotnet export PATH=${PATH}:/usr/local/dotnet
EOF EOF
fi
# Shut steamcmd up # Shut steamcmd up
if ! [ -d ${HOME}/.steam ] if ! [ -d ${HOME}/.steam ]
@ -25,6 +22,16 @@ then
fi fi
cd /web cd /web
if [ -n "${STEAMAPIKEY}" ]
then
cat > env.json <<EOF
{
"STEAMAPIKEY": "${STEAMAPIKEY}"
}
EOF
else
echo "{}" > env.json
fi
#export DEBUG=express:* #export DEBUG=express:*
npm run dev & npm run dev &
wait $! wait $!

View file

@ -5,23 +5,24 @@ const store = useAppStore()
</script> </script>
<template> <template>
<Dialog v-model:visible="store.stream" maximizable modal :style="{ width: '50rem' }"> <Dialog v-model:visible="store.stream" maximizable modal :style="{ width: '50rem' }">
<template #header> <template #header>
<div class="grid"> <div class="grid">
<div class="col align-content-center justify-content-center"><i class="pi pi-exclamation-circle" style="color: green;"></i></div> <div class="col align-content-center justify-content-center" style="font-size: 1.5em;">
<i v-if="store.streamLoading" class="pi pi-spin pi-cog" style="color: orange;"></i>
<i v-else class="pi pi-exclamation-circle" style="color: green;"></i>
</div>
<div class="col align-content-center justify-content-center white-space-nowrap">{{ $t('Server Output') }}</div> <div class="col align-content-center justify-content-center white-space-nowrap">{{ $t('Server Output') }}</div>
</div> </div>
</template> </template>
<template #default> <template #default>
<div class="steamcmd"> <div class="container">
<pre class="pre">{{ store.streamText }}</pre> <div class="autoscroll">{{ store.streamText }}</div>
</div> </div>
</template> </template>
<template #footer>
<i v-if="store.streamLoading" class="pi pi-spin pi-cog" style="color: red;"></i>
<i v-else class="pi pi-check" style="color: green;"></i>
</template>
</Dialog> </Dialog>
<Dialog v-model:visible="store.alert" modal :header="$t('Alert')" :style="{ width: '25rem' }"> <Dialog v-model:visible="store.alert" modal :header="$t('Alert')" :style="{ width: '25rem' }">
<template #header> <template #header>
<div class="grid"> <div class="grid">
@ -31,6 +32,7 @@ const store = useAppStore()
</template> </template>
{{ store.alertText }} {{ store.alertText }}
</Dialog> </Dialog>
<Dialog v-model:visible="store.error" modal :header="$t('Error')" :style="{ width: '25rem' }"> <Dialog v-model:visible="store.error" modal :header="$t('Error')" :style="{ width: '25rem' }">
<template #header> <template #header>
<div class="grid"> <div class="grid">
@ -40,13 +42,21 @@ const store = useAppStore()
</template> </template>
{{ store.errorText }} {{ store.errorText }}
</Dialog> </Dialog>
</template> </template>
<style scoped> <style scoped>
.steamcmd { .container {
height: 300px;
min-height: 300px; min-height: 300px;
} }
.pre { .autoscroll {
overflow-y: scroll;
scroll-behavior: smooth;
max-height: 100%;
display: flex;
flex-direction: column-reverse;
white-space: pre-wrap; white-space: pre-wrap;
font-family: monospace;
} }
</style> </style>

View file

@ -1,34 +1,46 @@
<script setup> <script setup>
import Button from 'primevue/button' import Button from 'primevue/button'
import { useFetch } from '@vueuse/core'
import { useAppStore } from '@/store.js' import { useAppStore } from '@/store.js'
const store = useAppStore() const store = useAppStore()
import { useI18n } from 'vue-i18n' async function install(which) {
const { t } = useI18n() const url = '/install/server/' + which
async function base() { store.setStreamLoading(true)
let which = '/installbase' const response = await fetch(url)
if (store.steamStatus.stableInstalled) { for await (const chunk of response.body) {
which = '/updatebase' store.setStream(new TextDecoder().decode(chunk), true)
}
store.setStreamLoading(false)
const data = JSON.parse(store.streamText.match(/({.*})/)[1])
if (data.errorCode === 0) {
store.setAlert(t('Successfully installed server files'))
store.steamStatus.installed[which] = true
} else if (data.errorMessage) {
store.setError(t(data.errorMessage))
} else {
store.setError(t('Unknown error'))
} }
const { data } = await useFetch(which).get().json()
store.setAlert(t(data.value.message))
} }
</script> </script>
<template> <template>
<div v-if="! store.steamStatus.loggedIn" class="grid">
<div class="col-6 col-offset-3">
{{ $t('Please log in to Steam to install server files') }}
</div>
</div>
<div class="grid"> <div class="grid">
<div class="col-6 col-offset-3"> <div class="col-6 col-offset-3">
<div> <div>
<Button @click="base('stable')" v-if="! store.steamStatus.stableInstalled">{{ store.steamStatus.stableInstalled ? $t('Update Stable Server Files') : $t('Install Stable Server Files') }}</Button> <Button v-if="! store.steamStatus.installed['stable']" @click="install('stable')" :disabled="! store.steamStatus.loggedIn">{{ $t('Install Stable Server Files') }}</Button>
<span v-else>{{ $t('Stable Server files are installed') }}</span> <Button v-else severity="warn" @click="install('stable')" :disabled="! store.steamStatus.loggedIn">{{ $t('Stable Server files are installed') + ' - ' + $t('Update Stable Server Files') }}</Button>
</div> </div>
</div> </div>
</div> </div>
<div class="grid"> <div class="grid">
<div class="col-6 col-offset-3"> <div class="col-6 col-offset-3">
<div> <div>
<Button @click="base('experimental')" v-if="! store.steamStatus.experimentalInstalled">{{ store.steamStatus.experimentalInstalled ? $t('Update Experimental Server Files') : $t('Install Experimental Server Files') }}</Button> <Button v-if="! store.steamStatus.installed['experimental']" @click="install('experimental')" :disabled="! store.steamStatus.loggedIn">{{ store.steamStatus.installed_experimental ? $t('Update Experimental Server Files') : $t('Install Experimental Server Files') }}</Button>
<span v-else>{{ $t('Experimental Server files are installed') }}</span> <Button v-else severity="warn" @click="install('experimental')" :disabled="! store.steamStatus.loggedIn">{{ $t('Experimental Server files are installed') + ' - ' + $t('Update Experimental Server Files') }}</Button>
</div> </div>
</div> </div>
</div> </div>

View file

@ -23,11 +23,11 @@ const test = async (type) => {
const continuous = async () => { const continuous = async () => {
const url = '/test?type=continuous' const url = '/test?type=continuous'
const response = await fetch(url) const response = await fetch(url)
store.setStream('') store.setStreamLoading(true)
for await (const chunk of response.body) { for await (const chunk of response.body) {
store.setStream(new TextDecoder().decode(chunk), true) store.setStream(new TextDecoder().decode(chunk), true)
} }
store.setSteamLoading(false) store.setStreamLoading(false)
} }
</script> </script>
@ -46,7 +46,7 @@ const continuous = async () => {
<div class="col-6 col-offset-3"> <div class="col-6 col-offset-3">
<div> <div>
{{ $t('Stable Server files installed') }}: {{ $t('Stable Server files installed') }}:
<span v-if="store.steamStatus.stableInstalled" class="pi pi-check" style="color: green"></span> <span v-if="store.steamStatus.installed['stable']" class="pi pi-check" style="color: green"></span>
<span v-else class="pi pi-times" style="color: red"></span> <span v-else class="pi pi-times" style="color: red"></span>
</div> </div>
</div> </div>
@ -55,15 +55,15 @@ const continuous = async () => {
<div class="col-6 col-offset-3"> <div class="col-6 col-offset-3">
<div> <div>
{{ $t('Experimental Server files installed') }}: {{ $t('Experimental Server files installed') }}:
<span v-if="store.steamStatus.experimentalInstalled" class="pi pi-check" style="color: green"></span> <span v-if="store.steamStatus.installed['experimental']" class="pi pi-check" style="color: green"></span>
<span v-else class="pi pi-times" style="color: red"></span> <span v-else class="pi pi-times" style="color: red"></span>
</div> </div>
</div> </div>
</div> </div>
<div class="grid"> <div class="grid">
<div class="col-6 col-offset-3"> <div class="col-6 col-offset-3">
<div v-if="store.steamStatus.version"> <div v-if="store.steamStatus.version_stable">
{{ $t('Version') }}: <span style="color: green;">{{ store.steamStatus.version }}</span> {{ $t('Version') }}: <span style="color: green;">{{ store.steamStatus.version_stable }}</span>
<span class="bold">({{ store.steamStatus.appid }})</span> <span class="bold">({{ store.steamStatus.appid }})</span>
</div> </div>
</div> </div>

View file

@ -9,24 +9,20 @@ import { useAppStore } from '@/store.js'
const store = useAppStore() const store = useAppStore()
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
const { t } = useI18n() const { t } = useI18n()
function steamStatus(data) {
console.log(data)
if (data.errorCode === 0) {
store.setAlert(t('Successfully logged in to Steam'))
store.steamStatus.loggedIn = true
} else {
store.setError(t(data.errorMessage))
store.steamStatus.loggedIn = false
}
}
async function logOut() { async function logOut() {
const { data } = await useFetch('/logout').get().json() const { data } = await useFetch('/logout').get().json()
steamStatus(data) if (data.value.errorCode === 0) {
store.setAlert(t('Successfully logged out of Steam'))
store.steamStatus.loggedIn = false
} else if (data.errorMessage) {
store.setError(t(data.errorMessage))
} else {
store.setError(t('Unknown error'))
}
} }
async function login(e) { async function login() {
loading.value = true const url = '/login'
e.preventDefault() const response = await fetch(url, {
const { data } = await useFetch('/login', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
@ -36,12 +32,23 @@ async function login(e) {
password: password.value, password: password.value,
remember: remember.value, remember: remember.value,
steamGuardCode: steamGuardCode.value steamGuardCode: steamGuardCode.value
}).post().json() })
}) })
loading.value = false store.setStreamLoading(true)
steamStatus(data) for await (const chunk of response.body) {
store.setStream(new TextDecoder().decode(chunk), true)
}
store.setStreamLoading(false)
const data = JSON.parse(store.streamText.match(/({.*})/)[1])
if (data.errorCode === 0) {
store.setAlert(t('Successfully logged in to Steam'))
store.steamStatus.loggedIn = true
} else if (data.errorMessage) {
store.setError(t(data.errorMessage))
} else {
store.setError(t('Unknown error'))
}
} }
let loading = ref(false)
let username = ref('') let username = ref('')
let password = ref('') let password = ref('')
let remember = ref(false) let remember = ref(false)
@ -49,46 +56,42 @@ let steamGuardCode = ref('')
</script> </script>
<template> <template>
<div> <div v-if="store.steamStatus.loggedIn" class="grid">
<div v-if="store.steamStatus.loggedIn" class="grid"> <div class="col-4 col-offset-4 text-center">{{ $t('Already logged in to steam') }}</div>
<div class="col-12">{{ $t('Already logged in to steam') }}</div> <div class="col-12">
<div class="col-12"> <Button @click="logOut" severity="danger">{{ $t('Log out') }}</Button>
<Button @click="logOut">{{ $t('Log out') }}</Button> </div>
</div>
<div v-else class="grid">
<div class="col-4 col-offset-4 text-left">
{{ $t('There are no saved Steam credentials. To install the server files and mods, please login to Steam') }}
</div>
<div class="col-4 col-offset-4 text-right">
<div class="col-6 text-left p-0">
<label for="username">{{ $t('Username') }}</label>
<InputText id="username" v-model="username" autofocus />
</div> </div>
</div> </div>
<div v-else class="grid"> <div class="col-4 col-offset-4 text-right">
<div class="col-12"> <div class="col-6 text-left p-0">
<h2>{{ $t('There are no saved Steam credentials. To install the server files and mods, please login to Steam') }}</h2>
</div>
<div class="col-12">
</div>
<div class="col-2 col-offset-4 text-right">
<label for="username">{{ $t('Username') }}</label>
</div>
<div class="col-2 text-left">
<InputText id="username" v-model="username" />
</div>
<div class="col-2 col-offset-4 text-right">
<label for="password">{{ $t('Password') }}</label> <label for="password">{{ $t('Password') }}</label>
</div>
<div class="col-2 text-left">
<Password id="password" v-model="password" :feedback="false" toggleMask /> <Password id="password" v-model="password" :feedback="false" toggleMask />
</div> </div>
<div class="col-2 col-offset-4 text-right"> </div>
<div class="col-4 col-offset-4 text-right">
<div class="col-6 text-left p-0">
<label for="steamGuardCode">{{ $t('Steam Guard Code') }}</label> <label for="steamGuardCode">{{ $t('Steam Guard Code') }}</label>
</div>
<div class="col-2 text-left">
<InputText id="steamGuardCode" v-model="steamGuardCode" /> <InputText id="steamGuardCode" v-model="steamGuardCode" />
</div> </div>
<div class="col-2 col-offset-4 text-right"> </div>
<div class="col-4 col-offset-4 text-right">
<div class="col-6 text-left p-0">
<label for="remember">{{ $t('Remember Credentials') }}</label> <label for="remember">{{ $t('Remember Credentials') }}</label>
<Checkbox inputId="remember" v-model="remember" binary />
</div> </div>
<div class="col-2 text-left"> </div>
<Checkbox id="remember" v-model="remember" binary /> <div class="col-4 col-offset-4 text-left">
</div> <Button type="button" @click="login" :loading="store.streamLoading" icon="pi pi-user" :label="$t('Submit')"></Button>
<div class="col-12 text-center">
<Button @click="login" :icon="loading ? 'pi pi-spin pi-spinner' : ''">{{ $t('Submit') }}</Button>
</div>
</div> </div>
</div> </div>
</template> </template>

View file

@ -1,36 +1,51 @@
/* // The STEAMAPIKEY is added to this file from the value in the .env file.
* The stable DayZ server Steam app ID. import env from '/web/env.json' assert { type: "json" }
*/
const stable_server_appid = 223350
/* // The stable DayZ server Steam app ID.
* The experimental DayZ server Steam app ID. const server_appid_stable = 223350
*/
const experimental_server_appid = 1042420
/* // The experimental DayZ server Steam app ID.
* DayZ release client Steam app ID. This is for mods, as only the release client has them. const server_appid_experimental = 1042420
*/
// DayZ release client Steam app ID. This is for mods, as only the release client has them.
const client_appid = 221100 const client_appid = 221100
const serverFiles = "/serverfiles" const server_files_stable = "/serverfiles"
const homeDir = "/home/user" const server_files_experimental = "/serverfiles_experimental"
const home_dir = "/home/user"
// const steamAPIKey = process?.env["STEAMAPIKEY"] || "" const search_url = `https://api.steampowered.com/IPublishedFileService/QueryFiles/v1/?numperpage=1000&appid=221100&return_short_description=true&strip_description_bbcode=true&key=${env.STEAMAPIKEY}&search_text=`
//
// const searchUrl = "https://api.steampowered.com/IPublishedFileService/QueryFiles/v1/?numperpage=1000&appid=221100&return_short_description=true&strip_description_bbcode=true&key=" + steamAPIKey + "&search_text="
const config = { const config = {
appid: {
client: client_appid,
experimental: server_appid_experimental,
stable: server_appid_stable,
},
client_appid: client_appid, client_appid: client_appid,
experimental_server_appid: experimental_server_appid, install_file: {
installFile: serverFiles + "/DayZServer", experimental: server_files_experimental + "/DayZServer",
loginFile: homeDir + "/steamlogin", stable: server_files_stable + "/DayZServer",
modDir: "/mods/" + client_appid, },
login_file: home_dir + "/steamlogin",
mod_dir: "/mods/" + client_appid,
port: 8000, port: 8000,
// searchUrl: searchUrl, search_url: search_url,
serverFiles: serverFiles, server_appid: {
stable_server_appid: stable_server_appid, experimental: server_appid_experimental,
stable: server_appid_stable,
},
server_files: {
experimental: server_files_experimental,
stable: server_files_stable,
},
steamUrl: 'https://steamcommunity.com/sharedfiles/filedetails/?id=', steamUrl: 'https://steamcommunity.com/sharedfiles/filedetails/?id=',
version: {
experimental: "1.26.56789",
stable: "1.26.123456",
},
version_experimental: "1.26.56789",
version_stable: "1.26.123456",
} }
export { config } export { config }

View file

@ -9,14 +9,19 @@ export const useAppStore = defineStore('app', {
modId: 0, modId: 0,
modFile: false, modFile: false,
mods: [], mods: [],
searchText: false, search: false,
searchText: '',
servers: [], servers: [],
steamStatus: { steamStatus: {
appid: 0, installed: {
experimentalInstalled: false, experimental: false,
stable: false,
},
loggedIn: false, loggedIn: false,
stableInstalled: false, version: {
version: '' stable: '',
experimental: '',
}
}, },
stream: false, stream: false,
streamLoading: false, streamLoading: false,
@ -28,15 +33,14 @@ export const useAppStore = defineStore('app', {
this.alert = true this.alert = true
}, },
setStream(streamText) { setStream(streamText) {
this.stream = true this.streamText += streamText
if (streamText) {
this.streamText += streamText
} else {
this.streamText = ''
}
}, },
setStreamLoading(streamLoading) { setStreamLoading(streamLoading) {
this.stream = true
this.streamLoading = streamLoading this.streamLoading = streamLoading
if (streamLoading) {
this.streamText = ''
}
}, },
setError(error) { setError(error) {
this.errorText = error this.errorText = error

View file

@ -12,7 +12,7 @@ import path from 'path'
import fs from 'fs' import fs from 'fs'
import https from 'https' import https from 'https'
import { spawn } from 'child_process' import { spawn } from 'child_process'
import { config } from "./docroot/src/config.js"; import { config } from './docroot/src/config.js'
/* /*
File path delimiter File path delimiter
@ -94,9 +94,9 @@ const getDirSize = (dirPath) => {
const getCustomXML = (modId) => { const getCustomXML = (modId) => {
const ret = [] const ret = []
if (! fs.existsSync(config.modDir)) return ret if (! fs.existsSync(config.mod_dir)) return ret
for(const file of configFiles) { for(const file of configFiles) {
if (fs.existsSync(config.modDir + d + modId + d + file)) { if (fs.existsSync(config.mod_dir + d + modId + d + file)) {
ret.push({name:file}) ret.push({name:file})
} }
} }
@ -104,7 +104,7 @@ const getCustomXML = (modId) => {
} }
const getModNameById = (id) => { const getModNameById = (id) => {
const files = fs.readdirSync(config.serverFiles, {encoding: 'utf8', withFileTypes: true}) const files = fs.readdirSync(config.server_files, {encoding: 'utf8', withFileTypes: true})
for (const file of files) { for (const file of files) {
if (file.isSymbolicLink()) { if (file.isSymbolicLink()) {
const sym = fs.readlinkSync(serverFiles + d + file.name) const sym = fs.readlinkSync(serverFiles + d + file.name)
@ -116,8 +116,8 @@ const getModNameById = (id) => {
const getMods = () => { const getMods = () => {
const mods = [] const mods = []
if (! fs.existsSync(config.modDir)) return mods if (! fs.existsSync(config.mod_dir)) return mods
fs.readdirSync(config.modDir).forEach(file => { fs.readdirSync(config.mod_dir).forEach(file => {
const name = getModNameById(file) const name = getModNameById(file)
mods.push({name:name,id:file}) mods.push({name:name,id:file})
}) })
@ -132,26 +132,27 @@ const sendAlert = (res, message) => {
res.send({"alert": message}) res.send({"alert": message})
} }
const steamcmd = async (args, res) => { const cmd = async (command, args, res) => {
return new Promise((resolve) => { return new Promise((resolve) => {
let stdout = '' let stdout = ''
let stderr = '' let stderr = ''
const command = 'steamcmd +force_install_dir ' + config.serverFiles + ' ' + args + ' +quit' const re = /(\u001b\[.*?m)/g
console.log(command) console.log(command, args)
const proc = spawn(command, {shell: true}) const proc = spawn(command, args)
proc.stdout.on('data', (data) => { proc.stdout.on('data', (data) => {
const out = "[OUT] " + data + "\n" const out = "[OUT] " + data.toString().replace(re,'') + "\n"
console.log(out) console.log(out)
res.write(out) res.write(out)
stdout += out stdout += out
}) })
proc.stderr.on('data', (data) => { proc.stderr.on('data', (data) => {
const err = "[ERROR] " + data + "\n" const err = "[ERROR] " + data.toString().replace(re,'') + "\n"
res.write(err)
console.log(err) console.log(err)
stderr += err stderr += err
}) })
proc.on('error', (data) => { proc.on('error', (data) => {
const err = "[ERROR] " + data + "\n" const err = "[ERROR] " + data.toString().replace(re,'') + "\n"
console.log(err) console.log(err)
stderr += err stderr += err
}) })
@ -162,6 +163,14 @@ const steamcmd = async (args, res) => {
}) })
} }
const steamcmd = async (args, which, res) => {
return await cmd('steamcmd', [ '+force_install_dir', config.server_files[which] ].concat(args).concat("+quit"), res)
}
const dz = async (args, res) => {
return cmd('dz', args, res)
}
const app = express() const app = express()
app.use(express.json()) app.use(express.json())
@ -170,35 +179,29 @@ app.use(express.urlencoded({extended: true}))
app.disable('etag') app.disable('etag')
app.use((req, res, next) => { app.use((req, res, next) => {
res.append('Access-Control-Allow-Origin', ['*']) res.append('Access-Control-Allow-Origin', '*')
res.append('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE') res.append('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE')
res.append('Access-Control-Allow-Headers', 'Content-Type') res.append('Access-Control-Allow-Headers', 'Content-Type')
next() next()
}) })
// Install a mod // Install a mod
app.get(('/install/:modId'), (req, res) => { app.get(('/install/mod/:modId'), async (req, res) => {
const modId = req.params["modId"] const modId = req.params["modId"]
// Shell out to steamcmd, monitor the process, and display the output as it runs // Shell out to steamcmd, monitor the process, and display the output as it runs
res.send(modId + " was installed") await dz('a', [ modId ], res)
res.end()
}) })
// Install base files // Install base files
app.get('/installbase', async (req, res) => { app.get('/install/server/:which', async (req, res) => {
let which = req.query?.which const which = req.params["which"]
const username = fs.readFileSync(config.loginFile, 'utf8') const appid = config.appid[which]
if (which === "experimental") { const username = fs.readFileSync(config.login_file, 'utf8')
which = config.experimental_server_appid let args = ['+login', username, '+app_update', appid, 'validate']
} else if (which === "stable") { const result = await steamcmd(args, which, res)
which = config.stable_server_appid res.write(JSON.stringify(result))
} else { res.end()
sendError(res, "Invalid base file type")
}
let args = `+login "${username}" +app_update "${which}" validate`
const result = await steamcmd(args, res)
if (result.errorCode === 0) {
}
res.send(result)
}) })
// Login to Steam // Login to Steam
@ -207,39 +210,40 @@ app.post(('/login'), async (req, res) => {
const password = req.body?.password; const password = req.body?.password;
const steamGuardCode = req.body?.steamGuardCode; const steamGuardCode = req.body?.steamGuardCode;
const remember = req.body?.remember; const remember = req.body?.remember;
let args = `+login "${username}" "${password}"` let args = ['+login', username, password ]
if (steamGuardCode) args += ` "${steamGuardCode}"` if (steamGuardCode) args.push(steamGuardCode)
const result = await steamcmd(args) const result = await steamcmd(args, 'stable', res)
if (result.errorCode === 0) { if (result.errorCode === 0) {
if (remember) { if (remember) {
console.log("Writing login file") console.log("Writing login file")
fs.writeFileSync(config.loginFile, username) fs.writeFileSync(config.login_file, username)
} else {
console.log("Not writing login file")
} }
} }
res.append('Content-Type', 'application/json') res.write(JSON.stringify(result))
res.send(result) res.end()
}) })
// Logout from Steam // Logout from Steam
app.get(('/logout'), async (req, res) => { app.get(('/logout'), async (req, res) => {
let result = {"status": 304} let result = {"errorCode": 0}
if (fs.existsSync(config.loginFile)) { if (fs.existsSync(config.login_file)) {
fs.unlinkSync(config.loginFile, (err) => { fs.rmSync('/home/user/.local/share/Steam/userdata', { recursive: true, force: true })
fs.unlinkSync(config.login_file, (err) => {
if (err) { if (err) {
result.status = 500 result.errorCoder = 1
result.error = err result.error = err
} }
result.status = 200
}) })
} }
res.append('Content-Type', 'application/json')
res.send(result) res.send(result)
}) })
// Get mod metadata by ID // Get mod metadata by ID
app.get('/mod/:modId', (req, res) => { app.get('/mod/:modId', (req, res) => {
const modId = req.params["modId"] const modId = req.params["modId"]
const modDir = config.modDir + d + modId const modDir = config.mod_dir + d + modId
const customXML = getCustomXML(modId) const customXML = getCustomXML(modId)
const ret = { const ret = {
id: modId, id: modId,
@ -254,8 +258,8 @@ app.get('/mod/:modId', (req, res) => {
app.get('/mod/:modId/:file', (req, res) => { app.get('/mod/:modId/:file', (req, res) => {
const modId = req.params["modId"] const modId = req.params["modId"]
const file = req.params["file"] const file = req.params["file"]
if (fs.existsSync(config.modDir + d + modId + d + file)) { if (fs.existsSync(config.mod_dir + d + modId + d + file)) {
const contents = fs.readFileSync(config.modDir + d + modId + d + file) const contents = fs.readFileSync(config.mod_dir + d + modId + d + file)
res.set('Content-type', 'application/xml') res.set('Content-type', 'application/xml')
res.send(contents) res.send(contents)
} }
@ -282,8 +286,7 @@ app.get(('/remove/:modId'), (req, res) => {
// Search for a mod // Search for a mod
app.get(('/search/:searchString'), (req, res) => { app.get(('/search/:searchString'), (req, res) => {
const searchString = req.params["searchString"] const searchString = req.params["searchString"]
const url = config.searchUrl + searchString const url = config.search_url + 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 => { https.get(url, resp => {
let data = ''; let data = '';
resp.on('data', chunk => { resp.on('data', chunk => {
@ -302,15 +305,19 @@ app.get(('/search/:searchString'), (req, res) => {
If the base files are installed, the version of the server, the appid (If release or experimental) If the base files are installed, the version of the server, the appid (If release or experimental)
*/ */
app.get('/status', (_, res) => { app.get('/status', (_, res) => {
const installed = fs.existsSync(config.installFile)
const loggedIn = fs.existsSync(config.loginFile)
const ret = { const ret = {
"appid": config.appid_version, "appid": config.version['stable'],
"installed": installed, "installed": {
"loggedIn": loggedIn, "experimental": fs.existsSync(config.install_file['experimental']),
"stable": fs.existsSync(config.install_file['stable']),
},
"loggedIn": fs.existsSync(config.login_file),
} }
if (installed) { if (ret.installed.stable) {
ret.version = getVersion() ret.version = {
stable: getVersion('stable'),
experimental: getVersion('experimental')
}
} }
res.send(ret) res.send(ret)
}) })
@ -331,29 +338,29 @@ app.get('/test', async (req, res) => {
res.send(ret) res.send(ret)
} else if (type === "continuous") { } else if (type === "continuous") {
res.set('Content-Type', 'text/plain') res.set('Content-Type', 'text/plain')
res.write("data: This is a test server continuous output 1\n") res.write("This is a test server continuous output 1\n")
await new Promise(resolve => setTimeout(resolve, 1000)); await new Promise(resolve => setTimeout(resolve, 1000))
res.write("data: This is a test server continuous output 2\n") res.write("This is a test server continuous output 2\n")
await new Promise(resolve => setTimeout(resolve, 2000)); await new Promise(resolve => setTimeout(resolve, 1000))
res.write("data: This is a test server continuous output 3 but it's a very long line intended to force wrapping of text because the length is so long and the girth is so gorth\n") res.write("This is a test server continuous output 3 but it's a very long line intended to force wrapping of text because the length is so long and the girth is so gorth\n")
await new Promise(resolve => setTimeout(resolve, 1000)); await new Promise(resolve => setTimeout(resolve, 1000))
res.write("data: This is a test server continuous output 4\nDone!") res.write("This is a test server continuous output 4\n")
await new Promise(resolve => setTimeout(resolve, 1000))
res.write("This is a test server continuous output 5 with a whole SHIT TON of text! Lorem ipsum ain't got nothing on this! Hell yeah! Let's add a lot\nof\n\nnewlines and ellipses and other garbage...\nthis and that...and the other!\nLet's keep pushing this down...WAY DOWN...\nDOWN\nDOWN\nDOWN...\n\n...")
await new Promise(resolve => setTimeout(resolve, 1000))
res.write("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean lacinia tristique porta. Integer luctus dui non augue egestas, vitae faucibus massa placerat. In ornare sodales risus quis faucibus. Cras viverra mauris vel neque sollicitudin pretium. Integer quis consectetur purus. Nulla sed accumsan tortor. Nulla felis eros, egestas quis eros ut, hendrerit aliquam ante. Nulla sagittis tortor nulla, eu consectetur tellus tempus eget. In hac habitasse platea dictumst. Mauris interdum cursus massa ac vestibulum. Morbi sodales justo sed feugiat consequat. Nunc purus nibh, faucibus id porttitor eget, dignissim eu purus. Duis efficitur varius libero vitae tristique. Mauris libero dolor, tempor at sagittis in, malesuada auctor sapien. Nulla eu accumsan odio. Phasellus dapibus dictum nulla ac feugiat.\n")
await new Promise(resolve => setTimeout(resolve, 500))
res.write("Pellentesque at massa vel eros auctor fringilla. Vestibulum at molestie augue. Proin dictum, tortor quis efficitur finibus, tortor nisi viverra libero, eget placerat lectus dolor a felis. Pellentesque vitae felis vulputate enim feugiat rutrum. Pellentesque auctor tempor eros sed consectetur. Integer id pellentesque massa, quis suscipit nisi. Fusce tempor cursus nulla nec imperdiet. Phasellus sodales iaculis eros, sed auctor lacus elementum vitae. Sed efficitur condimentum risus. Cras varius risus at quam condimentum, vitae cursus leo facilisis. Suspendisse pellentesque erat leo, a cursus augue blandit sed. Aliquam quis nibh vel sapien pulvinar feugiat quis eu diam. Pellentesque ullamcorper vestibulum leo non imperdiet.\n")
await new Promise(resolve => setTimeout(resolve, 500))
res.write("In et nulla risus. Fusce luctus ligula vitae velit lacinia egestas. Nullam semper, nisl vel ultrices semper, magna sem vestibulum ipsum, in pulvinar elit diam ac odio. Etiam id laoreet odio, a vehicula est. Sed luctus lobortis sollicitudin. Morbi hendrerit erat vel lacus pellentesque, eget pretium nisi faucibus. Nunc a orci sed mauris commodo cursus. Morbi at ipsum fermentum, placerat felis at, porta felis. Pellentesque sit amet sollicitudin est, aliquet consequat tortor. Cras efficitur egestas pulvinar. Morbi ultrices, ligula ac luctus ullamcorper, risus metus hendrerit eros, et ultricies diam justo eget lorem. Duis varius pulvinar nulla a luctus. Curabitur sed quam cursus risus pellentesque dignissim id vel arcu.\n")
await new Promise(resolve => setTimeout(resolve, 500))
res.write("This is a test server continuous output 5 with a whole SHIT TON of text! Lorem ipsum ain't got nothing on this! Hell yeah! Let's add a lot\nof\n\nnewlines and ellipses and other garbage...\nthis and that...and the other!\nLet's keep pushing this down...WAY DOWN...\nDOWN\nDOWN\nDOWN...\n\n...\n\nDone!")
res.end() res.end()
} else { } else {
res.send("Unknown test type") res.send("Unknown test type")
} }
}) })
// Update base files
app.get('/updatebase', (req, res) => {
res.send("Base files were updated")
})
// Update mods
app.get('/updatemods', (req, res) => {
res.send("Mod files were updated")
})
ViteExpress.listen(app, config.port, () => ViteExpress.listen(app, config.port, () =>
console.log(`Server is listening on port ${config.port}`) console.log(`Server is listening on port ${config.port}`)
) )