Browse Source

some usability improvment

mods_upload_support
nils 2 years ago
parent
commit
e025827b9c
11 changed files with 202 additions and 25 deletions
  1. +6
    -1
      Dockerfile
  2. +36
    -11
      backend/minecraft.js
  3. +59
    -3
      backend/routes.js
  4. +2
    -0
      backend/server.js
  5. +26
    -2
      frontend/css/index.css
  6. BIN
      frontend/img/enchanted-book-big.png
  7. BIN
      frontend/img/enchanted-book-big.png~
  8. +29
    -4
      frontend/index.html
  9. +29
    -1
      frontend/js/index.js
  10. +2
    -1
      package.json
  11. +13
    -2
      start.sh

+ 6
- 1
Dockerfile View File

@ -3,9 +3,14 @@ FROM cloudron/base:1.0.0
RUN mkdir -p /app/code
WORKDIR /app/code
RUN apt-get update && apt-get install -y openjdk-11-jre-headless
RUN apt-get update && apt-get install -y openjdk-8-jre-headless
# COPY minecraft_server.1.14.4.jar /app/code
# COPY minecraft_server.jar /app/code
# https://www.minecraft.net/de-de/download/server/
# RUN curl -L https://cloud.3es.io/s/gNExpMCNS8DTT8y/download -o minecraft_server.jar
RUN curl -L https://launcher.mojang.com/v1/objects/d0d0fe2b1dc6ab4c65554cb734270872b72dadd6/server.jar -o minecraft_server.jar
COPY frontend /app/code/frontend


+ 36
- 11
backend/minecraft.js View File

