Arma Reforger support

This commit is contained in:
Björn Dahlgren
2022-05-30 01:23:37 +02:00
parent b851139219
commit 323ee14f72
33 changed files with 796 additions and 24 deletions

View File

@ -35,6 +35,7 @@ A simple to use web admin panel for Arma servers.
- cwa (does not support linux)
- ofp
- ofpresistance
- reforger
## Config

10
app.js
View File

@ -1,5 +1,6 @@
var express = require('express')
var bodyParser = require('body-parser')
var fs = require('fs.extra')
var morgan = require('morgan')
var path = require('path')
var serveStatic = require('serve-static')
@ -15,6 +16,15 @@ var Mods = require('./lib/mods')
var Logs = require('./lib/logs')
var Settings = require('./lib/settings')
if (config.game === 'reforger') {
Logs = require('./lib/reforger/logs')
Manager = require('./lib/reforger/manager')
Missions = require('./lib/reforger/missions')
Mods = require('./lib/reforger/mods')
fs.mkdirp(config.reforger.profiles) // Needs to be created in advance or game will store profiles elsewhere
}
var app = express()
var server = require('http').Server(app)
var io = require('socket.io')(server)

View File

@ -1,8 +1,17 @@
for (var environmentVariable of ['GAME_TYPE', 'GAME_PATH']) {
['GAME_TYPE', 'GAME_PATH'].forEach(function (environmentVariable) {
if (!process.env[environmentVariable]) {
console.log('Missing required environment variable "' + environmentVariable + '"')
process.exit(1)
}
})
if (process.env.GAME_TYPE === 'reforger') {
['REFORGER_CONFIGS', 'REFORGER_PROFILES', 'REFORGER_REGION', 'REFORGER_WORKSHOP'].forEach(function (environmentVariable) {
if (!process.env[environmentVariable]) {
console.log('Missing required environment variable "' + environmentVariable + '"')
process.exit(1)
}
})
}
module.exports = {
@ -19,6 +28,12 @@ module.exports = {
username: process.env.AUTH_USERNAME,
password: process.env.AUTH_PROCESS
},
reforger: {
configs: process.env.REFORGER_CONFIGS,
profiles: process.env.REFORGER_PROFILES,
region: process.env.REFORGER_REGION,
workshop: process.env.REFORGER_WORKSHOP
},
prefix: process.env.SERVER_PREFIX,
suffix: process.env.SERVER_SUFFIX,
logFormat: process.env.LOG_FORMAT || 'dev'

View File

@ -1,5 +1,5 @@
module.exports = {
game: 'arma3', // arma3, arma2oa, arma2, arma1, cwa, ofpresistance, ofp
game: 'arma3', // arma3, arma2oa, arma2, arma1, cwa, ofpresistance, ofp, reforger
path: 'path-to-arma3-directory',
port: 3000,
host: '0.0.0.0', // Can be either an IP or a Hostname
@ -9,6 +9,12 @@ module.exports = {
'-noSound',
'-world=empty'
],
reforger: {
configs: 'path-to-configs-folder', // Folder where server config files will be stored
profiles: 'path-to-profiles-folder', // Folder where logs and persistence data will be stored. Each server will create a subfolder.
region: 'EU', // Region in which servers will be hosted, https://community.bistudio.com/wiki/Arma_Reforger:Server_Hosting#region
workshop: 'path-to-workshop-folder' // Folder where workshop mods will be stored
},
serverMods: [ // Mods used exclusively by server and not shared with clients
'@mod1',
'@mod2',

View File

@ -70,6 +70,7 @@ Mods.prototype.resolveModData = function (modPath, cb) {
}
cb(null, {
id: modPath,
name: modPath,
size: results.folderSize,
formattedSize: filesize(results.folderSize),

99
lib/reforger/logs.js Normal file
View File

@ -0,0 +1,99 @@
var async = require('async')
var filesize = require('filesize')
var fs = require('fs.extra')
var glob = require('glob')
var path = require('path')
var Logs = function (config) {
this.config = config
}
Logs.prototype.delete = function (filename, callback) {
callback = callback || function () {}
this.getLogFile(filename, function (err, logFile) {
if (err) {
return callback(err)
} else {
if (logFile && logFile.path) {
fs.unlink(logFile.path, callback)
} else {
return callback(new Error('File not found'))
}
}
})
}
Logs.prototype.logsPath = function () {
return this.config.reforger.profiles
}
Logs.prototype.logFiles = function (callback) {
var directory = this.logsPath()
if (directory === null) {
return callback(null, [])
}
glob('**/*.log', { cwd: directory }, function (err, files) {
if (err) {
callback(err)
return
}
files = files.map(function (file) {
return {
name: file,
path: path.join(directory, file)
}
})
async.filter(files, function (file, cb) {
fs.stat(file.path, function (err, stat) {
if (err) {
return cb(err)
}
file.created = stat.birthtime.toISOString()
file.modified = stat.mtime.toISOString()
file.formattedSize = filesize(stat.size)
file.size = stat.size
cb(null, stat.isFile())
})
}, function (err, files) {
if (err) {
return callback(err)
}
files.sort(function (a, b) {
return b.created.localeCompare(a.created) // Descending order
})
callback(null, files)
})
})
}
Logs.prototype.getLogFile = function (filename, callback) {
this.logFiles(function (err, files) {
if (err) {
callback(err)
} else {
var validLogs = files.filter(function (file) {
return file.name === filename
})
if (validLogs.length > 0) {
callback(null, validLogs[0])
} else {
callback(null, null)
}
}
})
}
Logs.prototype.readLogFile = function (filename, callback) {
fs.readFile(filename, callback)
}
module.exports = Logs

132
lib/reforger/manager.js Normal file
View File

@ -0,0 +1,132 @@
var events = require('events')
var fs = require('fs')
var Server = require('./server')
var filePath = 'servers.json'
var Manager = function (config, logs) {
this.config = config
this.logs = logs
this.serversArr = []
this.serversHash = {}
}
Manager.prototype = new events.EventEmitter()
Manager.prototype.addServer = function (options) {
var server = this._addServer(options)
this.save()
return server
}
Manager.prototype.removeServer = function (id) {
var server = this.serversHash[id]
if (!server) {
return {}
}
var index = this.serversArr.indexOf(server)
if (index > -1) {
this.serversArr.splice(index, 1)
}
this.save()
if (server.pid) {
server.stop()
}
return server
}
Manager.prototype._addServer = function (data) {
var server = new Server(this.config, this.logs, data)
this.serversArr.push(server)
this.serversArr.sort(function (a, b) {
return a.title.localeCompare(b.title)
})
this.serversHash[server.id] = server
var self = this
var save = function () {
self.save()
}
var statusChanged = function () {
self.emit('servers')
}
server.on('save', save)
server.on('state', statusChanged)
return server
}
Manager.prototype.getServer = function (id) {
return this.serversHash[id]
}
Manager.prototype.getServers = function () {
return this.serversArr
}
Manager.prototype.load = function () {
var self = this
fs.readFile(filePath, function (err, data) {
if (err) {
console.log('Could not load any existing servers configuration, starting fresh')
return
}
try {
JSON.parse(data).forEach(function (server) {
self._addServer(server)
})
} catch (e) {
console.error('Manager load error: ' + e)
}
self.getServers().map(function (server) {
if (server.auto_start) {
server.start()
}
})
})
}
Manager.prototype.save = function () {
var data = []
var self = this
this.serversArr.sort(function (a, b) {
return a.title.toLowerCase().localeCompare(b.title.toLowerCase())
})
this.serversHash = {}
this.serversArr.forEach(function (server) {
data.push({
admin_password: server.admin_password,
auto_start: server.auto_start,
battle_eye: server.battle_eye,
dedicatedServerId: server.dedicatedServerId,
max_players: server.max_players,
missions: server.missions,
mods: server.mods,
password: server.password,
port: server.port,
title: server.title
})
self.serversHash[server.id] = server
})
fs.writeFile(filePath, JSON.stringify(data), function (err) {
if (err) {
console.error('Manager save error: ' + err)
} else {
self.emit('servers')
}
})
}
module.exports = Manager

93
lib/reforger/missions.js Normal file
View File

@ -0,0 +1,93 @@
var async = require('async')
var events = require('events')
var fs = require('fs.extra')
var glob = require('glob')
var path = require('path')
var stripBOM = require('./stripBOM')
var Missions = function (config) {
this.config = config
this.missions = []
this.updateMissions()
}
Missions.prototype = new events.EventEmitter()
Missions.prototype.workshopPath = function () {
return this.config.reforger.workshop
}
Missions.prototype.updateMissions = function (cb) {
var self = this
var workshopFolder = this.workshopPath()
glob('**/ServerData.json', { cwd: workshopFolder }, function (err, files) {
if (err) {
console.log(err)
return
}
async.map(files, function (filename, cb) {
var serverDataFile = path.join(workshopFolder, filename)
fs.readFile(serverDataFile, 'utf-8', function (err, data) {
if (err) {
console.log(err)
return cb(err)
}
try {
var serverData = JSON.parse(stripBOM(data))
fs.stat(serverDataFile, function (err, stat) {
if (err) {
console.log(err)
return cb(err)
}
var missions = serverData.revision.scenarios.map(function (scenario) {
return {
dateCreated: new Date(stat.ctime),
dateModified: new Date(stat.mtime),
missionName: scenario.name,
name: scenario.gameId,
size: 0,
sizeFormatted: '',
worldName: ''
}
})
cb(null, missions)
})
} catch (err) {
console.log(err)
return cb(err)
}
})
}, function (err, missions) {
if (!err) {
missions = missions.flat()
self.missions = missions
self.emit('missions', missions)
}
if (cb) {
cb(err, missions)
}
})
})
}
Missions.prototype.handleUpload = function (uploadedFile, cb) {
cb(new Error('Not implemented'))
}
Missions.prototype.delete = function (missionName, cb) {
cb(new Error('Not implemented'))
}
Missions.prototype.downloadSteamWorkshop = function (id, cb) {
cb(new Error('Not implemented'))
}
module.exports = Missions

78
lib/reforger/mods.js Normal file
View File

@ -0,0 +1,78 @@
var async = require('async')
var events = require('events')
var fs = require('fs.extra')
var filesize = require('filesize')
var glob = require('glob')
var path = require('path')
var stripBOM = require('./stripBOM')
var Mods = function (config) {
this.config = config
this.mods = []
}
Mods.prototype = new events.EventEmitter()
Mods.prototype.workshopPath = function () {
return this.config.reforger.workshop
}
Mods.prototype.delete = function (mod, cb) {
var self = this
var workshopFolder = this.workshopPath()
fs.rmrf(path.join(workshopFolder, mod), function (err) {
cb(err)
if (!err) {
self.updateMods()
}
})
}
Mods.prototype.updateMods = function () {
var self = this
var workshopFolder = this.workshopPath()
glob('**/ServerData.json', { cwd: workshopFolder }, function (err, files) {
if (err) {
console.log(err)
return
}
async.map(files, function (filename, cb) {
var serverDataFile = path.join(workshopFolder, filename)
fs.readFile(serverDataFile, 'utf-8', function (err, data) {
if (err) {
console.log(err)
return cb(err)
}
try {
var serverData = JSON.parse(stripBOM(data))
var mod = {
id: serverData.id,
name: serverData.name,
size: 0,
formattedSize: filesize(0)
}
cb(null, mod)
} catch (err) {
console.log(err)
return cb(err)
}
})
}, function (err, mods) {
if (err) {
console.log(err)
return
}
self.mods = mods
self.emit('mods', mods)
})
})
}
module.exports = Mods

281
lib/reforger/server.js Normal file
View File

@ -0,0 +1,281 @@
var childProcess = require('child_process')
var events = require('events')
var fs = require('fs.extra')
var Gamedig = require('gamedig')
var path = require('path')
var publicIp = require('public-ip')
var slugify = require('slugify')
var queryInterval = 5000
var Server = function (config, logs, options) {
this.config = config
this.logs = logs
this.update(options)
}
Server.prototype = new events.EventEmitter()
Server.prototype.createServerTitle = function (title) {
if (this.config.prefix) {
title = this.config.prefix + title
}
if (this.config.suffix) {
title = title + this.config.suffix
}
return title
}
Server.prototype.generateId = function () {
return slugify(this.title).replace(/\./g, '-')
}
Server.prototype.update = function (options) {
this.admin_password = options.admin_password
this.auto_start = options.auto_start
this.battle_eye = options.battle_eye
this.dedicatedServerId = options.dedicatedServerId
this.max_players = options.max_players
this.missions = options.missions
this.mods = options.mods || []
this.password = options.password
this.port = options.port || 2001
this.title = options.title
this.id = this.generateId()
this.port = parseInt(this.port, 10) // If port is a string then gamedig fails
}
Server.prototype.steamQueryPort = function () {
return 10000 + this.port
}
Server.prototype.queryStatus = function () {
if (!this.instance) {
return
}
var self = this
Gamedig.query(
{
type: 'arma3',
host: self.ip,
port: self.steamQueryPort() - 1
},
function (state) {
if (!self.instance) {
return
}
if (state.error) {
self.state = null
} else {
self.state = state
}
self.emit('state')
}
)
}
Server.prototype.makeServerConfig = function () {
var scenarioId = '{59AD59368755F41A}Missions/21_GM_Eden.conf'
if (this.missions && this.missions.length > 0) {
scenarioId = this.missions[0].name
}
return {
dedicatedServerId: this.dedicatedServerId,
region: this.config.reforger.region,
gameHostBindAddress: '',
gameHostBindPort: this.port,
gameHostRegisterBindAddress: this.ip,
gameHostRegisterPort: this.port,
adminPassword: this.admin_password,
game: {
name: this.createServerTitle(this.title),
password: this.password,
scenarioId: scenarioId,
playerCountLimit: this.max_players,
autoJoinable: false,
visible: true,
gameProperties: {
serverMaxViewDistance: 2500,
serverMinGrassDistance: 50,
networkViewDistance: 1000,
disableThirdPerson: true,
fastValidation: true,
battlEye: this.battle_eye
},
mods: this.mods.map(function (mod) {
return {
modId: mod
}
})
},
a2sQueryEnabled: true,
steamQueryPort: this.steamQueryPort()
}
}
Server.prototype.serverConfigDirectory = function () {
return this.config.reforger.configs
}
Server.prototype.serverConfigFile = function () {
return path.join(this.serverConfigDirectory(), this.generateId() + '.json')
}
Server.prototype.readServerConfig = function (cb) {
var self = this
fs.readFile(self.serverConfigFile(), 'utf8', function (err, data) {
if (err) {
return cb(err)
}
try {
var serverConfig = JSON.parse(data)
cb(null, serverConfig)
} catch (err) {
cb(err)
}
})
}
Server.prototype.saveServerConfig = function (config, cb) {
var self = this
fs.mkdirp(self.serverConfigDirectory(), function (err) {
if (err) {
return cb(err)
}
fs.writeFile(self.serverConfigFile(), JSON.stringify(config), cb)
})
}
Server.prototype.serverBinary = function () {
return path.join(this.config.path, 'ArmaReforgerServer')
}
Server.prototype.serverArguments = function () {
var self = this
var id = self.generateId()
return [
'-addonsDir',
this.config.reforger.workshop,
'-addonDownloadDir',
this.config.reforger.workshop,
'-config',
this.serverConfigFile(),
'-profile',
path.join(this.config.reforger.profiles, id)
].map(function (argument) {
if (self.config.type === 'windows') {
return argument.replace(/\//g, '\\')
}
return argument
})
}
Server.prototype.start = function () {
if (this.instance) {
return this
}
var self = this
publicIp.v4().then(function (ip) {
self.ip = ip
var config = self.makeServerConfig()
self.saveServerConfig(config, function (err) {
if (err) {
console.log(err)
return
}
var instance = childProcess.spawn(self.serverBinary(), self.serverArguments(), { cwd: self.config.path })
instance.on('error', function (err) {
console.error('Failed to start server', self.title, err)
})
instance.on('close', function (code) {
clearInterval(self.queryStatusInterval)
self.state = null
self.pid = null
self.instance = null
self.emit('state')
})
self.pid = instance.pid
self.instance = instance
self.headlessClientInstances = []
self.queryStatusInterval = setInterval(function () {
self.queryStatus()
}, queryInterval)
self.emit('state')
})
})
return this
}
Server.prototype.stop = function (cb) {
var handled = false
var self = this
var finalHandler = function () {
if (!handled) {
handled = true
self.readServerConfig(function (err, serverConfig) {
if (err) {
console.log(err)
return
}
if (self.dedicatedServerId !== serverConfig.dedicatedServerId) {
self.dedicatedServerId = serverConfig.dedicatedServerId
self.emit('save')
}
})
if (cb) {
cb()
}
}
}
this.instance.on('close', finalHandler)
this.instance.kill()
setTimeout(finalHandler, 5000)
return this
}
Server.prototype.toJSON = function () {
return {
admin_password: this.admin_password,
auto_start: this.auto_start,
battle_eye: this.battle_eye,
dedicatedServerId: this.dedicatedServerId,
id: this.id,
max_players: this.max_players,
missions: this.missions,
mods: this.mods,
password: this.password,
pid: this.pid,
port: this.port,
state: this.state,
title: this.title
}
}
module.exports = Server

7
lib/reforger/stripBOM.js Normal file
View File

@ -0,0 +1,7 @@
module.exports = function stripBOM (data) {
if (data.charCodeAt(0) === 0xFEFF) {
return data.slice(1)
}
return data
}

View File

@ -39,6 +39,7 @@
"lodash": "^4.17.10",
"morgan": "^1.8.1",
"multer": "^1.3.0",
"public-ip": "^4.0.4",
"raw-loader": "^0.5.1",
"serve-static": "^1.12.1",
"slugify": "^1.1.0",

View File

@ -4,7 +4,7 @@ var Mission = require('app/models/mission')
module.exports = Backbone.Collection.extend({
comparator: function (a, b) {
return a.get('name').toLowerCase().localeCompare(b.get('name').toLowerCase())
return a.get('missionName').toLowerCase().localeCompare(b.get('missionName').toLowerCase())
},
model: Mission,
url: '/api/missions/'

View File

@ -4,7 +4,7 @@ var ServerMod = require('app/models/server_mod')
module.exports = Backbone.Collection.extend({
comparator: function (a, b) {
return a.get('name').toLowerCase().localeCompare(b.get('name').toLowerCase())
return a.get('id').toLowerCase().localeCompare(b.get('id').toLowerCase())
},
model: ServerMod
})

View File

@ -2,8 +2,9 @@ var Backbone = require('backbone')
module.exports = Backbone.Model.extend({
defaults: {
id: '',
name: ''
},
idAttribute: 'name',
idAttribute: 'id',
urlRoot: '/api/mods/'
})

View File

@ -8,6 +8,7 @@ module.exports = Backbone.Model.extend({
allowed_file_patching: 1,
auto_start: false,
battle_eye: false,
dedicatedServerId: '',
file_patching: false,
forcedDifficulty: '',
max_players: null,
@ -17,7 +18,7 @@ module.exports = Backbone.Model.extend({
parameters: [],
password: '',
persistent: false,
port: 2302,
port: 2001,
state: null,
title: '',
von: false,

View File

@ -2,9 +2,10 @@ var Backbone = require('backbone')
module.exports = Backbone.Model.extend({
defaults: {
id: '',
name: ''
},
idAttribute: 'name',
idAttribute: 'id',
isNew: function () {
return true
}

View File

@ -16,6 +16,6 @@ module.exports = Marionette.CompositeView.extend({
},
filter: function (child, index, collection) {
return child.get('name').toLowerCase().indexOf(this.filterValue.toLowerCase()) >= 0
return child.get('missionName').toLowerCase().indexOf(this.filterValue.toLowerCase()) >= 0
}
})

View File

@ -16,8 +16,13 @@ module.exports = Marionette.CompositeView.extend({
},
filter: function (child, index, collection) {
var id = child.get('id').toLowerCase()
var name = child.get('name').toLowerCase()
if (id.indexOf(this.filterValue.toLowerCase()) >= 0) {
return true
}
if (name.indexOf(this.filterValue.toLowerCase()) >= 0) {
return true
}

View File

@ -35,6 +35,13 @@ module.exports = Marionette.ItemView.extend({
title = modFile.name
}
var id = this.model.get('id')
var name = this.model.get('name')
if (id !== name) {
title = name
link = 'https://reforger.armaplatform.com/workshop/' + id
}
return {
link: link,
title: title

View File

@ -7,6 +7,29 @@ var tpl = require('tpl/servers/info.html')
module.exports = Marionette.LayoutView.extend({
template: _.template(tpl),
templateHelpers: function () {
var self = this
var modNames = this.model.get('mods').map(function (modId) {
var mod = self.mods.find(function (mod) {
return mod.get('id') === modId
})
if (!mod) {
return modId
}
return mod.get('name')
})
return {
modNames: modNames.join(', ')
}
},
initialize: function (options) {
this.mods = options.mods
},
events: {
'click #start': 'start',
'click #stop': 'stop'

View File

@ -24,7 +24,7 @@ module.exports = Marionette.ItemView.extend({
clone: function (e) {
var title = this.model.get('title') + ' Clone'
var clone = this.model.clone()
clone.set({ id: null, title: title, auto_start: false })
clone.set({ id: null, dedicatedServerId: null, title: title, auto_start: false })
clone.save()
},

View File

@ -14,7 +14,7 @@ module.exports = Marionette.CompositeView.extend({
},
filter: function (child, index, collection) {
return child.get('name').toLowerCase().indexOf(this.filterValue.toLowerCase()) >= 0
return child.get('missionName').toLowerCase().indexOf(this.filterValue.toLowerCase()) >= 0
},
buildChildView: function (item, ChildViewType, childViewOptions) {

View File

@ -10,8 +10,8 @@ module.exports = ModListItemView.extend({
template: template,
templateHelpers: function () {
var superTemplateHelpers = ModListItemView.prototype.templateHelpers.call(this)
var name = this.model.get('name')
var modSelected = this.options.selectedModsCollection.get(name)
var id = this.model.get('id')
var modSelected = this.options.selectedModsCollection.get(id)
return Object.assign({}, superTemplateHelpers, {
isDisabledButton: function () {
@ -27,6 +27,7 @@ module.exports = ModListItemView.extend({
addMod: function (e) {
e.preventDefault()
this.options.selectedModsCollection.add(new ServerMod({
id: this.model.get('id'),
name: this.model.get('name')
}))
}

View File

@ -55,7 +55,7 @@ module.exports = ModsView.extend({
return this.model.get('mods')
.map(function (mod) {
return {
name: mod
id: mod
}
})
},
@ -69,7 +69,7 @@ module.exports = ModsView.extend({
return {
mods: this.selectedModsCollection.toJSON()
.map(function (mod) {
return mod.name
return mod.id
})
}
}

View File

@ -16,6 +16,6 @@ module.exports = Marionette.CompositeView.extend({
addMod: function (e) {
e.preventDefault()
this.collection.add(new ServerMod({ name: 'Unlisted' }))
this.collection.add(new ServerMod({ id: 'Unlisted' }))
}
})

View File

@ -12,7 +12,7 @@ module.exports = ModListItemView.extend({
events: {
'click button.delete': 'delete',
'change select#difficulty': 'changed',
'change input#name': 'changed'
'change input#id': 'changed'
},
changed: function (e) {

View File

@ -39,7 +39,7 @@ module.exports = Marionette.LayoutView.extend({
},
onRender: function () {
this.infoView.show(new InfoView({ model: this.model }))
this.infoView.show(new InfoView({ model: this.model, mods: this.mods }))
this.missionsView.show(new MissionsView({ missions: this.missions, model: this.model }))
this.modsView.show(new ModsView({ model: this.model, mods: this.mods, server: this.model }))
this.parametersView.show(new ParametersListView({ model: this.model }))

View File

@ -1,5 +1,5 @@
<td>
<%-name%>
<%-id%>
<% if (link) { %>
<a href='<%-link%>' target=_blank rel='noopener noreferrer'>
<%-title%>

View File

@ -25,6 +25,15 @@
</div>
</div>
<% if (typeof(dedicatedServerId) != "undefined" && dedicatedServerId) { %>
<div class="form-group">
<label class="col-sm-1 control-label">Server ID</label>
<div class="col-sm-11">
<p class="form-control-static"><%= dedicatedServerId %></p>
</div>
</div>
<% } %>
<div class="form-group">
<label class="col-sm-1 control-label">Port</label>
<div class="col-sm-11">
@ -35,7 +44,7 @@
<div class="form-group">
<label class="col-sm-1 control-label">Mods</label>
<div class="col-sm-11">
<p class="form-control-static"><%- mods.join(', ') %></p>
<p class="form-control-static"><%- modNames %></p>
</div>
</div>

View File

@ -1,5 +1,5 @@
<td>
<%-name%>
<%-id%>
<% if (link) { %>
<a href='<%-link%>' target=_blank rel='noopener noreferrer'>
<%-title%>

View File

@ -1,9 +1,9 @@
<td>
<form class="form-horizontal" role="form">
<div class="form-group">
<label for="name" class="col-sm-2 control-label">Name</label>
<label for="id" class="col-sm-2 control-label">ID</label>
<div class="col-sm-10">
<input type="text" class="form-control" id="name" placeholder="Name" value="<%- name %>">
<input type="text" class="form-control" id="id" placeholder="ID" value="<%- id %>">
</div>
</div>

View File

@ -13,7 +13,7 @@ module.exports = function (logsManager) {
})
})
router.delete('/:log', function (req, res) {
router.delete('/:log(*)', function (req, res) {
var filename = req.params.log
logsManager.delete(filename, function (err) {
if (err) {
@ -24,7 +24,7 @@ module.exports = function (logsManager) {
})
})
router.get('/:log/:mode', function (req, res) {
router.get('/:log(*)/:mode', function (req, res) {
var requestedFilename = req.params.log
var mode = req.params.mode === 'view' ? 'view' : 'download'