improved ui for config route, added error reporting to json responses

This commit is contained in:
majormjr 2016-04-24 19:07:00 -04:00
parent a77a442513
commit 96139c8cad
17 changed files with 619 additions and 103 deletions

View File

@ -15,12 +15,18 @@ This tool runs on a Factorio server and allows management of saves, mods and man
Run the server and specify the directory of your Factorio server installation and the interface to run the HTTP server on.
```
Usage of ./factorio-server-manager:
-bin string
Location of Factorio Server binary file (default "bin/x64/factorio")
-config string
Specify location of Factorio config.ini file (default "config/config.ini")
-dir string
Specify location of Factorio config directory. (default "./")
Specify location of Factorio directory. (default "./")
-host string
Specify IP for webserver to listen on. (default "0.0.0.0")
-max-upload int
Maximum filesize for uploaded files. (default 100000)
-port string
Specify a port for the server (default "8080")
Specify a port for the server. (default "8080")
Example:

View File

@ -72,7 +72,11 @@
var _SavesContent2 = _interopRequireDefault(_SavesContent);
var _Index = __webpack_require__(239);
var _ConfigContent = __webpack_require__(239);
var _ConfigContent2 = _interopRequireDefault(_ConfigContent);
var _Index = __webpack_require__(241);
var _Index2 = _interopRequireDefault(_Index);
@ -87,7 +91,8 @@
_react2.default.createElement(_reactRouter.IndexRoute, { component: _Index2.default }),
_react2.default.createElement(_reactRouter.Route, { path: '/mods', component: _ModsContent2.default }),
_react2.default.createElement(_reactRouter.Route, { path: '/logs', component: _LogsContent2.default }),
_react2.default.createElement(_reactRouter.Route, { path: '/saves', component: _SavesContent2.default })
_react2.default.createElement(_reactRouter.Route, { path: '/saves', component: _SavesContent2.default }),
_react2.default.createElement(_reactRouter.Route, { path: '/config', component: _ConfigContent2.default })
)
), document.getElementById('app'));
@ -25505,7 +25510,7 @@
{ className: 'wrapper' },
_react2.default.createElement(_Header2.default, null),
_react2.default.createElement(_Sidebar2.default, null),
this.props.children,
_react2.default.cloneElement(this.props.children, { message: "" }),
_react2.default.createElement(_Footer2.default, null),
_react2.default.createElement(_HiddenSidebar2.default, null)
);
@ -25794,6 +25799,21 @@
'Saves'
)
)
),
_react2.default.createElement(
'li',
null,
_react2.default.createElement(
_reactRouter.Link,
{ to: '/config', activeClassName: 'active' },
_react2.default.createElement('i', { className: 'fa fa-link' }),
' ',
_react2.default.createElement(
'span',
null,
'Configuration'
)
)
)
)
)
@ -25845,11 +25865,7 @@
return _react2.default.createElement(
"footer",
{ className: "main-footer" },
_react2.default.createElement(
"div",
{ className: "pull-right hidden-xs" },
"Anything you want!!!!!!"
),
_react2.default.createElement("div", { className: "pull-right hidden-xs" }),
_react2.default.createElement(
"strong",
null,
@ -26114,7 +26130,7 @@
url: "/api/mods/list",
dataType: "json",
success: function success(data) {
_this2.setState({ listMods: data.mods });
_this2.setState({ listMods: data.data.mods });
},
error: function error(xhr, status, err) {
console.log('api/mods/list', status, err.toString());
@ -26130,7 +26146,7 @@
url: "/api/mods/list/installed",
dataType: "json",
success: function success(data) {
_this3.setState({ installedMods: data });
_this3.setState({ installedMods: data.data });
},
error: function error(xhr, status, err) {
console.log('api/mods/list', status, err.toString());
@ -26146,7 +26162,7 @@
url: "/api/mods/toggle/" + modName,
dataType: "json",
success: function success(data) {
_this4.setState({ listMods: data.mods });
_this4.setState({ listMods: data.data.mods });
},
error: function error(xhr, status, err) {
console.log('api/mods/toggle', status, err.toString());
@ -26194,7 +26210,7 @@
),
_react2.default.createElement(
'section',
{ className: 'content', style: { height: "100%" } },
{ className: 'content' },
_react2.default.createElement(_InstalledMods2.default, _extends({}, this.state, {
loadInstalledModList: this.loadInstalledModList
})),
@ -26472,6 +26488,9 @@
}, {
key: 'uploadFile',
value: function uploadFile(e) {
var _this2 = this;
e.preventDefault();
var fd = new FormData();
fd.append('modfile', this.refs.file.files[0]);
@ -26482,11 +26501,13 @@
processData: false,
contentType: false,
success: function success(data) {
alert(data);
var response = JSON.parse(data);
console.log(response.success);
if (response.success === true) {
_this2.updateInstalledMods();
}
}
});
e.preventDefault();
this.updateInstalledMods();
}
}, {
key: 'removeMod',
@ -26502,7 +26523,7 @@
}, {
key: 'render',
value: function render() {
var _this2 = this;
var _this3 = this;
return _react2.default.createElement(
'div',
@ -26526,12 +26547,21 @@
),
_react2.default.createElement(
'form',
{ ref: 'uploadForm', className: 'form', encType: 'multipart/form-data' },
{ ref: 'uploadForm', className: 'form-inline', encType: 'multipart/form-data' },
_react2.default.createElement(
'fieldset',
null,
_react2.default.createElement('input', { className: 'btn btn-default', ref: 'file', type: 'file', name: 'modfile', id: 'modfile' }),
_react2.default.createElement('input', { type: 'button', ref: 'button', value: 'Upload', onClick: this.uploadFile })
'div',
{ className: 'form-group' },
_react2.default.createElement(
'label',
{ 'for': 'modfile' },
'Upload Mod File...'
),
_react2.default.createElement('input', { className: 'form-control btn btn-default', ref: 'file', type: 'file', name: 'modfile', id: 'modfile' })
),
_react2.default.createElement(
'div',
{ className: 'form-group' },
_react2.default.createElement('input', { className: 'form-control btn btn-default', type: 'button', ref: 'button', value: 'Upload', onClick: this.uploadFile })
)
),
_react2.default.createElement(
@ -26594,7 +26624,7 @@
className: 'btn btn-danger btn-small',
ref: 'modInput',
type: 'button',
onClick: _this2.removeMod.bind(_this2, i) },
onClick: _this3.removeMod.bind(_this3, i) },
_react2.default.createElement('i', { className: 'fa fa-trash' }),
'  Delete'
)
@ -26679,7 +26709,7 @@
url: "/api/log/tail",
dataType: "json",
success: function success(data) {
_this2.setState({ log: data });
_this2.setState({ log: data.data });
},
error: function error(xhr, status, err) {
console.log('api/mods/list', status, err.toString());
@ -26702,7 +26732,7 @@
_react2.default.createElement(
'small',
null,
'Optional description'
'Analyze Factorio Logs'
)
),
_react2.default.createElement(
@ -26890,7 +26920,7 @@
url: "/api/saves/list",
dataType: "json",
success: function success(data) {
_this2.setState({ saves: data });
_this2.setState({ saves: data.data });
},
error: function error(xhr, status, err) {
console.log('api/mods/list', status, err.toString());
@ -26927,7 +26957,7 @@
_react2.default.createElement(
'small',
null,
'Optional description'
'Factorio Save Files'
)
),
_react2.default.createElement(
@ -27199,13 +27229,18 @@
}
_createClass(Save, [{
key: 'hours12',
value: function hours12(date) {
return (date.getHours() + 24) % 12 || 12;
}
}, {
key: 'render',
value: function render() {
var saveLocation = "/api/saves/dl/" + this.props.save.name;
var saveSize = parseFloat(this.props.save.size / 1024 / 1024).toFixed(3);
var saveLastMod = Date.parse(this.props.save.last_mod);
var date = new Date(saveLastMod);
var dateFmt = date.getFullYear() + '-' + date.getMonth() + '-' + date.getDay() + ' ' + date.getHours() + ':' + date.getMinutes() + ':' + date.getSeconds();
var dateFmt = date.getFullYear() + '-' + date.getMonth() + '-' + date.getDay() + ' ' + this.hours12(date) + ':' + date.getMinutes() + ':' + date.getSeconds();
return _react2.default.createElement(
'tr',
@ -27267,6 +27302,254 @@
/***/ },
/* 239 */
/***/ function(module, exports, __webpack_require__) {
'use strict';
Object.defineProperty(exports, "__esModule", {
value: true
});
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
var _react = __webpack_require__(1);
var _react2 = _interopRequireDefault(_react);
var _Settings = __webpack_require__(240);
var _Settings2 = _interopRequireDefault(_Settings);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
var ConfigContent = function (_React$Component) {
_inherits(ConfigContent, _React$Component);
function ConfigContent(props) {
_classCallCheck(this, ConfigContent);
var _this = _possibleConstructorReturn(this, Object.getPrototypeOf(ConfigContent).call(this, props));
_this.getConfig = _this.getConfig.bind(_this);
_this.state = {
config: {}
};
return _this;
}
_createClass(ConfigContent, [{
key: 'componentDidMount',
value: function componentDidMount() {
this.getConfig();
}
}, {
key: 'getConfig',
value: function getConfig() {
var _this2 = this;
$.ajax({
url: "/api/config",
dataType: "json",
success: function success(data) {
_this2.setState({ config: data.data });
console.log(_this2.state.config);
},
error: function error(xhr, status, err) {
console.log('/api/config/get', status, err.toString());
}
});
}
}, {
key: 'render',
value: function render() {
return _react2.default.createElement(
'div',
{ className: 'content-wrapper' },
_react2.default.createElement(
'section',
{ className: 'content-header' },
_react2.default.createElement(
'h1',
null,
'Config',
_react2.default.createElement(
'small',
null,
'Manage server configuration'
)
),
_react2.default.createElement(
'ol',
{ className: 'breadcrumb' },
_react2.default.createElement(
'li',
null,
_react2.default.createElement(
'a',
{ href: '#' },
_react2.default.createElement('i', { className: 'fa fa-dashboard' }),
' Level'
)
),
_react2.default.createElement(
'li',
{ className: 'active' },
'Here'
)
)
),
_react2.default.createElement(
'section',
{ className: 'content' },
_react2.default.createElement(
'div',
{ className: 'box' },
_react2.default.createElement(
'div',
{ className: 'box-header' },
_react2.default.createElement(
'h3',
{ className: 'box-title' },
'Manage Server Configuration'
)
),
_react2.default.createElement(
'div',
{ className: 'box-body' },
Object.keys(this.state.config).map(function (key) {
var conf = this.state.config[key];
return _react2.default.createElement(
'div',
{ className: 'settings-section', key: key },
_react2.default.createElement(
'h3',
null,
key
),
_react2.default.createElement(
'div',
{ className: 'table-responsive' },
_react2.default.createElement(
'table',
{ className: 'table table-striped' },
_react2.default.createElement(
'thead',
null,
_react2.default.createElement(
'tr',
null,
_react2.default.createElement(
'th',
null,
'Setting name'
),
_react2.default.createElement(
'th',
null,
'Setting value'
)
)
),
_react2.default.createElement(_Settings2.default, {
section: key,
config: conf
})
)
)
);
}, this)
)
)
)
);
}
}]);
return ConfigContent;
}(_react2.default.Component);
exports.default = ConfigContent;
/***/ },
/* 240 */
/***/ function(module, exports, __webpack_require__) {
'use strict';
Object.defineProperty(exports, "__esModule", {
value: true
});
var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol ? "symbol" : typeof obj; };
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
var _react = __webpack_require__(1);
var _react2 = _interopRequireDefault(_react);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
var Settings = function (_React$Component) {
_inherits(Settings, _React$Component);
function Settings(props) {
_classCallCheck(this, Settings);
return _possibleConstructorReturn(this, Object.getPrototypeOf(Settings).call(this, props));
}
_createClass(Settings, [{
key: 'render',
value: function render() {
return _react2.default.createElement(
'tbody',
null,
Object.keys(this.props.config).map(function (key) {
console.log(typeof key === 'undefined' ? 'undefined' : _typeof(key));
return _react2.default.createElement(
'tr',
{ key: key },
_react2.default.createElement(
'td',
null,
key
),
_react2.default.createElement(
'td',
null,
this.props.config[key]
)
);
}, this)
);
}
}]);
return Settings;
}(_react2.default.Component);
Settings.propTypes = {
section: _react2.default.PropTypes.string.isRequired,
config: _react2.default.PropTypes.object.isRequired
};
exports.default = Settings;
/***/ },
/* 241 */
/***/ function(module, exports, __webpack_require__) {
"use strict";

View File

@ -1,13 +1,12 @@
package main
import (
"encoding/json"
"log"
"github.com/go-ini/ini"
)
func loadConfig(filename string) ([]byte, error) {
func loadConfig(filename string) (map[string]map[string]string, error) {
log.Printf("Loading config file: %s", filename)
cfg, err := ini.Load(filename)
if err != nil {
@ -29,11 +28,6 @@ func loadConfig(filename string) ([]byte, error) {
result[sectionName] = s.KeysHash()
}
log.Printf("Encoding config.ini to JSON")
resp, err := json.Marshal(result)
if err != nil {
log.Printf("Error marshaling config.ini to JSON: %s", err)
return nil, err
}
return resp, nil
return result, nil
}

View File

@ -11,23 +11,39 @@ import (
"github.com/gorilla/mux"
)
type JSONResponse struct {
Success bool `json:"success"`
Data interface{} `json:"data,string"`
}
func Index(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "hello world")
}
// Returns JSON response of all mods installed in factorio/mods
func ListInstalledMods(w http.ResponseWriter, r *http.Request) {
var err error
resp := JSONResponse{
Success: false,
}
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
modDir := config.FactorioDir + "/mods"
mods, err := listInstalledMods(modDir)
resp.Data, err = listInstalledMods(modDir)
if err != nil {
log.Printf("Error in ListInstalledMods handler: %s", err)
resp.Data = fmt.Sprintf("Error in ListInstalledMods handler: %s", err)
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error in list mods: %s", err)
}
return
}
if err := json.NewEncoder(w).Encode(mods); err != nil {
resp.Success = true
fmt.Printf("%+v", resp)
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error in list mods: %s", err)
}
}
@ -35,6 +51,11 @@ func ListInstalledMods(w http.ResponseWriter, r *http.Request) {
// Toggles mod passed in through mod variable
// Updates mod-list.json file to toggle the enabled status of mods
func ToggleMod(w http.ResponseWriter, r *http.Request) {
var err error
resp := JSONResponse{
Success: false,
}
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
vars := mux.Vars(r)
@ -42,41 +63,66 @@ func ToggleMod(w http.ResponseWriter, r *http.Request) {
m, err := parseModList()
if err != nil {
log.Printf("Could not parse mod list: %s", err)
resp.Data = fmt.Sprintf("Could not parse mod list: %s", err)
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error in list mods: %s", err)
}
return
}
err = m.toggleMod(modName)
if err != nil {
log.Printf("Could not toggle mod: %s error: %s", modName, err)
resp.Data = fmt.Sprintf("Could not toggle mod: %s error: %s", modName, err)
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error in list mods: %s", err)
}
return
}
if err := json.NewEncoder(w).Encode(m); err != nil {
resp.Success = true
resp.Data = m
if err = json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error in toggle mod: %s", err)
}
}
// Returns JSON response of all mods in the mod-list.json file
func ListMods(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
m, err := parseModList()
if err != nil {
log.Printf("Could not parse mod list: %s", err)
var err error
resp := JSONResponse{
Success: false,
}
if err := json.NewEncoder(w).Encode(m); err != nil {
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
resp.Data, err = parseModList()
if err != nil {
resp.Data = fmt.Sprintf("Could not parse mod list: %s", err)
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error in list mods: %s", err)
}
return
}
resp.Success = true
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error listing mods: %s", err)
}
}
// Uploads mod to the mods directory
func UploadMod(w http.ResponseWriter, r *http.Request) {
var err error
resp := JSONResponse{
Success: false,
}
switch r.Method {
case "GET":
resp := "Unsupported method"
if err := json.NewEncoder(w).Encode(resp); err != nil {
resp.Data = "Unsupported method"
if err = json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error listing mods: %s", err)
}
case "POST":
@ -92,7 +138,8 @@ func UploadMod(w http.ResponseWriter, r *http.Request) {
out, err := os.Create(config.FactorioModsDir + "/" + header.Filename)
if err != nil {
json.NewEncoder(w).Encode(err.Error())
resp.Data = err.Error()
json.NewEncoder(w).Encode(resp)
log.Printf("Error in out")
return
}
@ -100,12 +147,14 @@ func UploadMod(w http.ResponseWriter, r *http.Request) {
_, err = io.Copy(out, file)
if err != nil {
json.NewEncoder(w).Encode(err.Error())
resp.Data = err.Error()
json.NewEncoder(w).Encode(resp)
log.Printf("Error in io copy")
return
}
log.Printf("Uploaded mod file: %s", header.Filename)
resp := "File '" + header.Filename + "' submitted successfully"
resp.Data = "File '" + header.Filename + "' submitted successfully"
resp.Success = true
json.NewEncoder(w).Encode(resp)
default:
@ -114,21 +163,27 @@ func UploadMod(w http.ResponseWriter, r *http.Request) {
}
func RemoveMod(w http.ResponseWriter, r *http.Request) {
var err error
resp := JSONResponse{
Success: false,
}
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
vars := mux.Vars(r)
modName := vars["mod"]
err := rmMod(modName)
err = rmMod(modName)
if err == nil {
// No error returned means mod was removed
resp := fmt.Sprintf("Removed mod: %s", modName)
resp.Data = fmt.Sprintf("Removed mod: %s", modName)
resp.Success = true
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error removing mod: %s", err)
}
} else {
log.Printf("Error in remove mod handler: %s", err)
resp := fmt.Sprintf("Error in remove mod handler: %s", err)
resp.Data = fmt.Sprintf("Error in remove mod handler: %s", err)
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error removing mod: %s", err)
@ -152,17 +207,27 @@ func DownloadMod(w http.ResponseWriter, r *http.Request) {
// Lists all save files in the factorio/saves directory
func ListSaves(w http.ResponseWriter, r *http.Request) {
var err error
resp := JSONResponse{
Success: false,
}
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
saveDir := config.FactorioDir + "/saves"
saves, err := listSaves(saveDir)
resp.Data, err = listSaves(saveDir)
if err != nil {
log.Printf("Error in ListSaves handler: %s", err)
resp.Data = fmt.Sprintf("Error listing save files: %s", err)
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error listing saves: %s", err)
}
return
}
if err := json.NewEncoder(w).Encode(saves); err != nil {
resp.Success = true
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error listing saves: %s", err)
}
}
@ -181,10 +246,16 @@ func DLSave(w http.ResponseWriter, r *http.Request) {
}
func UploadSave(w http.ResponseWriter, r *http.Request) {
var err error
resp := JSONResponse{
Success: false,
}
switch r.Method {
case "GET":
resp := "Unsupported method"
if err := json.NewEncoder(w).Encode(resp); err != nil {
resp.Data = "Unsupported method"
resp.Success = false
if err = json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error listing mods: %s", err)
}
case "POST":
@ -192,17 +263,19 @@ func UploadSave(w http.ResponseWriter, r *http.Request) {
r.ParseMultipartForm(32 << 20)
file, header, err := r.FormFile("savefile")
if err != nil {
json.NewEncoder(w).Encode(err.Error())
log.Printf("%+v", file)
log.Printf("%+v", header)
log.Printf("Error in formfile")
resp.Success = false
resp.Data = err.Error()
json.NewEncoder(w).Encode(resp)
log.Printf("Error in upload save formfile: %s", err.Error())
return
}
defer file.Close()
out, err := os.Create(config.FactorioSavesDir + "/" + header.Filename)
if err != nil {
json.NewEncoder(w).Encode(err.Error())
resp.Success = false
resp.Data = err.Error()
json.NewEncoder(w).Encode(resp)
log.Printf("Error in out: %s", err)
return
}
@ -210,12 +283,15 @@ func UploadSave(w http.ResponseWriter, r *http.Request) {
_, err = io.Copy(out, file)
if err != nil {
json.NewEncoder(w).Encode(err.Error())
resp.Success = false
resp.Data = err.Error()
json.NewEncoder(w).Encode(resp)
log.Printf("Error in io copy: %s", err)
return
}
log.Printf("Uploaded save file: %s", header.Filename)
resp := "File '" + header.Filename + "' uploaded successfully"
resp.Data = "File '" + header.Filename + "' uploaded successfully"
resp.Success = true
json.NewEncoder(w).Encode(resp)
default:
w.WriteHeader(http.StatusMethodNotAllowed)
@ -223,23 +299,28 @@ func UploadSave(w http.ResponseWriter, r *http.Request) {
}
// Deletes provided save
//TODO sanitize
func RemoveSave(w http.ResponseWriter, r *http.Request) {
var err error
resp := JSONResponse{
Success: false,
}
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
vars := mux.Vars(r)
saveName := vars["save"]
err := rmSave(saveName)
err = rmSave(saveName)
if err == nil {
// save was removed
resp := fmt.Sprintf("Removed save: %s", saveName)
resp.Data = fmt.Sprintf("Removed save: %s", saveName)
resp.Success = true
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error removing save %s", err)
}
} else {
log.Printf("Error in remove save handler: %s", err)
resp := fmt.Sprintf("Error in remove save handler: %s", err)
resp.Data = fmt.Sprintf("Error in remove save handler: %s", err)
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error removing save: %s", err)
@ -249,34 +330,54 @@ func RemoveSave(w http.ResponseWriter, r *http.Request) {
// Returns last lines of the factorio-current.log file
func LogTail(w http.ResponseWriter, r *http.Request) {
var err error
resp := JSONResponse{
Success: false,
}
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
logLines, err := tailLog(config.FactorioLog)
resp.Data, err = tailLog(config.FactorioLog)
if err != nil {
log.Printf("Could not tail %s: %s", config.FactorioLog, err)
resp.Data = fmt.Sprintf("Could not tail %s: %s", config.FactorioLog, err)
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Could not tail %s: %s", config.FactorioLog, err)
}
return
}
if err := json.NewEncoder(w).Encode(logLines); err != nil {
resp.Success = true
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error tailing logfile", err)
}
}
// Return JSON response of config.ini file
func LoadConfig(w http.ResponseWriter, r *http.Request) {
var err error
resp := JSONResponse{
Success: false,
}
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
configContents, err := loadConfig(config.FactorioConfigFile)
if err != nil {
log.Printf("Could not retrieve config.ini: %s", err)
resp := "Error getting config.ini"
resp.Data = "Error getting config.ini"
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error tailing logfile", err)
}
return
}
if _, err := w.Write(configContents); err != nil {
log.Printf("Error encoding config.ini response: %s", err)
resp.Data = configContents
resp.Success = true
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error encoding config file JSON reponse: ", err)
}
log.Printf("Sent config.ini response")
}

View File

@ -12,6 +12,7 @@ type Config struct {
FactorioModsDir string
FactorioConfigFile string
FactorioLog string
FactorioBinary string
ServerIP string
ServerPort string
MaxUploadSize int64
@ -24,7 +25,8 @@ func loadFlags() {
factorioIP := flag.String("host", "0.0.0.0", "Specify IP for webserver to listen on.")
factorioPort := flag.String("port", "8080", "Specify a port for the server.")
factorioConfigFile := flag.String("config", "config/config.ini", "Specify location of Factorio config.ini file")
factorioMaxUpload := flag.Int64("max-upload", 100000, "Maximum filesize for uploaded files.")
factorioMaxUpload := flag.Int64("max-upload", 1024*1024*20, "Maximum filesize for uploaded files (default 20MB).")
factorioBinary := flag.String("bin", "bin/x64/factorio", "Location of Factorio Server binary file")
flag.Parse()
@ -32,6 +34,7 @@ func loadFlags() {
config.FactorioSavesDir = config.FactorioDir + "/saves"
config.FactorioModsDir = config.FactorioDir + "/mods"
config.FactorioConfigFile = config.FactorioDir + "/" + *factorioConfigFile
config.FactorioBinary = config.FactorioDir + *factorioBinary
config.ServerIP = *factorioIP
config.ServerPort = *factorioPort
config.FactorioLog = config.FactorioDir + "/factorio-current.log"

View File

@ -43,6 +43,10 @@ func NewRouter() *mux.Router {
Methods("GET").
Name("Logs").
Handler(http.StripPrefix("/logs", http.FileServer(http.Dir("./app/"))))
r.Path("/config").
Methods("GET").
Name("Config").
Handler(http.StripPrefix("/config", http.FileServer(http.Dir("./app/"))))
r.PathPrefix("/").
Methods("GET").
Name("Index").
@ -112,7 +116,7 @@ var apiRoutes = Routes{
}, {
"LoadConfig",
"GET",
"/config/get",
"/config",
LoadConfig,
},
}

View File

@ -17,8 +17,11 @@ class App extends React.Component {
<Header />
<Sidebar />
{this.props.children}
{React.cloneElement(
this.props.children,
{message: ""}
)}
<Footer />

View File

@ -0,0 +1,31 @@
import React from 'react';
class Settings extends React.Component {
constructor(props) {
super(props)
}
render() {
return(
<tbody>
{Object.keys(this.props.config).map(function(key) {
console.log(typeof key);
return(
<tr key={key}>
<td>{key}</td>
<td>{this.props.config[key]}</td>
</tr>
)
}, this)}
</tbody>
)
}
}
Settings.propTypes = {
section: React.PropTypes.string.isRequired,
config: React.PropTypes.object.isRequired,
}
export default Settings

View File

@ -0,0 +1,82 @@
import React from 'react';
import Settings from './Config/Settings.jsx';
class ConfigContent extends React.Component {
constructor(props) {
super(props);
this.getConfig = this.getConfig.bind(this);
this.state = {
config: {}
}
}
componentDidMount() {
this.getConfig();
}
getConfig() {
$.ajax({
url: "/api/config",
dataType: "json",
success: (data) => {
this.setState({config: data.data})
console.log(this.state.config)
},
error: (xhr, status, err) => {
console.log('/api/config/get', status, err.toString());
}
});
}
render() {
return(
<div className="content-wrapper">
<section className="content-header">
<h1>
Config
<small>Manage server configuration</small>
</h1>
<ol className="breadcrumb">
<li><a href="#"><i className="fa fa-dashboard"></i> Level</a></li>
<li className="active">Here</li>
</ol>
</section>
<section className="content">
<div className="box">
<div className="box-header">
<h3 className="box-title">Manage Server Configuration</h3>
</div>
<div className="box-body">
{Object.keys(this.state.config).map(function(key) {
var conf = this.state.config[key]
return(
<div className="settings-section" key={key}>
<h3>{key}</h3>
<div className="table-responsive">
<table className="table table-striped">
<thead>
<tr>
<th>Setting name</th>
<th>Setting value</th>
</tr>
</thead>
<Settings
section={key}
config={conf}
/>
</table>
</div>
</div>
)
}, this)}
</div>
</div>
</section>
</div>
)
}
}
export default ConfigContent

View File

@ -5,7 +5,6 @@ class Footer extends React.Component {
return(
<footer className="main-footer">
<div className="pull-right hidden-xs">
Anything you want!!!!!!
</div>
<strong>Copyright &copy; 2016 <a href="https://roote.me">Mitch Roote</a>.</strong> MIT License.
</footer>

View File

@ -20,7 +20,7 @@ class LogsContent extends React.Component {
url: "/api/log/tail",
dataType: "json",
success: (data) => {
this.setState({log: data})
this.setState({log: data.data})
},
error: (xhr, status, err) => {
console.log('api/mods/list', status, err.toString());
@ -34,7 +34,7 @@ class LogsContent extends React.Component {
<section className="content-header">
<h1>
Logs
<small>Optional description</small>
<small>Analyze Factorio Logs</small>
</h1>
<ol className="breadcrumb">
<li><a href="#"><i className="fa fa-dashboard"></i> Level</a></li>

View File

@ -11,6 +11,7 @@ class InstalledMods extends React.Component {
}
uploadFile(e) {
e.preventDefault();
var fd = new FormData();
fd.append('modfile', this.refs.file.files[0]);
@ -21,11 +22,13 @@ class InstalledMods extends React.Component {
processData: false,
contentType: false,
success: (data) => {
alert(data)
var response = JSON.parse(data)
console.log(response.success);
if (response.success === true) {
this.updateInstalledMods();
}
}
});
e.preventDefault();
this.updateInstalledMods();
}
removeMod(i) {
@ -47,12 +50,14 @@ class InstalledMods extends React.Component {
<div className="box-body">
<h4>Upload Mod</h4>
<form ref="uploadForm" className="form" encType='multipart/form-data'>
<fieldset>
<input className="btn btn-default" ref="file" type="file" name="modfile" id="modfile" />
<input type="button" ref="button" value="Upload" onClick={this.uploadFile} />
</fieldset>
<form ref="uploadForm" className="form-inline" encType='multipart/form-data'>
<div className="form-group">
<label for="modfile">Upload Mod File...</label>
<input className="form-control btn btn-default" ref="file" type="file" name="modfile" id="modfile" />
</div>
<div className="form-group">
<input className="form-control btn btn-default" type="button" ref="button" value="Upload" onClick={this.uploadFile} />
</div>
</form>
<div className="table-responsive">

View File

@ -24,7 +24,7 @@ class ModsContent extends React.Component {
url: "/api/mods/list",
dataType: "json",
success: (data) => {
this.setState({listMods: data.mods})
this.setState({listMods: data.data.mods})
},
error: (xhr, status, err) => {
console.log('api/mods/list', status, err.toString());
@ -37,7 +37,7 @@ class ModsContent extends React.Component {
url: "/api/mods/list/installed",
dataType: "json",
success: (data) => {
this.setState({installedMods: data})
this.setState({installedMods: data.data})
},
error: (xhr, status, err) => {
console.log('api/mods/list', status, err.toString());
@ -50,7 +50,7 @@ class ModsContent extends React.Component {
url: "/api/mods/toggle/" + modName,
dataType: "json",
success: (data) => {
this.setState({listMods: data.mods})
this.setState({listMods: data.data.mods})
},
error: (xhr, status, err) => {
console.log('api/mods/toggle', status, err.toString());
@ -72,7 +72,7 @@ class ModsContent extends React.Component {
</ol>
</section>
<section className="content" style={{height: "100%"}}>
<section className="content">
<InstalledMods
{...this.state}

View File

@ -1,13 +1,15 @@
import React from 'react';
class Save extends React.Component {
hours12(date) { return (date.getHours() + 24) % 12 || 12; }
render() {
let saveLocation = "/api/saves/dl/" + this.props.save.name
let saveSize = parseFloat(this.props.save.size / 1024 / 1024).toFixed(3)
let saveLastMod = Date.parse(this.props.save.last_mod);
let date = new Date(saveLastMod)
let dateFmt = date.getFullYear() + '-' + date.getMonth() + '-' + date.getDay() + ' '
+ date.getHours() + ':' + date.getMinutes() + ':' + date.getSeconds();
let dateFmt = date.getFullYear() + '-' + date.getMonth() + '-' + date.getDay() + ' '
+ this.hours12(date) + ':' + date.getMinutes() + ':' + date.getSeconds();
return(
<tr>

View File

@ -20,7 +20,7 @@ class SavesContent extends React.Component {
url: "/api/saves/list",
dataType: "json",
success: (data) => {
this.setState({saves: data})
this.setState({saves: data.data})
},
error: (xhr, status, err) => {
console.log('api/mods/list', status, err.toString());
@ -47,7 +47,7 @@ class SavesContent extends React.Component {
<section className="content-header">
<h1>
Saves
<small>Optional description</small>
<small>Factorio Save Files</small>
</h1>
<ol className="breadcrumb">
<li><a href="#"><i className="fa fa-dashboard"></i> Level</a></li>

View File

@ -33,6 +33,7 @@ class Sidebar extends React.Component {
<li><Link to="/mods" activeClassName="active"><i className="fa fa-link"></i><span>Mods</span></Link></li>
<li><Link to="/logs" activeClassName="active"><i className="fa fa-link"></i> <span>Logs</span></Link></li>
<li><Link to="/saves" activeClassName="active"><i className="fa fa-link"></i> <span>Saves</span></Link></li>
<li><Link to="/config" activeClassName="active"><i className="fa fa-link"></i> <span>Configuration</span></Link></li>
</ul>
</section>
</aside>

View File

@ -5,6 +5,7 @@ import App from './App/App.jsx';
import ModsContent from './App/components/ModsContent.jsx';
import LogsContent from './App/components/LogsContent.jsx';
import SavesContent from './App/components/SavesContent.jsx';
import ConfigContent from './App/components/ConfigContent.jsx';
import Index from './App/components/Index.jsx';
ReactDOM.render((
@ -14,6 +15,7 @@ ReactDOM.render((
<Route path="/mods" component={ModsContent}/>
<Route path="/logs" component={LogsContent}/>
<Route path="/saves" component={SavesContent}/>
<Route path="/config" component={ConfigContent}/>
</Route>
</Router>
), document.getElementById('app'))