@ -1,37 +1,53 @@
'use strict';
var path = require('path'),
byline = require('byline');
byline = require('byline'),
fs = require('fs');
module.exports = exports = {
status: status,
start: start,
stop: stop,
command: command,
addLogListener: addLogListener
addLogListener: addLogListener,
getInstancesConfig: getInstancesConfig,
saveInstancesConfig: saveInstancesConfig,
activeInstance: activeInstance,
selectedInstance: selectedInstance,
};
var minecraft = null;
var logLineStream = null;
var logListeners = [];
var activeInstance = 'default';
var selectedInstance = 'default';
const baseDir = process.env.CLOUDRON ? '/app/data' : path.join(__dirname, '..')
function status() {
return { running: !!minecraft };
return { running: !!minecraft , 'activeInstance': activeInstance, 'selectedInstance': selectedInstance };
}
function start() {
console.log('start minecraft server');
var opts = { cwd: path.join(__dirname, '..') };
if (process.env.CLOUDRON) opts.cwd = '/app/data';
minecraft = require('child_process').spawn('java', ['-Xmx1024M', '-Xms1024M', '-jar', path.join(__dirname, '../minecraft_server.jar'), 'nogui'], opts);
console.log('start minecraft server');
var instancesConf = getInstancesConfig()
var instanceFolder = instancesConf.activeInstance == 'default' ? path.join(__dirname, '../') : path.join(baseDir, '/instances/', instancesConf.activeInstance)
activeInstance = instancesConf.activeInstance
selectedInstance = instancesConf.activeInstance
var opts = instancesConf.activeInstance == 'default' ? { cwd: path.join(__dirname, '../') } : { cwd: instanceFolder };
if (process.env.CLOUDRON && instancesConf.activeInstance == 'default' ) opts.cwd = '/app/data';
minecraft = require('child_process').spawn('java', ['-Xmx2048M', '-Xms2048M', '-jar', path.join(instanceFolder, 'minecraft_server.jar'), 'nogui'], opts);
activeInstance
logLineStream = byline(minecraft.stdout);
logLineStream.on('data', function (line) {
console.log(line.toString()); // also log to stdout
logListeners.forEach(function (l) {
l.emit('line', line.toString().split('[Server thread/INFO]:')[1]);
l.emit('line', line.toString());
});
});
@ -65,4 +81,13 @@ function addLogListener(socket) {
logListeners.push(socket);
}
function getInstancesConfig(){
var rawData = fs.readFileSync(path.join(baseDir, '/instances.json'))
return JSON.parse(rawData)
}
function saveInstancesConfig(conf){
selectedInstance = conf.activeInstance
var textData = JSON.stringify(conf);
fs.writeFileSync(path.join(baseDir, '/instances.json'), textData);
}

+ 59
- 3
backend/routes.js View File

@ -8,7 +8,8 @@ var assert = require('assert'),
minecraft = require('./minecraft.js'),
lastMile = require('connect-lastmile'),
HttpError = lastMile.HttpError,
HttpSuccess = lastMile.HttpSuccess;
HttpSuccess = lastMile.HttpSuccess,
sys = require('systeminformation') ;
module.exports = exports = {
healthcheck: healthcheck,
@ -28,6 +29,10 @@ module.exports = exports = {
list: modsList,
upload: modsUpload,
delete: modsDelete,
},
instances: {
list: instancesList,
setactive: instancesSetActive
}
};
@ -38,7 +43,7 @@ const LOCAL_AUTH_FILE = path.resolve('users.json');
const baseDir = process.env.CLOUDRON ? '/app/data' : path.join(__dirname, '..')
const configFilePath = path.join(baseDir, 'server.properties');
const modsFolderPath = path.join(baseDir, 'mods');
const instancesFolderPath = path.join(baseDir, 'instances');
var users = {};
@ -77,7 +82,9 @@ function auth(req, res, next) {
}
if (AUTH_METHOD === 'ldap') {
var ldapClient = ldapjs.createClient({ url: LDAP_URL });
var url = process.env.CLOUDRON_LDAP_URL
console.log('ldapurl : '+url)
var ldapClient = ldapjs.createClient({ url: url });
ldapClient.on('error', function (error) {
console.error('LDAP error', error);
});
@ -162,7 +169,11 @@ function command(req, res, next) {
});
}
// mods
function modsList(req, res, next) {
var conf = minecraft.getInstancesConfig();
var modsFolderPath = conf.activeInstance == "default" ? path.join(baseDir, '/mods') : path.join(instancesFolderPath, conf.activeInstance, '/mods');
fs.readdir(modsFolderPath, (error, files) => {
if (error) return next(new HttpError(500, 'can\'t read mods folder'));
@ -184,6 +195,9 @@ function modsList(req, res, next) {
function modsUpload(req, res, next) {
req.pipe(req.busboy);
var conf = minecraft.getInstancesConfig();
var modsFolderPath = conf.activeInstance == "default" ? path.join(baseDir, '/mods') : path.join(instancesFolderPath, conf.activeInstance, '/mods');
req.busboy.on('file', (fieldname, file, filename) => {
console.log(`Upload of '${filename}' started`);
const fstream = fs.createWriteStream(path.join(modsFolderPath, filename));
@ -196,6 +210,9 @@ function modsUpload(req, res, next) {
}
function modsDelete(req, res, next) {
var conf = minecraft.getInstancesConfig();
var modsFolderPath = conf.activeInstance == "default" ? path.join(baseDir, '/mods') : path.join(instancesFolderPath, conf.activeInstance, '/mods');
fs.unlink(path.join(modsFolderPath, req.body.fileName), (err) => {
if (err) return next(new HttpError(500, 'error deleting '+req.body.fileName));
next(new HttpSuccess(202, {}));
@ -203,3 +220,42 @@ function modsDelete(req, res, next) {
}
// instances
function instancesList(req, res, next) {
fs.readdir(instancesFolderPath, (error, files) => {
if (error) return next(new HttpError(500, 'can\'t read mods folder'));
var _files = files.map(f => {
return {icon : '/img/'+getIconByFilName(f), name: f}
})
next(new HttpSuccess(202, {files : _files}))
})
}
function instancesSetActive(req, res, next) {
var conf = minecraft.getInstancesConfig()
conf.activeInstance = req.body.dirName
minecraft.saveInstancesConfig(conf)
minecraft.activeInstance = req.body.dirName
next(new HttpSuccess(202, {files : []}))
}
// utils
function getIconByFilName(fileName){
var iconName = null
switch (path.extname(fileName)) {
case '.zip' : iconName = 'command-block.png'; break;
case undefined : iconName = 'book.png'; break;
default : iconName = 'paper.png'; break;
}
return iconName
}

+ 2
- 0
backend/server.js View File

@ -42,6 +42,8 @@ function start(port, callback) {
router.get('/api/v1/listmods', routes.auth, routes.mods.list );
router.post('/api/v1/uploadmod', routes.auth, routes.mods.upload );
router.post('/api/v1/deletemod', routes.auth, routes.mods.delete);
router.get('/api/v1/listinstances', routes.auth, routes.instances.list);
router.post('/api/v1/setactiveinstance', routes.auth, routes.instances.setactive)
// for log lines
io.on('connection', function (socket) {


+ 26
- 2
frontend/css/index.css View File

@ -63,7 +63,11 @@ footer {
color: greenyellow;
}
button {
.standby {
color: grey;
}
button, .inputfile + label {
height: 40px;
min-width: 200px;
outline: 2px solid black;
@ -86,7 +90,8 @@ button {
cursor: pointer;
}
button:hover {
button:hover, .inputfile:focus + label,
.inputfile + label:hover {
border-bottom: 4px solid #59639A;
border-right: 2px solid #59639A;
border-left: 2px solid #BDC6FF;
@ -162,4 +167,23 @@ textarea {
.file-icon{
width : 25px;
height: 25px;
}
.file{
display: inline-block;
}
.inputfile {
width: 0.1px;
height: 0.1px;
opacity: 0;
overflow: hidden;
position: absolute;
z-index: -1;
}
.files table{
width: 100%;
}
.files td{
padding: 10px
}

BIN
frontend/img/enchanted-book-big.png View File

Before After
Width: 128  |  Height: 128  |  Size: 20 KiB

BIN
frontend/img/enchanted-book-big.png~ View File

Before After
Width: 128  |  Height: 128  |  Size: 24 KiB

+ 29
- 4
frontend/index.html View File

@ -22,9 +22,14 @@
<div v-if="profile" class="container">
<p>
Server Status: <span style="padding-left: 20px;" :class="{ active: status.running, inactive: !status.running }">{{ status.running ? 'Running' : 'Stopped' }}</span>
<span style="padding-left: 20px;">Instance:</span> <span class="active" style="padding-left: 20px;">{{ instances.activeInstance }}</span>
<button v-show="!status.running" class="pull-right" @click="start()">Start</button>
<button v-show="status.running" class="pull-right" @click="stop()">Stop</button>
</p>
<p style="font-size: 14px; line-height: 25px;" class="standby" v-show="instances.activeInstance !== instances.selectedInstance">
Selected instance : <span style="padding-left: 20px;">{{ instances.selectedInstance }}</span> <br />
This instance will be deployed on next server restart.
</p>
<!-- <p>Settings:</p>
<textarea v-model="settings"></textarea>
<button>Save</button> -->
@ -32,6 +37,7 @@
<div class="tab-container">
<button class="tab" @click="setTab('terminal')">terminal</button>
<button class="tab" @click="setTab('mods')">mods</button>
<button class="tab" @click="setTab('instances')">instances</button>
</div>
<template v-if="tab === 'terminal'">
@ -47,20 +53,21 @@
<div class="files">
<table>
<tr class="file" v-for="mod in mods.files">
<tr v-for="mod in mods.files">
<td >
<img class="file-icon" :src="mod.icon"/>
</td><td>
{{ mod.name }}
</td><td>
<img src="/img/tnt.png" @click="deleteMod(mod.name)"/>
<img src="/img/tnt.png" @click="deleteMod(mod.name)" title="delete mod"/>
</td>
</tr>
</table>
<div class="file-upload">
<form enctype="multipart/form-data" novalidate v-if="!mods.isUploading">
<input type="file" class="custom-file-input" @change="uploadMod" multiple>
<input type="file" class="inputfile" name="modsinput" id="modsinput" @change="uploadMod" multiple>
<label for="modsinput">Upload mods</label>
</form>
<p v-if="mods.isUploading">your mods are being uploaded... pending mods : {{ mods.submittedFileCount }} </p>
</div>
@ -68,8 +75,26 @@
</template>
<template v-else-if="tab == 'instances'">
<div class="files">
<div style="width: 70px; padding: 10px;" @click="setActiveInstance('default')" class="file" :class="{ active: instances.selectedInstance == 'default', standby: instances.selectedInstance != 'default' }">
<img style="display: block; margin: auto; width: 70px; height: 70px" src="/img/enchanted-book-big.png" >
default
</div>
<div style="width: 70px; padding: 10px;" v-for="instance in instances.files" @click="setActiveInstance(instance.name)" class="file" :class="{ active: instances.selectedInstance == instance.name, standby: instances.selectedInstance != instance.name }">
<img style="display: block; margin: auto; width: 70px; height: 70px" src="/img/enchanted-book-big.png" >
{{ instance.name }}
</div>
</div>
</template>
<template v-else-if="tab === datapacks">
<template v-else-if="tab === 'datapacks'">
</template>


+ 29
- 1
frontend/js/index.js View File

@ -23,6 +23,11 @@ new Vue({
submittedFileCount : [],
isUploading : false,
},
instances: {
files: [],
activeInstance: null,
selectedInstance: null,
}
},
methods: {
onError: function (error) {
@ -40,7 +45,9 @@ new Vue({
if (error) return that.onError(error);
if (result.statusCode !== 200) return that.onError('Unexpected response: ' + result.statusCode + ' ' + result.text);
that.status = result.body.status;
that.status = result.body.status;
that.instances.activeInstance = result.body.status.activeInstance;
that.instances.selectedInstance = result.body.status.selectedInstance
setTimeout(that.refresh, 5000);
});
@ -133,6 +140,7 @@ new Vue({
switch (tab) {
case 'terminal' : that.tab = 'terminal'; break;
case 'mods' : that.tab = 'mods'; that.listMods(); break;
case 'instances' : that.tab = 'instances'; that.listInstances(); break;
default : return that.onError('unexpected tab'); break;
}
},
@ -176,6 +184,26 @@ new Vue({
that.listMods()
});
},
listInstances: function () {
var that = this;
superagent.get('/api/v1/listinstances').auth(this.login.username, this.login.password).end(function (error, result) {
if (error) return that.onError(error);
if (result.statusCode !== 202) return that.onError('Unexpected response: ' + result.statusCode + ' ' + result.text);
that.instances.files = result.body.files
});
},
setActiveInstance: function(dirName) {
var that = this;
superagent.post('/api/v1/setactiveinstance').send({ dirName: dirName }).auth(this.login.username, this.login.password).end(function (error, result) {
if (error) return that.onError(error);
if (result.statusCode !== 202) return that.onError('Unexpected response: ' + result.statusCode + ' ' + result.text);
that.instances.selectedInstance = dirName;
});
}
},


+ 2
- 1
package.json View File

@ -26,6 +26,7 @@
"socket.io": "^2.2.0",
"supererror": "^0.7.2",
"busboy": "^0.3.1",
"connect-busboy": "^0.0.2"
"connect-busboy": "^0.0.2",
"systeminformation": "^4.14.3"
}
}

+ 13
- 2
start.sh View File

@ -8,11 +8,22 @@ mkdir -p /app/data/
echo "=> creating mods folder"
mkdir -p /app/data/mods/
echo "=> creating instances folder"
mkdir -p /app/data/instances/
echo "=> Configuring instance"
if ! [ -f /app/data/instances.json ]; then
echo "Fresh installation, setting up default instance..."
touch /app/data/instances.json
echo "{\"activeInstance\": \"default\"}" > /app/data/instances.json
echo "Done."
fi
echo "=> Accept EULA"
echo "eula=true" > /app/data/eula.txt
echo "=> Ensure permissions"
chown -R cloudron:cloudron /app/data
chown -R cloudron:cloudron /app/data /app/data/mods /app/data/instances /app/data/instances.json
chmod -R ugo+rwx /app/data/instances
echo "=> Starting management server"
exec /usr/local/bin/gosu cloudron:cloudron node /app/code/index.js

Loading…
Cancel
Save