From 96139c8cad4052f7332b25dfe827f4b7de9ad86b Mon Sep 17 00:00:00 2001 From: majormjr Date: Sun, 24 Apr 2016 19:07:00 -0400 Subject: [PATCH] improved ui for config route, added error reporting to json responses --- README.md | 10 +- app/bundle.js | 337 +++++++++++++++++++++-- gameconfig.go | 10 +- handlers.go | 185 ++++++++++--- main.go | 5 +- routes.go | 6 +- ui/App/App.jsx | 7 +- ui/App/components/Config/Settings.jsx | 31 +++ ui/App/components/ConfigContent.jsx | 82 ++++++ ui/App/components/Footer.jsx | 1 - ui/App/components/LogsContent.jsx | 4 +- ui/App/components/Mods/InstalledMods.jsx | 23 +- ui/App/components/ModsContent.jsx | 8 +- ui/App/components/Saves/Save.jsx | 6 +- ui/App/components/SavesContent.jsx | 4 +- ui/App/components/Sidebar.jsx | 1 + ui/index.js | 2 + 17 files changed, 619 insertions(+), 103 deletions(-) create mode 100644 ui/App/components/Config/Settings.jsx create mode 100644 ui/App/components/ConfigContent.jsx diff --git a/README.md b/README.md index d1a823d..b0916f1 100644 --- a/README.md +++ b/README.md @@ -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: diff --git a/app/bundle.js b/app/bundle.js index ccbaedd..dcd5393 100644 --- a/app/bundle.js +++ b/app/bundle.js @@ -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"; diff --git a/gameconfig.go b/gameconfig.go index a2fc3da..d4c0bee 100644 --- a/gameconfig.go +++ b/gameconfig.go @@ -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 } diff --git a/handlers.go b/handlers.go index 4b4bf79..4b7979d 100644 --- a/handlers.go +++ b/handlers.go @@ -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") } diff --git a/main.go b/main.go index ba93c6d..dfbe121 100644 --- a/main.go +++ b/main.go @@ -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" diff --git a/routes.go b/routes.go index 2415b8d..e3bab08 100644 --- a/routes.go +++ b/routes.go @@ -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, }, } diff --git a/ui/App/App.jsx b/ui/App/App.jsx index 4e132a9..4b576fa 100644 --- a/ui/App/App.jsx +++ b/ui/App/App.jsx @@ -17,8 +17,11 @@ class App extends React.Component {
- - {this.props.children} + + {React.cloneElement( + this.props.children, + {message: ""} + )}