mirror of
https://github.com/OpenFactorioServerManager/factorio-server-manager.git
synced 2024-12-27 02:43:45 +02:00
Merge branch 'develop' into docker-fix
This commit is contained in:
commit
f1405e827c
62
.github/workflows/create-release-workflow.yml
vendored
Normal file
62
.github/workflows/create-release-workflow.yml
vendored
Normal file
@ -0,0 +1,62 @@
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
jobs:
|
||||
add-assets-to-release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v1
|
||||
- uses: actions/setup-go@v2
|
||||
- name: Get git tag
|
||||
id: tag_name
|
||||
uses: little-core-labs/get-git-tag@v3.0.2
|
||||
- name: Get release
|
||||
id: get_release
|
||||
uses: bruceadams/get-release@v1.2.2
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
- name: Create release bundles
|
||||
run: make gen_release
|
||||
- name: Upload release (windows)
|
||||
uses: actions/upload-release-asset@v1.0.2
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
with:
|
||||
upload_url: ${{ steps.get_release.outputs.upload_url }}
|
||||
asset_path: ./build/factorio-server-manager-windows.zip
|
||||
asset_name: factorio-server-manager-windows-${{ steps.tag_name.outputs.tag }}.zip
|
||||
asset_content_type: application/octet-stream
|
||||
- name: Upload release (linux)
|
||||
uses: actions/upload-release-asset@v1.0.2
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
with:
|
||||
upload_url: ${{ steps.get_release.outputs.upload_url }}
|
||||
asset_path: ./build/factorio-server-manager-linux.zip
|
||||
asset_name: factorio-server-manager-linux-${{ steps.tag_name.outputs.tag }}.zip
|
||||
asset_content_type: application/octet-stream
|
||||
docker-push:
|
||||
needs: ['add-assets-to-release']
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v1
|
||||
- uses: actions/setup-go@v2
|
||||
- uses: docker/setup-buildx-action@v1
|
||||
- name: Get git tag
|
||||
id: tag_name
|
||||
uses: little-core-labs/get-git-tag@v3.0.2
|
||||
- name: Login to dockerhub
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- run: make build; cp build/factorio-server-manager-linux.zip docker/factorio-server-manager-linux.zip
|
||||
- uses: docker/build-push-action@v2
|
||||
with:
|
||||
context: ./docker/
|
||||
file: ./docker/Dockerfile-local
|
||||
push: true
|
||||
tags: ofsm/ofsm:latest,ofsm/ofsm:${{ steps.tag_name.outputs.tag }}
|
||||
|
81
.github/workflows/test-workflow.yml
vendored
Normal file
81
.github/workflows/test-workflow.yml
vendored
Normal file
@ -0,0 +1,81 @@
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- '**'
|
||||
tags-ignore:
|
||||
- '*.*'
|
||||
pull_request:
|
||||
branches:
|
||||
- '**'
|
||||
env:
|
||||
factorio_password: ${{ secrets.FACTORIO_PASSWORD }}
|
||||
factorio_username: ${{ secrets.FACTORIO_USERNAME }}
|
||||
mod_dir: 'dev'
|
||||
mod_pack_dir: 'dev_pack'
|
||||
dir: '../'
|
||||
conf: '../../conf.json.example'
|
||||
jobs:
|
||||
test-npm:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v1
|
||||
- run: make app/bundle
|
||||
test-go:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest]
|
||||
go: ['1.14', '1.15', '1']
|
||||
runs-on: ${{ matrix.os }}
|
||||
name: test-go ${{ matrix.go }} (${{ matrix.os }})
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Setup go
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ${{ matrix.go }}
|
||||
- if: contains(matrix.os, 'ubuntu')
|
||||
run: |
|
||||
cd src
|
||||
if [[ -z "$factorio_password" ]]; then
|
||||
echo "run only short tests"
|
||||
go test ./... -v -test.short
|
||||
else
|
||||
echo "run full test suit"
|
||||
go test ./... -v
|
||||
fi
|
||||
- if: contains(matrix.os, 'windows')
|
||||
run: |
|
||||
cd src
|
||||
if ([Environment]::GetEnvironmentVariable('factorio_password', 'Machine')) {
|
||||
echo "run full test suit"
|
||||
go test ./... -v
|
||||
} else {
|
||||
echo "run only short tests"
|
||||
go test ./... -v "-test.short"
|
||||
}
|
||||
docker-push:
|
||||
needs: [test-npm, test-go]
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/develop'
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v1
|
||||
- uses: actions/setup-go@v2
|
||||
- uses: docker/setup-buildx-action@v1
|
||||
- name: Login to dockerhub
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- run: make build; cp build/factorio-server-manager-linux.zip docker/factorio-server-manager-linux.zip
|
||||
- id: docker_build
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
context: ./docker/
|
||||
file: ./docker/Dockerfile-local
|
||||
push: true
|
||||
tags: ofsm/ofsm:develop
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,5 +1,6 @@
|
||||
node_modules/
|
||||
dev/
|
||||
dev_packs/
|
||||
/factorio-server-manager*
|
||||
/factorio_server_manager*
|
||||
auth.leveldb*
|
||||
@ -9,7 +10,6 @@ build/
|
||||
/mod_packs/*
|
||||
npm-debug.log
|
||||
.idea/
|
||||
/package-lock.json
|
||||
factorio.auth
|
||||
/pkg/
|
||||
mix-manifest.json
|
||||
@ -17,3 +17,4 @@ mix-manifest.json
|
||||
/app/*.js*
|
||||
/app/*.css*
|
||||
.vscode
|
||||
.env
|
||||
|
40
.travis.yml
40
.travis.yml
@ -1,40 +0,0 @@
|
||||
language: go
|
||||
go:
|
||||
- 1.11.x
|
||||
- 1.12.x
|
||||
- 1.13.x
|
||||
- 1.x
|
||||
|
||||
os:
|
||||
- linux
|
||||
- windows
|
||||
|
||||
env:
|
||||
- GO111MODULE=on
|
||||
|
||||
script:
|
||||
- cd src/
|
||||
- go test -v
|
||||
|
||||
jobs:
|
||||
include:
|
||||
- stage: deploy
|
||||
go: 1.x
|
||||
os: linux
|
||||
before_install:
|
||||
- curl -sL https://deb.nodesource.com/setup_13.x | sudo -E bash -
|
||||
- sudo apt-get install -y nodejs
|
||||
script:
|
||||
- make gen_release
|
||||
- mv build/factorio-server-manager-linux.zip ~/factorio-server-manager-linux-${TRAVIS_TAG}.zip
|
||||
- mv build/factorio-server-manager-windows.zip ~/factorio-server-manager-windows-${TRAVIS_TAG}.zip
|
||||
deploy:
|
||||
provider: releases
|
||||
api_key: "${GITHUB_TOKEN}"
|
||||
draft: true
|
||||
skip_cleanup: true
|
||||
on:
|
||||
tags: true
|
||||
file:
|
||||
- ~/factorio-server-manager-linux-${TRAVIS_TAG}.zip
|
||||
- ~/factorio-server-manager-windows-${TRAVIS_TAG}.zip
|
159
CHANGELOG.md
Normal file
159
CHANGELOG.md
Normal file
@ -0,0 +1,159 @@
|
||||
# Changelog
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
|
||||
## [Unreleased]
|
||||
### Added
|
||||
- Autostart factorio, when starting the server-manager - Thanks to @Psychomantis71
|
||||
|
||||
### Changed
|
||||
- Complete rework of the UI - Thanks to @jannaahs
|
||||
- Backend is refactored and improved - Thanks to @knoxfighter and @jannaahs
|
||||
- Rework of the docker image, so it allows easy updating of factorio - Thanks to @ita-sammann
|
||||
|
||||
### Fixed
|
||||
- Console page is now working correctly (directly reloading still bugged until new UI) - Thanks to @jannaahs
|
||||
- Mod Search fixed by new implementation, which does not rely on the search endpoint of the mod portal - Thanks to @jannaahs
|
||||
- Listen on port 80, previously port 8080 was used. Can be changed with `--port <port>`
|
||||
|
||||
## [0.8.2] - 2020-01-08
|
||||
Many bugfixes and a few small features in this release.
|
||||
- Adds a flag for a custom glibc version, required on some distros such as CentOS
|
||||
- bugfixes with file handling
|
||||
- UI fixes and improvements
|
||||
- CI bug fixes and build improvements
|
||||
- and more bugfixes
|
||||
|
||||
Special thanks to @knoxfighter for all the contributions.
|
||||
|
||||
### Added
|
||||
- Support for 0.17 server-adminlist.json
|
||||
- Support for custom glibc location (RHEL/CENTOS)
|
||||
|
||||
### Changed
|
||||
- Use bootstrap-fileinputs for savefile upload
|
||||
- Login-Page uses bootstrap 4
|
||||
|
||||
### Fixed
|
||||
- Login Page Design
|
||||
- Sweetalert2 API changes
|
||||
- allow_commands not misinterpreted as boolean anymore
|
||||
- Fixed some filepaths on windows
|
||||
- Fixed hardcoded Settings Path
|
||||
- Fixed Upgrading, Removing Mods on Windows results in error
|
||||
|
||||
## [0.8.1] - 2019-03-01
|
||||
### Fixed
|
||||
- Fixed redirect, when not logged in
|
||||
- Fixed login page completely white
|
||||
|
||||
## [0.8.0] - 2019-02-27
|
||||
This release contains many bug fixes and features. Thanks to @knoxfighter @sean-callahan for the contributions!
|
||||
- Fixes error in Factorio 0.17 saves
|
||||
- Refactors and various bug fixes
|
||||
|
||||
## [0.7.5] - 2018-08-08
|
||||
## Fixed
|
||||
- fixes crash when mods have no basemodversion defined
|
||||
|
||||
## [0.7.4] - 2018-08-04
|
||||
- Ability to auto download mods used in a save file courtesy @knoxfighter
|
||||
- Fix bug in mod logging courtesy @c0nnex
|
||||
|
||||
## [0.7.3] - 2018-06-02
|
||||
- Fixes fields in the settings dialog unable to be set to false. Courtesy @winadam.
|
||||
- Various bugfixes in the mod settings page regarding version compatability. Courtesy @knoxfighter.
|
||||
|
||||
## [0.7.2] - 2018-05-02
|
||||
### Fixed
|
||||
- Fixes an error when searching in the mod portal.
|
||||
|
||||
## [0.7.1] - 2018-02-11
|
||||
### Fixed
|
||||
- Fixes an error in the configuration form where some fields were not editable.
|
||||
|
||||
## [0.7.0] - 2018-01-21
|
||||
- Rewritten mods section now supporting installing mods directly from the Factorio mod portal and many other features courtesy @knoxfighter
|
||||
- Various bug fixes
|
||||
|
||||
## [0.6.1] - 2017-12-22
|
||||
- Adds the ability to specify the IP address for the Factorio game server to bind too.
|
||||
- Updates the --rcon-password flag
|
||||
- Small fixes
|
||||
|
||||
## [0.6.0] - 2017-01-25
|
||||
This release adds a console feature using rcon to send commands and chat from the management interface.
|
||||
|
||||
## [0.5.2] - 2016-11-01
|
||||
This release moves the server-settings.json config file. It will now save the file in the factorio/config directory.
|
||||
|
||||
## [0.5.1] - 2016-10-31
|
||||
- Fixed bug where server-settings.json file is overwritten with default settings
|
||||
- Started adding UI for editing the server-settings.json file
|
||||
|
||||
## [0.5.0] - 2016-10-11
|
||||
- This release adds beta support for Windows users.
|
||||
- Various updates for Factorio 0.14 are also included.
|
||||
|
||||
## [0.4.3] - 2016-09-15
|
||||
This release has some small bug fixes in order to support Factorio server 0.14.
|
||||
- Made the --latency-ms optional as it is removed in version 0.14
|
||||
- Improved some error handling messages when starting the server.
|
||||
|
||||
## [0.4.2] - 2016-07-13
|
||||
This release fixes a bug with Factorio 0.13 where the full path for save files must be specified when launching the server.
|
||||
|
||||
## [0.4.1] - 2016-05-15
|
||||
This release fixes a bug where the UI reports an error when the Factorio Server was successfully started.
|
||||
|
||||
## [0.4.0] - 2016-05-15
|
||||
### New features
|
||||
- Abillity to create modpacks for easy distribution of mods
|
||||
- Multiple users are now supported, create and delete users in the settings menu
|
||||
|
||||
### Features
|
||||
- Allows control of the Factorio Server, starting and stopping the Factorio binary.
|
||||
- Allows the management of save files, upload, download and delete saves.
|
||||
- Manage installed mods, upload new ones, delete uneeded mods. Enable or disable individual mods.
|
||||
- Allow viewing of the server logs and current configuration.
|
||||
- Authentication for protecting against unauthorized users
|
||||
- Available as a Docker container
|
||||
- Abillity to create modpacks for easy distribution of mods
|
||||
- Multiple users are now supported, create and delete users in the settings menu
|
||||
|
||||
## [0.3.1] - 2016-05-03
|
||||
### Fixed
|
||||
Fixes bug in #24 where Docker container cannot find conf.json file.
|
||||
|
||||
## [0.3.0] - 2016-05-01
|
||||
### New features
|
||||
- This release adds an authentication feature in Factorio Server Manager.
|
||||
- Now able to be installed as a Docker container.
|
||||
- Admin user credentials are configured in the conf.json file included in the release zip file.
|
||||
|
||||
### Features
|
||||
- Allows control of the Factorio Server, starting and stopping the Factorio binary.
|
||||
- Allows the management of save files, upload, download and delete saves.
|
||||
- Manage installed mods, upload new ones, delete uneeded mods. Enable or disable individual mods.
|
||||
- Allow viewing of the server logs and current configuration.
|
||||
- Authentication for protecting against unauthorized users
|
||||
- Available as a Docker container
|
||||
|
||||
## [0.2.0] - 2016-04-27
|
||||
This release adds the ability to control the Factorio server. Allows stopping and starting of the server binary with advanced options.
|
||||
|
||||
### Features
|
||||
- Allows control of the Factorio Server, starting and stopping the Factorio binary.
|
||||
- Allows the management of save files, upload, download and delete saves.
|
||||
- Manage installed mods, upload new ones, delete uneeded mods. Enable or disable individual mods.
|
||||
- Allow viewing of the server logs and current configuration.
|
||||
|
||||
## [0.1.0] - 2016-04-25
|
||||
First release of Factorio Server Manager
|
||||
|
||||
### Features
|
||||
- Managing save files, create, download, delete saves
|
||||
- Managing installed mods
|
||||
- Factorio log tailing
|
||||
- Factorio server configuration viewing
|
6
Makefile
6
Makefile
@ -27,13 +27,13 @@ factorio-server-manager-linux:
|
||||
@echo "Building Backend - Linux"
|
||||
@mkdir -p factorio-server-manager
|
||||
@cd src; \
|
||||
GOOS=linux GOARCH=amd64 go build -o ../factorio-server-manager/factorio-server-manager .
|
||||
GO111MODULE=on GOOS=linux GOARCH=amd64 go build -ldflags="-extldflags=-static" -o ../factorio-server-manager/factorio-server-manager .
|
||||
|
||||
factorio-server-manager-windows:
|
||||
@echo "Building Backend - Windows"
|
||||
@mkdir -p factorio-server-manager
|
||||
@cd src; \
|
||||
GOOS=windows GOARCH=386 go build -o ../factorio-server-manager/factorio-server-manager.exe .
|
||||
GO111MODULE=on GOOS=windows GOARCH=386 go build -ldflags="-extldflags=-static" -o ../factorio-server-manager/factorio-server-manager.exe .
|
||||
|
||||
gen_release: build/factorio-server-manager-linux.zip build/factorio-server-manager-windows.zip
|
||||
@echo "Done"
|
||||
@ -47,6 +47,6 @@ clean:
|
||||
@-rm app/style.css.map
|
||||
@-rm -r app/fonts/vendor/
|
||||
@-rm -r app/images/vendor/
|
||||
@-rm -r node_modules/
|
||||
@-rm -rf node_modules/
|
||||
@-rm -r pkg/
|
||||
@-rm -r factorio-server-manager
|
||||
|
37
README.md
37
README.md
@ -1,4 +1,4 @@
|
||||
[![Build Status](https://travis-ci.org/mroote/factorio-server-manager.svg?branch=master)](https://travis-ci.org/mroote/factorio-server-manager)
|
||||
![.github/workflows/test-workflow.yml](https://github.com/OpenFactorioServerManager/factorio-server-manager/workflows/.github/workflows/test-workflow.yml/badge.svg)
|
||||
|
||||
# Factorio Server Manager
|
||||
|
||||
@ -17,17 +17,17 @@ This tool runs on a Factorio server and allows management of the Factorio server
|
||||
## Installation Docker
|
||||
1. Pull the Docker container from Docker Hub using the pull command
|
||||
```
|
||||
docker pull majormjr/factorio-server-manager
|
||||
docker pull ofsm/ofsm:latest
|
||||
```
|
||||
|
||||
2. Now you can start the container by running:
|
||||
```
|
||||
docker run --name factorio-manager -d -p 80:80 -p 443:443 -p 34197:34197/udp majormjr/factorio-server-manager
|
||||
docker run --name ofsm -d -p 80:80 -p 34197:34197/udp ofsm/ofsm:latest
|
||||
```
|
||||
|
||||
## Installation Linux
|
||||
1. Download the latest release
|
||||
* [https://github.com/mroote/factorio-server-manager/releases](https://github.com/mroote/factorio-server-manager/releases)
|
||||
* [https://github.com/OpenFactorioServerManager/factorio-server-manager/releases](https://github.com/OpenFactorioServerManager/factorio-server-manager/releases)
|
||||
2. Download the Factorio Standalone server and install to a known directory.
|
||||
3. Run the server binary file, use the --dir flag to point the management server to your Factorio installation. If you are using the steam installation, point FSM to the steam directory.
|
||||
* ```./factorio-server-manager --dir /home/user/.factorio ```
|
||||
@ -36,7 +36,7 @@ This tool runs on a Factorio server and allows management of the Factorio server
|
||||
|
||||
## Installation Windows
|
||||
1. Download the latest release
|
||||
* [https://github.com/mroote/factorio-server-manager/releases](https://github.com/mroote/factorio-server-manager/releases)
|
||||
* [https://github.com/OpenFactorioServerManager/factorio-server-manager/releases](https://github.com/OpenFactorioServerManager/factorio-server-manager/releases)
|
||||
2. Download the Factorio Standalone server and install to a known directory.
|
||||
3. Run the server binary file via cmd or Powershell, use the --dir flag to point the management server to your Factorio installation.
|
||||
* ```.\factorio-server-manager --dir C:/Users/username/Factorio```
|
||||
@ -66,7 +66,10 @@ Usage of ./factorio-server-manager:
|
||||
Path to the glibc ld.so file (default "/opt/glibc-2.18/lib/ld-2.18.so")
|
||||
-glibc-lib-loc
|
||||
Path to the glibc lib folder (default "/opt/glibc-2.18/lib")
|
||||
|
||||
-autostart
|
||||
Autostarts Factorio Server when FSM is starting. Default false [true/false]
|
||||
(If no IP and/or port provided at startup, it will bind the factorio server to all interfaces
|
||||
and set the server port to the default 34197, always loads latest save)
|
||||
Example:
|
||||
|
||||
./factorio-server-manager --dir /home/user/.factorio --host 10.0.0.1
|
||||
@ -78,16 +81,13 @@ Custom glibc example:
|
||||
```
|
||||
|
||||
## Manage Factorio Server
|
||||
![Factorio Server Manager Screenshot](http://i.imgur.com/q7tbzdH.png "Factorio Server Manager")
|
||||
![Factorio Server Manager Screenshot](screenshots/Screenshot_Controls.png)
|
||||
|
||||
## Manage save files
|
||||
![Factorio Server Manager Screenshot](http://i.imgur.com/M7kBAhI.png "Factorio Server Manager")
|
||||
![Factorio Server Manager Screenshot](screenshots/Screenshot_Saves.png)
|
||||
|
||||
## Manage mods
|
||||
![Factorio Server Manager Screenshot](https://imgur.com/QIb0Kr4.png "Factorio Server Manager")
|
||||
|
||||
## Manage modpacks
|
||||
![Factorio Server Manager Screenshot](https://imgur.com/O701fB8.png "Factorio Server Manager")
|
||||
![Factorio Server Manager Screenshot](screenshots/Screenshot_Mods.png)
|
||||
|
||||
|
||||
|
||||
@ -110,19 +110,19 @@ curl --cookie cookie.txt --insecure https://localhost/api/server/status
|
||||
|
||||
#### Requirements
|
||||
+ Go 1.11
|
||||
+ NodeJS
|
||||
+ NodeJS >10.13.0
|
||||
|
||||
#### Building Releases
|
||||
Creates a release zip for windows and linux: (this will install the dependencies listed in gopkgdeps)
|
||||
```
|
||||
git clone https://github.com/mroote/factorio-server-manager.git
|
||||
git clone git@github.com:OpenFactorioServerManager/factorio-server-manager.git
|
||||
cd factorio-server-manager
|
||||
make gen_release
|
||||
```
|
||||
|
||||
#### Building a Testing Binary:
|
||||
```
|
||||
git clone https://github.com/mroote/factorio-server-manager.git
|
||||
git clone git@github.com:OpenFactorioServerManager/factorio-server-manager.git
|
||||
cd factorio-server-manager
|
||||
make
|
||||
./factorio-server-manager/factorio-server-manager
|
||||
@ -152,7 +152,7 @@ In every of those cases, also images and fonts will be copied to the app-folder.
|
||||
|
||||
### Building for Windows
|
||||
1. Download the latest release source zip file
|
||||
* [https://github.com/mroote/factorio-server-manager/releases](https://github.com/mroote/factorio-server-manager/releases)
|
||||
* [https://github.com/OpenFactorioServerManager/factorio-server-manager/releases](https://github.com/OpenFactorioServerManager/factorio-server-manager/releases)
|
||||
2. Unzip the Factorio Standalone server and move it to a known directory.
|
||||
3. Download and install Go 1.11 or newer. https://golang.org/dl/
|
||||
4. Download and install NodeJS 64-bit or 32-bit depending on your operating system, most users need 64-bit nowadays.
|
||||
@ -185,15 +185,18 @@ go build
|
||||
1. Fork it!
|
||||
2. Create your feature branch: `git checkout -b my-new-feature`
|
||||
3. Commit your changes: `git commit -am 'Add some feature'`
|
||||
4. Add your changes a in human readable way into CHANGELOG.md
|
||||
4. Push to the branch: `git push origin my-new-feature`
|
||||
5. Submit a pull request :D
|
||||
|
||||
## Authors
|
||||
|
||||
* **Mitch Roote** - [roote.ca](https://roote.ca)
|
||||
* **[knoxfighter](https://github.com/knoxfighter)**
|
||||
* **[Jannaahs](https://github.com/jannaahs)**
|
||||
|
||||
## Special Thanks
|
||||
- **[All Contributions](https://github.com/mroote/factorio-server-manager/graphs/contributors)**
|
||||
- **[All Contributions](https://github.com/OpenFactorioServerManager/factorio-server-manager/graphs/contributors)**
|
||||
- **mickael9** for reverseengineering the factorio-save-file: https://forums.factorio.com/viewtopic.php?f=5&t=8568#
|
||||
|
||||
## License
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 190 KiB |
Binary file not shown.
Before Width: | Height: | Size: 15 KiB |
@ -4,15 +4,14 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<title>Factorio Server Manager</title>
|
||||
<link rel="icon" type="image/png" href="./images/favicon.ico">
|
||||
<!-- Tell the browser to be responsive to screen width -->
|
||||
<meta content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" name="viewport">
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="./style.css">
|
||||
</head>
|
||||
<body class="hold-transition skin-blue sidebar-mini">
|
||||
<body class="bg-black">
|
||||
<div id="app"></div>
|
||||
|
||||
<div id="modal-root"></div>
|
||||
<!-- Main application -->
|
||||
<script src="./bundle.js"></script>
|
||||
</body>
|
||||
|
2
docker/.dockerignore
Normal file
2
docker/.dockerignore
Normal file
@ -0,0 +1,2 @@
|
||||
/fsm-data
|
||||
/factorio-data
|
6
docker/.env
Normal file
6
docker/.env
Normal file
@ -0,0 +1,6 @@
|
||||
ADMIN_USER=admin
|
||||
ADMIN_PASS=factorio
|
||||
RCON_PASS=
|
||||
COOKIE_ENCRYPTION_KEY=
|
||||
DOMAIN_NAME=<YOUR DOMAIN NAME>
|
||||
EMAIL_ADDRESS=<YOUR EMAIL ADDRESS>
|
2
docker/.gitignore
vendored
Normal file
2
docker/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
/fsm-data
|
||||
/factorio-data
|
@ -1,29 +1,38 @@
|
||||
# glibc is required for Factorio Server binaries to run
|
||||
# Glibc is required for Factorio Server binaries to run
|
||||
FROM frolvlad/alpine-glibc
|
||||
|
||||
ENV FACTORIO_VERSION=stable \
|
||||
MANAGER_VERSION=0.8.2 \
|
||||
ADMIN_PASSWORD=factorio
|
||||
ADMIN_USER=admin \
|
||||
ADMIN_PASS=factorio \
|
||||
RCON_PASS="" \
|
||||
COOKIE_ENCRYPTION_KEY=""
|
||||
|
||||
VOLUME /opt/factorio/saves /opt/factorio/mods /opt/factorio/config /security
|
||||
VOLUME /opt/fsm-data /opt/factorio/saves /opt/factorio/mods /opt/factorio/config
|
||||
|
||||
RUN apk add --no-cache curl tar unzip nginx openssl xz
|
||||
EXPOSE 80/tcp 34197/udp
|
||||
|
||||
WORKDIR /opt/
|
||||
RUN apk add --no-cache curl tar xz unzip jq
|
||||
|
||||
WORKDIR /opt
|
||||
|
||||
# Install Factorio
|
||||
RUN curl -s -L -S -k https://www.factorio.com/get-download/$FACTORIO_VERSION/headless/linux64 -o /tmp/factorio_$FACTORIO_VERSION.tar.xz && \
|
||||
tar Jxf /tmp/factorio_$FACTORIO_VERSION.tar.xz && \
|
||||
rm /tmp/factorio_$FACTORIO_VERSION.tar.xz && \
|
||||
curl -sLSk https://github.com/OpenFactorioServerManager/factorio-server-manager/releases/download/$MANAGER_VERSION/factorio-server-manager-linux-$MANAGER_VERSION.zip \
|
||||
--cacert /opt/github.pem -o /tmp/factorio-server-manager-linux_$MANAGER_VERSION.zip && \
|
||||
unzip -qq /tmp/factorio-server-manager-linux_$MANAGER_VERSION.zip && \
|
||||
rm /tmp/factorio-server-manager-linux_$MANAGER_VERSION.zip && \
|
||||
|
||||
# Install FSM
|
||||
RUN curl --location "https://github.com/OpenFactorioServerManager/factorio-server-manager/releases/download/$MANAGER_VERSION/factorio-server-manager-linux-$MANAGER_VERSION.zip" \
|
||||
--output /tmp/factorio-server-manager-linux_${MANAGER_VERSION}.zip \
|
||||
&& unzip /tmp/factorio-server-manager-linux_${MANAGER_VERSION}.zip \
|
||||
&& rm /tmp/factorio-server-manager-linux_${MANAGER_VERSION}.zip \
|
||||
&& mv factorio-server-manager fsm
|
||||
|
||||
# Setup Nginx
|
||||
mkdir -p /run/nginx && \
|
||||
chown nginx:root /var/lib/nginx
|
||||
|
||||
COPY "init.sh" "/opt/init.sh"
|
||||
COPY "nginx.conf" "/etc/nginx/nginx.conf"
|
||||
|
||||
EXPOSE 80/tcp 443/tcp 34190-34200/udp
|
||||
COPY ./entrypoint.sh /opt/entrypoint.sh
|
||||
|
||||
ENTRYPOINT ["/opt/init.sh"]
|
||||
ENTRYPOINT ["/opt/entrypoint.sh"]
|
@ -1,20 +1,23 @@
|
||||
FROM alpine:latest
|
||||
FROM alpine:latest as build
|
||||
|
||||
RUN apk add --no-cache git make musl-dev go nodejs npm zip
|
||||
|
||||
ENV FAC_BRANCH=develop
|
||||
ENV FACTORIO_BRANCH=develop
|
||||
ENV GOROOT /usr/lib/go
|
||||
ENV GOPATH /go
|
||||
ENV PATH /go/bin:$PATH
|
||||
ENV FAC_ROOT /go/src/factorio-server-manager
|
||||
ENV FACTORIO_ROOT /go/src/factorio-server-manager
|
||||
|
||||
COPY build.sh /usr/local/bin/build.sh
|
||||
COPY build-release.sh /usr/local/bin/build-release.sh
|
||||
|
||||
RUN mkdir -p ${GOPATH}/bin
|
||||
RUN chmod u+x /usr/local/bin/build.sh
|
||||
RUN chmod u+x /usr/local/bin/build-release.sh
|
||||
|
||||
WORKDIR $FAC_ROOT
|
||||
WORKDIR $FACTORIO_ROOT
|
||||
|
||||
VOLUME /build
|
||||
|
||||
CMD ["/usr/local/bin/build.sh"]
|
||||
RUN ["/usr/local/bin/build-release.sh"]
|
||||
|
||||
FROM scratch as output
|
||||
COPY --from=build ${FACTORIO_ROOT}/build/* .
|
||||
|
26
docker/Dockerfile-local
Normal file
26
docker/Dockerfile-local
Normal file
@ -0,0 +1,26 @@
|
||||
# Glibc is required for Factorio Server binaries to run
|
||||
FROM frolvlad/alpine-glibc
|
||||
|
||||
ENV FACTORIO_VERSION=latest \
|
||||
ADMIN_USER=admin \
|
||||
ADMIN_PASS=factorio \
|
||||
RCON_PASS="" \
|
||||
COOKIE_ENCRYPTION_KEY=""
|
||||
|
||||
VOLUME /opt/fsm-data /opt/factorio/saves /opt/factorio/mods /opt/factorio/config
|
||||
|
||||
EXPOSE 80/tcp 34197/udp
|
||||
|
||||
RUN apk add --no-cache curl tar xz unzip jq
|
||||
|
||||
WORKDIR /opt
|
||||
|
||||
# Install FSM
|
||||
COPY factorio-server-manager-linux.zip /factorio-server-manager-linux.zip
|
||||
RUN unzip /factorio-server-manager-linux.zip \
|
||||
&& rm /factorio-server-manager-linux.zip \
|
||||
&& mv factorio-server-manager fsm
|
||||
|
||||
COPY entrypoint.sh /opt
|
||||
|
||||
ENTRYPOINT ["/opt/entrypoint.sh"]
|
@ -1,27 +0,0 @@
|
||||
# Variables can be overridden by setting environment variables
|
||||
|
||||
FACTORIO_PATH ?= ~/.factorio
|
||||
SECURITY_PATH ?= $(FACTORIO_PATH)/security
|
||||
SAVES_PATH ?= $(FACTORIO_PATH)/saves
|
||||
MODS_PATH ?= $(FACTORIO_PATH)/mods
|
||||
PORT_FORWARD ?= -p 80:80 -p 443:443 -p 34197:34197/udp
|
||||
FACTORIO_BRANCH ?= develop
|
||||
|
||||
build:
|
||||
docker build --build-arg FAC_BRANCH=$FACTORIO_BRANCH -f Dockerfile-build -t fsm-build .
|
||||
docker build -t factorio-server-manager .
|
||||
|
||||
logs:
|
||||
docker logs factorio-server -f
|
||||
|
||||
run:
|
||||
docker run -d --name factorio-server -v $(SECURITY_PATH):/security -v $(SAVES_PATH):/opt/factorio/saves -v $(MODS_PATH):/opt/factorio/mods $(PORT_FORWARD) factorio-server-manager
|
||||
|
||||
stop:
|
||||
docker stop factorio-server
|
||||
docker rm factorio-server
|
||||
|
||||
clean:
|
||||
docker rmi factorio-server-manager
|
||||
docker stop fsm-build
|
||||
docker rmi fsm-build
|
@ -1,40 +1,64 @@
|
||||
# Factorio Server Manager Docker Image
|
||||
|
||||
## Prerequisites
|
||||
You need to have [Docker](https://www.digitalocean.com/community/tutorials/how-to-install-and-use-docker-on-ubuntu-20-04)
|
||||
and [Docker Compose](https://docs.docker.com/compose/install/) installed.
|
||||
|
||||
## Getting started?
|
||||
|
||||
Pull the Docker container from Docker Hub using the pull command
|
||||
Copy `docker-compose.yaml` and `.env` files from this repository to somewhere on your server.
|
||||
|
||||
```
|
||||
docker pull majormjr/factorio-server-manager
|
||||
```
|
||||
Edit values in the `.env` file:
|
||||
* `ADMIN_USER` (default `admin`): Name of the default user created for FSM UI.
|
||||
* `ADMIN_PASS` (default `factorio`): Default user password. \
|
||||
__Important:__ _For security reasons, please change the default user name and password. Never use the defaults._
|
||||
* `RCON_PASS` (default empty string): Password for Factorio RCON (FSM uses it to communicate with the Factorio server). \
|
||||
If left empty, a random password will be generated and saved on the first start of the server. You can see the password in `fsm-data/conf.json` file.
|
||||
* `COOKIE_ENCRYPTION_KEY` (default empty string): The key used to encrypt auth cookie for FSM UI. \
|
||||
If left empty, a random key will be generated and saved on the first start of the server. You can see the key in `fsm-data/conf.json` file.
|
||||
* `DOMAIN_NAME` (must be set manually): The domain name where your FSM UI will be available. Must be set,
|
||||
so [Let's Encrypt](https://letsencrypt.org/) service can issue a valid HTTPS certificate for this domain.
|
||||
* `EMAIL_ADDRESS` (must be set manually): Your email address. Used only by Let's Encrypt service.
|
||||
|
||||
Alternatively you can ignore `.env` file and edit this values directly in `environment` section of `docker-compose.yaml`.
|
||||
But remember that if `.env` file is present, values set there take precedence over values set in `docker-compose.yaml`.
|
||||
|
||||
Now you can start the container by running:
|
||||
|
||||
```
|
||||
docker run --name factorio-manager -d \
|
||||
-p 80:80 \
|
||||
-p 443:443 \
|
||||
-p 34197:34197/udp \
|
||||
majormjr/factorio-server-manager
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
If you want persistent data in your container also mount the data volumes when starting:
|
||||
### Simple configuration without HTTPS
|
||||
|
||||
If you don't care about HTTPS and want to run just the Factorio Server Manager, or want to run it on local machine you can use `docker-compose.simple.yaml`.
|
||||
|
||||
Ignore `DOMAIN_NAME` and `EMAIL_ADDREESS` variables in `.env` file and run
|
||||
```
|
||||
docker run --name factorio-manager -d \
|
||||
-v [yourpath]:/opt/factorio/saves \
|
||||
-v [yourpath]:/opt/factorio/mods \
|
||||
-v [yourpath]:/opt/factorio/config \
|
||||
-v [yourpath]:/security \
|
||||
-p 80:80 \
|
||||
-p 443:443 \
|
||||
-p 34197:34197/udp \
|
||||
majormjr/factorio-server-manager
|
||||
docker-compose -f docker-compose.simple.yaml up -d
|
||||
```
|
||||
|
||||
### Factorio version
|
||||
|
||||
By default container will download the latest version of factorio. If you want to use specific version, you can change
|
||||
the value of `FACTORIO_VERSION=latest` variable in the `docker-compose.yaml` file.
|
||||
|
||||
## Accessing the application
|
||||
|
||||
Go to the port specified in your `docker run` command in your web browser. If running on localhost host access the application at https://localhost
|
||||
Go to the domain specified in your `.env` file in your web browser. If running on localhost host access the application at http://localhost
|
||||
|
||||
### First start
|
||||
|
||||
When container starts it begins to dowload Factorio headless server archive, and only after that Factorio Server Manager server starts.
|
||||
So when Docker Compose writes
|
||||
```
|
||||
Creating factorio-server-manager ... done
|
||||
```
|
||||
you have to wait several seconds before FSM UI becomes available.
|
||||
|
||||
It may take some time for Let's Encrypt to issue the certificate, so for the first couple of minutes after starting the container you may see
|
||||
"Your connection is not private" error when you open your Factorio Server Manager address in your browser. This error should disappear within
|
||||
a couple of minutes, if configuration parameters are set correctly.
|
||||
|
||||
## Updating Credentials, adding and deleting users.
|
||||
|
||||
@ -48,25 +72,26 @@ For now you can't update/downgrade the Factorio version from the UI.
|
||||
|
||||
You can however do this using docker images while sustaining your security settings and map/modfiles.
|
||||
|
||||
This guide assumes that you mounted the volumes /security, /opt/factorio/saves, /opt/factorio/config and /opt/factorio/mods to your file system. Before doing anything we need to stop the old container using `docker stop factorio-manager`. To update Factorio you should then open the Dockerfile and change the Factorio version to the one desired. After that you need to rebuild the image using `docker build -t factorio-server-manager .`. Once completed you can simply rerun the command that you used to run the image in the first place. It's recommended to change the name to something including the version to keep track of the containers.
|
||||
If you want to update Factorio to the latest version:
|
||||
1. Save your game and stop Factorio server in FSM UI.
|
||||
2. Run `docker-compose restart` (or `docker-compose -f docker-compose.simple.yaml restart` if you are using simple configuration).
|
||||
|
||||
Pull the latest container with `docker pull majormjr/factorio-server-manager` and start with the `docker run` command.
|
||||
After container starts, latest Factorio version will be downloaded and installed.
|
||||
|
||||
## Security
|
||||
|
||||
A self generated SSL/TLS certificate is created when the container is first created and the webserver is accessible via HTTPS.
|
||||
|
||||
Authentication is supported in the application but it is recommended to ensure access to the Factorio manager UI is accessible via VPN or internal network.
|
||||
|
||||
### Changing SSL/TLS certificate
|
||||
## Development
|
||||
For development purposes it also has the ability to create the docker image from local sourcecode. This is done by running `build.sh` in the `docker` directory. This will delete all old executables and the node_modules directory (runs `make build`). The created docker image will have the tag `factorio-server-manager:dev`.
|
||||
|
||||
If you have your own SSL/TLS certificate then you can supply it to the Factorio Server Manager container.
|
||||
### Creating release bundles
|
||||
A Dockerfile-build file is included for creating the release bundles.
|
||||
|
||||
When first running the container you need to mount the security volume to your host machine by adding the security volume parameter `-v [yourpath]:/security`
|
||||
|
||||
The directory will contain a "server.key" file and a "server.crt" file.
|
||||
|
||||
If you replace these with a trusted SSL certificate and key, you should ensure that "server.crt" contains the whole certificate chain from the root of your CA.
|
||||
To create the bundle build the Dockerfile-build file with the following command. The release bundles are output to the ./dist directory.
|
||||
```
|
||||
DOCKER_BUILDKIT=1 docker build --no-cache -f Dockerfile-build -t ofsm-build --target=output -o dist .
|
||||
```
|
||||
|
||||
## For everyone who actually read this thing to the end
|
||||
|
||||
|
9
docker/build-release.sh
Executable file
9
docker/build-release.sh
Executable file
@ -0,0 +1,9 @@
|
||||
#!/bin/sh
|
||||
|
||||
echo "Cloning ${FACTORIO_BRANCH}"
|
||||
git clone -b ${FACTORIO_BRANCH} https://github.com/mroote/factorio-server-manager.git ${FACTORIO_ROOT}
|
||||
echo "Creating build..."
|
||||
make gen_release
|
||||
echo "Copying build artifacts from ${PWD}"
|
||||
mkdir -p /build
|
||||
cp -v build/factorio-server-manager-linux.zip build/factorio-server-manager-windows.zip /build/
|
@ -1,8 +1,10 @@
|
||||
#!/bin/sh
|
||||
#!/bin/bash
|
||||
set -eou pipefail
|
||||
(
|
||||
cd ..
|
||||
make build
|
||||
cp build/factorio-server-manager-linux.zip docker/factorio-server-manager-linux.zip
|
||||
)
|
||||
docker build -f Dockerfile-local -t factorio-server-manager:dev .
|
||||
|
||||
echo "Cloning ${FAC_BRANCH}"
|
||||
git clone -b ${FAC_BRANCH} https://github.com/mroote/factorio-server-manager.git ${FAC_ROOT}
|
||||
echo "Creating build..."
|
||||
make gen_release
|
||||
echo "Copying build artifacts..."
|
||||
cp -v build/* /build/
|
||||
rm factorio-server-manager-linux.zip
|
||||
|
20
docker/docker-compose.simple.yaml
Normal file
20
docker/docker-compose.simple.yaml
Normal file
@ -0,0 +1,20 @@
|
||||
version: "3"
|
||||
services:
|
||||
factorio-server-manager:
|
||||
image: "ofsm/ofsm:develop"
|
||||
container_name: "factorio-server-manager"
|
||||
restart: "unless-stopped"
|
||||
environment:
|
||||
- "FACTORIO_VERSION=latest"
|
||||
- "ADMIN_USER"
|
||||
- "ADMIN_PASS"
|
||||
- "RCON_PASS"
|
||||
- "COOKIE_ENCRYPTION_KEY"
|
||||
ports:
|
||||
- "80:80"
|
||||
- "34197:34197/udp"
|
||||
volumes:
|
||||
- "./fsm-data:/opt/fsm-data"
|
||||
- "./factorio-data/saves:/opt/factorio/saves"
|
||||
- "./factorio-data/mods:/opt/factorio/mods"
|
||||
- "./factorio-data/config:/opt/factorio/config"
|
65
docker/docker-compose.yaml
Normal file
65
docker/docker-compose.yaml
Normal file
@ -0,0 +1,65 @@
|
||||
version: "3"
|
||||
services:
|
||||
factorio-server-manager:
|
||||
image: "ofsm/ofsm:develop"
|
||||
container_name: "factorio-server-manager"
|
||||
restart: "unless-stopped"
|
||||
environment:
|
||||
- "FACTORIO_VERSION=latest"
|
||||
- "ADMIN_USER"
|
||||
- "ADMIN_PASS"
|
||||
- "RCON_PASS"
|
||||
- "COOKIE_ENCRYPTION_KEY"
|
||||
volumes:
|
||||
- "./fsm-data:/opt/fsm-data"
|
||||
- "./factorio-data/saves:/opt/factorio/saves"
|
||||
- "./factorio-data/mods:/opt/factorio/mods"
|
||||
- "./factorio-data/config:/opt/factorio/config"
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
|
||||
- "traefik.http.routers.fsm.entrypoints=websecure"
|
||||
- "traefik.http.routers.fsm.rule=Host(`${DOMAIN_NAME}`)"
|
||||
- "traefik.http.routers.fsm.tls=true"
|
||||
- "traefik.http.routers.fsm.tls.certResolver=default"
|
||||
- "traefik.http.routers.fsm.service=fsm"
|
||||
#- "traefik.http.routers.fsm.middlewares=fsm-auth"
|
||||
- "traefik.http.services.fsm.loadbalancer.server.port=80"
|
||||
|
||||
- "traefik.udp.routers.fsm.entrypoints=factorio"
|
||||
- "traefik.udp.routers.fsm.service=fsm"
|
||||
- "traefik.udp.services.fsm.loadbalancer.server.port=34197"
|
||||
traefik:
|
||||
image: "traefik:v2.2"
|
||||
container_name: "traefik"
|
||||
restart: "always"
|
||||
command:
|
||||
- "--entrypoints.web.address=:80"
|
||||
- "--entrypoints.websecure.address=:443"
|
||||
- "--entrypoints.factorio.address=:34197/udp"
|
||||
|
||||
- "--entrypoints.web.http.redirections.entryPoint.to=websecure"
|
||||
- "--entrypoints.web.http.redirections.entryPoint.scheme=https"
|
||||
|
||||
- "--providers.docker"
|
||||
- "--providers.docker.exposedByDefault=false"
|
||||
|
||||
- "--certificatesresolvers.default.acme.email=${EMAIL_ADDRESS}"
|
||||
- "--certificatesresolvers.default.acme.storage=/etc/traefik/acme.json"
|
||||
- "--certificatesresolvers.default.acme.tlschallenge=true"
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
- "34197:34197/udp"
|
||||
volumes:
|
||||
- "/var/run/docker.sock:/var/run/docker.sock"
|
||||
- "./traefik-data:/etc/traefik"
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
|
||||
#- "traefik.http.middlewares.fsm-auth.basicauth.usersfile=/etc/traefik/.htpasswd"
|
||||
#- "traefik.http.middlewares.fsm-auth.basicauth.realm=FSM"
|
||||
#networks:
|
||||
# default:
|
||||
# external:
|
||||
# name: "traefik"
|
@ -1,17 +0,0 @@
|
||||
version: '2'
|
||||
services:
|
||||
factorio-manager:
|
||||
container_name: factorio-manager
|
||||
image: "majormjr/factorio-server-manager"
|
||||
restart: always
|
||||
volumes:
|
||||
- "/etc/localtime:/etc/localtime:ro"
|
||||
- "/etc/timezone:/etc/timezone:ro"
|
||||
- "[yourPath_optional]:/security"
|
||||
- "[yourPath]:/opt/factorio/saves"
|
||||
- "[yourPath]:/opt/factorio/mods"
|
||||
- "[yourPath]:/opt/factorio/config"
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
- "34197:34197/udp"
|
51
docker/entrypoint.sh
Executable file
51
docker/entrypoint.sh
Executable file
@ -0,0 +1,51 @@
|
||||
#!/bin/sh
|
||||
|
||||
init_config() {
|
||||
jq_cmd='.'
|
||||
|
||||
if [ -n $ADMIN_USER ]; then
|
||||
jq_cmd="${jq_cmd} | .username = \"$ADMIN_USER\""
|
||||
echo "Admin username is '$ADMIN_USER'"
|
||||
fi
|
||||
if [ -n $ADMIN_PASS ]; then
|
||||
jq_cmd="${jq_cmd} | .password = \"$ADMIN_PASS\""
|
||||
echo "Admin password is '$ADMIN_PASS'"
|
||||
fi
|
||||
echo "IMPORTANT! Please create new user and delete default admin user ASAP."
|
||||
|
||||
if [ -z $RCON_PASS ]; then
|
||||
RCON_PASS="$(random_pass)"
|
||||
fi
|
||||
jq_cmd="${jq_cmd} | .rcon_pass = \"$RCON_PASS\""
|
||||
echo "Factorio rcon password is '$RCON_PASS'"
|
||||
|
||||
if [ -z $COOKIE_ENCRYPTION_KEY ]; then
|
||||
COOKIE_ENCRYPTION_KEY="$(random_pass)"
|
||||
fi
|
||||
jq_cmd="${jq_cmd} | .cookie_encryption_key = \"$COOKIE_ENCRYPTION_KEY\""
|
||||
|
||||
jq_cmd="${jq_cmd} | .database_file = \"/opt/fsm-data/auth.leveldb\""
|
||||
jq_cmd="${jq_cmd} | .log_file = \"/opt/fsm-data/factorio-server-manager.log\""
|
||||
|
||||
jq "${jq_cmd}" /opt/fsm/conf.json >/opt/fsm-data/conf.json
|
||||
}
|
||||
|
||||
random_pass() {
|
||||
LC_ALL=C tr -dc 'a-zA-Z0-9' </dev/urandom | fold -w 24 | head -n 1
|
||||
}
|
||||
|
||||
install_game() {
|
||||
curl --location "https://www.factorio.com/get-download/${FACTORIO_VERSION}/headless/linux64" \
|
||||
--output /tmp/factorio_${FACTORIO_VERSION}.tar.xz
|
||||
tar -xf /tmp/factorio_${FACTORIO_VERSION}.tar.xz
|
||||
rm /tmp/factorio_${FACTORIO_VERSION}.tar.xz
|
||||
}
|
||||
|
||||
if [ ! -f /opt/fsm-data/conf.json ]; then
|
||||
init_config
|
||||
fi
|
||||
|
||||
install_game
|
||||
|
||||
cd /opt/fsm && ./factorio-server-manager --conf /opt/fsm-data/conf.json --dir /opt/factorio -port 80
|
||||
|
@ -1,18 +0,0 @@
|
||||
#!/bin/sh
|
||||
mkdir -p /security
|
||||
if [ ! -f /security/server.key ]; then
|
||||
echo "No SSL key found. generating new key and certificate"
|
||||
openssl req \
|
||||
-new \
|
||||
-newkey rsa:2048 \
|
||||
-days 365 \
|
||||
-nodes\
|
||||
-x509 \
|
||||
-subj "/CN=localhost" \
|
||||
-keyout /security/server.key \
|
||||
-out /security/server.crt
|
||||
fi
|
||||
|
||||
nohup nginx &
|
||||
cd /opt/factorio-server-manager
|
||||
./factorio-server-manager -dir '/opt/factorio'
|
@ -1,74 +0,0 @@
|
||||
user nginx;
|
||||
worker_processes 1;
|
||||
|
||||
error_log logs/error.log warn;
|
||||
pid run/nginx.pid;
|
||||
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
|
||||
http {
|
||||
include mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
sendfile on;
|
||||
#tcp_nopush on;
|
||||
|
||||
keepalive_timeout 65;
|
||||
client_max_body_size 100m;
|
||||
#gzip on;
|
||||
|
||||
upstream goapp {
|
||||
server 127.0.0.1:8080;
|
||||
}
|
||||
server {
|
||||
listen 80 default_server;
|
||||
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
server {
|
||||
listen 443 default_server;
|
||||
|
||||
ssl on;
|
||||
ssl_certificate /security/server.crt;
|
||||
ssl_certificate_key /security/server.key;
|
||||
|
||||
ssl_session_timeout 5m;
|
||||
|
||||
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
|
||||
ssl_ciphers "HIGH:!aNULL:!MD5 or HIGH:!aNULL:!MD5:!3DES";
|
||||
ssl_prefer_server_ciphers on;
|
||||
|
||||
location /ws {
|
||||
proxy_pass http://goapp;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
proxy_set_header X-Forwarded-Server $host;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "Upgrade";
|
||||
proxy_redirect off;
|
||||
}
|
||||
|
||||
location /api {
|
||||
proxy_pass http://goapp;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
proxy_set_header X-Forwarded-Server $host;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_redirect off;
|
||||
}
|
||||
|
||||
location / {
|
||||
root /opt/factorio-server-manager/app;
|
||||
try_files $uri /index.html;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
8672
package-lock.json
generated
Normal file
8672
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
43
package.json
43
package.json
@ -10,36 +10,34 @@
|
||||
"watch-poll": "npm run watch -- --watch-poll",
|
||||
"build": "npm run production",
|
||||
"prod": "npm run production",
|
||||
"production": "webpack --config=webpack.config.js --mode=production --hide-modules --progress",
|
||||
"production": "cross-env NODE_ENV=production webpack --config=webpack.config.js --mode=production --hide-modules --progress",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/mroote/factorio-server-manager.git"
|
||||
"url": "git+https://github.com/OpenFactorioServerManager/factorio-server-manager.git"
|
||||
},
|
||||
"author": "Mitch Roote <mitch@r00t.ca>",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/mroote/factorio-server-manager/issues"
|
||||
"url": "https://github.com/OpenFactorioServerManager/factorio-server-manager/issues"
|
||||
},
|
||||
"homepage": "https://github.com/mroote/factorio-server-manager#readme",
|
||||
"homepage": "https://github.com/OpenFactorioServerManager/factorio-server-manager#readme",
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.4.5",
|
||||
"@babel/preset-env": "^7.4.5",
|
||||
"@babel/preset-react": "^7.0.0",
|
||||
"@fortawesome/fontawesome-free": "^5.8.2",
|
||||
"admin-lte": "^3.0.0-alpha.2",
|
||||
"@babel/core": "^7.11.6",
|
||||
"@babel/preset-env": "^7.11.5",
|
||||
"@babel/preset-react": "^7.10.4",
|
||||
"autoprefixer": "^9.8.6",
|
||||
"babel-loader": "^8.0.6",
|
||||
"bootstrap": "^4.3.1",
|
||||
"bootstrap-fileinput": "^5.0.3",
|
||||
"classnames": "^2.2.6",
|
||||
"cross-env": "^7.0.2",
|
||||
"css-loader": "^2.1.0",
|
||||
"file-loader": "^3.0.1",
|
||||
"jquery": "^3.4.1",
|
||||
"locks": "^0.2.2",
|
||||
"mini-css-extract-plugin": "^0.7.0",
|
||||
"node-sass": "^4.12.0",
|
||||
"optimize-css-assets-webpack-plugin": "^5.0.1",
|
||||
"optimize-css-assets-webpack-plugin": "^5.0.4",
|
||||
"postcss-loader": "^3.0.0",
|
||||
"prop-types": "^15.7.2",
|
||||
"react": "^16.8.6",
|
||||
"react-dom": "^16.8.6",
|
||||
@ -48,11 +46,20 @@
|
||||
"react-router-dom": "^5.0.0",
|
||||
"resolve-url-loader": "^3.1.0",
|
||||
"sass-loader": "^7.1.0",
|
||||
"semver": "^6.1.1",
|
||||
"sweetalert2": "^8.11.6",
|
||||
"sweetalert2-react-content": "^1.1.0",
|
||||
"webpack": "^4.32.2",
|
||||
"webpack-cli": "^3.3.2",
|
||||
"style-loader": "^1.3.0",
|
||||
"webpack": "^4.44.2",
|
||||
"webpack-cli": "^3.3.12",
|
||||
"webpack-fix-style-only-entries": "^0.2.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "^1.2.32",
|
||||
"@fortawesome/free-solid-svg-icons": "^5.15.1",
|
||||
"@fortawesome/react-fontawesome": "^0.1.11",
|
||||
"axios": "^0.19.2",
|
||||
"fuse.js": "^6.4.1",
|
||||
"react-hook-form": "^5.7.2",
|
||||
"regenerator-runtime": "^0.13.7",
|
||||
"semver": "^6.1.1",
|
||||
"tailwindcss": "^1.8.13"
|
||||
}
|
||||
}
|
||||
|
BIN
screenshots/Screenshot_Controls.png
Normal file
BIN
screenshots/Screenshot_Controls.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 32 KiB |
BIN
screenshots/Screenshot_Mods.png
Normal file
BIN
screenshots/Screenshot_Mods.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 55 KiB |
BIN
screenshots/Screenshot_Saves.png
Normal file
BIN
screenshots/Screenshot_Saves.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 48 KiB |
5
src/.env.example
Normal file
5
src/.env.example
Normal file
@ -0,0 +1,5 @@
|
||||
factorio_username=
|
||||
factorio_password=
|
||||
conf=../../conf.json.example
|
||||
mod_dir=dev
|
||||
mod_pack_dir=dev_pack
|
@ -1,8 +1,10 @@
|
||||
package main
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/mroote/factorio-server-manager/bootstrap"
|
||||
"log"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"github.com/apexskier/httpauth"
|
||||
)
|
||||
@ -19,8 +21,18 @@ type User struct {
|
||||
Email string `json:"email"`
|
||||
}
|
||||
|
||||
func initAuth() *AuthHTTP {
|
||||
return &AuthHTTP{}
|
||||
var once sync.Once
|
||||
var instantiated *AuthHTTP
|
||||
|
||||
func GetAuth() *AuthHTTP {
|
||||
once.Do(func() {
|
||||
Auth := &AuthHTTP{}
|
||||
config := bootstrap.GetConfig()
|
||||
_ = Auth.CreateAuth(config.DatabaseFile, config.CookieEncryptionKey)
|
||||
_ = Auth.CreateOrUpdateUser(config.Username, config.Password, "admin", "")
|
||||
instantiated = Auth
|
||||
})
|
||||
return instantiated
|
||||
}
|
||||
|
||||
func (auth *AuthHTTP) CreateAuth(backendFile string, cookieKey string) error {
|
659
src/api/handlers.go
Normal file
659
src/api/handlers.go
Normal file
@ -0,0 +1,659 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/mroote/factorio-server-manager/bootstrap"
|
||||
"github.com/mroote/factorio-server-manager/factorio"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
const readHttpBodyError = "Could not read the Request Body."
|
||||
|
||||
type JSONResponseFileInput struct {
|
||||
Success bool `json:"success"`
|
||||
Data interface{} `json:"data,string"`
|
||||
Error string `json:"error"`
|
||||
ErrorKeys []int `json:"errorkeys"`
|
||||
}
|
||||
|
||||
func WriteResponse(w http.ResponseWriter, data interface{}) {
|
||||
if err := json.NewEncoder(w).Encode(data); err != nil {
|
||||
log.Printf("Error writing response: %s", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
func ReadRequestBody(w http.ResponseWriter, r *http.Request, resp *interface{}) (body []byte, err error) {
|
||||
if r.Body == nil {
|
||||
*resp = fmt.Sprintf("%s: no request body", readHttpBodyError)
|
||||
log.Println(*resp)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return nil, errors.New("no request body")
|
||||
}
|
||||
|
||||
body, err = ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
*resp = fmt.Sprintf("%s: %s", readHttpBodyError, err)
|
||||
log.Println(*resp)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Lists all save files in the factorio/saves directory
|
||||
func ListSaves(w http.ResponseWriter, r *http.Request) {
|
||||
var resp interface{}
|
||||
config := bootstrap.GetConfig()
|
||||
defer func() {
|
||||
WriteResponse(w, resp)
|
||||
}()
|
||||
|
||||
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
|
||||
|
||||
savesList, err := factorio.ListSaves(config.FactorioSavesDir)
|
||||
if err != nil {
|
||||
resp = fmt.Sprintf("Error listing save files: %s", err)
|
||||
log.Println(resp)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
loadLatest := factorio.Save{Name: "Load Latest"}
|
||||
savesList = append(savesList, loadLatest)
|
||||
|
||||
resp = savesList
|
||||
}
|
||||
|
||||
func DLSave(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/octet-stream")
|
||||
config := bootstrap.GetConfig()
|
||||
vars := mux.Vars(r)
|
||||
save := vars["save"]
|
||||
saveName := filepath.Join(config.FactorioSavesDir, save)
|
||||
|
||||
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", save))
|
||||
log.Printf("%s downloading: %s", r.Host, saveName)
|
||||
|
||||
http.ServeFile(w, r, saveName)
|
||||
}
|
||||
|
||||
func UploadSave(w http.ResponseWriter, r *http.Request) {
|
||||
var resp interface{}
|
||||
|
||||
defer func() {
|
||||
WriteResponse(w, resp)
|
||||
}()
|
||||
|
||||
log.Println("Uploading save file")
|
||||
|
||||
r.ParseMultipartForm(32 << 20)
|
||||
config := bootstrap.GetConfig()
|
||||
|
||||
for _, saveFile := range r.MultipartForm.File["savefile"] {
|
||||
ext := filepath.Ext(saveFile.Filename)
|
||||
if ext != "zip" {
|
||||
// Only zip-files allowed
|
||||
resp = fmt.Sprintf("Fileformat {%s} is not allowed", ext)
|
||||
w.WriteHeader(http.StatusUnsupportedMediaType)
|
||||
}
|
||||
|
||||
file, err := saveFile.Open()
|
||||
if err != nil {
|
||||
resp = fmt.Sprintf("Error opening uploaded saveFile: %s", err)
|
||||
log.Println(resp)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
out, err := os.Create(filepath.Join(config.FactorioSavesDir, saveFile.Filename))
|
||||
if err != nil {
|
||||
resp = fmt.Sprintf("Error creating new savefile to copy uploaded on to: %s", err)
|
||||
log.Println(resp)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
_, err = io.Copy(out, file)
|
||||
if err != nil {
|
||||
resp = fmt.Sprintf("Error coping uploaded file to created file on disk: %s", err)
|
||||
log.Println(resp)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
resp = "Uploading files successful"
|
||||
}
|
||||
|
||||
// Deletes provided save
|
||||
func RemoveSave(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
var resp interface{}
|
||||
|
||||
defer func() {
|
||||
WriteResponse(w, resp)
|
||||
}()
|
||||
|
||||
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
|
||||
|
||||
vars := mux.Vars(r)
|
||||
name := vars["save"]
|
||||
|
||||
save, err := factorio.FindSave(name)
|
||||
if err != nil {
|
||||
resp = fmt.Sprintf("Error finding save {%s}: %s", name, err)
|
||||
log.Println(resp)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
err = save.Remove()
|
||||
if err != nil {
|
||||
resp = fmt.Sprintf("Error removing save {%s}: %s", name, err)
|
||||
log.Println(resp)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// save was removed
|
||||
resp = fmt.Sprintf("Removed save: %s", save.Name)
|
||||
}
|
||||
|
||||
// Launches Factorio server binary with --create flag to create save
|
||||
// Url must include save name for creation of savefile
|
||||
func CreateSaveHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
var resp interface{}
|
||||
|
||||
defer func() {
|
||||
WriteResponse(w, resp)
|
||||
}()
|
||||
|
||||
vars := mux.Vars(r)
|
||||
saveName := vars["save"]
|
||||
|
||||
if saveName == "" {
|
||||
resp = fmt.Sprintf("Error creating save, no save name provided: %s", err)
|
||||
log.Println(resp)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
config := bootstrap.GetConfig()
|
||||
saveFile := filepath.Join(config.FactorioSavesDir, saveName)
|
||||
cmdOut, err := factorio.CreateSave(saveFile)
|
||||
if err != nil {
|
||||
resp = fmt.Sprintf("Error creating save {%s}: %s", saveName, err)
|
||||
log.Println(resp)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
resp = fmt.Sprintf("Save %s created successfully. Command output: \n%s", saveName, cmdOut)
|
||||
}
|
||||
|
||||
// LogTail returns last lines of the factorio-current.log file
|
||||
func LogTail(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
var resp interface{}
|
||||
|
||||
defer func() {
|
||||
WriteResponse(w, resp)
|
||||
}()
|
||||
|
||||
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
|
||||
config := bootstrap.GetConfig()
|
||||
resp, err = factorio.TailLog()
|
||||
if err != nil {
|
||||
resp = fmt.Sprintf("Could not tail %s: %s", config.FactorioLog, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// LoadConfig returns JSON response of config.ini file
|
||||
func LoadConfig(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
var resp interface{}
|
||||
|
||||
defer func() {
|
||||
WriteResponse(w, resp)
|
||||
}()
|
||||
|
||||
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
|
||||
config := bootstrap.GetConfig()
|
||||
configContents, err := factorio.LoadConfig(config.FactorioConfigFile)
|
||||
if err != nil {
|
||||
resp = fmt.Sprintf("Could not retrieve config.ini: %s", err)
|
||||
log.Println(resp)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
resp = configContents
|
||||
|
||||
log.Printf("Sent config.ini response")
|
||||
}
|
||||
|
||||
func StartServer(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
var resp interface{}
|
||||
var server = factorio.GetFactorioServer()
|
||||
defer func() {
|
||||
WriteResponse(w, resp)
|
||||
}()
|
||||
|
||||
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
|
||||
|
||||
if server.GetRunning() {
|
||||
resp = "Factorio server is already running"
|
||||
w.WriteHeader(http.StatusConflict)
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("Starting Factorio server.")
|
||||
|
||||
body, err := ReadRequestBody(w, r, &resp)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("Starting Factorio server with settings: %v", string(body))
|
||||
|
||||
err = json.Unmarshal(body, &server)
|
||||
if err != nil {
|
||||
resp = fmt.Sprintf("Error unmarshalling server settings JSON: %s", err)
|
||||
log.Println(resp)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if savefile was submitted with request to start server.
|
||||
if server.Savefile == "" {
|
||||
resp = "Error starting Factorio server: No save file provided"
|
||||
log.Println(resp)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
go func() {
|
||||
err = server.Run()
|
||||
if err != nil {
|
||||
log.Printf("Error starting Factorio server: %+v", err)
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
timeout := 0
|
||||
for timeout <= 3 {
|
||||
time.Sleep(1 * time.Second)
|
||||
if server.GetRunning() {
|
||||
log.Printf("Running Factorio server detected")
|
||||
break
|
||||
} else {
|
||||
log.Printf("Did not detect running Factorio server attempt: %+v", timeout)
|
||||
}
|
||||
|
||||
timeout++
|
||||
}
|
||||
|
||||
if server.GetRunning() == false {
|
||||
resp = fmt.Sprintf("Error starting Factorio server: %s", err)
|
||||
log.Println(resp)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
resp = fmt.Sprintf("Factorio server with save: %s started on port: %d", server.Savefile, server.Port)
|
||||
log.Println(resp)
|
||||
}
|
||||
|
||||
func StopServer(w http.ResponseWriter, r *http.Request) {
|
||||
var resp interface{}
|
||||
|
||||
defer func() {
|
||||
WriteResponse(w, resp)
|
||||
}()
|
||||
|
||||
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
|
||||
var server = factorio.GetFactorioServer()
|
||||
if server.GetRunning() {
|
||||
err := server.Stop()
|
||||
if err != nil {
|
||||
resp = fmt.Sprintf("Error stopping factorio server: %s", err)
|
||||
log.Println(resp)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
resp = fmt.Sprintf("Factorio server stopped")
|
||||
log.Println(resp)
|
||||
} else {
|
||||
resp = "Factorio server is not running"
|
||||
w.WriteHeader(http.StatusConflict)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func KillServer(w http.ResponseWriter, r *http.Request) {
|
||||
var resp interface{}
|
||||
|
||||
defer func() {
|
||||
WriteResponse(w, resp)
|
||||
}()
|
||||
|
||||
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
|
||||
var server = factorio.GetFactorioServer()
|
||||
if server.GetRunning() {
|
||||
err := server.Kill()
|
||||
if err != nil {
|
||||
resp = fmt.Sprintf("Error killing factorio server: %s", err)
|
||||
log.Println(resp)
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("Killed Factorio server.")
|
||||
resp = fmt.Sprintf("Factorio server killed")
|
||||
} else {
|
||||
resp = "Factorio server is not running"
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
}
|
||||
}
|
||||
|
||||
func CheckServer(w http.ResponseWriter, r *http.Request) {
|
||||
resp := map[string]string{}
|
||||
|
||||
defer func() {
|
||||
WriteResponse(w, resp)
|
||||
}()
|
||||
|
||||
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
|
||||
var server = factorio.GetFactorioServer()
|
||||
if server.GetRunning() {
|
||||
resp["status"] = "running"
|
||||
resp["port"] = strconv.Itoa(server.Port)
|
||||
resp["savefile"] = server.Savefile
|
||||
resp["address"] = server.BindIP
|
||||
} else {
|
||||
resp["status"] = "stopped"
|
||||
}
|
||||
}
|
||||
|
||||
func FactorioVersion(w http.ResponseWriter, r *http.Request) {
|
||||
resp := map[string]string{}
|
||||
|
||||
defer func() {
|
||||
WriteResponse(w, resp)
|
||||
}()
|
||||
|
||||
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
|
||||
var server = factorio.GetFactorioServer()
|
||||
resp["version"] = server.Version.String()
|
||||
resp["base_mod_version"] = server.BaseModVersion
|
||||
}
|
||||
|
||||
// Unmarshall the User object from the given bytearray
|
||||
// This function has side effects (it will write to resp and to w, in case of an error)
|
||||
func UnmarshallUserJson(body []byte, resp *interface{}, w http.ResponseWriter) (user User, err error) {
|
||||
err = json.Unmarshal(body, &user)
|
||||
if err != nil {
|
||||
*resp = fmt.Sprintf("Unable to parse the request body: %s", err)
|
||||
log.Println(*resp)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func LoginUser(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
var resp interface{}
|
||||
|
||||
// add resp to the response
|
||||
defer func() {
|
||||
WriteResponse(w, resp)
|
||||
}()
|
||||
|
||||
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
|
||||
|
||||
body, err := ReadRequestBody(w, r, &resp)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
user, err := UnmarshallUserJson(body, &resp, w)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("Logging in user: %s", user.Username)
|
||||
Auth := GetAuth()
|
||||
err = Auth.aaa.Login(w, r, user.Username, user.Password, "/")
|
||||
if err != nil {
|
||||
resp = fmt.Sprintf("Error loggin in user: %s, error: %s", user.Username, err)
|
||||
log.Println(resp)
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("User: %s, logged in successfully", user.Username)
|
||||
}
|
||||
|
||||
func LogoutUser(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
var resp interface{}
|
||||
|
||||
defer func() {
|
||||
WriteResponse(w, resp)
|
||||
}()
|
||||
|
||||
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
|
||||
Auth := GetAuth()
|
||||
if err = Auth.aaa.Logout(w, r); err != nil {
|
||||
log.Printf("Error logging out current user")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
resp = "User logged out successfully."
|
||||
}
|
||||
|
||||
func GetCurrentLogin(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
var resp interface{}
|
||||
|
||||
// add resp to the response
|
||||
defer func() {
|
||||
WriteResponse(w, resp)
|
||||
}()
|
||||
|
||||
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
|
||||
Auth := GetAuth()
|
||||
user, err := Auth.aaa.CurrentUser(w, r)
|
||||
if err != nil {
|
||||
resp = fmt.Sprintf("Error getting user status: %s, error: %s", user.Username, err)
|
||||
log.Println(resp)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
resp = user
|
||||
}
|
||||
|
||||
func ListUsers(w http.ResponseWriter, r *http.Request) {
|
||||
var resp interface{}
|
||||
|
||||
defer func() {
|
||||
WriteResponse(w, resp)
|
||||
}()
|
||||
|
||||
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
|
||||
Auth := GetAuth()
|
||||
users, err := Auth.listUsers()
|
||||
if err != nil {
|
||||
resp = fmt.Sprintf("Error listing users: %s", err)
|
||||
log.Println(resp)
|
||||
return
|
||||
}
|
||||
|
||||
resp = users
|
||||
}
|
||||
|
||||
func AddUser(w http.ResponseWriter, r *http.Request) {
|
||||
var resp interface{}
|
||||
|
||||
defer func() {
|
||||
WriteResponse(w, resp)
|
||||
}()
|
||||
|
||||
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
|
||||
|
||||
body, err := ReadRequestBody(w, r, &resp)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("Adding user: %v", string(body))
|
||||
|
||||
user, err := UnmarshallUserJson(body, &resp, w)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
Auth := GetAuth()
|
||||
err = Auth.addUser(user.Username, user.Password, user.Email, user.Role)
|
||||
if err != nil {
|
||||
resp = fmt.Sprintf("Error in adding user {%s}: %s", user.Username, err)
|
||||
log.Println(resp)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
resp = fmt.Sprintf("User: %s successfully added.", user.Username)
|
||||
}
|
||||
|
||||
func RemoveUser(w http.ResponseWriter, r *http.Request) {
|
||||
var resp interface{}
|
||||
|
||||
defer func() {
|
||||
WriteResponse(w, resp)
|
||||
}()
|
||||
|
||||
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
|
||||
|
||||
body, err := ReadRequestBody(w, r, &resp)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
user, err := UnmarshallUserJson(body, &resp, w)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
Auth := GetAuth()
|
||||
err = Auth.removeUser(user.Username)
|
||||
if err != nil {
|
||||
resp = fmt.Sprintf("Error in removing user {%s}, error: %s", user.Username, err)
|
||||
log.Println(resp)
|
||||
return
|
||||
}
|
||||
|
||||
resp = fmt.Sprintf("User: %s successfully removed.", user.Username)
|
||||
}
|
||||
|
||||
// GetServerSettings returns JSON response of server-settings.json file
|
||||
func GetServerSettings(w http.ResponseWriter, r *http.Request) {
|
||||
var resp interface{}
|
||||
|
||||
defer func() {
|
||||
WriteResponse(w, resp)
|
||||
}()
|
||||
|
||||
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
|
||||
var server = factorio.GetFactorioServer()
|
||||
resp = server.Settings
|
||||
|
||||
log.Printf("Sent server settings response")
|
||||
}
|
||||
|
||||
func UpdateServerSettings(w http.ResponseWriter, r *http.Request) {
|
||||
var resp interface{}
|
||||
|
||||
defer func() {
|
||||
WriteResponse(w, resp)
|
||||
}()
|
||||
|
||||
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
|
||||
|
||||
body, err := ReadRequestBody(w, r, &resp)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
log.Printf("Received settings JSON: %s", body)
|
||||
var server = factorio.GetFactorioServer()
|
||||
|
||||
// Race Condition while unmarshal possible
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
|
||||
go func() {
|
||||
err = json.Unmarshal(body, &server.Settings)
|
||||
wg.Done()
|
||||
}()
|
||||
|
||||
// Wait for unmarshal to avoid race condition
|
||||
wg.Wait()
|
||||
|
||||
if err != nil {
|
||||
resp = fmt.Sprintf("Error unmarhaling server settings JSON: %s", err)
|
||||
log.Println(resp)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
settings, err := json.MarshalIndent(&server.Settings, "", " ")
|
||||
if err != nil {
|
||||
resp = fmt.Sprintf("Failed to marshal server settings: %s", err)
|
||||
log.Println(resp)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
config := bootstrap.GetConfig()
|
||||
err = ioutil.WriteFile(filepath.Join(config.FactorioConfigDir, config.SettingsFile), settings, 0644)
|
||||
if err != nil {
|
||||
resp = fmt.Sprintf("Failed to save server settings: %v\n", err)
|
||||
log.Println(resp)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("Saved Factorio server settings in server-settings.json")
|
||||
|
||||
if (server.Version.Greater(factorio.Version{0, 17, 0})) {
|
||||
// save admins to adminJson
|
||||
admins, err := json.MarshalIndent(server.Settings["admins"], "", " ")
|
||||
if err != nil {
|
||||
resp = fmt.Sprintf("Failed to marshal admins-Setting: %s", err)
|
||||
log.Println(resp)
|
||||
return
|
||||
}
|
||||
|
||||
err = ioutil.WriteFile(filepath.Join(config.FactorioConfigDir, config.FactorioAdminFile), admins, 0664)
|
||||
if err != nil {
|
||||
resp = fmt.Sprintf("Failed to save admins: %s", err)
|
||||
log.Println(resp)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
resp = fmt.Sprintf("Settings successfully saved")
|
||||
}
|
522
src/api/mod_modpack_handler.go
Normal file
522
src/api/mod_modpack_handler.go
Normal file
@ -0,0 +1,522 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/mroote/factorio-server-manager/bootstrap"
|
||||
"github.com/mroote/factorio-server-manager/factorio"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func CheckModPackExists(modPackMap factorio.ModPackMap, modPackName string, w http.ResponseWriter, resp interface{}) error {
|
||||
exists := modPackMap.CheckModPackExists(modPackName)
|
||||
if !exists {
|
||||
resp = fmt.Sprintf("requested modPack {%s} does not exist", modPackName)
|
||||
log.Println(resp)
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return errors.New("requested modPack does not exist")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func CreateNewModPackMap(w http.ResponseWriter, resp *interface{}) (modPackMap factorio.ModPackMap, err error) {
|
||||
modPackMap, err = factorio.NewModPackMap()
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
*resp = fmt.Sprintf("Error creating modpackmap aka. list of all modpacks files : %s", err)
|
||||
log.Println(*resp)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func ReadModPackRequest(w http.ResponseWriter, r *http.Request, resp *interface{}) (err error, packMap factorio.ModPackMap, modPackName string) {
|
||||
vars := mux.Vars(r)
|
||||
modPackName = vars["modpack"]
|
||||
|
||||
packMap, err = CreateNewModPackMap(w, resp)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err = CheckModPackExists(packMap, modPackName, w, resp); err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
//////////////////////
|
||||
// Mod Pack Handler //
|
||||
//////////////////////
|
||||
|
||||
func ModPackListHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
var resp interface{}
|
||||
|
||||
defer func() {
|
||||
WriteResponse(w, resp)
|
||||
}()
|
||||
|
||||
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
|
||||
|
||||
modPackMap, err := CreateNewModPackMap(w, &resp)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
resp = modPackMap.ListInstalledModPacks()
|
||||
}
|
||||
|
||||
func ModPackCreateHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
var resp interface{}
|
||||
|
||||
defer func() {
|
||||
WriteResponse(w, resp)
|
||||
}()
|
||||
|
||||
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
|
||||
|
||||
var modPackStruct struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
err = ReadFromRequestBody(w, r, &resp, &modPackStruct)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
modPackMap, err := CreateNewModPackMap(w, &resp)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = modPackMap.CreateModPack(modPackStruct.Name)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
resp = fmt.Sprintf("Error creating modpack file: %s", err)
|
||||
log.Println(resp)
|
||||
return
|
||||
}
|
||||
|
||||
resp = modPackMap.ListInstalledModPacks()
|
||||
}
|
||||
|
||||
func ModPackDeleteHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
var resp interface{}
|
||||
|
||||
defer func() {
|
||||
WriteResponse(w, resp)
|
||||
}()
|
||||
|
||||
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
|
||||
|
||||
err, modPackMap, modPackName := ReadModPackRequest(w, r, &resp)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = modPackMap.DeleteModPack(modPackName)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
resp = fmt.Sprintf("Error deleting modpack file: %s", err)
|
||||
log.Println(resp)
|
||||
return
|
||||
}
|
||||
|
||||
resp = modPackName
|
||||
}
|
||||
|
||||
func ModPackDownloadHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
var resp interface{}
|
||||
|
||||
err, _, modPackName := ReadModPackRequest(w, r, &resp)
|
||||
if err != nil {
|
||||
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
|
||||
WriteResponse(w, resp)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s.zip\"", modPackName))
|
||||
|
||||
zipWriter := zip.NewWriter(w)
|
||||
defer zipWriter.Close()
|
||||
|
||||
config := bootstrap.GetConfig()
|
||||
|
||||
//iterate over folder and create everything in the zip
|
||||
err = filepath.Walk(filepath.Join(config.FactorioModPackDir, modPackName), func(path string, info os.FileInfo, err error) error {
|
||||
if info.IsDir() == false {
|
||||
writer, err := zipWriter.Create(info.Name())
|
||||
if err != nil {
|
||||
log.Printf("error on creating new file inside zip: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
log.Printf("error on opening modfile: %s", err)
|
||||
return err
|
||||
}
|
||||
// Close file, when function returns
|
||||
defer func() {
|
||||
err2 := file.Close()
|
||||
if err == nil && err2 != nil {
|
||||
log.Printf("Error closing file: %s", err2)
|
||||
err = err2
|
||||
}
|
||||
}()
|
||||
|
||||
_, err = io.Copy(writer, file)
|
||||
if err != nil {
|
||||
log.Printf("error on copying file into zip: %s", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
resp = fmt.Sprintf("error on walking over the modpack: %s", err)
|
||||
log.Println(resp)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
|
||||
WriteResponse(w, resp)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/zip;charset=UTF-8")
|
||||
}
|
||||
|
||||
func ModPackLoadHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
var resp interface{}
|
||||
|
||||
defer func() {
|
||||
WriteResponse(w, resp)
|
||||
}()
|
||||
|
||||
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
|
||||
|
||||
err, modPackMap, modPackName := ReadModPackRequest(w, r, &resp)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = modPackMap[modPackName].LoadModPack()
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
resp = fmt.Sprintf("Error loading modpack file: %s", err)
|
||||
log.Println(resp)
|
||||
return
|
||||
}
|
||||
|
||||
resp = modPackMap[modPackName].Mods.ListInstalledMods()
|
||||
}
|
||||
|
||||
//////////////////////////////////
|
||||
// Mods inside Mod Pack Handler //
|
||||
//////////////////////////////////
|
||||
func ModPackModListHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var resp interface{}
|
||||
|
||||
defer func() {
|
||||
WriteResponse(w, resp)
|
||||
}()
|
||||
|
||||
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
|
||||
|
||||
err, modPackMap, modPackName := ReadModPackRequest(w, r, &resp)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
resp = modPackMap[modPackName].Mods.ListInstalledMods()
|
||||
}
|
||||
|
||||
func ModPackModToggleHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
var resp interface{}
|
||||
|
||||
defer func() {
|
||||
WriteResponse(w, resp)
|
||||
}()
|
||||
|
||||
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
|
||||
|
||||
err, packMap, packName := ReadModPackRequest(w, r, &resp)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var modPackStruct struct {
|
||||
ModName string `json:"name"`
|
||||
}
|
||||
ReadFromRequestBody(w, r, &resp, &modPackStruct)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err, resp = packMap[packName].Mods.ModSimpleList.ToggleMod(modPackStruct.ModName)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
resp = fmt.Sprintf("Error toggling mod inside modPack: %s", err)
|
||||
log.Println(resp)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func ModPackModDeleteHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
var resp interface{}
|
||||
|
||||
defer func() {
|
||||
WriteResponse(w, resp)
|
||||
}()
|
||||
|
||||
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
|
||||
|
||||
err, packMap, packName := ReadModPackRequest(w, r, &resp)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var modPackStruct struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
err = ReadFromRequestBody(w, r, &resp, &modPackStruct)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = packMap[packName].Mods.DeleteMod(modPackStruct.Name)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
resp = fmt.Sprintf("Error deleting mod {%s} in modpack {%s}: %s", modPackStruct.Name, packName, err)
|
||||
log.Println(resp)
|
||||
return
|
||||
}
|
||||
|
||||
resp = true
|
||||
}
|
||||
|
||||
func ModPackModUpdateHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
var resp interface{}
|
||||
|
||||
defer func() {
|
||||
WriteResponse(w, resp)
|
||||
}()
|
||||
|
||||
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
|
||||
|
||||
//Get Data out of the request
|
||||
var modPackStruct struct {
|
||||
ModName string `json:"modName"`
|
||||
DownloadUrl string `json:"downloadUrl"`
|
||||
Filename string `json:"filename"`
|
||||
}
|
||||
err = ReadFromRequestBody(w, r, &resp, &modPackStruct)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err, packMap, packName := ReadModPackRequest(w, r, &resp)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = packMap[packName].Mods.UpdateMod(modPackStruct.ModName, modPackStruct.DownloadUrl, modPackStruct.Filename)
|
||||
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
resp = fmt.Sprintf("Error updating mod {%s} in modpack {%s}: %s", modPackStruct.ModName, packName, err)
|
||||
log.Println(resp)
|
||||
return
|
||||
}
|
||||
|
||||
installedMods := packMap[packName].Mods.ListInstalledMods().ModsResult
|
||||
var found = false
|
||||
for _, mod := range installedMods {
|
||||
if mod.Name == modPackStruct.ModName {
|
||||
resp = mod
|
||||
found = true
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
resp = fmt.Sprintf(`Could not find mod %s`, modPackStruct.ModName)
|
||||
log.Println(resp)
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func ModPackModDeleteAllHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var resp interface{}
|
||||
|
||||
defer func() {
|
||||
WriteResponse(w, resp)
|
||||
}()
|
||||
|
||||
err, packMap, packName := ReadModPackRequest(w, r, &resp)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Delete Modpack
|
||||
err = packMap.DeleteModPack(packName)
|
||||
if err != nil {
|
||||
resp = fmt.Sprintf("Error deleting modPackDir: %s", err)
|
||||
log.Println(resp)
|
||||
return
|
||||
}
|
||||
|
||||
// recreate modPack without mods
|
||||
err = packMap.CreateEmptyModPack(packName)
|
||||
if err != nil {
|
||||
resp = fmt.Sprintf("Error recreating modPackDir: %s", err)
|
||||
log.Println(resp)
|
||||
return
|
||||
}
|
||||
|
||||
resp = true
|
||||
}
|
||||
|
||||
func ModPackModUploadHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
var resp interface{}
|
||||
|
||||
defer func() {
|
||||
WriteResponse(w, resp)
|
||||
}()
|
||||
|
||||
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
|
||||
|
||||
formFile, fileHeader, err := r.FormFile("mod_file")
|
||||
if err != nil {
|
||||
resp = fmt.Sprintf("error getting uploaded file: %s", err)
|
||||
log.Println(resp)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
defer formFile.Close()
|
||||
|
||||
err, modPackMap, modPackName := ReadModPackRequest(w, r, &resp)
|
||||
|
||||
err = modPackMap[modPackName].Mods.UploadMod(formFile, fileHeader)
|
||||
if err != nil {
|
||||
resp = fmt.Sprintf("error saving file to modPack: %s", err)
|
||||
log.Println(resp)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
resp = modPackMap[modPackName].Mods.ListInstalledMods()
|
||||
}
|
||||
|
||||
func ModPackModPortalInstallHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
var resp interface{}
|
||||
|
||||
defer func() {
|
||||
WriteResponse(w, resp)
|
||||
}()
|
||||
|
||||
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
|
||||
|
||||
// Get Data out of the request
|
||||
var data struct {
|
||||
DownloadURL string `json:"downloadUrl"`
|
||||
Filename string `json:"fileName"`
|
||||
ModName string `json:"modName"`
|
||||
}
|
||||
err = ReadFromRequestBody(w, r, &resp, &data)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err, packMap, packName := ReadModPackRequest(w, r, &resp)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
modList := packMap[packName].Mods
|
||||
|
||||
err = modList.DownloadMod(data.DownloadURL, data.Filename, data.ModName)
|
||||
if err != nil {
|
||||
resp = fmt.Sprintf("Error downloading a mod: %s", err)
|
||||
log.Println(resp)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
resp = modList.ListInstalledMods()
|
||||
}
|
||||
|
||||
func ModPackModPortalInstallMultipleHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
var resp interface{}
|
||||
|
||||
defer func() {
|
||||
WriteResponse(w, resp)
|
||||
}()
|
||||
|
||||
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
|
||||
|
||||
var data []struct {
|
||||
Name string `json:"name"`
|
||||
Version factorio.Version `json:"version"`
|
||||
}
|
||||
err = ReadFromRequestBody(w, r, &resp, &data)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err, packMap, packName := ReadModPackRequest(w, r, &resp)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
modList := packMap[packName].Mods
|
||||
for _, datum := range data {
|
||||
details, err, statusCode := factorio.ModPortalModDetails(datum.Name)
|
||||
if err != nil || statusCode != http.StatusOK {
|
||||
resp = fmt.Sprintf("Error in getting mod details from mod portal: %s", err)
|
||||
log.Println(resp)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
//find correct mod-version
|
||||
var found = false
|
||||
|
||||
for _, release := range details.Releases {
|
||||
if release.Version.Equals(datum.Version) {
|
||||
found = true
|
||||
|
||||
err := modList.DownloadMod(release.DownloadURL, release.FileName, details.Name)
|
||||
if err != nil {
|
||||
resp = fmt.Sprintf("Error downloading mod {%s}, error: %s", details.Name, err)
|
||||
log.Println(resp)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
log.Printf("Error downloading mod {%s}, error: %s", details.Name, "version not found")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
resp = modList.ListInstalledMods()
|
||||
}
|
660
src/api/mod_modpack_handler_test.go
Normal file
660
src/api/mod_modpack_handler_test.go
Normal file
@ -0,0 +1,660 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/mroote/factorio-server-manager/bootstrap"
|
||||
"github.com/mroote/factorio-server-manager/factorio"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func SetupModPacks(t *testing.T, empty bool, emptyMods bool) {
|
||||
var err error
|
||||
|
||||
config := bootstrap.GetConfig()
|
||||
|
||||
// check if dev directory exists and create it
|
||||
if _, err = os.Stat(config.FactorioModPackDir); os.IsNotExist(err) {
|
||||
err = os.Mkdir(config.FactorioModPackDir, 0775)
|
||||
}
|
||||
assert.NoError(t, err, `Error creating %s directory`, config.FactorioModPackDir)
|
||||
|
||||
if !empty {
|
||||
// check if dev directory exists and create it
|
||||
if _, err = os.Stat(config.FactorioModPackDir + "/test"); os.IsNotExist(err) {
|
||||
err = os.Mkdir(config.FactorioModPackDir+"/test", 0775)
|
||||
}
|
||||
assert.NoError(t, err, `Error creating "%s/test" directory`, config.FactorioModPackDir)
|
||||
|
||||
modList, err := factorio.NewMods(config.FactorioModPackDir + "/test")
|
||||
assert.NoError(t, err, "error creating mods")
|
||||
|
||||
if !emptyMods {
|
||||
err = modList.DownloadMod("/download/belt-balancer/5e9f9db4bf9d30000c5303f2", "belt-balancer_2.1.3.zip", "belt-balancer")
|
||||
assert.NoError(t, err, `Error downloading Mod "belt-balancer"`)
|
||||
|
||||
err = modList.DownloadMod("/download/train-station-overview/5e8a0a8ee8864f000d0cb022", "train-station-overview_2.0.3.zip", "train-station-overview")
|
||||
assert.NoError(t, err, `Error downloading Mod "train-station-overview"`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func CleanupModPacks(t *testing.T) {
|
||||
config := bootstrap.GetConfig()
|
||||
err := os.RemoveAll(config.FactorioModPackDir)
|
||||
assert.NoError(t, err, `Error removing directory %s`, config.FactorioModPackDir)
|
||||
}
|
||||
|
||||
func UnknownModpackTest(t *testing.T, method string, baseRoute string, route string, handlerFunc http.HandlerFunc) {
|
||||
t.Run("unknown modpack", func(t *testing.T) {
|
||||
SetupModPacks(t, true, true)
|
||||
defer CleanupModPacks(t)
|
||||
|
||||
requestBody := strings.NewReader(`{"name": "belt-balancer"}`)
|
||||
|
||||
CallRoute(t, method, baseRoute, route, requestBody, handlerFunc, http.StatusNotFound, "")
|
||||
})
|
||||
}
|
||||
|
||||
func ModPackUnknownModTest(t *testing.T, method string, baseRoute string, route string, handlerFunc http.HandlerFunc) {
|
||||
t.Run("unknown mod", func(t *testing.T) {
|
||||
SetupModPacks(t, false, false)
|
||||
defer CleanupModPacks(t)
|
||||
|
||||
requestBody := strings.NewReader(`{"name": "askhdbali"}`)
|
||||
|
||||
CallRoute(t, method, baseRoute, route, requestBody, handlerFunc, http.StatusInternalServerError, "")
|
||||
})
|
||||
}
|
||||
|
||||
func ModPackEmptyBodyTest(t *testing.T, method string, baseRoute string, route string, handlerFunc http.HandlerFunc) {
|
||||
t.Run("empty body", func(t *testing.T) {
|
||||
SetupModPacks(t, false, true)
|
||||
defer CleanupModPacks(t)
|
||||
SetupMods(t, false)
|
||||
defer CleanupMods(t)
|
||||
|
||||
CallRoute(t, method, baseRoute, route, nil, handlerFunc, http.StatusBadRequest, "")
|
||||
})
|
||||
}
|
||||
|
||||
func ModPackInvalidJsonBodyTest(t *testing.T, method string, baseRoute string, route string, handlerFunc http.HandlerFunc) {
|
||||
t.Run("invalid json body", func(t *testing.T) {
|
||||
SetupModPacks(t, false, true)
|
||||
defer CleanupModPacks(t)
|
||||
SetupMods(t, true)
|
||||
defer CleanupMods(t)
|
||||
|
||||
requestBody := strings.NewReader(`{`)
|
||||
|
||||
CallRoute(t, method, baseRoute, route, requestBody, handlerFunc, http.StatusBadRequest, "")
|
||||
})
|
||||
}
|
||||
|
||||
func TestModPackListHandler(t *testing.T) {
|
||||
CheckShort(t)
|
||||
|
||||
method := "GET"
|
||||
route := "/mods/packs/list"
|
||||
handlerFunc := ModPackListHandler
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
SetupModPacks(t, false, false)
|
||||
defer CleanupModPacks(t)
|
||||
|
||||
expected := `[{"name":"test","mods":{"mods":[{"name":"belt-balancer","version":"2.1.3","title":"Belt Balancer","author":"knoxfighter","file_name":"belt-balancer_2.1.3.zip","factorio_version":"0.18.0.0","dependencies":null,"compatibility":true,"enabled":true},{"name":"train-station-overview","version":"2.0.3","title":"Train Station Overview","author":"knoxfighter","file_name":"train-station-overview_2.0.3.zip","factorio_version":"0.18.0.0","dependencies":null,"compatibility":true,"enabled":true}]}}]`
|
||||
|
||||
CallRoute(t, method, route, route, nil, handlerFunc, http.StatusOK, expected)
|
||||
})
|
||||
|
||||
t.Run("empty modpack", func(t *testing.T) {
|
||||
SetupModPacks(t, true, false)
|
||||
defer CleanupModPacks(t)
|
||||
|
||||
expected := `[]`
|
||||
|
||||
CallRoute(t, method, route, route, nil, handlerFunc, http.StatusOK, expected)
|
||||
})
|
||||
}
|
||||
|
||||
func TestModPackCreateHandler(t *testing.T) {
|
||||
CheckShort(t)
|
||||
|
||||
method := "POST"
|
||||
route := "/mods/packs/create"
|
||||
handlerFunc := ModPackCreateHandler
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
SetupModPacks(t, true, false)
|
||||
defer CleanupModPacks(t)
|
||||
SetupMods(t, false)
|
||||
defer CleanupMods(t)
|
||||
|
||||
requestBody := strings.NewReader(`{"name": "test"}`)
|
||||
expected := `[{"name":"test","mods":{"mods":[{"name":"belt-balancer","version":"2.1.3","title":"Belt Balancer","author":"knoxfighter","file_name":"belt-balancer_2.1.3.zip","factorio_version":"0.18.0.0","dependencies":null,"compatibility":true,"enabled":true},{"name":"train-station-overview","version":"2.0.3","title":"Train Station Overview","author":"knoxfighter","file_name":"train-station-overview_2.0.3.zip","factorio_version":"0.18.0.0","dependencies":null,"compatibility":true,"enabled":true}]}}]`
|
||||
|
||||
CallRoute(t, method, route, route, requestBody, handlerFunc, http.StatusOK, expected)
|
||||
})
|
||||
|
||||
ModPackEmptyBodyTest(t, method, route, route, handlerFunc)
|
||||
|
||||
ModPackInvalidJsonBodyTest(t, method, route, route, handlerFunc)
|
||||
}
|
||||
|
||||
func TestModPackDeleteHandler(t *testing.T) {
|
||||
CheckShort(t)
|
||||
|
||||
method := "POST"
|
||||
baseRoute := "/mods/packs/{modpack}/delete"
|
||||
route := "/mods/packs/test/delete"
|
||||
handlerFunc := ModPackDeleteHandler
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
SetupModPacks(t, false, false)
|
||||
defer CleanupModPacks(t)
|
||||
|
||||
CallRoute(t, method, baseRoute, route, nil, handlerFunc, http.StatusOK, `"test"`)
|
||||
})
|
||||
|
||||
UnknownModpackTest(t, method, baseRoute, route, handlerFunc)
|
||||
}
|
||||
|
||||
func TestModPackLoadHandler(t *testing.T) {
|
||||
CheckShort(t)
|
||||
|
||||
method := "POST"
|
||||
baseRoute := "/mods/packs/{modpack}/load"
|
||||
route := "/mods/packs/test/load"
|
||||
handlerFunc := ModPackLoadHandler
|
||||
|
||||
t.Run("load mods", func(t *testing.T) {
|
||||
config := bootstrap.GetConfig()
|
||||
SetupModPacks(t, false, false)
|
||||
defer CleanupModPacks(t)
|
||||
SetupMods(t, true)
|
||||
defer CleanupMods(t)
|
||||
|
||||
expected := `{"mods":[{"name":"belt-balancer","version":"2.1.3","title":"Belt Balancer","author":"knoxfighter","file_name":"belt-balancer_2.1.3.zip","factorio_version":"0.18.0.0","dependencies":null,"compatibility":true,"enabled":true},{"name":"train-station-overview","version":"2.0.3","title":"Train Station Overview","author":"knoxfighter","file_name":"train-station-overview_2.0.3.zip","factorio_version":"0.18.0.0","dependencies":null,"compatibility":true,"enabled":true}]}`
|
||||
|
||||
CallRoute(t, method, baseRoute, route, nil, handlerFunc, http.StatusOK, expected)
|
||||
|
||||
// check if mods are really loaded
|
||||
packMap, err := factorio.NewModPackMap()
|
||||
assert.NoError(t, err, "Error creating modPackMap")
|
||||
|
||||
modList, err := factorio.NewMods(config.FactorioModsDir)
|
||||
assert.NoError(t, err, "Error creating mods object")
|
||||
|
||||
packModsJson, err := json.Marshal(packMap["test"].Mods)
|
||||
assert.NoError(t, err, "Error marshalling mods from modPack")
|
||||
|
||||
modsJson, err := json.Marshal(modList)
|
||||
assert.NoError(t, err, "Error marshalling mods object")
|
||||
|
||||
assert.JSONEq(t, string(packModsJson), string(modsJson), "loaded mods and modPack are not identical")
|
||||
})
|
||||
|
||||
t.Run("load empty modpack", func(t *testing.T) {
|
||||
SetupModPacks(t, false, true)
|
||||
defer CleanupModPacks(t)
|
||||
SetupMods(t, false)
|
||||
defer CleanupMods(t)
|
||||
|
||||
expected := `{"mods":[]}`
|
||||
|
||||
CallRoute(t, method, baseRoute, route, nil, handlerFunc, http.StatusOK, expected)
|
||||
|
||||
// check if mods are really loaded
|
||||
packMap, err := factorio.NewModPackMap()
|
||||
assert.NoError(t, err, "Error creating modPackMap")
|
||||
|
||||
config := bootstrap.GetConfig()
|
||||
|
||||
modList, err := factorio.NewMods(config.FactorioModsDir)
|
||||
assert.NoError(t, err, "Error creating mods object")
|
||||
|
||||
packModsJson, err := json.Marshal(packMap["test"].Mods)
|
||||
assert.NoError(t, err, "Error marshalling mods from modPack")
|
||||
|
||||
modsJson, err := json.Marshal(modList)
|
||||
assert.NoError(t, err, "Error marshalling mods object")
|
||||
|
||||
assert.JSONEq(t, string(packModsJson), string(modsJson), "loaded mods and modPack are not identical")
|
||||
})
|
||||
|
||||
UnknownModpackTest(t, method, baseRoute, route, handlerFunc)
|
||||
}
|
||||
|
||||
func TestModPackModListHandler(t *testing.T) {
|
||||
CheckShort(t)
|
||||
|
||||
method := "GET"
|
||||
baseRoute := "/mods/packs/{modpack}/list"
|
||||
route := "/mods/packs/test/list"
|
||||
handlerFunc := ModPackModListHandler
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
SetupModPacks(t, false, false)
|
||||
defer CleanupModPacks(t)
|
||||
|
||||
expected := `{"mods":[{"name":"belt-balancer","version":"2.1.3","title":"Belt Balancer","author":"knoxfighter","file_name":"belt-balancer_2.1.3.zip","factorio_version":"0.18.0.0","dependencies":null,"compatibility":true,"enabled":true},{"name":"train-station-overview","version":"2.0.3","title":"Train Station Overview","author":"knoxfighter","file_name":"train-station-overview_2.0.3.zip","factorio_version":"0.18.0.0","dependencies":null,"compatibility":true,"enabled":true}]}`
|
||||
|
||||
CallRoute(t, method, baseRoute, route, nil, handlerFunc, http.StatusOK, expected)
|
||||
})
|
||||
|
||||
UnknownModpackTest(t, method, baseRoute, route, handlerFunc)
|
||||
}
|
||||
|
||||
func TestModPackModToggleHandler(t *testing.T) {
|
||||
CheckShort(t)
|
||||
|
||||
method := "POST"
|
||||
baseRoute := "/mods/packs/{modpack}/mod/toggle"
|
||||
route := "/mods/packs/test/mod/toggle"
|
||||
handlerFunc := ModPackModToggleHandler
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
SetupModPacks(t, false, false)
|
||||
defer CleanupModPacks(t)
|
||||
|
||||
requestBody := strings.NewReader(`{"name": "belt-balancer"}`)
|
||||
|
||||
// mod is now deactivated
|
||||
expected := "false"
|
||||
|
||||
CallRoute(t, method, baseRoute, route, requestBody, handlerFunc, http.StatusOK, expected)
|
||||
|
||||
// check if changes happened
|
||||
packMap, err := factorio.NewModPackMap()
|
||||
assert.NoError(t, err, "Error creating modPackMap")
|
||||
|
||||
found := false
|
||||
for _, mod := range packMap["test"].Mods.ModSimpleList.Mods {
|
||||
if mod.Name == "belt-balancer" {
|
||||
// this mod has to be deactivated now
|
||||
if mod.Enabled {
|
||||
t.Fatalf("Mod is wrongly enabled, it should be disabled by now")
|
||||
}
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatalf("Mod not found")
|
||||
}
|
||||
|
||||
// toggle again, to check if the other direction also works
|
||||
// mod is now activated again
|
||||
expected = "true"
|
||||
|
||||
// reset request body, it has to be red again
|
||||
requestBody.Seek(0, 0)
|
||||
|
||||
CallRoute(t, method, baseRoute, route, requestBody, handlerFunc, http.StatusOK, expected)
|
||||
|
||||
packMap, err = factorio.NewModPackMap()
|
||||
assert.NoError(t, err, "Error creating modPackMap")
|
||||
|
||||
found = false
|
||||
for _, mod := range packMap["test"].Mods.ModSimpleList.Mods {
|
||||
if mod.Name == "belt-balancer" {
|
||||
// this mod has to be deactivated now
|
||||
if !mod.Enabled {
|
||||
t.Fatalf("Mod is wrongly disabled, it should be enabled again")
|
||||
}
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatalf("Mod not found")
|
||||
}
|
||||
})
|
||||
|
||||
UnknownModpackTest(t, method, baseRoute, route, handlerFunc)
|
||||
|
||||
ModPackUnknownModTest(t, method, baseRoute, route, handlerFunc)
|
||||
|
||||
ModPackEmptyBodyTest(t, method, baseRoute, route, handlerFunc)
|
||||
|
||||
ModPackInvalidJsonBodyTest(t, method, baseRoute, route, handlerFunc)
|
||||
}
|
||||
|
||||
func TestModPackModDeleteHandler(t *testing.T) {
|
||||
CheckShort(t)
|
||||
|
||||
method := "POST"
|
||||
baseRoute := "/mods/packs/{modpack}/mod/delete"
|
||||
route := "/mods/packs/test/mod/delete"
|
||||
handlerFunc := ModPackModDeleteHandler
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
SetupModPacks(t, false, false)
|
||||
defer CleanupModPacks(t)
|
||||
|
||||
requestBody := strings.NewReader(`{"name": "belt-balancer"}`)
|
||||
|
||||
CallRoute(t, method, baseRoute, route, requestBody, handlerFunc, http.StatusOK, `true`)
|
||||
|
||||
// check if mod is really not installed anymore
|
||||
packMap, err := factorio.NewModPackMap()
|
||||
assert.NoError(t, err, "Error creating modPackMap")
|
||||
|
||||
if packMap["test"].Mods.ModSimpleList.CheckModExists("belt-balancer") {
|
||||
t.Fatalf("Mod is still installed, it should be gone by now")
|
||||
}
|
||||
})
|
||||
|
||||
UnknownModpackTest(t, method, baseRoute, route, handlerFunc)
|
||||
|
||||
ModPackUnknownModTest(t, method, baseRoute, route, handlerFunc)
|
||||
|
||||
ModPackEmptyBodyTest(t, method, baseRoute, route, handlerFunc)
|
||||
|
||||
ModPackInvalidJsonBodyTest(t, method, baseRoute, route, handlerFunc)
|
||||
}
|
||||
|
||||
func TestModPackModDeleteAllHandler(t *testing.T) {
|
||||
CheckShort(t)
|
||||
|
||||
method := "POST"
|
||||
baseRoute := "/mods/packs/{modpack}/mod/delete/all"
|
||||
route := "/mods/packs/test/mod/delete/all"
|
||||
handlerFunc := ModPackModDeleteAllHandler
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
SetupModPacks(t, false, false)
|
||||
defer CleanupModPacks(t)
|
||||
|
||||
CallRoute(t, method, baseRoute, route, nil, handlerFunc, http.StatusOK, "true")
|
||||
|
||||
// check if really empty
|
||||
packMap, err := factorio.NewModPackMap()
|
||||
assert.NoError(t, err, "Error creating modPackMap")
|
||||
|
||||
if len(packMap["test"].Mods.ModInfoList.Mods) != 0 {
|
||||
t.Fatal("There are still mods in the modpack")
|
||||
}
|
||||
})
|
||||
|
||||
UnknownModpackTest(t, method, baseRoute, route, handlerFunc)
|
||||
}
|
||||
|
||||
func TestModPackModUpdateHandler(t *testing.T) {
|
||||
CheckShort(t)
|
||||
|
||||
method := "POST"
|
||||
baseRoute := "/mods/packs/{modpack}/mod/update"
|
||||
route := "/mods/packs/test/mod/update"
|
||||
handlerFunc := ModPackModUpdateHandler
|
||||
|
||||
requestBodySuccess := `{"modName": "belt-balancer", "downloadUrl": "/download/belt-balancer/5e711cd95bcf4f000b96b22c", "fileName": "belt-balancer_2.1.2.zip"}`
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
SetupModPacks(t, false, false)
|
||||
defer CleanupModPacks(t)
|
||||
|
||||
expected := `{"name":"belt-balancer","version":"2.1.2","title":"Belt Balancer","author":"knoxfighter","file_name":"belt-balancer_2.1.2.zip","factorio_version":"0.18.0.0","dependencies":null,"compatibility":true,"enabled":true}`
|
||||
|
||||
CallRoute(t, method, baseRoute, route, strings.NewReader(requestBodySuccess), handlerFunc, http.StatusOK, expected)
|
||||
})
|
||||
|
||||
t.Run("success with disabled mod", func(t *testing.T) {
|
||||
SetupModPacks(t, false, false)
|
||||
defer CleanupModPacks(t)
|
||||
|
||||
// disable "belt-balancer" mod, so we can test, if it is still deactivated after
|
||||
packMap, err := factorio.NewModPackMap()
|
||||
assert.NoError(t, err, "Error creating modPackMap")
|
||||
|
||||
err, _ = packMap["test"].Mods.ModSimpleList.ToggleMod("belt-balancer")
|
||||
assert.NoError(t, err, "Error toggling mod")
|
||||
|
||||
expected := `{"name":"belt-balancer","version":"2.1.2","title":"Belt Balancer","author":"knoxfighter","file_name":"belt-balancer_2.1.2.zip","factorio_version":"0.18.0.0","dependencies":null,"compatibility":true,"enabled":false}`
|
||||
|
||||
CallRoute(t, method, baseRoute, route, strings.NewReader(requestBodySuccess), handlerFunc, http.StatusOK, expected)
|
||||
})
|
||||
|
||||
UnknownModpackTest(t, method, baseRoute, route, handlerFunc)
|
||||
|
||||
ModPackEmptyBodyTest(t, method, baseRoute, route, handlerFunc)
|
||||
|
||||
ModPackInvalidJsonBodyTest(t, method, baseRoute, route, handlerFunc)
|
||||
|
||||
t.Run("unknown mod", func(t *testing.T) {
|
||||
SetupModPacks(t, false, false)
|
||||
defer CleanupModPacks(t)
|
||||
|
||||
requestBody := `{"modName": "asldbsac", "downloadUrl": "/download/belt-balancer/5e711cd95bcf4f000b96b22c", "fileName": "belt-balancer_2.1.2.zip"}`
|
||||
|
||||
CallRoute(t, method, baseRoute, route, strings.NewReader(requestBody), handlerFunc, http.StatusNotFound, "")
|
||||
})
|
||||
|
||||
t.Run("wrong download link", func(t *testing.T) {
|
||||
SetupModPacks(t, false, false)
|
||||
defer CleanupModPacks(t)
|
||||
|
||||
requestBody := `{"modName": "asldbsac", "downloadUrl": "/download/belt-balancer/95bcf4f000b96b22c", "fileName": "belt-balancer_2.1.2.zip"}`
|
||||
|
||||
CallRoute(t, method, baseRoute, route, strings.NewReader(requestBody), handlerFunc, http.StatusInternalServerError, "")
|
||||
|
||||
// check if old mod is still there
|
||||
packMap, err := factorio.NewModPackMap()
|
||||
assert.NoError(t, err, "Error creating modPackMap")
|
||||
|
||||
var found = false
|
||||
for _, mod := range packMap["test"].Mods.ModInfoList.Mods {
|
||||
if mod.Name == "belt-balancer" {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
t.Fatal(`Mod "belt-balancer" is not there anymore`)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func ModPackModUploadRequest(t *testing.T, body bool, filePath string) *httptest.ResponseRecorder {
|
||||
method := "POST"
|
||||
baseRoute := "/mods/packs/{modpack}/mod/upload"
|
||||
route := "/mods/packs/test/mod/upload"
|
||||
handlerFunc := ModPackModUploadHandler
|
||||
|
||||
var err error
|
||||
|
||||
requestBody := &bytes.Buffer{}
|
||||
|
||||
writer := multipart.NewWriter(requestBody)
|
||||
|
||||
if body {
|
||||
file, err := os.Open(filePath)
|
||||
if err == nil {
|
||||
assert.NoError(t, err, "error opening mod file")
|
||||
|
||||
formFile, err := writer.CreateFormFile("mod_file", filepath.Base(filePath))
|
||||
assert.NoError(t, err, "error creating formFileWriter")
|
||||
|
||||
_, err = io.Copy(formFile, file)
|
||||
assert.NoError(t, err, "error copying file to form")
|
||||
}
|
||||
}
|
||||
|
||||
err = writer.Close()
|
||||
if err != nil {
|
||||
t.Fatalf("error closing the multipart writer: %s", err)
|
||||
}
|
||||
|
||||
// create request to send
|
||||
request, err := http.NewRequest(method, route, requestBody)
|
||||
assert.NoError(t, err, "Error creating request")
|
||||
request.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
|
||||
// create response recorder
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
// get the handler, where the request is handled
|
||||
router := mux.NewRouter()
|
||||
router.HandleFunc(baseRoute, handlerFunc)
|
||||
|
||||
// call the handler directly
|
||||
router.ServeHTTP(recorder, request)
|
||||
|
||||
return recorder
|
||||
}
|
||||
|
||||
func TestModPackModUploadHandler(t *testing.T) {
|
||||
CheckShort(t)
|
||||
|
||||
method := "POST"
|
||||
baseRoute := "/mods/packs/{modpack}/mod/upload"
|
||||
route := "/mods/packs/test/mod/upload"
|
||||
handlerFunc := ModPackModUploadHandler
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
SetupModPacks(t, false, true)
|
||||
defer CleanupModPacks(t)
|
||||
|
||||
recorder := ModPackModUploadRequest(t, true, "../factorio_testfiles/belt-balancer_2.1.3.zip")
|
||||
|
||||
// status has to be 200
|
||||
if recorder.Code != http.StatusOK {
|
||||
t.Fatalf("Wrong Status Code. expected %v - got %v", http.StatusOK, recorder.Code)
|
||||
}
|
||||
|
||||
// check if mod is uploaded correctly
|
||||
packMap, err := factorio.NewModPackMap()
|
||||
assert.NoError(t, err, "error creating modPackMap")
|
||||
|
||||
expected := factorio.ModsResultList{
|
||||
ModsResult: []factorio.ModsResult{
|
||||
{
|
||||
ModInfo: factorio.ModInfo{
|
||||
Name: "belt-balancer",
|
||||
Version: "2.1.3",
|
||||
Title: "Belt Balancer",
|
||||
Author: "knoxfighter",
|
||||
FileName: "belt-balancer_2.1.3.zip",
|
||||
FactorioVersion: factorio.Version{0, 18, 0, 0},
|
||||
Dependencies: nil,
|
||||
Compatibility: true,
|
||||
},
|
||||
Enabled: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
actual := packMap["test"].Mods.ListInstalledMods()
|
||||
assert.Equal(t, expected, actual, `New mod is not correctly installed. expected "%v" - actual "%v"`, expected, actual)
|
||||
})
|
||||
|
||||
ModPackEmptyBodyTest(t, method, baseRoute, route, handlerFunc)
|
||||
|
||||
t.Run("empty file", func(t *testing.T) {
|
||||
SetupModPacks(t, false, true)
|
||||
defer CleanupModPacks(t)
|
||||
|
||||
recorder := ModPackModUploadRequest(t, true, "")
|
||||
assert.Equal(t, http.StatusBadRequest, recorder.Code, "wrong response code.")
|
||||
})
|
||||
|
||||
t.Run("invalid mod file (txt-file)", func(t *testing.T) {
|
||||
SetupModPacks(t, false, true)
|
||||
defer CleanupModPacks(t)
|
||||
|
||||
recorder := ModPackModUploadRequest(t, false, "../factorio_testfiles/file_usage.txt")
|
||||
assert.Equal(t, http.StatusBadRequest, recorder.Code, "wrong response code.")
|
||||
})
|
||||
|
||||
t.Run("invalid mod file (zip-file)", func(t *testing.T) {
|
||||
SetupModPacks(t, false, true)
|
||||
defer CleanupModPacks(t)
|
||||
|
||||
recorder := ModPackModUploadRequest(t, true, "../factorio_testfiles/invalid_mod.zip")
|
||||
assert.Equal(t, http.StatusInternalServerError, recorder.Code, "wrong response code.")
|
||||
})
|
||||
}
|
||||
|
||||
func TestModPackModPortalInstallHandler(t *testing.T) {
|
||||
CheckShort(t)
|
||||
|
||||
method := "POST"
|
||||
baseRoute := "/mods/packs/{modpack}/portal/install"
|
||||
route := "/mods/packs/test/portal/install"
|
||||
handlerFunc := ModPackModPortalInstallHandler
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
SetupModPacks(t, false, true)
|
||||
defer CleanupModPacks(t)
|
||||
|
||||
requestBody := strings.NewReader(`{"modName": "belt-balancer", "downloadUrl": "/download/belt-balancer/5e711cd95bcf4f000b96b22c", "fileName": "belt-balancer_2.1.2.zip"}`)
|
||||
|
||||
expected := `{"mods":[{"name":"belt-balancer","version":"2.1.2","title":"Belt Balancer","author":"knoxfighter","file_name":"belt-balancer_2.1.2.zip","factorio_version":"0.18.0.0","dependencies":null,"compatibility":true,"enabled":true}]}`
|
||||
|
||||
CallRoute(t, method, baseRoute, route, requestBody, handlerFunc, http.StatusOK, expected)
|
||||
})
|
||||
|
||||
ModPackEmptyBodyTest(t, method, baseRoute, route, handlerFunc)
|
||||
|
||||
ModPackInvalidJsonBodyTest(t, method, baseRoute, route, handlerFunc)
|
||||
|
||||
t.Run("wrong download link", func(t *testing.T) {
|
||||
SetupModPacks(t, false, true)
|
||||
defer CleanupModPacks(t)
|
||||
|
||||
requestBody := strings.NewReader(`{"modName": "belt-balancer", "downloadUrl": "/download/belt-balancer/95bcf4f000b96b22c", "fileName": "belt-balancer_2.1.2.zip"}`)
|
||||
|
||||
CallRoute(t, method, baseRoute, route, requestBody, handlerFunc, http.StatusInternalServerError, "")
|
||||
})
|
||||
}
|
||||
|
||||
func TestModPackModPortalInstallMultipleHandler(t *testing.T) {
|
||||
CheckShort(t)
|
||||
|
||||
method := "POST"
|
||||
baseRoute := "/mods/packs/{modpack}/portal/install/multiple"
|
||||
route := "/mods/packs/test/portal/install/multiple"
|
||||
handlerFunc := ModPackModPortalInstallMultipleHandler
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
SetupModPacks(t, false, true)
|
||||
defer CleanupModPacks(t)
|
||||
|
||||
requestBody := strings.NewReader(`[{"name": "belt-balancer", "version": "2.1.2"}, {"name": "train-station-overview", "version": "2.0.2"}]`)
|
||||
|
||||
expected := `{"mods":[{"name":"belt-balancer","version":"2.1.2","title":"Belt Balancer","author":"knoxfighter","file_name":"belt-balancer_2.1.2.zip","factorio_version":"0.18.0.0","dependencies":null,"compatibility":true,"enabled":true},{"name":"train-station-overview","version":"2.0.2","title":"Train Station Overview","author":"knoxfighter","file_name":"train-station-overview_2.0.2.zip","factorio_version":"0.18.0.0","dependencies":null,"compatibility":true,"enabled":true}]}`
|
||||
|
||||
CallRoute(t, method, baseRoute, route, requestBody, handlerFunc, http.StatusOK, expected)
|
||||
})
|
||||
|
||||
t.Run("unknown mod", func(t *testing.T) {
|
||||
SetupModPacks(t, false, true)
|
||||
defer CleanupModPacks(t)
|
||||
|
||||
requestBody := strings.NewReader(`[{"name": "askdhcb", "version": "2.1.2"}]`)
|
||||
|
||||
CallRoute(t, method, baseRoute, route, requestBody, handlerFunc, http.StatusInternalServerError, "")
|
||||
})
|
||||
|
||||
t.Run("unknown version", func(t *testing.T) {
|
||||
SetupModPacks(t, false, true)
|
||||
defer CleanupModPacks(t)
|
||||
|
||||
requestBody := strings.NewReader(`[{"name": "belt-balancer", "version": "0.1.12"}]`)
|
||||
|
||||
CallRoute(t, method, baseRoute, route, requestBody, handlerFunc, http.StatusInternalServerError, "")
|
||||
})
|
||||
|
||||
ModPackEmptyBodyTest(t, method, baseRoute, route, handlerFunc)
|
||||
|
||||
ModPackInvalidJsonBodyTest(t, method, baseRoute, route, handlerFunc)
|
||||
}
|
223
src/api/mod_portal_handler.go
Normal file
223
src/api/mod_portal_handler.go
Normal file
@ -0,0 +1,223 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/mroote/factorio-server-manager/factorio"
|
||||
"log"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func ModPortalListModsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
var resp interface{}
|
||||
|
||||
defer func() {
|
||||
WriteResponse(w, resp)
|
||||
}()
|
||||
|
||||
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
|
||||
|
||||
var statusCode int
|
||||
resp, err, statusCode = factorio.ModPortalList()
|
||||
w.WriteHeader(statusCode)
|
||||
if err != nil {
|
||||
resp = fmt.Sprintf("Error in listing mods from mod portal: %s\nresponse: %+v", err, resp)
|
||||
log.Println(resp)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// ModPortalModInfoHandler returns JSON response with the mod details
|
||||
func ModPortalModInfoHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
var resp interface{}
|
||||
|
||||
defer func() {
|
||||
WriteResponse(w, resp)
|
||||
}()
|
||||
|
||||
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
|
||||
|
||||
vars := mux.Vars(r)
|
||||
modId := vars["mod"]
|
||||
|
||||
var statusCode int
|
||||
resp, err, statusCode = factorio.ModPortalModDetails(modId)
|
||||
|
||||
if err != nil {
|
||||
resp = fmt.Sprintf("Error in getting mod details from mod portal: %s", err)
|
||||
log.Println(resp)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(statusCode)
|
||||
}
|
||||
|
||||
func ModPortalInstallHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
var resp interface{}
|
||||
|
||||
defer func() {
|
||||
WriteResponse(w, resp)
|
||||
}()
|
||||
|
||||
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
|
||||
|
||||
// Get Data out of the request
|
||||
var data struct {
|
||||
DownloadURL string `json:"downloadUrl"`
|
||||
Filename string `json:"fileName"`
|
||||
ModName string `json:"modName"`
|
||||
}
|
||||
err = ReadFromRequestBody(w, r, &resp, &data)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
mods, err := CreateNewMods(w, &resp)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = mods.DownloadMod(data.DownloadURL, data.Filename, data.ModName)
|
||||
if err != nil {
|
||||
resp = fmt.Sprintf("Error downloading a mod: %s", err)
|
||||
log.Println(resp)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
resp = mods.ListInstalledMods()
|
||||
}
|
||||
|
||||
func ModPortalLoginHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
var resp interface{}
|
||||
|
||||
defer func() {
|
||||
WriteResponse(w, resp)
|
||||
}()
|
||||
|
||||
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
|
||||
|
||||
var data struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
err = ReadFromRequestBody(w, r, &resp, &data)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err, statusCode := factorio.FactorioLogin(data.Username, data.Password)
|
||||
w.WriteHeader(statusCode)
|
||||
if err != nil {
|
||||
resp = fmt.Sprintf("Error trying to login into Factorio: %s", err)
|
||||
log.Println(resp)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func ModPortalLoginStatusHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
var resp interface{}
|
||||
|
||||
defer func() {
|
||||
WriteResponse(w, resp)
|
||||
}()
|
||||
|
||||
var credentials factorio.Credentials
|
||||
resp, err = credentials.Load()
|
||||
|
||||
if err != nil {
|
||||
resp = fmt.Sprintf("Error getting the factorio credentials: %s", err)
|
||||
log.Println(resp)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func ModPortalLogoutHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
var resp interface{}
|
||||
|
||||
defer func() {
|
||||
WriteResponse(w, resp)
|
||||
}()
|
||||
|
||||
var credentials factorio.Credentials
|
||||
err = credentials.Del()
|
||||
|
||||
if err != nil {
|
||||
resp = fmt.Sprintf("Error on logging out of factorio: %s", err)
|
||||
log.Println(resp)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
resp = false
|
||||
}
|
||||
|
||||
func ModPortalInstallMultipleHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
var resp interface{}
|
||||
|
||||
defer func() {
|
||||
WriteResponse(w, resp)
|
||||
}()
|
||||
|
||||
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
|
||||
|
||||
var data []struct {
|
||||
Name string `json:"name"`
|
||||
Version factorio.Version `json:"version"`
|
||||
}
|
||||
err = ReadFromRequestBody(w, r, &resp, &data)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
modList, err := CreateNewMods(w, &resp)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, datum := range data {
|
||||
// skip base mod because it is already included in factorio
|
||||
if datum.Name == "base" {
|
||||
continue
|
||||
}
|
||||
details, err, statusCode := factorio.ModPortalModDetails(datum.Name)
|
||||
if err != nil || statusCode != http.StatusOK {
|
||||
resp = fmt.Sprintf("Error in getting mod details from mod portal: %s", err)
|
||||
log.Println(resp)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
//find correct mod-version
|
||||
var found = false
|
||||
for _, release := range details.Releases {
|
||||
if release.Version.Equals(datum.Version) {
|
||||
found = true
|
||||
|
||||
err := modList.DownloadMod(release.DownloadURL, release.FileName, details.Name)
|
||||
if err != nil {
|
||||
resp = fmt.Sprintf("Error downloading mod {%s}, error: %s", details.Name, err)
|
||||
log.Println(resp)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
log.Printf("Error downloading mod {%s}, error: %s", details.Name, "version not found")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
resp = modList.ListInstalledMods()
|
||||
}
|
80
src/api/mod_portal_handler_test.go
Normal file
80
src/api/mod_portal_handler_test.go
Normal file
@ -0,0 +1,80 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestModPortalInstallHandler(t *testing.T) {
|
||||
CheckShort(t)
|
||||
|
||||
method := "POST"
|
||||
route := "/api/mods/portal/install"
|
||||
handlerFunc := ModPortalInstallHandler
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
SetupMods(t, true)
|
||||
defer CleanupMods(t)
|
||||
|
||||
requestBody := strings.NewReader(`{"modName": "belt-balancer", "downloadUrl": "/download/belt-balancer/5e711cd95bcf4f000b96b22c", "fileName": "belt-balancer_2.1.2.zip"}`)
|
||||
|
||||
expected := `{"mods":[{"name":"belt-balancer","version":"2.1.2","title":"Belt Balancer","author":"knoxfighter","file_name":"belt-balancer_2.1.2.zip","factorio_version":"0.18.0.0","dependencies":null,"compatibility":true,"enabled":true}]}`
|
||||
|
||||
CallRoute(t, method, route, route, requestBody, handlerFunc, http.StatusOK, expected)
|
||||
})
|
||||
|
||||
ModEmptyBodyTest(t, method, route, handlerFunc)
|
||||
|
||||
ModInvalidJsonTest(t, method, route, handlerFunc)
|
||||
|
||||
t.Run("wrong download link", func(t *testing.T) {
|
||||
SetupMods(t, true)
|
||||
defer CleanupMods(t)
|
||||
|
||||
requestBody := strings.NewReader(`{"modName": "belt-balancer", "downloadUrl": "/download/belt-balancer/95bcf4f000b96b22c", "fileName": "belt-balancer_2.1.2.zip"}`)
|
||||
|
||||
CallRoute(t, method, route, route, requestBody, handlerFunc, http.StatusInternalServerError, "")
|
||||
})
|
||||
}
|
||||
|
||||
func TestModPortalInstallMultipleHandler(t *testing.T) {
|
||||
CheckShort(t)
|
||||
|
||||
method := "POST"
|
||||
route := "/api/mods/portal/install/multiple"
|
||||
handlerFunc := ModPortalInstallMultipleHandler
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
SetupMods(t, true)
|
||||
defer CleanupMods(t)
|
||||
|
||||
requestBody := strings.NewReader(`[{"name": "belt-balancer", "version": "2.1.2"}, {"name": "train-station-overview", "version": "2.0.2"}]`)
|
||||
|
||||
expected := `{"mods":[{"name":"belt-balancer","version":"2.1.2","title":"Belt Balancer","author":"knoxfighter","file_name":"belt-balancer_2.1.2.zip","factorio_version":"0.18.0.0","dependencies":null,"compatibility":true,"enabled":true},{"name":"train-station-overview","version":"2.0.2","title":"Train Station Overview","author":"knoxfighter","file_name":"train-station-overview_2.0.2.zip","factorio_version":"0.18.0.0","dependencies":null,"compatibility":true,"enabled":true}]}`
|
||||
|
||||
CallRoute(t, method, route, route, requestBody, handlerFunc, http.StatusOK, expected)
|
||||
})
|
||||
|
||||
t.Run("unknown mod", func(t *testing.T) {
|
||||
SetupMods(t, true)
|
||||
defer CleanupMods(t)
|
||||
|
||||
requestBody := strings.NewReader(`[{"name": "askdhcb", "version": "2.1.2"}]`)
|
||||
|
||||
CallRoute(t, method, route, route, requestBody, handlerFunc, http.StatusInternalServerError, "")
|
||||
})
|
||||
|
||||
t.Run("unknown version", func(t *testing.T) {
|
||||
SetupMods(t, true)
|
||||
defer CleanupMods(t)
|
||||
|
||||
requestBody := strings.NewReader(`[{"name": "belt-balancer", "version": "0.1.12"}]`)
|
||||
|
||||
CallRoute(t, method, route, route, requestBody, handlerFunc, http.StatusInternalServerError, "")
|
||||
})
|
||||
|
||||
ModEmptyBodyTest(t, method, route, handlerFunc)
|
||||
|
||||
ModInvalidJsonTest(t, method, route, handlerFunc)
|
||||
}
|
339
src/api/mods_handler.go
Normal file
339
src/api/mods_handler.go
Normal file
@ -0,0 +1,339 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/mroote/factorio-server-manager/bootstrap"
|
||||
"github.com/mroote/factorio-server-manager/factorio"
|
||||
"github.com/mroote/factorio-server-manager/lockfile"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func CreateNewMods(w http.ResponseWriter, resp *interface{}) (modList factorio.Mods, err error) {
|
||||
config := bootstrap.GetConfig()
|
||||
modList, err = factorio.NewMods(config.FactorioModsDir)
|
||||
if err != nil {
|
||||
*resp = fmt.Sprintf("Error creating mods object: %s", err)
|
||||
log.Println(*resp)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func ReadFromRequestBody(w http.ResponseWriter, r *http.Request, resp *interface{}, data interface{}) (err error) {
|
||||
//Get Data out of the request
|
||||
body, err := ReadRequestBody(w, r, resp)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = json.Unmarshal(body, data)
|
||||
if err != nil {
|
||||
*resp = fmt.Sprintf("Error unmarshalling requested struct JSON: %s", err)
|
||||
log.Println(*resp)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Returns JSON response of all mods installed in factorio/mods
|
||||
func ListInstalledModsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
var resp interface{}
|
||||
|
||||
defer func() {
|
||||
WriteResponse(w, resp)
|
||||
}()
|
||||
|
||||
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
|
||||
|
||||
modList, err := CreateNewMods(w, &resp)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
resp = modList.ListInstalledMods().ModsResult
|
||||
}
|
||||
|
||||
func ModToggleHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
var resp interface{}
|
||||
|
||||
defer func() {
|
||||
WriteResponse(w, resp)
|
||||
}()
|
||||
|
||||
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
|
||||
|
||||
var data struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
err = ReadFromRequestBody(w, r, &resp, &data)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
mods, err := CreateNewMods(w, &resp)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err, resp = mods.ModSimpleList.ToggleMod(data.Name)
|
||||
if err != nil {
|
||||
resp = fmt.Sprintf("Error in toggling mod in simple list: %s", err)
|
||||
log.Println(resp)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func ModDeleteHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
var resp interface{}
|
||||
|
||||
defer func() {
|
||||
WriteResponse(w, resp)
|
||||
}()
|
||||
|
||||
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
|
||||
|
||||
var data struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// Get Data out of the request
|
||||
err = ReadFromRequestBody(w, r, &resp, &data)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
modList, err := CreateNewMods(w, &resp)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = modList.DeleteMod(data.Name)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
resp = fmt.Sprintf("Error in deleting mod {%s}: %s", data.Name, err)
|
||||
log.Println(resp)
|
||||
return
|
||||
}
|
||||
|
||||
resp = data.Name
|
||||
}
|
||||
|
||||
func ModDeleteAllHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
var resp interface{}
|
||||
|
||||
defer func() {
|
||||
WriteResponse(w, resp)
|
||||
}()
|
||||
|
||||
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
|
||||
|
||||
//delete mods folder
|
||||
err = factorio.DeleteAllMods()
|
||||
if err != nil {
|
||||
resp = fmt.Sprintf("Error deleting all mods: %s", err)
|
||||
log.Println(resp)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
resp = nil
|
||||
}
|
||||
|
||||
func ModUpdateHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
var resp interface{}
|
||||
|
||||
defer func() {
|
||||
WriteResponse(w, resp)
|
||||
}()
|
||||
|
||||
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
|
||||
|
||||
//Get Data out of the request
|
||||
var modData struct {
|
||||
Name string `json:"modName"`
|
||||
DownloadUrl string `json:"downloadUrl"`
|
||||
Filename string `json:"fileName"`
|
||||
}
|
||||
|
||||
err = ReadFromRequestBody(w, r, &resp, &modData)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
mods, err := CreateNewMods(w, &resp)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = mods.UpdateMod(modData.Name, modData.DownloadUrl, modData.Filename)
|
||||
if err != nil {
|
||||
resp = fmt.Sprintf("Error updating mod {%s}: %s", modData.Name, err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
installedMods := mods.ListInstalledMods().ModsResult
|
||||
for _, mod := range installedMods {
|
||||
if mod.Name == modData.Name {
|
||||
resp = mod
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
resp = fmt.Sprintf(`Could not find mod %s`, modData.Name)
|
||||
log.Println(resp)
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
func ModUploadHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
var resp interface{}
|
||||
|
||||
defer func() {
|
||||
WriteResponse(w, resp)
|
||||
}()
|
||||
|
||||
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
|
||||
|
||||
formFile, fileHeader, err := r.FormFile("mod_file")
|
||||
if err != nil {
|
||||
resp = fmt.Sprintf("error getting uploaded file: %s", err)
|
||||
log.Println(resp)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
defer formFile.Close()
|
||||
|
||||
mods, err := CreateNewMods(w, &resp)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = mods.UploadMod(formFile, fileHeader)
|
||||
if err != nil {
|
||||
resp = fmt.Sprintf("error saving file to mods: %s", err)
|
||||
log.Println(resp)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
resp = mods.ListInstalledMods()
|
||||
}
|
||||
|
||||
func ModDownloadHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
|
||||
zipWriter := zip.NewWriter(w)
|
||||
defer zipWriter.Close()
|
||||
config := bootstrap.GetConfig()
|
||||
//iterate over folder and create everything in the zip
|
||||
err = filepath.Walk(config.FactorioModsDir, func(path string, info os.FileInfo, err error) error {
|
||||
if info.IsDir() == false {
|
||||
//Lock the file, that we are want to read
|
||||
err := factorio.FileLock.RLock(path)
|
||||
if err != nil {
|
||||
log.Printf("error locking file for reading, something else has locked it")
|
||||
return err
|
||||
}
|
||||
defer factorio.FileLock.RUnlock(path)
|
||||
|
||||
writer, err := zipWriter.Create(info.Name())
|
||||
if err != nil {
|
||||
log.Printf("error on creating new file inside zip: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
log.Printf("error on opening modfile: %s", err)
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
_, err = io.Copy(writer, file)
|
||||
if err != nil {
|
||||
log.Printf("error on copying file into zip: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
err = file.Close()
|
||||
if err != nil {
|
||||
log.Printf("error closing file: %s", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err == lockfile.ErrorAlreadyLocked {
|
||||
w.WriteHeader(http.StatusLocked)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
log.Printf("error on walking over the mods: %s", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
writerHeader := w.Header()
|
||||
writerHeader.Set("Content-Type", "application/zip;charset=UTF-8")
|
||||
writerHeader.Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", "all_installed_mods.zip"))
|
||||
}
|
||||
|
||||
//LoadModsFromSaveHandler returns JSON response with the found mods
|
||||
func LoadModsFromSaveHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
var resp interface{}
|
||||
|
||||
defer func() {
|
||||
WriteResponse(w, resp)
|
||||
}()
|
||||
|
||||
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
|
||||
|
||||
//Get Data out of the request
|
||||
var saveFileStruct struct {
|
||||
Name string `json:"saveFile"`
|
||||
}
|
||||
err = ReadFromRequestBody(w, r, &resp, &saveFileStruct)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
config := bootstrap.GetConfig()
|
||||
path := filepath.Join(config.FactorioSavesDir, saveFileStruct.Name)
|
||||
f, err := factorio.OpenArchiveFile(path, "level.dat")
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
resp = fmt.Sprintf("cannot open save level file: %v", err)
|
||||
log.Println(resp)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
var header factorio.SaveHeader
|
||||
err = header.ReadFrom(f)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
resp = fmt.Sprintf("cannot read save header: %v", err)
|
||||
log.Println(resp)
|
||||
return
|
||||
}
|
||||
|
||||
resp = header
|
||||
}
|
486
src/api/mods_handler_test.go
Normal file
486
src/api/mods_handler_test.go
Normal file
@ -0,0 +1,486 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/joho/godotenv"
|
||||
"github.com/mroote/factorio-server-manager/bootstrap"
|
||||
"github.com/mroote/factorio-server-manager/factorio"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"io"
|
||||
"log"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
var err error
|
||||
|
||||
godotenv.Load("../.env")
|
||||
|
||||
// basic setup stuff
|
||||
bootstrap.NewConfig([]string{
|
||||
"--dir", os.Getenv("dir"),
|
||||
"--conf", os.Getenv("conf"),
|
||||
"--mod-pack-dir", os.Getenv("mod_pack_dir"),
|
||||
"--mod-dir", os.Getenv("mod_dir"),
|
||||
})
|
||||
|
||||
factorio.SetFactorioServer(factorio.Server{
|
||||
Version: factorio.Version{0, 18, 30, 0},
|
||||
BaseModVersion: "0.18.30",
|
||||
})
|
||||
|
||||
// check login status
|
||||
var cred factorio.Credentials
|
||||
load, err := cred.Load()
|
||||
if err != nil {
|
||||
log.Fatalf("Error loading factorio credentials: %s", err)
|
||||
return
|
||||
}
|
||||
if !load {
|
||||
// no credentials found, login...
|
||||
err, _ = factorio.FactorioLogin(os.Getenv("factorio_username"), os.Getenv("factorio_password"))
|
||||
if err != nil {
|
||||
log.Printf("Error logging in into factorio: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
func CheckShort(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Do not run in Short-mode")
|
||||
}
|
||||
}
|
||||
|
||||
func SetupMods(t *testing.T, empty bool) {
|
||||
var err error
|
||||
|
||||
config := bootstrap.GetConfig()
|
||||
|
||||
// check if dev directory exists and create it
|
||||
if _, err = os.Stat(config.FactorioModsDir); os.IsNotExist(err) {
|
||||
err = os.Mkdir(config.FactorioModsDir, 0775)
|
||||
}
|
||||
if err != nil {
|
||||
log.Fatalf(`Error creating "dev" directory: %s`, err)
|
||||
return
|
||||
}
|
||||
|
||||
mod, err := factorio.NewMods(config.FactorioModsDir)
|
||||
if err != nil {
|
||||
t.Fatalf("couldn't create Mods object: %s", err)
|
||||
}
|
||||
|
||||
if !empty {
|
||||
err := mod.DownloadMod("/download/belt-balancer/5e9f9db4bf9d30000c5303f2", "belt-balancer_2.1.3.zip", "belt-balancer")
|
||||
if err != nil {
|
||||
t.Fatalf(`Error downloading Mod "belt-balancer": %s`, err)
|
||||
}
|
||||
|
||||
err = mod.DownloadMod("/download/train-station-overview/5e8a0a8ee8864f000d0cb022", "train-station-overview_2.0.3.zip", "train-station-overview")
|
||||
if err != nil {
|
||||
t.Fatalf(`Error downloading Mod "train-station-overview": %s`, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func CleanupMods(t *testing.T) {
|
||||
config := bootstrap.GetConfig()
|
||||
err := os.RemoveAll(config.FactorioModsDir)
|
||||
if err != nil {
|
||||
t.Fatalf("Error removing dev directory: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func CallRoute(t *testing.T, method string, baseRoute string, route string, body io.Reader, handlerFunc http.HandlerFunc, statusCode int, expected string) {
|
||||
// create request to send
|
||||
request, err := http.NewRequest(method, route, body)
|
||||
if err != nil {
|
||||
t.Fatalf("Error creating request: %s", err)
|
||||
}
|
||||
|
||||
// create response recorder
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
// get the handler, where the request is handled
|
||||
router := mux.NewRouter()
|
||||
router.HandleFunc(baseRoute, handlerFunc)
|
||||
|
||||
// call the handler directly
|
||||
router.ServeHTTP(recorder, request)
|
||||
//handler.ServeHTTP(recorder, request)
|
||||
|
||||
// status has to be 200
|
||||
if recorder.Code != statusCode {
|
||||
t.Fatalf("Wrong Status Code. expected %v - got %v", statusCode, recorder.Code)
|
||||
}
|
||||
|
||||
if expected != "" {
|
||||
actual := recorder.Body.String()
|
||||
|
||||
require.JSONEqf(t, expected, actual, `Wrong Body for route "%s". expected "%v" - actual "%v"`, route, expected, actual)
|
||||
}
|
||||
}
|
||||
|
||||
func ModEmptyBodyTest(t *testing.T, method string, route string, handlerFunc http.HandlerFunc) {
|
||||
t.Run("empty body", func(t *testing.T) {
|
||||
SetupMods(t, true)
|
||||
defer CleanupMods(t)
|
||||
|
||||
CallRoute(t, method, route, route, nil, handlerFunc, http.StatusBadRequest, "")
|
||||
})
|
||||
}
|
||||
|
||||
func ModInvalidJsonTest(t *testing.T, method, route string, handlerFunc http.HandlerFunc) {
|
||||
t.Run("invalid json body", func(t *testing.T) {
|
||||
SetupMods(t, true)
|
||||
defer CleanupMods(t)
|
||||
|
||||
requestBody := strings.NewReader(`{"name": "asdc"`)
|
||||
|
||||
CallRoute(t, method, route, route, requestBody, handlerFunc, http.StatusBadRequest, "")
|
||||
})
|
||||
}
|
||||
|
||||
func ModNotExistTest(t *testing.T, method, route string, handlerFunc http.HandlerFunc) {
|
||||
t.Run("mod not exist", func(t *testing.T) {
|
||||
requestBody := strings.NewReader(`{"name": "lasdg"}`)
|
||||
|
||||
CallRoute(t, method, route, route, requestBody, handlerFunc, http.StatusInternalServerError, "")
|
||||
})
|
||||
}
|
||||
|
||||
func TestListInstalledModsHandler(t *testing.T) {
|
||||
CheckShort(t)
|
||||
|
||||
SetupMods(t, false)
|
||||
defer CleanupMods(t)
|
||||
|
||||
route := "/api/mods/list"
|
||||
|
||||
expected := `[{"name":"belt-balancer","version":"2.1.3","title":"Belt Balancer","author":"knoxfighter","file_name":"belt-balancer_2.1.3.zip","factorio_version":"0.18.0.0","dependencies":null,"compatibility":true,"enabled":true},{"name":"train-station-overview","version":"2.0.3","title":"Train Station Overview","author":"knoxfighter","file_name":"train-station-overview_2.0.3.zip","factorio_version":"0.18.0.0","dependencies":null,"compatibility":true,"enabled":true}]`
|
||||
|
||||
CallRoute(t, "GET", route, route, nil, ListInstalledModsHandler, http.StatusOK, expected)
|
||||
}
|
||||
|
||||
func TestModToggleHandler(t *testing.T) {
|
||||
CheckShort(t)
|
||||
|
||||
method := "POST"
|
||||
route := "/api/mods/toggle"
|
||||
handlerFunc := ModToggleHandler
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
SetupMods(t, false)
|
||||
defer CleanupMods(t)
|
||||
|
||||
requestBody := strings.NewReader(`{"name": "belt-balancer"}`)
|
||||
|
||||
// mod is now deactivated
|
||||
expected := "false"
|
||||
|
||||
CallRoute(t, method, route, route, requestBody, handlerFunc, http.StatusOK, expected)
|
||||
|
||||
// check if changes happenes
|
||||
config := bootstrap.GetConfig()
|
||||
modList, err := factorio.NewMods(config.FactorioModsDir)
|
||||
if err != nil {
|
||||
t.Fatalf("Error creating Mods object: %s", err)
|
||||
}
|
||||
found := false
|
||||
for _, mod := range modList.ModSimpleList.Mods {
|
||||
if mod.Name == "belt-balancer" {
|
||||
// this mod has to be deactivated now
|
||||
if mod.Enabled {
|
||||
t.Fatalf("Mod is wrongly enabled, it should be disabled by now")
|
||||
}
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatalf("Mod not found")
|
||||
}
|
||||
|
||||
// toggle again, to check if the other direction also works
|
||||
// mod is now activated again
|
||||
expected = "true"
|
||||
|
||||
// reset request body, it has to be red again
|
||||
requestBody.Seek(0, 0)
|
||||
|
||||
CallRoute(t, method, route, route, requestBody, handlerFunc, http.StatusOK, expected)
|
||||
|
||||
modList, err = factorio.NewMods(config.FactorioModsDir)
|
||||
if err != nil {
|
||||
t.Fatalf("Error creating Mods object: %s", err)
|
||||
}
|
||||
found = false
|
||||
for _, mod := range modList.ModSimpleList.Mods {
|
||||
if mod.Name == "belt-balancer" {
|
||||
// this mod has to be deactivated now
|
||||
if !mod.Enabled {
|
||||
t.Fatalf("Mod is wrongly disabled, it should be enabled again")
|
||||
}
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatalf("Mod not found")
|
||||
}
|
||||
})
|
||||
|
||||
ModEmptyBodyTest(t, method, route, handlerFunc)
|
||||
|
||||
ModInvalidJsonTest(t, method, route, handlerFunc)
|
||||
|
||||
ModNotExistTest(t, method, route, handlerFunc)
|
||||
}
|
||||
|
||||
func TestModDeleteHandler(t *testing.T) {
|
||||
CheckShort(t)
|
||||
|
||||
method := "POST"
|
||||
route := "/api/mods/delete"
|
||||
handlerFunc := ModDeleteHandler
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
SetupMods(t, false)
|
||||
defer CleanupMods(t)
|
||||
|
||||
requestBody := strings.NewReader(`{"name": "belt-balancer"}`)
|
||||
|
||||
CallRoute(t, method, route, route, requestBody, handlerFunc, http.StatusOK, `"belt-balancer"`)
|
||||
|
||||
// check if mod is really not installed anymore
|
||||
config := bootstrap.GetConfig()
|
||||
modList, err := factorio.NewMods(config.FactorioModsDir)
|
||||
if err != nil {
|
||||
t.Fatalf("Error creating Mods object: %s", err)
|
||||
}
|
||||
if modList.ModSimpleList.CheckModExists("belt-balancer") {
|
||||
t.Fatalf("Mod is still installed, it should be gone by now")
|
||||
}
|
||||
})
|
||||
|
||||
ModEmptyBodyTest(t, method, route, handlerFunc)
|
||||
|
||||
ModInvalidJsonTest(t, method, route, handlerFunc)
|
||||
|
||||
ModNotExistTest(t, method, route, handlerFunc)
|
||||
}
|
||||
|
||||
func TestModDeleteAllHandler(t *testing.T) {
|
||||
CheckShort(t)
|
||||
|
||||
method := "POST"
|
||||
route := "/api/mods/delete/all"
|
||||
handlerFunc := ModDeleteAllHandler
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
SetupMods(t, false)
|
||||
defer CleanupMods(t)
|
||||
|
||||
CallRoute(t, method, route, route, nil, handlerFunc, http.StatusOK, "null")
|
||||
|
||||
// check if no mods are there
|
||||
config := bootstrap.GetConfig()
|
||||
modList, err := factorio.NewMods(config.FactorioModsDir)
|
||||
if err != nil {
|
||||
t.Fatalf("Error creating mods object: %s", err)
|
||||
}
|
||||
if len(modList.ListInstalledMods().ModsResult) != 0 {
|
||||
t.Fatalf("Mods are still there!")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestModUpdateHandler(t *testing.T) {
|
||||
CheckShort(t)
|
||||
|
||||
method := "POST"
|
||||
route := "/api/mods/update"
|
||||
handlerFunc := ModUpdateHandler
|
||||
|
||||
requestBodySuccess := `{"modName": "belt-balancer", "downloadUrl": "/download/belt-balancer/5e711cd95bcf4f000b96b22c", "fileName": "belt-balancer_2.1.2.zip"}`
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
SetupMods(t, false)
|
||||
defer CleanupMods(t)
|
||||
|
||||
expected := `{"name":"belt-balancer","version":"2.1.2","title":"Belt Balancer","author":"knoxfighter","file_name":"belt-balancer_2.1.2.zip","factorio_version":"0.18.0.0","dependencies":null,"compatibility":true,"enabled":true}`
|
||||
|
||||
CallRoute(t, method, route, route, strings.NewReader(requestBodySuccess), handlerFunc, http.StatusOK, expected)
|
||||
})
|
||||
|
||||
t.Run("success with disabled mod", func(t *testing.T) {
|
||||
SetupMods(t, false)
|
||||
defer CleanupMods(t)
|
||||
|
||||
// disable "belt-balancer" mod, so we can test, if it is still deactivated after
|
||||
config := bootstrap.GetConfig()
|
||||
modList, err := factorio.NewMods(config.FactorioModsDir)
|
||||
if err != nil {
|
||||
t.Fatalf("Error creating mods object: %s", err)
|
||||
}
|
||||
err, _ = modList.ModSimpleList.ToggleMod("belt-balancer")
|
||||
if err != nil {
|
||||
t.Fatalf("Error toggling mod: %s", err)
|
||||
}
|
||||
|
||||
expected := `{"name":"belt-balancer","version":"2.1.2","title":"Belt Balancer","author":"knoxfighter","file_name":"belt-balancer_2.1.2.zip","factorio_version":"0.18.0.0","dependencies":null,"compatibility":true,"enabled":false}`
|
||||
|
||||
CallRoute(t, method, route, route, strings.NewReader(requestBodySuccess), handlerFunc, http.StatusOK, expected)
|
||||
})
|
||||
|
||||
ModEmptyBodyTest(t, method, route, handlerFunc)
|
||||
|
||||
ModInvalidJsonTest(t, method, route, handlerFunc)
|
||||
|
||||
t.Run("mod not exist", func(t *testing.T) {
|
||||
SetupMods(t, false)
|
||||
defer CleanupMods(t)
|
||||
|
||||
requestBody := strings.NewReader(`{"modName": "alfbasd", "downloadUrl": "/download/belt-balancer/5e711cd95bcf4f000b96b22c", "fileName": "belt-balancer_2.1.2.zip"}`)
|
||||
|
||||
CallRoute(t, method, route, route, requestBody, handlerFunc, http.StatusNotFound, "")
|
||||
})
|
||||
|
||||
t.Run("downloadUrl invalid", func(t *testing.T) {
|
||||
SetupMods(t, false)
|
||||
defer CleanupMods(t)
|
||||
|
||||
requestBody := strings.NewReader(`{"modName": "belt-balancer", "downloadUrl": "/download/belt-balancer/cd95bcf4f000b96b22c", "fileName": "belt-balancer_2.1.2.zip"}`)
|
||||
|
||||
CallRoute(t, method, route, route, requestBody, handlerFunc, http.StatusInternalServerError, "")
|
||||
})
|
||||
}
|
||||
|
||||
func ModUploadRequest(t *testing.T, body bool, filePath string) *httptest.ResponseRecorder {
|
||||
CheckShort(t)
|
||||
|
||||
var err error
|
||||
method := "POST"
|
||||
route := "/api/mods/upload"
|
||||
handlerFunc := ModUploadHandler
|
||||
|
||||
requestBody := &bytes.Buffer{}
|
||||
|
||||
writer := multipart.NewWriter(requestBody)
|
||||
|
||||
if body {
|
||||
file, err := os.Open(filePath)
|
||||
if err == nil {
|
||||
assert.NoError(t, err, "error opening mod file")
|
||||
|
||||
formFile, err := writer.CreateFormFile("mod_file", filepath.Base(filePath))
|
||||
assert.NoError(t, err, "error creating formFileWriter")
|
||||
|
||||
_, err = io.Copy(formFile, file)
|
||||
assert.NoError(t, err, "error copying file to form")
|
||||
}
|
||||
}
|
||||
|
||||
err = writer.Close()
|
||||
if err != nil {
|
||||
t.Fatalf("error closing the multipart writer: %s", err)
|
||||
}
|
||||
|
||||
// create request to send
|
||||
request, err := http.NewRequest(method, route, requestBody)
|
||||
assert.NoError(t, err, "Error creating request")
|
||||
request.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
|
||||
// create response recorder
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
// get the handler, where the request is handled
|
||||
handler := http.HandlerFunc(handlerFunc)
|
||||
|
||||
// call the handler directly
|
||||
handler.ServeHTTP(recorder, request)
|
||||
|
||||
return recorder
|
||||
}
|
||||
|
||||
func TestModUploadHandler(t *testing.T) {
|
||||
CheckShort(t)
|
||||
|
||||
method := "POST"
|
||||
route := "/api/mods/upload"
|
||||
handlerFunc := ModUploadHandler
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
SetupMods(t, true)
|
||||
defer CleanupMods(t)
|
||||
|
||||
recorder := ModUploadRequest(t, true, "../factorio_testfiles/belt-balancer_2.1.3.zip")
|
||||
|
||||
// status has to be 200
|
||||
if recorder.Code != http.StatusOK {
|
||||
t.Fatalf("Wrong Status Code. expected %v - got %v", http.StatusOK, recorder.Code)
|
||||
}
|
||||
|
||||
// check if mod is uploaded correctly
|
||||
config := bootstrap.GetConfig()
|
||||
modList, err := factorio.NewMods(config.FactorioModsDir)
|
||||
assert.NoError(t, err, "error creating mods object")
|
||||
|
||||
expected := factorio.ModsResultList{
|
||||
ModsResult: []factorio.ModsResult{
|
||||
{
|
||||
ModInfo: factorio.ModInfo{
|
||||
Name: "belt-balancer",
|
||||
Version: "2.1.3",
|
||||
Title: "Belt Balancer",
|
||||
Author: "knoxfighter",
|
||||
FileName: "belt-balancer_2.1.3.zip",
|
||||
FactorioVersion: factorio.Version{0, 18, 0, 0},
|
||||
Dependencies: nil,
|
||||
Compatibility: true,
|
||||
},
|
||||
Enabled: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
actual := modList.ListInstalledMods()
|
||||
assert.Equal(t, expected, actual, `New mod is not correctly installed. expected "%v" - actual "%v"`, expected, actual)
|
||||
})
|
||||
|
||||
ModEmptyBodyTest(t, method, route, handlerFunc)
|
||||
|
||||
t.Run("empty file", func(t *testing.T) {
|
||||
SetupMods(t, true)
|
||||
defer CleanupMods(t)
|
||||
|
||||
recorder := ModUploadRequest(t, true, "")
|
||||
assert.Equal(t, http.StatusBadRequest, recorder.Code, "wrong response code.")
|
||||
})
|
||||
|
||||
t.Run("invalid mod file (txt-file)", func(t *testing.T) {
|
||||
SetupMods(t, true)
|
||||
defer CleanupMods(t)
|
||||
|
||||
recorder := ModUploadRequest(t, false, "../factorio_testfiles/file_usage.txt")
|
||||
assert.Equal(t, http.StatusBadRequest, recorder.Code, "wrong response code.")
|
||||
})
|
||||
|
||||
t.Run("invalid mod file (zip-file)", func(t *testing.T) {
|
||||
SetupMods(t, true)
|
||||
defer CleanupMods(t)
|
||||
|
||||
recorder := ModUploadRequest(t, true, "../factorio_testfiles/invalid_mod.zip")
|
||||
assert.Equal(t, http.StatusInternalServerError, recorder.Code, "wrong response code.")
|
||||
})
|
||||
}
|
@ -1,22 +1,13 @@
|
||||
package main
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/mroote/factorio-server-manager/api/websocket"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
var upgrader = websocket.Upgrader{
|
||||
ReadBufferSize: 1024,
|
||||
WriteBufferSize: 1024,
|
||||
//TODO Proper origin check
|
||||
CheckOrigin: func(r *http.Request) bool { return true },
|
||||
}
|
||||
|
||||
type Handler func(*Client, interface{})
|
||||
|
||||
type Route struct {
|
||||
Name string
|
||||
Method string
|
||||
@ -26,13 +17,8 @@ type Route struct {
|
||||
|
||||
type Routes []Route
|
||||
|
||||
type WSRouter struct {
|
||||
rules map[string]Handler
|
||||
}
|
||||
|
||||
func NewRouter() *mux.Router {
|
||||
r := mux.NewRouter().StrictSlash(true)
|
||||
ws := NewWSRouter()
|
||||
|
||||
// API subrouter
|
||||
// Serves all JSON REST handlers prefixed with /api
|
||||
@ -57,9 +43,15 @@ func NewRouter() *mux.Router {
|
||||
r.Path("/ws").
|
||||
Methods("GET").
|
||||
Name("Websocket").
|
||||
Handler(AuthorizeHandler(ws))
|
||||
ws.Handle("command send", commandSend)
|
||||
ws.Handle("log subscribe", logSubscribe)
|
||||
Handler(
|
||||
AuthorizeHandler(
|
||||
http.HandlerFunc(
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
websocket.ServeWs(w, r)
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
// Serves the frontend application from the app directory
|
||||
// Uses basic file server to serve index.html and Javascript application
|
||||
@ -68,34 +60,40 @@ func NewRouter() *mux.Router {
|
||||
Methods("GET").
|
||||
Name("Login").
|
||||
Handler(http.StripPrefix("/login", http.FileServer(http.Dir("./app/"))))
|
||||
r.Path("/settings").
|
||||
Methods("GET").
|
||||
Name("Settings").
|
||||
Handler(AuthorizeHandler(http.StripPrefix("/settings", http.FileServer(http.Dir("./app/")))))
|
||||
r.Path("/mods").
|
||||
Methods("GET").
|
||||
Name("Mods").
|
||||
Handler(AuthorizeHandler(http.StripPrefix("/mods", http.FileServer(http.Dir("./app/")))))
|
||||
r.Path("/saves").
|
||||
Methods("GET").
|
||||
Name("Saves").
|
||||
Handler(AuthorizeHandler(http.StripPrefix("/saves", http.FileServer(http.Dir("./app/")))))
|
||||
r.Path("/mods").
|
||||
Methods("GET").
|
||||
Name("Mods").
|
||||
Handler(AuthorizeHandler(http.StripPrefix("/mods", http.FileServer(http.Dir("./app/")))))
|
||||
r.Path("/server-settings").
|
||||
Methods("GET").
|
||||
Name("Server settings").
|
||||
Handler(AuthorizeHandler(http.StripPrefix("/server-settings", http.FileServer(http.Dir("./app/")))))
|
||||
r.Path("/game-settings").
|
||||
Methods("GET").
|
||||
Name("Game settings").
|
||||
Handler(AuthorizeHandler(http.StripPrefix("/game-settings", http.FileServer(http.Dir("./app/")))))
|
||||
r.Path("/console").
|
||||
Methods("GET").
|
||||
Name("Console").
|
||||
Handler(AuthorizeHandler(http.StripPrefix("/console", http.FileServer(http.Dir("./app/")))))
|
||||
r.Path("/logs").
|
||||
Methods("GET").
|
||||
Name("Logs").
|
||||
Handler(AuthorizeHandler(http.StripPrefix("/logs", http.FileServer(http.Dir("./app/")))))
|
||||
r.Path("/config").
|
||||
r.Path("/user-management").
|
||||
Methods("GET").
|
||||
Name("Config").
|
||||
Handler(AuthorizeHandler(http.StripPrefix("/config", http.FileServer(http.Dir("./app/")))))
|
||||
r.Path("/server").
|
||||
Name("User management").
|
||||
Handler(AuthorizeHandler(http.StripPrefix("/user-management", http.FileServer(http.Dir("./app/")))))
|
||||
r.Path("/help").
|
||||
Methods("GET").
|
||||
Name("Server").
|
||||
Handler(AuthorizeHandler(http.StripPrefix("/server", http.FileServer(http.Dir("./app/")))))
|
||||
r.Path("/console").
|
||||
Methods("GET").
|
||||
Name("Server").
|
||||
Handler(AuthorizeHandler(http.StripPrefix("/console", http.FileServer(http.Dir("./app/")))))
|
||||
Name("Help").
|
||||
Handler(AuthorizeHandler(http.StripPrefix("/help", http.FileServer(http.Dir("./app/")))))
|
||||
|
||||
// catch all route
|
||||
r.PathPrefix("/").
|
||||
Methods("GET").
|
||||
Name("Index").
|
||||
@ -108,6 +106,7 @@ func NewRouter() *mux.Router {
|
||||
// Redirects user to login page if no session is found
|
||||
func AuthorizeHandler(h http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
Auth := GetAuth()
|
||||
if err := Auth.aaa.Authorize(w, r, true); err != nil {
|
||||
log.Printf("Unauthenticated request %s %s %s", r.Method, r.Host, r.RequestURI)
|
||||
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
||||
@ -117,113 +116,10 @@ func AuthorizeHandler(h http.Handler) http.Handler {
|
||||
})
|
||||
}
|
||||
|
||||
func NewWSRouter() *WSRouter {
|
||||
return &WSRouter{
|
||||
rules: make(map[string]Handler),
|
||||
}
|
||||
}
|
||||
|
||||
func (ws *WSRouter) Handle(msgName string, handler Handler) {
|
||||
ws.rules[msgName] = handler
|
||||
}
|
||||
|
||||
func (ws *WSRouter) FindHandler(msgName string) (Handler, bool) {
|
||||
handler, found := ws.rules[msgName]
|
||||
return handler, found
|
||||
}
|
||||
|
||||
func (ws *WSRouter) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
socket, err := upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
log.Printf("Error opening ws connection: %s", err)
|
||||
return
|
||||
}
|
||||
client := NewClient(socket, ws.FindHandler)
|
||||
defer client.Close()
|
||||
go client.Write()
|
||||
client.Read()
|
||||
}
|
||||
|
||||
// Defines all API REST endpoints
|
||||
// All routes are prefixed with /api
|
||||
var apiRoutes = Routes{
|
||||
Route{
|
||||
"ListInstalledMods",
|
||||
"GET",
|
||||
"/mods/list/installed",
|
||||
listInstalledModsHandler,
|
||||
}, {
|
||||
"LoginFactorioModPortal",
|
||||
"POST",
|
||||
"/mods/factorio/login",
|
||||
LoginFactorioModPortal,
|
||||
}, {
|
||||
"LoginstatusFactorioModPortal",
|
||||
"POST",
|
||||
"/mods/factorio/status",
|
||||
LoginstatusFactorioModPortal,
|
||||
}, {
|
||||
"LogoutFactorioModPortal",
|
||||
"POST",
|
||||
"/mods/factorio/logout",
|
||||
LogoutFactorioModPortalHandler,
|
||||
}, {
|
||||
"SearchModPortal",
|
||||
"GET",
|
||||
"/mods/search",
|
||||
ModPortalSearchHandler,
|
||||
}, {
|
||||
"GetModDetails",
|
||||
"POST",
|
||||
"/mods/details",
|
||||
ModPortalDetailsHandler,
|
||||
}, {
|
||||
"ModPortalInstall",
|
||||
"POST",
|
||||
"/mods/install",
|
||||
ModPortalInstallHandler,
|
||||
}, {
|
||||
"ModPortalInstallMultiple",
|
||||
"POST",
|
||||
"/mods/install/multiple",
|
||||
ModPortalInstallMultipleHandler,
|
||||
}, {
|
||||
"ToggleMod",
|
||||
"POST",
|
||||
"/mods/toggle",
|
||||
ToggleModHandler,
|
||||
}, {
|
||||
"DeleteMod",
|
||||
"POST",
|
||||
"/mods/delete",
|
||||
DeleteModHandler,
|
||||
}, {
|
||||
"DeleteAllMods",
|
||||
"POST",
|
||||
"/mods/delete/all",
|
||||
DeleteAllModsHandler,
|
||||
}, {
|
||||
"UpdateMod",
|
||||
"POST",
|
||||
"/mods/update",
|
||||
UpdateModHandler,
|
||||
}, {
|
||||
"UploadMod",
|
||||
"POST",
|
||||
"/mods/upload",
|
||||
UploadModHandler,
|
||||
}, {
|
||||
"DownloadMods",
|
||||
"GET",
|
||||
"/mods/download",
|
||||
DownloadModsHandler,
|
||||
}, {
|
||||
"LoadModsFromSave",
|
||||
"POST",
|
||||
"/mods/save/load",
|
||||
LoadModsFromSaveHandler,
|
||||
}, {
|
||||
{
|
||||
"ListSaves",
|
||||
"GET",
|
||||
"/saves/list",
|
||||
@ -248,6 +144,11 @@ var apiRoutes = Routes{
|
||||
"GET",
|
||||
"/saves/create/{save}",
|
||||
CreateSaveHandler,
|
||||
}, {
|
||||
"LoadModsFromSave",
|
||||
"POST",
|
||||
"/saves/mods",
|
||||
LoadModsFromSaveHandler,
|
||||
}, {
|
||||
"LogTail",
|
||||
"GET",
|
||||
@ -258,11 +159,6 @@ var apiRoutes = Routes{
|
||||
"GET",
|
||||
"/config",
|
||||
LoadConfig,
|
||||
}, {
|
||||
"StartServer",
|
||||
"GET",
|
||||
"/server/start",
|
||||
StartServer,
|
||||
}, {
|
||||
"StartServer",
|
||||
"POST",
|
||||
@ -313,46 +209,6 @@ var apiRoutes = Routes{
|
||||
"POST",
|
||||
"/user/remove",
|
||||
RemoveUser,
|
||||
}, {
|
||||
"ListModPacks",
|
||||
"GET",
|
||||
"/mods/packs/list",
|
||||
ListModPacksHandler,
|
||||
}, {
|
||||
"DownloadModPack",
|
||||
"GET",
|
||||
"/mods/packs/download/{modpack}",
|
||||
DownloadModPackHandler,
|
||||
}, {
|
||||
"DeleteModPack",
|
||||
"POST",
|
||||
"/mods/packs/delete",
|
||||
DeleteModPackHandler,
|
||||
}, {
|
||||
"CreateModPack",
|
||||
"POST",
|
||||
"/mods/packs/create",
|
||||
CreateModPackHandler,
|
||||
}, {
|
||||
"LoadModPack",
|
||||
"POST",
|
||||
"/mods/packs/load",
|
||||
LoadModPackHandler,
|
||||
}, {
|
||||
"ModPackToggleMod",
|
||||
"POST",
|
||||
"/mods/packs/mod/toggle",
|
||||
ModPackToggleModHandler,
|
||||
}, {
|
||||
"ModPackDeleteMod",
|
||||
"POST",
|
||||
"/mods/packs/mod/delete",
|
||||
ModPackDeleteModHandler,
|
||||
}, {
|
||||
"ModPackUpdateMod",
|
||||
"POST",
|
||||
"/mods/packs/mod/update",
|
||||
ModPackUpdateModHandler,
|
||||
}, {
|
||||
"GetServerSettings",
|
||||
"GET",
|
||||
@ -364,4 +220,147 @@ var apiRoutes = Routes{
|
||||
"/settings/update",
|
||||
UpdateServerSettings,
|
||||
},
|
||||
// Mod Portal Stuff
|
||||
{
|
||||
"ModPortalListAllMods",
|
||||
"GET",
|
||||
"/mods/portal/list",
|
||||
ModPortalListModsHandler,
|
||||
}, {
|
||||
"ModPortalGetModInfo",
|
||||
"GET",
|
||||
"/mods/portal/info/{mod}",
|
||||
ModPortalModInfoHandler,
|
||||
}, {
|
||||
"ModPortalInstallMod",
|
||||
"POST",
|
||||
"/mods/portal/install",
|
||||
ModPortalInstallHandler,
|
||||
}, {
|
||||
"ModPortalLogin",
|
||||
"POST",
|
||||
"/mods/portal/login",
|
||||
ModPortalLoginHandler,
|
||||
}, {
|
||||
"ModPortalLoginStatus",
|
||||
"GET",
|
||||
"/mods/portal/loginstatus",
|
||||
ModPortalLoginStatusHandler,
|
||||
}, {
|
||||
"ModPortalLogout",
|
||||
"GET",
|
||||
"/mods/portal/logout",
|
||||
ModPortalLogoutHandler,
|
||||
}, {
|
||||
"ModPortalInstallMultiple",
|
||||
"POST",
|
||||
"/mods/portal/install/multiple",
|
||||
ModPortalInstallMultipleHandler,
|
||||
},
|
||||
// Mods Stuff
|
||||
{
|
||||
"ListInstalledMods",
|
||||
"GET",
|
||||
"/mods/list",
|
||||
ListInstalledModsHandler,
|
||||
}, {
|
||||
"ToggleMod",
|
||||
"POST",
|
||||
"/mods/toggle",
|
||||
ModToggleHandler,
|
||||
}, {
|
||||
"DeleteMod",
|
||||
"POST",
|
||||
"/mods/delete",
|
||||
ModDeleteHandler,
|
||||
}, {
|
||||
"DeleteAllMods",
|
||||
"POST",
|
||||
"/mods/delete/all",
|
||||
ModDeleteAllHandler,
|
||||
}, {
|
||||
"UpdateMod",
|
||||
"POST",
|
||||
"/mods/update",
|
||||
ModUpdateHandler,
|
||||
}, {
|
||||
"UploadMod",
|
||||
"POST",
|
||||
"/mods/upload",
|
||||
ModUploadHandler,
|
||||
}, {
|
||||
"DownloadMods",
|
||||
"GET",
|
||||
"/mods/download",
|
||||
ModDownloadHandler,
|
||||
},
|
||||
// Mod Packs
|
||||
{
|
||||
"ModPacksList",
|
||||
"GET",
|
||||
"/mods/packs/list",
|
||||
ModPackListHandler,
|
||||
}, {
|
||||
"ModPackCreate",
|
||||
"POST",
|
||||
"/mods/packs/create",
|
||||
ModPackCreateHandler,
|
||||
}, {
|
||||
"ModPackDelete",
|
||||
"POST",
|
||||
"/mods/packs/{modpack}/delete",
|
||||
ModPackDeleteHandler,
|
||||
}, {
|
||||
"ModPackDownload",
|
||||
"GET",
|
||||
"/mods/packs/{modpack}/download",
|
||||
ModPackDownloadHandler,
|
||||
}, {
|
||||
"LoadModPack",
|
||||
"POST",
|
||||
"/mods/packs/{modpack}/load",
|
||||
ModPackLoadHandler,
|
||||
},
|
||||
// Mods inside Mod Packs
|
||||
{
|
||||
"ModPackListMods",
|
||||
"GET",
|
||||
"/mods/packs/{modpack}/list",
|
||||
ModPackModListHandler,
|
||||
}, {
|
||||
"ModPackToggleMod",
|
||||
"POST",
|
||||
"/mods/packs/{modpack}/mod/toggle",
|
||||
ModPackModToggleHandler,
|
||||
}, {
|
||||
"ModPackDeleteMod",
|
||||
"POST",
|
||||
"/mods/packs/{modpack}/mod/delete",
|
||||
ModPackModDeleteHandler,
|
||||
}, {
|
||||
"ModPackDeleteAllMod",
|
||||
"POST",
|
||||
"/mods/packs/{modpack}/mod/delete/all",
|
||||
ModPackModDeleteAllHandler,
|
||||
}, {
|
||||
"ModPackUpdateMod",
|
||||
"POST",
|
||||
"/mods/packs/{modpack}/mod/update",
|
||||
ModPackModUpdateHandler,
|
||||
}, {
|
||||
"ModPackUploadMod",
|
||||
"POST",
|
||||
"/mods/packs/{modpack}/mod/upload",
|
||||
ModPackModUploadHandler,
|
||||
}, {
|
||||
"ModPackModPortalInstallMod",
|
||||
"POST",
|
||||
"/mods/packs/{modpack}/portal/install",
|
||||
ModPackModPortalInstallHandler,
|
||||
}, {
|
||||
"ModPackModPortalInstallMultiple",
|
||||
"POST",
|
||||
"/mods/packs/{modpack}/portal/install/multiple",
|
||||
ModPackModPortalInstallMultipleHandler,
|
||||
},
|
||||
}
|
171
src/api/websocket/wsclient.go
Normal file
171
src/api/websocket/wsclient.go
Normal file
@ -0,0 +1,171 @@
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
const (
|
||||
// Timeout for sending a message
|
||||
writeWait = 10 * time.Second
|
||||
|
||||
// Timeout between the answer of two pong messages
|
||||
pongWait = 60 * time.Second
|
||||
|
||||
// Period in which a new ping message is sent. Must be less than pongWait.
|
||||
pingPeriod = (pongWait * 9) / 10
|
||||
|
||||
// Maximum message size sent from a client
|
||||
maxMessageSize = 2048
|
||||
)
|
||||
|
||||
// The upgrader to upgrade from http to ws protocol
|
||||
var upgrader = websocket.Upgrader{
|
||||
ReadBufferSize: 4096,
|
||||
WriteBufferSize: 4096,
|
||||
}
|
||||
|
||||
// The websocket client, that is the middleman between a websocket connection and the hub.
|
||||
// It manages every communication between the hub and the websocket connection.
|
||||
type wsClient struct {
|
||||
// The hub this client is registered to.
|
||||
hub *wsHub
|
||||
|
||||
// The websocket connection.
|
||||
conn *websocket.Conn
|
||||
|
||||
// channel to send messages to the websocket connection.
|
||||
send chan wsMessage
|
||||
}
|
||||
|
||||
// read messages from the websocket connection, choose what has to be done with it and execute that action
|
||||
// messages with room name, send to the room
|
||||
// messages with empty room name and controls set, will execute that control, nothing will be sent to other clients
|
||||
// messages with empty room name and empty controls will send the message to all clients registered in the hub.
|
||||
//
|
||||
// This pump has to be executed in a goroutine!
|
||||
func (client *wsClient) readPump() {
|
||||
// When this pump closes, unregister the client and close the websocket connection
|
||||
defer func() {
|
||||
client.hub.unregister <- client
|
||||
client.conn.Close()
|
||||
}()
|
||||
|
||||
// Setup some websocket connection settings
|
||||
client.conn.SetReadLimit(maxMessageSize)
|
||||
client.conn.SetReadDeadline(time.Now().Add(pongWait))
|
||||
client.conn.SetPongHandler(func(string) error {
|
||||
client.conn.SetReadDeadline(time.Now().Add(pongWait))
|
||||
return nil
|
||||
})
|
||||
|
||||
for {
|
||||
// wait and read the next incoming message on the websocket.
|
||||
var message wsMessage
|
||||
err := client.conn.ReadJSON(&message)
|
||||
if err != nil {
|
||||
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
|
||||
log.Printf("error: %v", err)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
if message.RoomName == "" {
|
||||
// controls messages will not sent to other clients, they only are relevant for the server
|
||||
if message.Controls != (WsControls{}) {
|
||||
// this message is a control message, do its job!
|
||||
switch message.Controls.Type {
|
||||
case "subscribe":
|
||||
room := client.hub.GetRoom(message.Controls.Value)
|
||||
room.register <- client
|
||||
case "unsubscribe":
|
||||
room := client.hub.GetRoom(message.Controls.Value)
|
||||
room.unregister <- client
|
||||
default:
|
||||
for _, handler := range client.hub.controlHandlers {
|
||||
go handler(message.Controls)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
client.hub.broadcast <- message
|
||||
}
|
||||
} else {
|
||||
// Send the message to the defined room
|
||||
room := client.hub.GetRoom(message.RoomName)
|
||||
room.send <- message
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// write message to the websocket connection.
|
||||
// messages from client.send channel are sent
|
||||
// Also starts a timer ticker to send ping messages
|
||||
//
|
||||
// This pump has to be executed in a goroutine!
|
||||
func (client *wsClient) writePump() {
|
||||
// setup ping message ticker
|
||||
ticker := time.NewTicker(pingPeriod)
|
||||
|
||||
// stop the ticker and close the websocket connection, when this pump is finished
|
||||
defer func() {
|
||||
ticker.Stop()
|
||||
client.conn.Close()
|
||||
}()
|
||||
|
||||
for {
|
||||
|
||||
select {
|
||||
case message, ok := <-client.send:
|
||||
// Setup timeout
|
||||
client.conn.SetWriteDeadline(time.Now().Add(writeWait))
|
||||
|
||||
if !ok {
|
||||
// The hub closed the channel. Therefore notify the client.
|
||||
client.conn.WriteMessage(websocket.CloseMessage, []byte{})
|
||||
return
|
||||
}
|
||||
|
||||
// send the message as json
|
||||
err := client.conn.WriteJSON(message)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
case <-ticker.C:
|
||||
// Setup timeout
|
||||
client.conn.SetWriteDeadline(time.Now().Add(writeWait))
|
||||
|
||||
// send a ping message
|
||||
if err := client.conn.WriteMessage(websocket.PingMessage, nil); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// serveWs is the http handler to upgrade from http to ws..
|
||||
// Also the startup point for a client
|
||||
func ServeWs(w http.ResponseWriter, r *http.Request) {
|
||||
// upgrade the connection
|
||||
conn, err := upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
// setup the client
|
||||
client := &wsClient{
|
||||
hub: WebsocketHub,
|
||||
conn: conn,
|
||||
send: make(chan wsMessage, 256),
|
||||
}
|
||||
|
||||
// register this client in the hub
|
||||
client.hub.register <- client
|
||||
|
||||
// start the pipes for the new client in goroutines
|
||||
go client.writePump()
|
||||
go client.readPump()
|
||||
}
|
231
src/api/websocket/wshub.go
Normal file
231
src/api/websocket/wshub.go
Normal file
@ -0,0 +1,231 @@
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"github.com/mroote/factorio-server-manager/bootstrap"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
// the hub, that is exported and can be used anywhere to work with the websocket
|
||||
var WebsocketHub *wsHub
|
||||
|
||||
var LogCache []string
|
||||
|
||||
// a controlHandler is used to determine, if something has to be done, on a specific command.
|
||||
// register a handler with `wsHub.RegisterControlHandler`
|
||||
// unregister a handler with `wsHub.UnregisterControlHandler`
|
||||
type controlHandler func(controls WsControls)
|
||||
|
||||
// The type for of control messages.
|
||||
// Type ans Value both have to be set, if controls are sent.
|
||||
// Currently supported Type:
|
||||
// - `subscribe` - Value used as room name
|
||||
// - `unsubscribe` - Value used as room name
|
||||
// - `command` - Value contains the command to execute
|
||||
type WsControls struct {
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
// the main message of our websocket protocol.
|
||||
// if the room_name is an empty string, this will be sent as broadcast
|
||||
// if controls is not empty, this will not be sent anywhere, but used as commands, to join/leave rooms and to send commands to the factorio server
|
||||
type wsMessage struct {
|
||||
RoomName string `json:"room_name"`
|
||||
Message interface{} `json:"message,omitempty"`
|
||||
Controls WsControls `json:"controls,omitempty"`
|
||||
}
|
||||
|
||||
type wsRoom struct {
|
||||
// same as the key of the map in the wsHub
|
||||
name string
|
||||
|
||||
// the wsHub this room is part of
|
||||
hub *wsHub
|
||||
|
||||
// clients that are in this room. This list is a sublist of the one inside the hub
|
||||
clients map[*wsClient]bool
|
||||
|
||||
// register a client to this room
|
||||
register chan *wsClient
|
||||
|
||||
// unregister a client from this room, if no clients remain, this room will be deleted
|
||||
unregister chan *wsClient
|
||||
|
||||
// send a message to all clients in this room
|
||||
send chan wsMessage
|
||||
}
|
||||
|
||||
// Hub is the basic setup of the server.
|
||||
// It contains everything needed for the websocket to run.
|
||||
// Only the controlHandler Subscriptions are public, everything else can be controlled with the functions and the wsClient.
|
||||
type wsHub struct {
|
||||
// list of all connected clients
|
||||
clients map[*wsClient]bool
|
||||
|
||||
// Messages that should be sent to ALL clients
|
||||
broadcast chan wsMessage
|
||||
|
||||
// a list of all rooms
|
||||
rooms map[string]*wsRoom
|
||||
|
||||
// register a client to this hub
|
||||
register chan *wsClient
|
||||
|
||||
// unregister a client from this hub
|
||||
unregister chan *wsClient
|
||||
|
||||
// run a control message on all registered controlHandler
|
||||
runControl chan WsControls
|
||||
|
||||
// list of all registered controlHandlers
|
||||
controlHandlers map[reflect.Value]controlHandler
|
||||
|
||||
// register a controlHandler
|
||||
RegisterControlHandler chan controlHandler
|
||||
|
||||
// unregister a controlHandler
|
||||
UnregisterControlHandler chan controlHandler
|
||||
}
|
||||
|
||||
// initialize and run the mein websocket hub.
|
||||
func init() {
|
||||
WebsocketHub = &wsHub{
|
||||
broadcast: make(chan wsMessage),
|
||||
register: make(chan *wsClient),
|
||||
rooms: make(map[string]*wsRoom),
|
||||
unregister: make(chan *wsClient),
|
||||
clients: make(map[*wsClient]bool),
|
||||
runControl: make(chan WsControls),
|
||||
controlHandlers: make(map[reflect.Value]controlHandler),
|
||||
RegisterControlHandler: make(chan controlHandler),
|
||||
UnregisterControlHandler: make(chan controlHandler),
|
||||
}
|
||||
|
||||
go WebsocketHub.run()
|
||||
}
|
||||
|
||||
// remove a client from this hub and all of its rooms
|
||||
func (hub *wsHub) removeClient(client *wsClient) {
|
||||
delete(hub.clients, client)
|
||||
close(client.send)
|
||||
for _, room := range hub.rooms {
|
||||
room.unregister <- client
|
||||
}
|
||||
}
|
||||
|
||||
// run starts a websocket hub, this has to be done in a subroutine `go hub.run()`
|
||||
func (hub *wsHub) run() {
|
||||
for {
|
||||
select {
|
||||
case client := <-hub.register:
|
||||
hub.clients[client] = true
|
||||
case client := <-hub.unregister:
|
||||
if _, ok := hub.clients[client]; ok {
|
||||
hub.removeClient(client)
|
||||
}
|
||||
case message := <-hub.broadcast:
|
||||
for client := range hub.clients {
|
||||
select {
|
||||
case client.send <- message:
|
||||
default:
|
||||
hub.removeClient(client)
|
||||
}
|
||||
}
|
||||
case function := <-hub.RegisterControlHandler:
|
||||
hub.controlHandlers[reflect.ValueOf(function)] = function
|
||||
case function := <-hub.UnregisterControlHandler:
|
||||
delete(hub.controlHandlers, reflect.ValueOf(function))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Broadcast a message to all connected clients (only clients connected to this room).
|
||||
func (hub *wsHub) Broadcast(message interface{}) {
|
||||
hub.broadcast <- wsMessage{
|
||||
RoomName: "",
|
||||
Message: message,
|
||||
}
|
||||
}
|
||||
|
||||
// get a websocket room or create it, if it doesn't exist yet.
|
||||
// Also starts the rooms subroutine `wsRoom.run()`
|
||||
func (hub *wsHub) GetRoom(name string) *wsRoom {
|
||||
if room, ok := hub.rooms[name]; ok {
|
||||
return room
|
||||
} else {
|
||||
room := &wsRoom{
|
||||
name: name,
|
||||
hub: hub,
|
||||
clients: make(map[*wsClient]bool),
|
||||
register: make(chan *wsClient),
|
||||
unregister: make(chan *wsClient),
|
||||
send: make(chan wsMessage),
|
||||
}
|
||||
hub.rooms[name] = room
|
||||
go room.run()
|
||||
return room
|
||||
}
|
||||
}
|
||||
|
||||
// run starts a websocket room. This has to be run as a subroutine `go room.run()`
|
||||
func (room *wsRoom) run() {
|
||||
for {
|
||||
select {
|
||||
case client := <-room.register:
|
||||
room.clients[client] = true
|
||||
|
||||
// some hardcoded stuff for gamelog room
|
||||
if room.name == "gamelog" {
|
||||
// send cached log to registered client
|
||||
for _, logLine := range LogCache {
|
||||
client.send <- wsMessage{
|
||||
RoomName: "gamelog",
|
||||
Message: logLine,
|
||||
}
|
||||
}
|
||||
}
|
||||
case client := <-room.unregister:
|
||||
if _, ok := room.clients[client]; ok {
|
||||
delete(room.clients, client)
|
||||
if len(room.clients) == 0 {
|
||||
// remove this room
|
||||
delete(room.hub.rooms, room.name)
|
||||
return
|
||||
}
|
||||
}
|
||||
case message := <-room.send:
|
||||
for client := range room.clients {
|
||||
select {
|
||||
case client.send <- message:
|
||||
default:
|
||||
room.unregister <- client
|
||||
}
|
||||
}
|
||||
|
||||
// some hardcoded stuff for gamelog room
|
||||
if room.name == "gamelog" {
|
||||
// add the line to the cache
|
||||
LogCache = append(LogCache, message.Message.(string))
|
||||
config := bootstrap.GetConfig()
|
||||
|
||||
// Set ConsoleCacheSize to 25 if not set!
|
||||
if config.ConsoleCacheSize == 0 {
|
||||
config.ConsoleCacheSize = 25
|
||||
}
|
||||
|
||||
// When cache is bigger than max size, delete one line
|
||||
if len(LogCache) > config.ConsoleCacheSize {
|
||||
LogCache = LogCache[1:]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Send a message into this room.
|
||||
func (room *wsRoom) Send(message interface{}) {
|
||||
room.send <- wsMessage{
|
||||
RoomName: room.name,
|
||||
Message: message,
|
||||
}
|
||||
}
|
142
src/bootstrap/config.go
Normal file
142
src/bootstrap/config.go
Normal file
@ -0,0 +1,142 @@
|
||||
package bootstrap
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/jessevdk/go-flags"
|
||||
"log"
|
||||
"math/rand"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Flags struct {
|
||||
ConfFile string `long:"conf" default:"./conf.json" description:"Specify location of Factorio Server Manager config file."`
|
||||
FactorioDir string `long:"dir" default:"./" description:"Specify location of Factorio directory."`
|
||||
ServerIP string `long:"host" default:"0.0.0.0" description:"Specify IP for webserver to listen on."`
|
||||
FactorioIP string `long:"game-bind-address" default:"0.0.0.0" description:"Specify IP for Fcatorio gamer server to listen on."`
|
||||
FactorioPort string `long:"port" default:"80" description:"Specify a port for the server."`
|
||||
FactorioConfigFile string `long:"config" default:"config/config.ini" description:"Specify location of Factorio config.ini file"`
|
||||
FactorioMaxUpload int64 `long:"max-upload" default:"20.971.520" description:"Maximum filesize for uploaded files (default 20MB)."`
|
||||
FactorioBinary string `long:"bin" default:"bin/x64/factorio" description:"Location of Factorio Server binary file"`
|
||||
GlibcCustom string `long:"glibc-custom" default:"false" description:"By default false, if custom glibc is required set this to true and add glibc-loc and glibc-lib-loc parameters"`
|
||||
GlibcLocation string `long:"glibc-loc" default:"/opt/glibc-2.18/lib/ld-2.18.so" description:"Location glibc ld.so file if needed (ex. /opt/glibc-2.18/lib/ld-2.18.so)"`
|
||||
GlibcLibLoc string `long:"glibc-lib-loc" default:"/opt/glibc-2.18/lib" description:"Location of glibc lib folder (ex. /opt/glibc-2.18/lib)"`
|
||||
Autostart string `long:"autostart" default:"false" description:"Autostart factorio server on bootup of FSM, default false [true/false]"`
|
||||
ModPackDir string `long:"mod-pack-dir" default:"./mod_packs" description:"Directory to store mod packs."`
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
FactorioDir string `json:"factorio_dir"`
|
||||
FactorioSavesDir string `json:"saves_dir"`
|
||||
FactorioModsDir string `json:"mods_dir"`
|
||||
FactorioModPackDir string `json:"mod_pack_dir"`
|
||||
FactorioConfigFile string `json:"config_file"`
|
||||
FactorioConfigDir string `json:"config_directory"`
|
||||
FactorioLog string `json:"logfile"`
|
||||
FactorioBinary string `json:"factorio_binary"`
|
||||
FactorioRconPort int `json:"rcon_port"`
|
||||
FactorioRconPass string `json:"rcon_pass"`
|
||||
FactorioCredentialsFile string `json:"factorio_credentials_file"`
|
||||
FactorioIP string `json:"factorio_ip"`
|
||||
FactorioAdminFile string `json:"-"`
|
||||
ServerIP string `json:"server_ip"`
|
||||
ServerPort string `json:"server_port"`
|
||||
MaxUploadSize int64 `json:"max_upload_size"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
DatabaseFile string `json:"database_file"`
|
||||
CookieEncryptionKey string `json:"cookie_encryption_key"`
|
||||
SettingsFile string `json:"settings_file"`
|
||||
LogFile string `json:"log_file"`
|
||||
ConfFile string
|
||||
GlibcCustom string
|
||||
GlibcLocation string
|
||||
GlibcLibLoc string
|
||||
Autostart string
|
||||
ConsoleCacheSize int `json:"console_cache_size"` // the amount of cached lines, inside the factorio output cache
|
||||
}
|
||||
|
||||
var instantiated Config
|
||||
|
||||
func NewConfig(args []string) Config {
|
||||
var opts Flags
|
||||
_, err := flags.NewParser(&opts, flags.IgnoreUnknown).ParseArgs(args)
|
||||
if err != nil {
|
||||
failOnError(err, "Failed to parse arguments")
|
||||
}
|
||||
instantiated = mapFlags(opts)
|
||||
instantiated.loadServerConfig()
|
||||
|
||||
abs, err := filepath.Abs(instantiated.FactorioModPackDir)
|
||||
println(abs)
|
||||
|
||||
return instantiated
|
||||
}
|
||||
|
||||
func GetConfig() Config {
|
||||
return instantiated
|
||||
}
|
||||
|
||||
// Loads server configuration files
|
||||
// JSON config file contains default values,
|
||||
// config file will overwrite any provided flags
|
||||
func (config *Config) loadServerConfig() {
|
||||
file, err := os.Open(config.ConfFile)
|
||||
failOnError(err, "Error loading config file.")
|
||||
|
||||
decoder := json.NewDecoder(file)
|
||||
err = decoder.Decode(&config)
|
||||
failOnError(err, "Error decoding JSON config file.")
|
||||
|
||||
config.FactorioRconPort = randomPort()
|
||||
}
|
||||
|
||||
// Returns random port to use for rcon connection
|
||||
func randomPort() int {
|
||||
// rand needs to be initialized, else we always get the same number
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
// get a random number between 40000 and 45000
|
||||
return rand.Intn(5000) + 40000
|
||||
}
|
||||
|
||||
func mapFlags(flags Flags) Config {
|
||||
var config = Config{
|
||||
Autostart: flags.Autostart,
|
||||
GlibcCustom: flags.GlibcCustom,
|
||||
GlibcLocation: flags.GlibcLocation,
|
||||
GlibcLibLoc: flags.GlibcLibLoc,
|
||||
ConfFile: flags.ConfFile,
|
||||
FactorioDir: flags.FactorioDir,
|
||||
ServerIP: flags.ServerIP,
|
||||
ServerPort: flags.FactorioPort,
|
||||
FactorioIP: flags.FactorioIP,
|
||||
FactorioSavesDir: filepath.Join(flags.FactorioDir, "saves"),
|
||||
FactorioModsDir: filepath.Join(flags.FactorioDir, "mods"),
|
||||
FactorioModPackDir: flags.ModPackDir,
|
||||
FactorioConfigDir: filepath.Join(flags.FactorioDir, "config"),
|
||||
FactorioConfigFile: filepath.Join(flags.FactorioDir, flags.FactorioConfigFile),
|
||||
FactorioBinary: filepath.Join(flags.FactorioDir, flags.FactorioBinary),
|
||||
FactorioCredentialsFile: "./factorio.auth",
|
||||
FactorioAdminFile: "server-adminlist.json",
|
||||
MaxUploadSize: flags.FactorioMaxUpload,
|
||||
}
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
appdata := os.Getenv("APPDATA")
|
||||
config.FactorioLog = filepath.Join(appdata, "Factorio", "factorio-current.log")
|
||||
} else {
|
||||
config.FactorioLog = filepath.Join(config.FactorioDir, "factorio-current.log")
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
func failOnError(err error, msg string) {
|
||||
if err != nil {
|
||||
log.Printf("%s: %s", msg, err)
|
||||
panic(fmt.Sprintf("%s: %s", msg, err))
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package main
|
||||
package factorio
|
||||
|
||||
import (
|
||||
"log"
|
||||
@ -6,8 +6,8 @@ import (
|
||||
"github.com/go-ini/ini"
|
||||
)
|
||||
|
||||
// Loads config.ini file from the factorio config directory
|
||||
func loadConfig(filename string) (map[string]map[string]string, error) {
|
||||
// Loads config.ini file from the factorio bootstrap directory
|
||||
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 {
|
74
src/factorio/credentials.go
Normal file
74
src/factorio/credentials.go
Normal file
@ -0,0 +1,74 @@
|
||||
package factorio
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"github.com/mroote/factorio-server-manager/bootstrap"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
)
|
||||
|
||||
type Credentials struct {
|
||||
Username string `json:"username"`
|
||||
Userkey string `json:"userkey"`
|
||||
}
|
||||
|
||||
func (credentials *Credentials) Save() error {
|
||||
var err error
|
||||
config := bootstrap.GetConfig()
|
||||
credentialsJson, err := json.Marshal(credentials)
|
||||
if err != nil {
|
||||
log.Printf("error mashalling the credentials: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
err = ioutil.WriteFile(config.FactorioCredentialsFile, credentialsJson, 0664)
|
||||
if err != nil {
|
||||
log.Printf("error on saving the credentials. %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (credentials *Credentials) Load() (bool, error) {
|
||||
var err error
|
||||
config := bootstrap.GetConfig()
|
||||
if _, err := os.Stat(config.FactorioCredentialsFile); os.IsNotExist(err) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
fileBytes, err := ioutil.ReadFile(config.FactorioCredentialsFile)
|
||||
if err != nil {
|
||||
credentials.Del()
|
||||
log.Printf("error reading CredentialsFile: %s", err)
|
||||
return false, err
|
||||
}
|
||||
|
||||
err = json.Unmarshal(fileBytes, credentials)
|
||||
if err != nil {
|
||||
credentials.Del()
|
||||
log.Printf("error on unmarshal credentials_file: %s", err)
|
||||
return false, err
|
||||
}
|
||||
|
||||
if credentials.Userkey != "" && credentials.Username != "" {
|
||||
return true, nil
|
||||
} else {
|
||||
credentials.Del()
|
||||
return false, errors.New("incredients incomplete")
|
||||
}
|
||||
}
|
||||
|
||||
func (credentials *Credentials) Del() error {
|
||||
var err error
|
||||
config := bootstrap.GetConfig()
|
||||
err = os.Remove(config.FactorioCredentialsFile)
|
||||
if err != nil {
|
||||
log.Printf("error delete the credentialfile: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -1,14 +1,16 @@
|
||||
package main
|
||||
package factorio
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/hpcloud/tail"
|
||||
"github.com/mroote/factorio-server-manager/bootstrap"
|
||||
"log"
|
||||
)
|
||||
|
||||
func tailLog(filename string) ([]string, error) {
|
||||
func TailLog() ([]string, error) {
|
||||
result := []string{}
|
||||
|
||||
config := bootstrap.GetConfig()
|
||||
|
||||
t, err := tail.TailFile(config.FactorioLog, tail.Config{Follow: false})
|
||||
if err != nil {
|
||||
log.Printf("Error tailing log %s", err)
|
@ -1,4 +1,4 @@
|
||||
package main
|
||||
package factorio
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
@ -26,9 +26,9 @@ type ModsResultList struct {
|
||||
ModsResult []ModsResult `json:"mods"`
|
||||
}
|
||||
|
||||
var fileLock lockfile.FileLock = lockfile.NewLock()
|
||||
var FileLock lockfile.FileLock = lockfile.NewLock()
|
||||
|
||||
func newMods(destination string) (Mods, error) {
|
||||
func NewMods(destination string) (Mods, error) {
|
||||
var err error
|
||||
var mods Mods
|
||||
|
||||
@ -47,8 +47,8 @@ func newMods(destination string) (Mods, error) {
|
||||
return mods, nil
|
||||
}
|
||||
|
||||
func (mods *Mods) listInstalledMods() ModsResultList {
|
||||
var result ModsResultList
|
||||
func (mods *Mods) ListInstalledMods() ModsResultList {
|
||||
result := ModsResultList{make([]ModsResult, 0)}
|
||||
|
||||
for _, modInfo := range mods.ModInfoList.Mods {
|
||||
var modsResult ModsResult
|
||||
@ -73,7 +73,7 @@ func (mods *Mods) listInstalledMods() ModsResultList {
|
||||
return result
|
||||
}
|
||||
|
||||
func (mods *Mods) deleteMod(modName string) error {
|
||||
func (mods *Mods) DeleteMod(modName string) error {
|
||||
var err error
|
||||
|
||||
err = mods.ModInfoList.deleteMod(modName)
|
||||
@ -95,13 +95,7 @@ func (mods *Mods) createMod(modName string, fileName string, fileRc io.Reader) e
|
||||
var err error
|
||||
|
||||
//check if mod already exists and delete it
|
||||
if mods.ModSimpleList.checkModExists(modName) {
|
||||
err = mods.ModSimpleList.deleteMod(modName)
|
||||
if err != nil {
|
||||
log.Printf("error when deleting mod: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if mods.ModSimpleList.CheckModExists(modName) {
|
||||
err = mods.ModInfoList.deleteMod(modName)
|
||||
if err != nil {
|
||||
log.Printf("error when deleting mod: %s", err)
|
||||
@ -113,22 +107,33 @@ func (mods *Mods) createMod(modName string, fileName string, fileRc io.Reader) e
|
||||
err = mods.ModInfoList.createMod(modName, fileName, fileRc)
|
||||
if err != nil {
|
||||
log.Printf("error on creating mod-file: %s", err)
|
||||
|
||||
// removing mod completely
|
||||
err2 := mods.ModSimpleList.deleteMod(modName)
|
||||
if err2 != nil {
|
||||
log.Printf("error deleting mod from modSimpleList: %s", err2)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
err = mods.ModSimpleList.createMod(modName)
|
||||
if err != nil {
|
||||
log.Printf("error on adding mod to the mod-list.json: %s", err)
|
||||
return err
|
||||
|
||||
// also add to ModSimpleList if not there yet
|
||||
if !mods.ModSimpleList.CheckModExists(modName) {
|
||||
err = mods.ModSimpleList.createMod(modName)
|
||||
if err != nil {
|
||||
log.Printf("error creating mod in modSimpleList: %s", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (mods *Mods) downloadMod(url string, filename string, modId string) error {
|
||||
func (mods *Mods) DownloadMod(url string, filename string, modId string) error {
|
||||
var err error
|
||||
|
||||
var credentials FactorioCredentials
|
||||
status, err := credentials.load()
|
||||
var credentials Credentials
|
||||
status, err := credentials.Load()
|
||||
if err != nil {
|
||||
log.Printf("error loading credentials: %s", err)
|
||||
return err
|
||||
@ -170,21 +175,14 @@ func (mods *Mods) downloadMod(url string, filename string, modId string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (mods *Mods) uploadMod(header *multipart.FileHeader) error {
|
||||
func (mods *Mods) UploadMod(file multipart.File, header *multipart.FileHeader) error {
|
||||
var err error
|
||||
|
||||
if filepath.Ext(header.Filename) != ".zip" {
|
||||
log.Print("The uploaded file wasn't a zip-file -> ignore it")
|
||||
return nil //simply do nothing xD
|
||||
log.Print("The uploaded file wasn't a zip-file")
|
||||
return errors.New("the uploaded file wasn't a zip-file")
|
||||
}
|
||||
|
||||
file, err := header.Open()
|
||||
if err != nil {
|
||||
log.Printf("error on open file via fileHeader. %s", err)
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
fileByteArray, err := ioutil.ReadAll(file)
|
||||
if err != nil {
|
||||
log.Printf("error reading file: %s", err)
|
||||
@ -213,10 +211,10 @@ func (mods *Mods) uploadMod(header *multipart.FileHeader) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (mods *Mods) updateMod(modName string, url string, filename string) error {
|
||||
func (mods *Mods) UpdateMod(modName string, url string, filename string) error {
|
||||
var err error
|
||||
|
||||
err = mods.downloadMod(url, filename, modName)
|
||||
err = mods.DownloadMod(url, filename, modName)
|
||||
if err != nil {
|
||||
log.Printf("updateMod ... error when downloading the new Mod: %s", err)
|
||||
return err
|
@ -1,4 +1,4 @@
|
||||
package main
|
||||
package factorio
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
@ -50,7 +50,7 @@ func (modInfoList *ModInfoList) listInstalledMods() error {
|
||||
err = filepath.Walk(modInfoList.Destination, func(path string, info os.FileInfo, err error) error {
|
||||
if !info.IsDir() && filepath.Ext(path) == ".zip" {
|
||||
|
||||
err = fileLock.RLock(path)
|
||||
err = FileLock.RLock(path)
|
||||
if err != nil && err == lockfile.ErrorAlreadyLocked {
|
||||
log.Println(err)
|
||||
return nil
|
||||
@ -58,7 +58,7 @@ func (modInfoList *ModInfoList) listInstalledMods() error {
|
||||
log.Printf("error locking file: %s", err)
|
||||
return err
|
||||
}
|
||||
defer fileLock.RUnlock(path)
|
||||
defer FileLock.RUnlock(path)
|
||||
|
||||
zipFile, err := zip.OpenReader(path)
|
||||
if err != nil {
|
||||
@ -83,6 +83,7 @@ func (modInfoList *ModInfoList) listInstalledMods() error {
|
||||
continue
|
||||
}
|
||||
|
||||
// skip optional and incompatible dependencies
|
||||
parts := strings.Split(dep, " ")
|
||||
if len(parts) > 3 {
|
||||
log.Printf("skipping dependency '%s' in '%s': invalid format\n", dep, modInfo.Name)
|
||||
@ -107,11 +108,13 @@ func (modInfoList *ModInfoList) listInstalledMods() error {
|
||||
break
|
||||
}
|
||||
|
||||
server := GetFactorioServer()
|
||||
|
||||
if !base.Equals(NilVersion) {
|
||||
modInfo.Compatibility = FactorioServ.Version.Compare(base, op)
|
||||
modInfo.Compatibility = server.Version.Compare(base, op)
|
||||
} else {
|
||||
log.Println("error finding basemodDependency. Using FactorioVersion...")
|
||||
modInfo.Compatibility = !FactorioServ.Version.Less(modInfo.FactorioVersion)
|
||||
modInfo.Compatibility = !server.Version.Less(modInfo.FactorioVersion)
|
||||
}
|
||||
|
||||
modInfoList.Mods = append(modInfoList.Mods, modInfo)
|
||||
@ -136,10 +139,10 @@ func (modInfoList *ModInfoList) deleteMod(modName string) error {
|
||||
if mod.Name == modName {
|
||||
filePath := filepath.Join(modInfoList.Destination, mod.FileName)
|
||||
|
||||
fileLock.LockW(filePath)
|
||||
FileLock.LockW(filePath)
|
||||
//delete mod
|
||||
err = os.Remove(filePath)
|
||||
fileLock.Unlock(filePath)
|
||||
FileLock.Unlock(filePath)
|
||||
if err != nil {
|
||||
log.Printf("ModInfoList ... error when deleting mod: %s", err)
|
||||
return err
|
||||
@ -156,8 +159,8 @@ func (modInfoList *ModInfoList) deleteMod(modName string) error {
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("the mod-file for mod %s doesn't exists!", modName)
|
||||
return nil
|
||||
log.Printf("the mod-file for mod %s doesn't exist!", modName)
|
||||
return errors.New("the mod-file for mod " + modName + " doesn't exist!")
|
||||
}
|
||||
|
||||
func (modInfo *ModInfo) getModInfo(reader *zip.Reader) error {
|
||||
@ -207,7 +210,7 @@ func (modInfoList *ModInfoList) createMod(modName string, fileName string, modFi
|
||||
}
|
||||
defer newFile.Close()
|
||||
|
||||
fileLock.LockW(filePath)
|
||||
FileLock.LockW(filePath)
|
||||
|
||||
_, err = io.Copy(newFile, modFile)
|
||||
if err != nil {
|
||||
@ -221,7 +224,7 @@ func (modInfoList *ModInfoList) createMod(modName string, fileName string, modFi
|
||||
return err
|
||||
}
|
||||
|
||||
fileLock.Unlock(filePath)
|
||||
FileLock.Unlock(filePath)
|
||||
|
||||
//reload the list
|
||||
err = modInfoList.listInstalledMods()
|
@ -1,7 +1,8 @@
|
||||
package main
|
||||
package factorio
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
@ -107,7 +108,7 @@ func (modSimpleList *ModSimpleList) deleteMod(modName string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (modSimpleList *ModSimpleList) checkModExists(modName string) bool {
|
||||
func (modSimpleList *ModSimpleList) CheckModExists(modName string) bool {
|
||||
for _, singleMod := range modSimpleList.Mods {
|
||||
if singleMod.Name == modName {
|
||||
return true
|
||||
@ -138,18 +139,24 @@ func (modSimpleList *ModSimpleList) createMod(modName string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (modSimpleList *ModSimpleList) toggleMod(modName string) (error, bool) {
|
||||
func (modSimpleList *ModSimpleList) ToggleMod(modName string) (error, bool) {
|
||||
var err error
|
||||
var newEnabled bool
|
||||
|
||||
var found bool
|
||||
for index, mod := range modSimpleList.Mods {
|
||||
if mod.Name == modName {
|
||||
newEnabled = !modSimpleList.Mods[index].Enabled
|
||||
modSimpleList.Mods[index].Enabled = newEnabled
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
return errors.New("mod is not installed"), newEnabled
|
||||
}
|
||||
|
||||
err = modSimpleList.saveModInfoJson()
|
||||
if err != nil {
|
||||
log.Printf("error on savin new ModSimpleList: %s", err)
|
@ -1,7 +1,8 @@
|
||||
package main
|
||||
package factorio
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/mroote/factorio-server-manager/bootstrap"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
@ -18,11 +19,8 @@ type ModPackResult struct {
|
||||
Name string `json:"name"`
|
||||
Mods ModsResultList `json:"mods"`
|
||||
}
|
||||
type ModPackResultList struct {
|
||||
ModPacks []ModPackResult `json:"mod_packs"`
|
||||
}
|
||||
|
||||
func newModPackMap() (ModPackMap, error) {
|
||||
func NewModPackMap() (ModPackMap, error) {
|
||||
var err error
|
||||
modPackMap := make(ModPackMap)
|
||||
|
||||
@ -39,7 +37,7 @@ func newModPack(modPackFolder string) (*ModPack, error) {
|
||||
var err error
|
||||
var modPack ModPack
|
||||
|
||||
modPack.Mods, err = newMods(modPackFolder)
|
||||
modPack.Mods, err = NewMods(modPackFolder)
|
||||
if err != nil {
|
||||
log.Printf("error on loading mods in mod_pack_dir: %s", err)
|
||||
return &modPack, err
|
||||
@ -51,6 +49,7 @@ func newModPack(modPackFolder string) (*ModPack, error) {
|
||||
func (modPackMap *ModPackMap) reload() error {
|
||||
var err error
|
||||
newModPackMap := make(ModPackMap)
|
||||
config := bootstrap.GetConfig()
|
||||
|
||||
err = filepath.Walk(config.FactorioModPackDir, func(path string, info os.FileInfo, err error) error {
|
||||
if path == config.FactorioModPackDir || !info.IsDir() {
|
||||
@ -77,26 +76,26 @@ func (modPackMap *ModPackMap) reload() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (modPackMap *ModPackMap) listInstalledModPacks() ModPackResultList {
|
||||
var modPackResultList ModPackResultList
|
||||
func (modPackMap *ModPackMap) ListInstalledModPacks() []ModPackResult {
|
||||
list := make([]ModPackResult, 0)
|
||||
|
||||
for modPackName, modPack := range *modPackMap {
|
||||
var modPackResult ModPackResult
|
||||
modPackResult.Name = modPackName
|
||||
modPackResult.Mods = modPack.Mods.listInstalledMods()
|
||||
modPackResult.Mods = modPack.Mods.ListInstalledMods()
|
||||
|
||||
modPackResultList.ModPacks = append(modPackResultList.ModPacks, modPackResult)
|
||||
list = append(list, modPackResult)
|
||||
}
|
||||
|
||||
return modPackResultList
|
||||
return list
|
||||
}
|
||||
|
||||
func (modPackMap *ModPackMap) createModPack(modPackName string) error {
|
||||
func (modPackMap *ModPackMap) CreateModPack(modPackName string) error {
|
||||
var err error
|
||||
|
||||
config := bootstrap.GetConfig()
|
||||
modPackFolder := filepath.Join(config.FactorioModPackDir, modPackName)
|
||||
|
||||
if modPackMap.checkModPackExists(modPackName) == true {
|
||||
if modPackMap.CheckModPackExists(modPackName) == true {
|
||||
log.Printf("ModPack %s already existis", modPackName)
|
||||
return errors.New("ModPack " + modPackName + " already exists, please choose a different name")
|
||||
}
|
||||
@ -116,7 +115,7 @@ func (modPackMap *ModPackMap) createModPack(modPackName string) error {
|
||||
|
||||
files, err := ioutil.ReadDir(config.FactorioModsDir)
|
||||
if err != nil {
|
||||
log.Printf("error on reading the dactorio mods dir: %s", err)
|
||||
log.Printf("error on reading the factorio mods dir: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
@ -153,14 +152,39 @@ func (modPackMap *ModPackMap) createModPack(modPackName string) error {
|
||||
//reload the ModPackList
|
||||
err = modPackMap.reload()
|
||||
if err != nil {
|
||||
log.Printf("error on reloading ModPack: %s", err)
|
||||
log.Printf("error reloading ModPack: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (modPackMap *ModPackMap) checkModPackExists(modPackName string) bool {
|
||||
func (modPackMap *ModPackMap) CreateEmptyModPack(packName string) error {
|
||||
var err error
|
||||
config := bootstrap.GetConfig()
|
||||
modPackFolder := filepath.Join(config.FactorioModPackDir, packName)
|
||||
|
||||
if modPackMap.CheckModPackExists(packName) == true {
|
||||
log.Printf("ModPack %s already existis", packName)
|
||||
return errors.New("ModPack " + packName + " already exists, please choose a different name")
|
||||
}
|
||||
|
||||
// Create the modPack-folder
|
||||
err = os.MkdirAll(modPackFolder, 0777)
|
||||
if err != nil {
|
||||
log.Printf("error creating the new ModPack directory: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
err = modPackMap.reload()
|
||||
if err != nil {
|
||||
log.Printf("error reloading ModPack: %s", err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (modPackMap *ModPackMap) CheckModPackExists(modPackName string) bool {
|
||||
for modPackId := range *modPackMap {
|
||||
if modPackId == modPackName {
|
||||
return true
|
||||
@ -170,9 +194,9 @@ func (modPackMap *ModPackMap) checkModPackExists(modPackName string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (modPackMap *ModPackMap) deleteModPack(modPackName string) error {
|
||||
func (modPackMap *ModPackMap) DeleteModPack(modPackName string) error {
|
||||
var err error
|
||||
|
||||
config := bootstrap.GetConfig()
|
||||
modPackDir := filepath.Join(config.FactorioModPackDir, modPackName)
|
||||
|
||||
err = os.RemoveAll(modPackDir)
|
||||
@ -190,9 +214,9 @@ func (modPackMap *ModPackMap) deleteModPack(modPackName string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (modPack *ModPack) loadModPack() error {
|
||||
func (modPack *ModPack) LoadModPack() error {
|
||||
var err error
|
||||
|
||||
config := bootstrap.GetConfig()
|
||||
//get filemode, so it can be restored
|
||||
fileInfo, err := os.Stat(config.FactorioModsDir)
|
||||
if err != nil {
|
149
src/factorio/mod_portal.go
Normal file
149
src/factorio/mod_portal.go
Normal file
@ -0,0 +1,149 @@
|
||||
package factorio
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ModPortalStruct struct {
|
||||
DownloadsCount int `json:"downloads_count"`
|
||||
Name string `json:"name"`
|
||||
Owner string `json:"owner"`
|
||||
Releases []struct {
|
||||
DownloadURL string `json:"download_url"`
|
||||
FileName string `json:"file_name"`
|
||||
InfoJSON struct {
|
||||
FactorioVersion Version `json:"factorio_version"`
|
||||
} `json:"info_json"`
|
||||
ReleasedAt time.Time `json:"released_at"`
|
||||
Sha1 string `json:"sha1"`
|
||||
Version Version `json:"version"`
|
||||
Compatibility bool `json:"compatibility"`
|
||||
} `json:"releases"`
|
||||
Summary string `json:"summary"`
|
||||
Title string `json:"title"`
|
||||
}
|
||||
|
||||
// get all mods uploaded to the factorio modPortal
|
||||
func ModPortalList() (interface{}, error, int) {
|
||||
req, err := http.NewRequest(http.MethodGet, "https://mods.factorio.com/api/mods?page_size=max", nil)
|
||||
if err != nil {
|
||||
return "error", err, http.StatusInternalServerError
|
||||
}
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return "error", err, http.StatusInternalServerError
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
text, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "error", err, http.StatusInternalServerError
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, errors.New(string(text)), resp.StatusCode
|
||||
}
|
||||
|
||||
var jsonVal interface{}
|
||||
err = json.Unmarshal(text, &jsonVal)
|
||||
if err != nil {
|
||||
return "error", err, http.StatusInternalServerError
|
||||
}
|
||||
|
||||
return jsonVal, nil, resp.StatusCode
|
||||
}
|
||||
|
||||
// get the details (mod-info, releases, etc.) from a specific mod from the modPortal
|
||||
func ModPortalModDetails(modId string) (ModPortalStruct, error, int) {
|
||||
var mod ModPortalStruct
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, "https://mods.factorio.com/api/mods/"+modId, nil)
|
||||
if err != nil {
|
||||
return mod, err, http.StatusInternalServerError
|
||||
}
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return mod, err, http.StatusInternalServerError
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
text, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return mod, err, http.StatusInternalServerError
|
||||
}
|
||||
|
||||
err = json.Unmarshal(text, &mod)
|
||||
if err != nil {
|
||||
return mod, err, http.StatusInternalServerError
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return ModPortalStruct{}, errors.New(string(text)), resp.StatusCode
|
||||
}
|
||||
|
||||
server := GetFactorioServer()
|
||||
|
||||
installedBaseVersion := Version{}
|
||||
_ = installedBaseVersion.UnmarshalText([]byte(server.BaseModVersion))
|
||||
requiredVersion := NilVersion
|
||||
|
||||
for key, release := range mod.Releases {
|
||||
requiredVersion = release.InfoJSON.FactorioVersion
|
||||
areVersionIdentical := requiredVersion.Equals(installedBaseVersion)
|
||||
isException := installedBaseVersion.Equals(Version{1, 0, 0, 0}) && requiredVersion.Equals(Version{0, 18, 0, 0})
|
||||
release.Compatibility = areVersionIdentical || isException
|
||||
mod.Releases[key] = release
|
||||
}
|
||||
|
||||
return mod, nil, resp.StatusCode
|
||||
}
|
||||
|
||||
//Log the user into factorio, so mods can be downloaded
|
||||
func FactorioLogin(username string, password string) (error, int) {
|
||||
var err error
|
||||
|
||||
resp, err := http.PostForm("https://auth.factorio.com/api-login",
|
||||
url.Values{"require_game_ownership": {"true"}, "username": {username}, "password": {password}})
|
||||
|
||||
if err != nil {
|
||||
return err, http.StatusInternalServerError
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
bodyBytes, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err, http.StatusInternalServerError
|
||||
}
|
||||
|
||||
bodyString := string(bodyBytes)
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return errors.New(bodyString), resp.StatusCode
|
||||
}
|
||||
|
||||
var successResponse []string
|
||||
err = json.Unmarshal(bodyBytes, &successResponse)
|
||||
if err != nil {
|
||||
return err, http.StatusInternalServerError
|
||||
}
|
||||
|
||||
credentials := Credentials{
|
||||
Username: username,
|
||||
Userkey: successResponse[0],
|
||||
}
|
||||
|
||||
err = credentials.Save()
|
||||
if err != nil {
|
||||
return err, http.StatusInternalServerError
|
||||
}
|
||||
|
||||
return nil, http.StatusOK
|
||||
}
|
@ -1,14 +1,13 @@
|
||||
package main
|
||||
package factorio
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"github.com/mroote/factorio-server-manager/bootstrap"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@ -21,171 +20,10 @@ type LoginErrorResponse struct {
|
||||
type LoginSuccessResponse struct {
|
||||
UserKey []string `json:""`
|
||||
}
|
||||
type FactorioCredentials struct {
|
||||
Username string `json:"username"`
|
||||
Userkey string `json:"userkey"`
|
||||
}
|
||||
|
||||
func (credentials *FactorioCredentials) save() error {
|
||||
func DeleteAllMods() error {
|
||||
var err error
|
||||
|
||||
credentialsJson, err := json.Marshal(credentials)
|
||||
if err != nil {
|
||||
log.Printf("error mashalling the credentials: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
err = ioutil.WriteFile(config.FactorioCredentialsFile, credentialsJson, 0664)
|
||||
if err != nil {
|
||||
log.Printf("error on saving the credentials. %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (credentials *FactorioCredentials) load() (bool, error) {
|
||||
var err error
|
||||
|
||||
if _, err := os.Stat(config.FactorioCredentialsFile); os.IsNotExist(err) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
fileBytes, err := ioutil.ReadFile(config.FactorioCredentialsFile)
|
||||
if err != nil {
|
||||
credentials.del()
|
||||
log.Printf("error reading CredentialsFile: %s", err)
|
||||
return false, err
|
||||
}
|
||||
|
||||
err = json.Unmarshal(fileBytes, credentials)
|
||||
if err != nil {
|
||||
credentials.del()
|
||||
log.Printf("error on unmarshal credentials_file: %s", err)
|
||||
return false, err
|
||||
}
|
||||
|
||||
if credentials.Userkey != "" && credentials.Username != "" {
|
||||
return true, nil
|
||||
} else {
|
||||
credentials.del()
|
||||
return false, errors.New("incredients incomplete")
|
||||
}
|
||||
}
|
||||
|
||||
func (credentials *FactorioCredentials) del() error {
|
||||
var err error
|
||||
|
||||
err = os.Remove(config.FactorioCredentialsFile)
|
||||
if err != nil {
|
||||
log.Printf("error delete the credentialfile: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
//Log the user into factorio, so mods can be downloaded
|
||||
func factorioLogin(username string, password string) (string, error, int) {
|
||||
var err error
|
||||
|
||||
resp, err := http.PostForm("https://auth.factorio.com/api-login",
|
||||
url.Values{"require_game_ownership": {"true"}, "username": {username}, "password": {password}})
|
||||
|
||||
if err != nil {
|
||||
log.Printf("error on logging in: %s", err)
|
||||
return "", err, resp.StatusCode
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
bodyBytes, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
log.Printf("error on reading resp.Body: %s", err)
|
||||
return "", err, http.StatusInternalServerError
|
||||
}
|
||||
|
||||
bodyString := string(bodyBytes)
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
log.Println("error Statuscode not 200")
|
||||
return bodyString, errors.New(bodyString), resp.StatusCode
|
||||
}
|
||||
|
||||
var successResponse []string
|
||||
err = json.Unmarshal(bodyBytes, &successResponse)
|
||||
if err != nil {
|
||||
log.Printf("error on unmarshal body: %s", err)
|
||||
return err.Error(), err, http.StatusInternalServerError
|
||||
}
|
||||
|
||||
credentials := FactorioCredentials{
|
||||
Username: username,
|
||||
Userkey: successResponse[0],
|
||||
}
|
||||
|
||||
err = credentials.save()
|
||||
if err != nil {
|
||||
log.Printf("error saving the credentials. %s", err)
|
||||
return err.Error(), err, http.StatusInternalServerError
|
||||
}
|
||||
|
||||
return "", nil, http.StatusOK
|
||||
}
|
||||
|
||||
//Search inside the factorio mod portal
|
||||
func searchModPortal(keyword string) (string, error, int) {
|
||||
req, err := http.NewRequest(http.MethodGet, "https://mods.factorio.com/api/mods", nil)
|
||||
if err != nil {
|
||||
return "error", err, 500
|
||||
}
|
||||
|
||||
query := req.URL.Query()
|
||||
query.Add("q", keyword)
|
||||
req.URL.RawQuery = query.Encode()
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return "error", err, 500
|
||||
}
|
||||
|
||||
text, err := ioutil.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
if err != nil {
|
||||
return "error", err, 500
|
||||
}
|
||||
|
||||
textString := string(text)
|
||||
|
||||
return textString, nil, resp.StatusCode
|
||||
}
|
||||
|
||||
func getModDetails(modId string) (string, error, int) {
|
||||
var err error
|
||||
newLink := "https://mods.factorio.com/api/mods/" + modId
|
||||
resp, err := http.Get(newLink)
|
||||
|
||||
if err != nil {
|
||||
return "error", err, http.StatusInternalServerError
|
||||
}
|
||||
|
||||
//get the response-text
|
||||
text, err := ioutil.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
|
||||
textString := string(text)
|
||||
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
return "error", err, resp.StatusCode
|
||||
}
|
||||
|
||||
return textString, nil, resp.StatusCode
|
||||
}
|
||||
|
||||
func deleteAllMods() error {
|
||||
var err error
|
||||
|
||||
config := bootstrap.GetConfig()
|
||||
modsDirInfo, err := os.Stat(config.FactorioModsDir)
|
||||
if err != nil {
|
||||
log.Printf("error getting stats of FactorioModsDir: %s", err)
|
||||
@ -209,13 +47,12 @@ func deleteAllMods() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func modStartUp() {
|
||||
var err error
|
||||
|
||||
func ModStartUp() {
|
||||
config := bootstrap.GetConfig()
|
||||
//get main-folder info
|
||||
factorioDirInfo, err := os.Stat(config.FactorioDir)
|
||||
if err != nil {
|
||||
log.Printf("error getting stats from FactorioDir: %s", err)
|
||||
log.Printf("error getting stats from FactorioDir %s with error %s", config.FactorioDir, err)
|
||||
return
|
||||
}
|
||||
factorioDirPerm := factorioDirInfo.Mode().Perm()
|
||||
@ -229,7 +66,7 @@ func modStartUp() {
|
||||
//crate mod_pack dir
|
||||
if _, err = os.Stat(config.FactorioModPackDir); os.IsNotExist(err) {
|
||||
log.Println("no ModPackDir found ... creating one ...")
|
||||
os.Mkdir(config.FactorioModPackDir, factorioDirPerm)
|
||||
_ = os.Mkdir(config.FactorioModPackDir, factorioDirPerm)
|
||||
}
|
||||
|
||||
oldModpackDir := filepath.Join(config.FactorioDir, "modpacks")
|
||||
@ -276,7 +113,7 @@ func modStartUp() {
|
||||
}
|
||||
newJson, _ := json.Marshal(modSimpleList)
|
||||
|
||||
err = ioutil.WriteFile(filepath.Join(modSimpleList.Destination,"mod-list.json"), newJson, 0664)
|
||||
err = ioutil.WriteFile(filepath.Join(modSimpleList.Destination, "mod-list.json"), newJson, 0664)
|
||||
if err != nil {
|
||||
log.Printf("error when writing new mod-list: %s", err)
|
||||
return err
|
||||
@ -288,7 +125,7 @@ func modStartUp() {
|
||||
}
|
||||
defer modPackFile.Close()
|
||||
|
||||
mods, err := newMods(modPackDir)
|
||||
mods, err := NewMods(modPackDir)
|
||||
if err != nil {
|
||||
log.Printf("error reading mods: %s", err)
|
||||
return err
|
@ -1,6 +1,7 @@
|
||||
package main
|
||||
package factorio
|
||||
|
||||
import (
|
||||
"github.com/mroote/factorio-server-manager/bootstrap"
|
||||
"log"
|
||||
"strconv"
|
||||
|
||||
@ -9,8 +10,10 @@ import (
|
||||
|
||||
func connectRC() error {
|
||||
var err error
|
||||
config := bootstrap.GetConfig()
|
||||
rconAddr := config.ServerIP + ":" + strconv.Itoa(config.FactorioRconPort)
|
||||
FactorioServ.Rcon, err = rcon.Dial(rconAddr, config.FactorioRconPass)
|
||||
server := GetFactorioServer()
|
||||
server.Rcon, err = rcon.Dial(rconAddr, config.FactorioRconPass)
|
||||
if err != nil {
|
||||
log.Printf("Cannot create rcon session: %s", err)
|
||||
return err
|
@ -1,4 +1,4 @@
|
||||
package main
|
||||
package factorio
|
||||
|
||||
import (
|
||||
"archive/zip"
|
326
src/factorio/save_test.go
Normal file
326
src/factorio/save_test.go
Normal file
@ -0,0 +1,326 @@
|
||||
package factorio
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
// 0.18 Binary seems equal to 0.17 binary, just the default values changed
|
||||
func Test0_18(t *testing.T) {
|
||||
file, err := OpenArchiveFile("../factorio_testfiles/test_0_18.zip", "level.dat")
|
||||
if err != nil {
|
||||
t.Fatalf("Error opening level.dat: %s", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
var header SaveHeader
|
||||
err = header.ReadFrom(file)
|
||||
if err != nil {
|
||||
t.Fatalf("Error reading header: %s", err)
|
||||
}
|
||||
|
||||
testHeader := SaveHeader{
|
||||
FactorioVersion: Version{0, 18, 2, 2},
|
||||
Campaign: "transport-belt-madness",
|
||||
Name: "level-01",
|
||||
BaseMod: "base",
|
||||
Difficulty: 1,
|
||||
Finished: false,
|
||||
PlayerWon: false,
|
||||
NextLevel: "",
|
||||
CanContinue: false,
|
||||
FinishedButContinuing: false,
|
||||
SavingReplay: false,
|
||||
AllowNonAdminDebugOptions: true,
|
||||
LoadedFrom: Version{0, 18, 2},
|
||||
LoadedFromBuild: 49204,
|
||||
AllowedCommands: 1,
|
||||
Mods: []Mod{
|
||||
{
|
||||
Version: Version{0, 18, 2},
|
||||
Name: "base",
|
||||
},
|
||||
{
|
||||
Version: Version{2, 0, 0},
|
||||
Name: "belt-balancer",
|
||||
},
|
||||
{
|
||||
Version: Version{2, 0, 1},
|
||||
Name: "train-station-overview",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
header.Equals(testHeader, t)
|
||||
}
|
||||
|
||||
func Test0_17(t *testing.T) {
|
||||
file, err := OpenArchiveFile("../factorio_testfiles/test_0_17.zip", "level.dat")
|
||||
if err != nil {
|
||||
t.Fatalf("Error opening level.dat: %s", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
var header SaveHeader
|
||||
err = header.ReadFrom(file)
|
||||
if err != nil {
|
||||
t.Fatalf("Error reading header: %s", err)
|
||||
}
|
||||
|
||||
testHeader := SaveHeader{
|
||||
FactorioVersion: Version{0, 17, 1, 1},
|
||||
Campaign: "transport-belt-madness",
|
||||
Name: "level-01",
|
||||
BaseMod: "base",
|
||||
Difficulty: 0,
|
||||
Finished: false,
|
||||
PlayerWon: false,
|
||||
NextLevel: "",
|
||||
CanContinue: false,
|
||||
FinishedButContinuing: false,
|
||||
SavingReplay: true,
|
||||
AllowNonAdminDebugOptions: true,
|
||||
LoadedFrom: Version{0, 17, 1},
|
||||
LoadedFromBuild: 43001,
|
||||
AllowedCommands: 1,
|
||||
Mods: []Mod{
|
||||
{
|
||||
Version: Version{0, 2, 0},
|
||||
Name: "Warehousing",
|
||||
},
|
||||
{
|
||||
Version: Version{0, 17, 1},
|
||||
Name: "base",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
header.Equals(testHeader, t)
|
||||
}
|
||||
|
||||
func Test0_16(t *testing.T) {
|
||||
file, err := OpenArchiveFile("../factorio_testfiles/test_0_16.zip", "level.dat")
|
||||
if err != nil {
|
||||
t.Fatalf("Error opening level.dat: %s", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
var header SaveHeader
|
||||
err = header.ReadFrom(file)
|
||||
if err != nil {
|
||||
t.Fatalf("Error reading header: %s", err)
|
||||
}
|
||||
|
||||
testHeader := SaveHeader{
|
||||
FactorioVersion: Version{0, 16, 51, 0},
|
||||
Campaign: "transport-belt-madness",
|
||||
Name: "level-01",
|
||||
BaseMod: "base",
|
||||
Difficulty: 0,
|
||||
Finished: false,
|
||||
PlayerWon: false,
|
||||
NextLevel: "",
|
||||
CanContinue: false,
|
||||
FinishedButContinuing: false,
|
||||
SavingReplay: true,
|
||||
AllowNonAdminDebugOptions: true,
|
||||
LoadedFrom: Version{0, 16, 51},
|
||||
LoadedFromBuild: 36654,
|
||||
AllowedCommands: 1,
|
||||
Mods: []Mod{
|
||||
{
|
||||
Version: Version{0, 1, 3},
|
||||
Name: "Warehousing",
|
||||
},
|
||||
{
|
||||
Version: Version{0, 16, 51},
|
||||
Name: "base",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
header.Equals(testHeader, t)
|
||||
}
|
||||
|
||||
func Test0_15(t *testing.T) {
|
||||
file, err := OpenArchiveFile("../factorio_testfiles/test_0_15.zip", "level.dat")
|
||||
if err != nil {
|
||||
t.Fatalf("Error opening level.dat: %s", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
var header SaveHeader
|
||||
err = header.ReadFrom(file)
|
||||
if err != nil {
|
||||
t.Fatalf("Error reading header: %s", err)
|
||||
}
|
||||
|
||||
testHeader := SaveHeader{
|
||||
FactorioVersion: Version{0, 15, 40, 0},
|
||||
Campaign: "transport-belt-madness",
|
||||
Name: "level-01",
|
||||
BaseMod: "base",
|
||||
Difficulty: 0,
|
||||
Finished: false,
|
||||
PlayerWon: false,
|
||||
NextLevel: "",
|
||||
CanContinue: false,
|
||||
FinishedButContinuing: false,
|
||||
SavingReplay: true,
|
||||
LoadedFrom: Version{0, 15, 40},
|
||||
LoadedFromBuild: 30950,
|
||||
AllowedCommands: 1,
|
||||
Mods: []Mod{
|
||||
{
|
||||
Version: Version{0, 0, 13},
|
||||
Name: "Warehousing",
|
||||
},
|
||||
{
|
||||
Version: Version{0, 15, 40},
|
||||
Name: "base",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
header.Equals(testHeader, t)
|
||||
}
|
||||
|
||||
func Test0_14(t *testing.T) {
|
||||
file, err := OpenArchiveFile("../factorio_testfiles/test_0_14.zip", "level.dat")
|
||||
if err != nil {
|
||||
t.Fatalf("Error opening level.dat: %s", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
var header SaveHeader
|
||||
err = header.ReadFrom(file)
|
||||
if err != nil {
|
||||
t.Fatalf("Error reading header: %s", err)
|
||||
}
|
||||
|
||||
testHeader := SaveHeader{
|
||||
FactorioVersion: Version{0, 14, 23, 0},
|
||||
Campaign: "transport-belt-madness",
|
||||
Name: "level-01",
|
||||
BaseMod: "base",
|
||||
Difficulty: 1,
|
||||
Finished: false,
|
||||
PlayerWon: false,
|
||||
NextLevel: "",
|
||||
CanContinue: false,
|
||||
FinishedButContinuing: false,
|
||||
SavingReplay: true,
|
||||
LoadedFrom: Version{0, 14, 23},
|
||||
LoadedFromBuild: 25374,
|
||||
AllowedCommands: 1,
|
||||
Mods: []Mod{
|
||||
{
|
||||
Version: Version{0, 0, 11},
|
||||
Name: "Warehousing",
|
||||
},
|
||||
{
|
||||
Version: Version{0, 14, 23},
|
||||
Name: "base",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
header.Equals(testHeader, t)
|
||||
}
|
||||
|
||||
func Test0_13(t *testing.T) {
|
||||
file, err := OpenArchiveFile("../factorio_testfiles/test_0_13.zip", "level.dat")
|
||||
if err != nil {
|
||||
t.Fatalf("Error opening level.dat: %s", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
var header SaveHeader
|
||||
err = header.ReadFrom(file)
|
||||
if err != nil {
|
||||
t.Fatalf("Error reading header: %s", err)
|
||||
}
|
||||
|
||||
testHeader := SaveHeader{
|
||||
FactorioVersion: Version{0, 13, 20, 0},
|
||||
Campaign: "transport-belt-madness",
|
||||
Name: "level-01",
|
||||
BaseMod: "base",
|
||||
Difficulty: 1,
|
||||
Finished: false,
|
||||
PlayerWon: false,
|
||||
NextLevel: "",
|
||||
CanContinue: false,
|
||||
FinishedButContinuing: false,
|
||||
SavingReplay: true,
|
||||
LoadedFrom: Version{0, 13, 20},
|
||||
LoadedFromBuild: 24011,
|
||||
AllowedCommands: 1,
|
||||
Mods: []Mod{
|
||||
{
|
||||
Version: Version{1, 1, 0},
|
||||
Name: "Extra-Virtual-Signals",
|
||||
},
|
||||
{
|
||||
Version: Version{0, 13, 20},
|
||||
Name: "base",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
header.Equals(testHeader, t)
|
||||
}
|
||||
|
||||
func (h *SaveHeader) Equals(other SaveHeader, t *testing.T) {
|
||||
if h.FactorioVersion != other.FactorioVersion {
|
||||
t.Errorf("FactorioVersion not equal: %s --- %s", h.FactorioVersion, other.FactorioVersion)
|
||||
}
|
||||
if h.Campaign != other.Campaign {
|
||||
t.Errorf("Campaign not equal: %s --- %s", h.Campaign, other.Campaign)
|
||||
}
|
||||
if h.Name != other.Name {
|
||||
t.Errorf("Name not equal: %s --- %s", h.Name, other.Name)
|
||||
}
|
||||
if h.BaseMod != other.BaseMod {
|
||||
t.Errorf("BaseMod not equal: %s --- %s", h.BaseMod, other.BaseMod)
|
||||
}
|
||||
if h.Difficulty != other.Difficulty {
|
||||
t.Errorf("Difficulty not equal: %d --- %d", h.Difficulty, other.Difficulty)
|
||||
}
|
||||
if h.Finished != other.Finished {
|
||||
t.Errorf("Finished not equal: %t --- %t", h.Finished, other.Finished)
|
||||
}
|
||||
if h.PlayerWon != other.PlayerWon {
|
||||
t.Errorf("PlayerWon not equal: %t --- %t", h.PlayerWon, other.PlayerWon)
|
||||
}
|
||||
if h.NextLevel != other.NextLevel {
|
||||
t.Errorf("NextLevel not equal: %s --- %s", h.NextLevel, other.NextLevel)
|
||||
}
|
||||
if h.CanContinue != other.CanContinue {
|
||||
t.Errorf("CanContinue not equal: %t --- %t", h.CanContinue, other.CanContinue)
|
||||
}
|
||||
if h.FinishedButContinuing != other.FinishedButContinuing {
|
||||
t.Errorf("FinishedButContinuing not equal: %t --- %t", h.FinishedButContinuing, other.FinishedButContinuing)
|
||||
}
|
||||
if h.SavingReplay != other.SavingReplay {
|
||||
t.Errorf("SavingReplay not equal: %t --- %t", h.SavingReplay, other.SavingReplay)
|
||||
}
|
||||
if h.AllowNonAdminDebugOptions != other.AllowNonAdminDebugOptions {
|
||||
t.Errorf("AllowNonAdminDebugOptions not equal: %t --- %t", h.AllowNonAdminDebugOptions, other.AllowNonAdminDebugOptions)
|
||||
}
|
||||
if h.LoadedFrom != other.LoadedFrom {
|
||||
t.Errorf("LoadedFrom not equal: %s --- %s", h.LoadedFrom, other.LoadedFrom)
|
||||
}
|
||||
if h.LoadedFromBuild != other.LoadedFromBuild {
|
||||
t.Errorf("LoadedFromBuild not equal: %d --- %d", h.LoadedFromBuild, other.LoadedFromBuild)
|
||||
}
|
||||
if h.AllowedCommands != other.AllowedCommands {
|
||||
t.Errorf("AllowedCommands not equal: %d --- %d", h.AllowedCommands, other.AllowedCommands)
|
||||
}
|
||||
for k := range h.Mods {
|
||||
if h.Mods[k].Name != other.Mods[k].Name {
|
||||
t.Errorf("ModNames not equal: %s --- %s", h.Mods[k].Name, other.Mods[k].Name)
|
||||
} else if h.Mods[k].Version != other.Mods[k].Version {
|
||||
t.Errorf("ModVersions of Mod %s are not equal: %s --- %s", h.Mods[k].Name, h.Mods[k].Version, other.Mods[k].Version)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,8 +1,9 @@
|
||||
package main
|
||||
package factorio
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/mroote/factorio-server-manager/bootstrap"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
@ -21,7 +22,7 @@ func (s Save) String() string {
|
||||
}
|
||||
|
||||
// Lists save files in factorio/saves
|
||||
func listSaves(saveDir string) (saves []Save, err error) {
|
||||
func ListSaves(saveDir string) (saves []Save, err error) {
|
||||
saves = []Save{}
|
||||
err = filepath.Walk(saveDir, func(path string, info os.FileInfo, err error) error {
|
||||
if info.IsDir() && info.Name() == "saves" {
|
||||
@ -37,8 +38,9 @@ func listSaves(saveDir string) (saves []Save, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func findSave(name string) (*Save, error) {
|
||||
saves, err := listSaves(config.FactorioSavesDir)
|
||||
func FindSave(name string) (*Save, error) {
|
||||
config := bootstrap.GetConfig()
|
||||
saves, err := ListSaves(config.FactorioSavesDir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error listing saves: %v", err)
|
||||
}
|
||||
@ -52,23 +54,24 @@ func findSave(name string) (*Save, error) {
|
||||
return nil, errors.New("save not found")
|
||||
}
|
||||
|
||||
func (s *Save) remove() error {
|
||||
func (s *Save) Remove() error {
|
||||
if s.Name == "" {
|
||||
return errors.New("save name cannot be blank")
|
||||
}
|
||||
|
||||
config := bootstrap.GetConfig()
|
||||
return os.Remove(filepath.Join(config.FactorioSavesDir, s.Name))
|
||||
}
|
||||
|
||||
// Create savefiles for Factorio
|
||||
func createSave(filePath string) (string, error) {
|
||||
err := os.MkdirAll(filepath.Base(filePath), 0755)
|
||||
func CreateSave(filePath string) (string, error) {
|
||||
err := os.MkdirAll(filepath.Dir(filePath), 0755)
|
||||
if err != nil {
|
||||
log.Printf("Error in creating Factorio save: %s", err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
args := []string{"--create", filePath}
|
||||
config := bootstrap.GetConfig()
|
||||
cmdOutput, err := exec.Command(config.FactorioBinary, args...).Output()
|
||||
if err != nil {
|
||||
log.Printf("Error in creating Factorio save: %s", err)
|
@ -1,33 +1,31 @@
|
||||
package main
|
||||
package factorio
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/mroote/factorio-server-manager/api/websocket"
|
||||
"github.com/mroote/factorio-server-manager/bootstrap"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"math/rand"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"regexp"
|
||||
"sync"
|
||||
|
||||
"github.com/majormjr/rcon"
|
||||
)
|
||||
|
||||
type FactorioServer struct {
|
||||
type Server struct {
|
||||
Cmd *exec.Cmd `json:"-"`
|
||||
Savefile string `json:"savefile"`
|
||||
Latency int `json:"latency"`
|
||||
BindIP string `json:"bindip"`
|
||||
Port int `json:"port"`
|
||||
Running bool `json:"running"`
|
||||
running bool `json:"running"`
|
||||
Version Version `json:"fac_version"`
|
||||
BaseModVersion string `json:"base_mod_version"`
|
||||
StdOut io.ReadCloser `json:"-"`
|
||||
@ -38,73 +36,117 @@ type FactorioServer struct {
|
||||
LogChan chan []string `json:"-"`
|
||||
}
|
||||
|
||||
func randomPort() int {
|
||||
// Returns random port to use for rcon connection
|
||||
return rand.Intn(45000-40000) + 40000
|
||||
var instantiated Server
|
||||
var once sync.Once
|
||||
|
||||
func (server *Server) SetRunning(newState bool) {
|
||||
if server.running != newState {
|
||||
log.Println("new state, will also send to correct room")
|
||||
server.running = newState
|
||||
wsRoom := websocket.WebsocketHub.GetRoom("server_status")
|
||||
wsRoom.Send("Server status has changed")
|
||||
}
|
||||
}
|
||||
|
||||
func initFactorio() (f *FactorioServer, err error) {
|
||||
f = new(FactorioServer)
|
||||
f.Settings = make(map[string]interface{})
|
||||
func (server *Server) GetRunning() bool {
|
||||
return server.running
|
||||
}
|
||||
|
||||
func (server *Server) autostart() {
|
||||
var err error
|
||||
if server.BindIP == "" {
|
||||
server.BindIP = "0.0.0.0"
|
||||
|
||||
}
|
||||
if server.Port == 0 {
|
||||
server.Port = 34197
|
||||
}
|
||||
server.Savefile = "Load Latest"
|
||||
|
||||
err = server.Run()
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Error starting Factorio server: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func SetFactorioServer(server Server) {
|
||||
instantiated = server
|
||||
}
|
||||
|
||||
func NewFactorioServer() (err error) {
|
||||
server := Server{}
|
||||
server.Settings = make(map[string]interface{})
|
||||
config := bootstrap.GetConfig()
|
||||
if err = os.MkdirAll(config.FactorioConfigDir, 0755); err != nil {
|
||||
return nil, fmt.Errorf("failed to create config directory: %v", err)
|
||||
log.Printf("failed to create config directory: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
settingsPath := filepath.Join(config.FactorioConfigDir, config.SettingsFile)
|
||||
var settings *os.File
|
||||
|
||||
if _, err := os.Stat(settingsPath); os.IsNotExist(err) {
|
||||
if _, err = os.Stat(settingsPath); os.IsNotExist(err) {
|
||||
// copy example settings to supplied settings file, if not exists
|
||||
log.Printf("Server settings at %s not found, copying example server settings.\n", settingsPath)
|
||||
|
||||
examplePath := filepath.Join(config.FactorioDir, "data", "server-settings.example.json")
|
||||
|
||||
example, err := os.Open(examplePath)
|
||||
var example *os.File
|
||||
example, err = os.Open(examplePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open example server settings: %v", err)
|
||||
log.Printf("failed to open example server settings: %v", err)
|
||||
return
|
||||
}
|
||||
defer example.Close()
|
||||
|
||||
settings, err = os.Create(settingsPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create server settings file: %v", err)
|
||||
log.Printf("failed to create server settings file: %v", err)
|
||||
return
|
||||
}
|
||||
defer settings.Close()
|
||||
|
||||
_, err = io.Copy(settings, example)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to copy example server settings: %v", err)
|
||||
log.Printf("failed to copy example server settings: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
err = example.Close()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to close example server settings: %s", err)
|
||||
log.Printf("failed to close example server settings: %s", err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// otherwise, open file normally
|
||||
settings, err = os.Open(settingsPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open server settings file: %v", err)
|
||||
log.Printf("failed to open server settings file: %v", err)
|
||||
return
|
||||
}
|
||||
defer settings.Close()
|
||||
}
|
||||
|
||||
// before reading reset offset
|
||||
if _, err = settings.Seek(0, 0); err != nil {
|
||||
return nil, fmt.Errorf("error while seeking in settings file: %v", err)
|
||||
log.Printf("error while seeking in settings file: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = json.NewDecoder(settings).Decode(&f.Settings); err != nil {
|
||||
return nil, fmt.Errorf("error reading %s: %v", settingsPath, err)
|
||||
if err = json.NewDecoder(settings).Decode(&server.Settings); err != nil {
|
||||
log.Printf("error reading %s: %v", settingsPath, err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("Loaded Factorio settings from %s\n", settingsPath)
|
||||
|
||||
out := []byte{}
|
||||
//Load factorio version
|
||||
if config.glibcCustom == "true" {
|
||||
out, err = exec.Command(config.glibcLocation, "--library-path", config.glibcLibLoc, config.FactorioBinary, "--version").Output()
|
||||
if config.GlibcCustom == "true" {
|
||||
out, err = exec.Command(config.GlibcLocation, "--library-path", config.GlibcLibLoc, config.FactorioBinary, "--version").Output()
|
||||
} else {
|
||||
out, err = exec.Command(config.FactorioBinary, "--version").Output()
|
||||
}
|
||||
@ -116,7 +158,7 @@ func initFactorio() (f *FactorioServer, err error) {
|
||||
|
||||
reg := regexp.MustCompile("Version.*?((\\d+\\.)?(\\d+\\.)?(\\*|\\d+)+)")
|
||||
found := reg.FindStringSubmatch(string(out))
|
||||
err = f.Version.UnmarshalText([]byte(found[1]))
|
||||
err = server.Version.UnmarshalText([]byte(found[1]))
|
||||
if err != nil {
|
||||
log.Printf("could not parse version: %v", err)
|
||||
return
|
||||
@ -136,38 +178,50 @@ func initFactorio() (f *FactorioServer, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
f.BaseModVersion = modInfo.Version
|
||||
server.BaseModVersion = modInfo.Version
|
||||
|
||||
// load admins from additional file
|
||||
if(f.Version.Greater(Version{0,17,0})) {
|
||||
if _, err := os.Stat(filepath.Join(config.FactorioConfigDir, config.FactorioAdminFile)); os.IsNotExist(err) {
|
||||
if (server.Version.Greater(Version{0, 17, 0})) {
|
||||
if _, err = os.Stat(filepath.Join(config.FactorioConfigDir, config.FactorioAdminFile)); os.IsNotExist(err) {
|
||||
//save empty admins-file
|
||||
ioutil.WriteFile(filepath.Join(config.FactorioConfigDir, config.FactorioAdminFile), []byte("[]"), 0664)
|
||||
_ = ioutil.WriteFile(filepath.Join(config.FactorioConfigDir, config.FactorioAdminFile), []byte("[]"), 0664)
|
||||
} else {
|
||||
data, err := ioutil.ReadFile(filepath.Join(config.FactorioConfigDir, config.FactorioAdminFile))
|
||||
var data []byte
|
||||
data, err = ioutil.ReadFile(filepath.Join(config.FactorioConfigDir, config.FactorioAdminFile))
|
||||
if err != nil {
|
||||
log.Printf("Error loading FactorioAdminFile: %s", err)
|
||||
return f, err
|
||||
return
|
||||
}
|
||||
|
||||
var jsonData interface{}
|
||||
err = json.Unmarshal(data, &jsonData)
|
||||
if err != nil {
|
||||
log.Printf("Error unmarshalling FactorioAdminFile: %s", err)
|
||||
return f, err
|
||||
return
|
||||
}
|
||||
|
||||
f.Settings["admins"] = jsonData
|
||||
server.Settings["admins"] = jsonData
|
||||
}
|
||||
}
|
||||
|
||||
SetFactorioServer(server)
|
||||
|
||||
// autostart factorio is configured to do so
|
||||
if config.Autostart == "true" {
|
||||
go instantiated.autostart()
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (f *FactorioServer) Run() error {
|
||||
var err error
|
||||
func GetFactorioServer() (f *Server) {
|
||||
return &instantiated
|
||||
}
|
||||
|
||||
data, err := json.MarshalIndent(f.Settings, "", " ")
|
||||
func (server *Server) Run() error {
|
||||
var err error
|
||||
config := bootstrap.GetConfig()
|
||||
data, err := json.MarshalIndent(server.Settings, "", " ")
|
||||
if err != nil {
|
||||
log.Println("Failed to marshal FactorioServerSettings: ", err)
|
||||
} else {
|
||||
@ -178,98 +232,104 @@ func (f *FactorioServer) Run() error {
|
||||
|
||||
//The factorio server refenences its executable-path, since we execute the ld.so file and pass the factorio binary as a parameter
|
||||
//the game would use the path to the ld.so file as it's executable path and crash, to prevent this the parameter "--executable-path" is added
|
||||
if config.glibcCustom == "true" {
|
||||
log.Println("Custom glibc selected, glibc.so location:", config.glibcLocation, " lib location:", config.glibcLibLoc)
|
||||
args = append(args, "--library-path", config.glibcLibLoc, config.FactorioBinary, "--executable-path", config.FactorioBinary)
|
||||
if config.GlibcCustom == "true" {
|
||||
log.Println("Custom glibc selected, glibc.so location:", config.GlibcLocation, " lib location:", config.GlibcLibLoc)
|
||||
args = append(args, "--library-path", config.GlibcLibLoc, config.FactorioBinary, "--executable-path", config.FactorioBinary)
|
||||
}
|
||||
|
||||
args = append(args,
|
||||
"--bind", (f.BindIP),
|
||||
"--port", strconv.Itoa(f.Port),
|
||||
"--bind", server.BindIP,
|
||||
"--port", strconv.Itoa(server.Port),
|
||||
"--server-settings", filepath.Join(config.FactorioConfigDir, config.SettingsFile),
|
||||
"--rcon-port", strconv.Itoa(config.FactorioRconPort),
|
||||
"--rcon-password", config.FactorioRconPass)
|
||||
|
||||
if(f.Version.Greater(Version{0,17,0})) {
|
||||
if (server.Version.Greater(Version{0, 17, 0})) {
|
||||
args = append(args, "--server-adminlist", filepath.Join(config.FactorioConfigDir, config.FactorioAdminFile))
|
||||
}
|
||||
|
||||
if f.Savefile == "Load Latest" {
|
||||
if server.Savefile == "Load Latest" {
|
||||
args = append(args, "--start-server-load-latest")
|
||||
} else {
|
||||
args = append(args, "--start-server", filepath.Join(config.FactorioSavesDir, f.Savefile))
|
||||
args = append(args, "--start-server", filepath.Join(config.FactorioSavesDir, server.Savefile))
|
||||
}
|
||||
|
||||
if config.glibcCustom == "true" {
|
||||
log.Println("Starting server with command: ", config.glibcLocation, args)
|
||||
f.Cmd = exec.Command(config.glibcLocation, args...)
|
||||
if config.GlibcCustom == "true" {
|
||||
log.Println("Starting server with command: ", config.GlibcLocation, args)
|
||||
server.Cmd = exec.Command(config.GlibcLocation, args...)
|
||||
} else {
|
||||
log.Println("Starting server with command: ", config.FactorioBinary, args)
|
||||
f.Cmd = exec.Command(config.FactorioBinary, args...)
|
||||
server.Cmd = exec.Command(config.FactorioBinary, args...)
|
||||
}
|
||||
|
||||
f.StdOut, err = f.Cmd.StdoutPipe()
|
||||
server.StdOut, err = server.Cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
log.Printf("Error opening stdout pipe: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
f.StdIn, err = f.Cmd.StdinPipe()
|
||||
server.StdIn, err = server.Cmd.StdinPipe()
|
||||
if err != nil {
|
||||
log.Printf("Error opening stdin pipe: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
f.StdErr, err = f.Cmd.StderrPipe()
|
||||
server.StdErr, err = server.Cmd.StderrPipe()
|
||||
if err != nil {
|
||||
log.Printf("Error opening stderr pipe: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
go f.parseRunningCommand(f.StdOut)
|
||||
go f.parseRunningCommand(f.StdErr)
|
||||
go server.parseRunningCommand(server.StdOut)
|
||||
go server.parseRunningCommand(server.StdErr)
|
||||
|
||||
err = f.Cmd.Start()
|
||||
err = server.Cmd.Start()
|
||||
if err != nil {
|
||||
log.Printf("Factorio process failed to start: %s", err)
|
||||
return err
|
||||
}
|
||||
f.Running = true
|
||||
server.SetRunning(true)
|
||||
|
||||
err = f.Cmd.Wait()
|
||||
err = server.Cmd.Wait()
|
||||
if err != nil {
|
||||
log.Printf("Factorio process exited with error: %s", err)
|
||||
f.Running = false
|
||||
server.SetRunning(false)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *FactorioServer) parseRunningCommand(std io.ReadCloser) (err error) {
|
||||
func (server *Server) parseRunningCommand(std io.ReadCloser) (err error) {
|
||||
stdScanner := bufio.NewScanner(std)
|
||||
for stdScanner.Scan() {
|
||||
log.Printf("Factorio Server: %s", stdScanner.Text())
|
||||
if err := f.writeLog(stdScanner.Text()); err != nil {
|
||||
text := stdScanner.Text()
|
||||
|
||||
log.Printf("Factorio Server: %s", text)
|
||||
if err := server.writeLog(text); err != nil {
|
||||
log.Printf("Error: %s", err)
|
||||
}
|
||||
|
||||
line := strings.Fields(stdScanner.Text())
|
||||
// send the reported line per websocket
|
||||
wsRoom := websocket.WebsocketHub.GetRoom("gamelog")
|
||||
go wsRoom.Send(text)
|
||||
|
||||
line := strings.Fields(text)
|
||||
// Ensure logline slice is in bounds
|
||||
if len(line) > 1 {
|
||||
// Check if Factorio Server reports any errors if so handle it
|
||||
if line[1] == "Error" {
|
||||
err := f.checkLogError(line)
|
||||
err := server.checkLogError(line)
|
||||
if err != nil {
|
||||
log.Printf("Error checking Factorio Server Error: %s", err)
|
||||
}
|
||||
}
|
||||
// If rcon port opens indicated in log connect to rcon
|
||||
rconLog := "Starting RCON interface at port " + strconv.Itoa(config.FactorioRconPort)
|
||||
rconLog := "Starting RCON interface at IP"
|
||||
// check if slice index is greater than 2 to prevent panic
|
||||
if len(line) > 2 {
|
||||
// log line for opened rcon connection
|
||||
if strings.Join(line[3:], " ") == rconLog {
|
||||
if strings.Contains(strings.Join(line, " "), rconLog) {
|
||||
log.Printf("Rcon running on Factorio Server")
|
||||
err = connectRC()
|
||||
if err != nil {
|
||||
@ -286,7 +346,8 @@ func (f *FactorioServer) parseRunningCommand(std io.ReadCloser) (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *FactorioServer) writeLog(logline string) error {
|
||||
func (server *Server) writeLog(logline string) error {
|
||||
config := bootstrap.GetConfig()
|
||||
logfileName := filepath.Join(config.FactorioDir, "factorio-server-console.log")
|
||||
file, err := os.OpenFile(logfileName, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0644)
|
||||
if err != nil {
|
||||
@ -305,92 +366,33 @@ func (f *FactorioServer) writeLog(logline string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *FactorioServer) checkLogError(logline []string) error {
|
||||
func (server *Server) checkLogError(logline []string) error {
|
||||
// TODO Handle errors generated by running Factorio Server
|
||||
log.Println(logline)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *FactorioServer) Stop() error {
|
||||
if runtime.GOOS == "windows" {
|
||||
|
||||
// Disable our own handling of CTRL+C, so we don't close when we send it to the console.
|
||||
setCtrlHandlingIsDisabledForThisProcess(true)
|
||||
|
||||
// Send CTRL+C to all processes attached to the console (ourself, and the factorio server instance)
|
||||
sendCtrlCToPid(0)
|
||||
log.Println("Sent SIGINT to Factorio process. Factorio shutting down...")
|
||||
|
||||
// Somehow, the Factorio devs managed to code the game to react appropriately to CTRL+C, including
|
||||
// saving the game, but not actually exit. So, we still have to manually kill the process, and
|
||||
// for extra fun, there's no way to know when the server save has actually completed (unless we want
|
||||
// to inject filesystem logic into what should be a process-level Stop() routine), so our best option
|
||||
// is to just wait an arbitrary amount of time and hope that the save is successful in that time.
|
||||
time.Sleep(2 * time.Second)
|
||||
f.Cmd.Process.Signal(os.Kill)
|
||||
|
||||
// Re-enable handling of CTRL+C after we're sure that the factrio server is shut down.
|
||||
setCtrlHandlingIsDisabledForThisProcess(false)
|
||||
|
||||
f.Running = false
|
||||
return nil
|
||||
}
|
||||
|
||||
err := f.Cmd.Process.Signal(os.Interrupt)
|
||||
if err != nil {
|
||||
if err.Error() == "os: process already finished" {
|
||||
f.Running = false
|
||||
return err
|
||||
}
|
||||
log.Printf("Error sending SIGINT to Factorio process: %s", err)
|
||||
return err
|
||||
}
|
||||
f.Running = false
|
||||
log.Printf("Sent SIGINT to Factorio process. Factorio shutting down...")
|
||||
|
||||
err = f.Rcon.Close()
|
||||
if err != nil {
|
||||
log.Printf("Error close rcon connection: %s", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
func init() {
|
||||
websocket.WebsocketHub.RegisterControlHandler <- serverWebsocketControl
|
||||
}
|
||||
|
||||
func (f *FactorioServer) Kill() error {
|
||||
if runtime.GOOS == "windows" {
|
||||
// react to websocket control messages and run the command if it is requested
|
||||
func serverWebsocketControl(controls websocket.WsControls) {
|
||||
log.Println(controls)
|
||||
if controls.Type == "command" {
|
||||
command := controls.Value
|
||||
server := GetFactorioServer()
|
||||
if server.GetRunning() {
|
||||
log.Printf("Received command: %v", command)
|
||||
|
||||
err := f.Cmd.Process.Signal(os.Kill)
|
||||
if err != nil {
|
||||
if err.Error() == "os: process already finished" {
|
||||
f.Running = false
|
||||
return err
|
||||
reqId, err := server.Rcon.Write(command)
|
||||
if err != nil {
|
||||
log.Printf("Error sending rcon command: %s", err)
|
||||
return
|
||||
}
|
||||
log.Printf("Error sending SIGKILL to Factorio process: %s", err)
|
||||
return err
|
||||
|
||||
log.Printf("Command send to Factorio: %s, with rcon request id: %v", command, reqId)
|
||||
}
|
||||
f.Running = false
|
||||
log.Println("Sent SIGKILL to Factorio process. Factorio forced to exit.")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
err := f.Cmd.Process.Signal(os.Kill)
|
||||
if err != nil {
|
||||
if err.Error() == "os: process already finished" {
|
||||
f.Running = false
|
||||
return err
|
||||
}
|
||||
log.Printf("Error sending SIGKILL to Factorio process: %s", err)
|
||||
return err
|
||||
}
|
||||
f.Running = false
|
||||
log.Printf("Sent SIGKILL to Factorio process. Factorio forced to exit.")
|
||||
|
||||
err = f.Rcon.Close()
|
||||
if err != nil {
|
||||
log.Printf("Error close rcon connection: %s", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
53
src/factorio/server_linux.go
Normal file
53
src/factorio/server_linux.go
Normal file
@ -0,0 +1,53 @@
|
||||
// use this file only when compiling not windows (all unix systems)
|
||||
// +build !windows
|
||||
|
||||
package factorio
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
)
|
||||
|
||||
// Stubs for windows-only functions
|
||||
|
||||
func (server *Server) Kill() error {
|
||||
err := server.Cmd.Process.Signal(os.Kill)
|
||||
if err != nil {
|
||||
if err.Error() == "os: process already finished" {
|
||||
server.SetRunning(false)
|
||||
return err
|
||||
}
|
||||
log.Printf("Error sending SIGKILL to Factorio process: %s", err)
|
||||
return err
|
||||
}
|
||||
server.SetRunning(false)
|
||||
log.Printf("Sent SIGKILL to Factorio process. Factorio forced to exit.")
|
||||
|
||||
err = server.Rcon.Close()
|
||||
if err != nil {
|
||||
log.Printf("Error close rcon connection: %s", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (server *Server) Stop() error {
|
||||
err := server.Cmd.Process.Signal(os.Interrupt)
|
||||
if err != nil {
|
||||
if err.Error() == "os: process already finished" {
|
||||
server.SetRunning(false)
|
||||
return err
|
||||
}
|
||||
log.Printf("Error sending SIGINT to Factorio process: %s", err)
|
||||
return err
|
||||
}
|
||||
server.SetRunning(false)
|
||||
log.Printf("Sent SIGINT to Factorio process. Factorio shutting down...")
|
||||
|
||||
err = server.Rcon.Close()
|
||||
if err != nil {
|
||||
log.Printf("Error close rcon connection: %s", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
82
src/factorio/server_windows.go
Normal file
82
src/factorio/server_windows.go
Normal file
@ -0,0 +1,82 @@
|
||||
package factorio
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
func sendCtrlCToPid(pid int) {
|
||||
d, e := syscall.LoadDLL("kernel32.dll")
|
||||
if e != nil {
|
||||
log.Fatalf("LoadDLL: %v\n", e)
|
||||
}
|
||||
p, e := d.FindProc("GenerateConsoleCtrlEvent")
|
||||
if e != nil {
|
||||
log.Fatalf("FindProc: %v\n", e)
|
||||
}
|
||||
r, _, e := p.Call(uintptr(syscall.CTRL_C_EVENT), uintptr(pid))
|
||||
if r == 0 {
|
||||
log.Fatalf("GenerateConsoleCtrlEvent: %v\n", e)
|
||||
}
|
||||
}
|
||||
|
||||
func setCtrlHandlingIsDisabledForThisProcess(disabled bool) {
|
||||
disabledInt := 0
|
||||
if disabled {
|
||||
disabledInt = 1
|
||||
}
|
||||
|
||||
d, e := syscall.LoadDLL("kernel32.dll")
|
||||
if e != nil {
|
||||
log.Fatalf("LoadDLL: %v\n", e)
|
||||
}
|
||||
p, e := d.FindProc("SetConsoleCtrlHandler")
|
||||
if e != nil {
|
||||
log.Fatalf("FindProc: %v\n", e)
|
||||
}
|
||||
r, _, e := p.Call(uintptr(0), uintptr(disabledInt))
|
||||
if r == 0 {
|
||||
log.Fatalf("SetConsoleCtrlHandler: %v\n", e)
|
||||
}
|
||||
}
|
||||
|
||||
func (server *Server) Kill() error {
|
||||
err := server.Cmd.Process.Signal(os.Kill)
|
||||
if err != nil {
|
||||
if err.Error() == "os: process already finished" {
|
||||
server.SetRunning(false)
|
||||
return err
|
||||
}
|
||||
log.Printf("Error sending SIGKILL to Factorio process: %s", err)
|
||||
return err
|
||||
}
|
||||
server.SetRunning(false)
|
||||
log.Println("Sent SIGKILL to Factorio process. Factorio forced to exit.")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (server *Server) Stop() error {
|
||||
// Disable our own handling of CTRL+C, so we don't close when we send it to the console.
|
||||
setCtrlHandlingIsDisabledForThisProcess(true)
|
||||
|
||||
// Send CTRL+C to all processes attached to the console (ourself, and the factorio server instance)
|
||||
sendCtrlCToPid(0)
|
||||
log.Println("Sent SIGINT to Factorio process. Factorio shutting down...")
|
||||
|
||||
// Somehow, the Factorio devs managed to code the game to react appropriately to CTRL+C, including
|
||||
// saving the game, but not actually exit. So, we still have to manually kill the process, and
|
||||
// for extra fun, there's no way to know when the server save has actually completed (unless we want
|
||||
// to inject filesystem logic into what should be a process-level Stop() routine), so our best option
|
||||
// is to just wait an arbitrary amount of time and hope that the save is successful in that time.
|
||||
time.Sleep(2 * time.Second)
|
||||
server.Cmd.Process.Signal(os.Kill)
|
||||
|
||||
// Re-enable handling of CTRL+C after we're sure that the factrio server is shut down.
|
||||
setCtrlHandlingIsDisabledForThisProcess(false)
|
||||
|
||||
server.SetRunning(false)
|
||||
return nil
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package main
|
||||
package factorio
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
@ -1,277 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test0_17(t *testing.T) {
|
||||
file, err := OpenArchiveFile("factorio_save_testfiles/test_0_17.zip", "level.dat")
|
||||
if err != nil {
|
||||
t.Fatalf("Error opening level.dat: %s", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
var header SaveHeader
|
||||
err = header.ReadFrom(file)
|
||||
if err != nil {
|
||||
t.Fatalf("Error reading header: %s", err)
|
||||
}
|
||||
|
||||
testHeader := SaveHeader{
|
||||
FactorioVersion: Version{0,17,1,1},
|
||||
Campaign: "transport-belt-madness",
|
||||
Name: "level-01",
|
||||
BaseMod: "base",
|
||||
Difficulty: 0,
|
||||
Finished: false,
|
||||
PlayerWon: false,
|
||||
NextLevel: "",
|
||||
CanContinue: false,
|
||||
FinishedButContinuing: false,
|
||||
SavingReplay: true,
|
||||
AllowNonAdminDebugOptions: true,
|
||||
LoadedFrom: Version{0,17,1},
|
||||
LoadedFromBuild: 43001,
|
||||
AllowedCommands: 1,
|
||||
Mods: []Mod {
|
||||
{
|
||||
Version: Version{0,2,0},
|
||||
Name: "Warehousing",
|
||||
},
|
||||
{
|
||||
Version: Version{0,17,1},
|
||||
Name: "base",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
header.Equals(testHeader, t)
|
||||
}
|
||||
|
||||
func Test0_16(t *testing.T) {
|
||||
file, err := OpenArchiveFile("factorio_save_testfiles/test_0_16.zip", "level.dat")
|
||||
if err != nil {
|
||||
t.Fatalf("Error opening level.dat: %s", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
var header SaveHeader
|
||||
err = header.ReadFrom(file)
|
||||
if err != nil {
|
||||
t.Fatalf("Error reading header: %s", err)
|
||||
}
|
||||
|
||||
testHeader := SaveHeader{
|
||||
FactorioVersion: Version{0,16,51,0},
|
||||
Campaign: "transport-belt-madness",
|
||||
Name: "level-01",
|
||||
BaseMod: "base",
|
||||
Difficulty: 0,
|
||||
Finished: false,
|
||||
PlayerWon: false,
|
||||
NextLevel: "",
|
||||
CanContinue: false,
|
||||
FinishedButContinuing: false,
|
||||
SavingReplay: true,
|
||||
AllowNonAdminDebugOptions: true,
|
||||
LoadedFrom: Version{0,16,51},
|
||||
LoadedFromBuild: 36654,
|
||||
AllowedCommands: 1,
|
||||
Mods: []Mod {
|
||||
{
|
||||
Version: Version{0,1,3},
|
||||
Name: "Warehousing",
|
||||
},
|
||||
{
|
||||
Version: Version{0,16,51},
|
||||
Name: "base",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
header.Equals(testHeader, t)
|
||||
}
|
||||
|
||||
func Test0_15(t *testing.T) {
|
||||
file, err := OpenArchiveFile("factorio_save_testfiles/test_0_15.zip", "level.dat")
|
||||
if err != nil {
|
||||
t.Fatalf("Error opening level.dat: %s", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
var header SaveHeader
|
||||
err = header.ReadFrom(file)
|
||||
if err != nil {
|
||||
t.Fatalf("Error reading header: %s", err)
|
||||
}
|
||||
|
||||
testHeader := SaveHeader{
|
||||
FactorioVersion: Version{0,15,40,0},
|
||||
Campaign: "transport-belt-madness",
|
||||
Name: "level-01",
|
||||
BaseMod: "base",
|
||||
Difficulty: 0,
|
||||
Finished: false,
|
||||
PlayerWon: false,
|
||||
NextLevel: "",
|
||||
CanContinue: false,
|
||||
FinishedButContinuing: false,
|
||||
SavingReplay: true,
|
||||
LoadedFrom: Version{0,15,40},
|
||||
LoadedFromBuild: 30950,
|
||||
AllowedCommands: 1,
|
||||
Mods: []Mod {
|
||||
{
|
||||
Version: Version{0,0,13},
|
||||
Name: "Warehousing",
|
||||
},
|
||||
{
|
||||
Version: Version{0,15,40},
|
||||
Name: "base",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
header.Equals(testHeader, t)
|
||||
}
|
||||
|
||||
func Test0_14(t *testing.T) {
|
||||
file, err := OpenArchiveFile("factorio_save_testfiles/test_0_14.zip", "level.dat")
|
||||
if err != nil {
|
||||
t.Fatalf("Error opening level.dat: %s", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
var header SaveHeader
|
||||
err = header.ReadFrom(file)
|
||||
if err != nil {
|
||||
t.Fatalf("Error reading header: %s", err)
|
||||
}
|
||||
|
||||
testHeader := SaveHeader{
|
||||
FactorioVersion: Version{0,14,23,0},
|
||||
Campaign: "transport-belt-madness",
|
||||
Name: "level-01",
|
||||
BaseMod: "base",
|
||||
Difficulty: 1,
|
||||
Finished: false,
|
||||
PlayerWon: false,
|
||||
NextLevel: "",
|
||||
CanContinue: false,
|
||||
FinishedButContinuing: false,
|
||||
SavingReplay: true,
|
||||
LoadedFrom: Version{0,14,23},
|
||||
LoadedFromBuild: 25374,
|
||||
AllowedCommands: 1,
|
||||
Mods: []Mod {
|
||||
{
|
||||
Version: Version{0,0,11},
|
||||
Name: "Warehousing",
|
||||
},
|
||||
{
|
||||
Version: Version{0,14,23},
|
||||
Name: "base",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
header.Equals(testHeader, t)
|
||||
}
|
||||
|
||||
func Test0_13(t *testing.T) {
|
||||
file, err := OpenArchiveFile("factorio_save_testfiles/test_0_13.zip", "level.dat")
|
||||
if err != nil {
|
||||
t.Fatalf("Error opening level.dat: %s", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
var header SaveHeader
|
||||
err = header.ReadFrom(file)
|
||||
if err != nil {
|
||||
t.Fatalf("Error reading header: %s", err)
|
||||
}
|
||||
|
||||
testHeader := SaveHeader{
|
||||
FactorioVersion: Version{0,13,20,0},
|
||||
Campaign: "transport-belt-madness",
|
||||
Name: "level-01",
|
||||
BaseMod: "base",
|
||||
Difficulty: 1,
|
||||
Finished: false,
|
||||
PlayerWon: false,
|
||||
NextLevel: "",
|
||||
CanContinue: false,
|
||||
FinishedButContinuing: false,
|
||||
SavingReplay: true,
|
||||
LoadedFrom: Version{0,13,20},
|
||||
LoadedFromBuild: 24011,
|
||||
AllowedCommands: 1,
|
||||
Mods: []Mod {
|
||||
{
|
||||
Version: Version{1,1,0},
|
||||
Name: "Extra-Virtual-Signals",
|
||||
},
|
||||
{
|
||||
Version: Version{0,13,20},
|
||||
Name: "base",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
header.Equals(testHeader, t)
|
||||
}
|
||||
|
||||
func (h *SaveHeader) Equals(other SaveHeader, t *testing.T) {
|
||||
if h.FactorioVersion != other.FactorioVersion {
|
||||
t.Errorf("FactorioVersion not equal: %s --- %s", h.FactorioVersion, other.FactorioVersion)
|
||||
}
|
||||
if h.Campaign != other.Campaign {
|
||||
t.Errorf("Campaign not equal: %s --- %s", h.Campaign, other.Campaign)
|
||||
}
|
||||
if h.Name != other.Name {
|
||||
t.Errorf("Name not equal: %s --- %s", h.Name, other.Name)
|
||||
}
|
||||
if h.BaseMod != other.BaseMod {
|
||||
t.Errorf("BaseMod not equal: %s --- %s", h.BaseMod, other.BaseMod)
|
||||
}
|
||||
if h.Difficulty != other.Difficulty {
|
||||
t.Errorf("Difficulty not equal: %d --- %d", h.Difficulty, other.Difficulty)
|
||||
}
|
||||
if h.Finished != other.Finished {
|
||||
t.Errorf("Finished not equal: %t --- %t", h.Finished, other.Finished)
|
||||
}
|
||||
if h.PlayerWon != other.PlayerWon {
|
||||
t.Errorf("PlayerWon not equal: %t --- %t", h.PlayerWon, other.PlayerWon)
|
||||
}
|
||||
if h.NextLevel != other.NextLevel {
|
||||
t.Errorf("NextLevel not equal: %s --- %s", h.NextLevel, other.NextLevel)
|
||||
}
|
||||
if h.CanContinue != other.CanContinue {
|
||||
t.Errorf("CanContinue not equal: %t --- %t", h.CanContinue, other.CanContinue)
|
||||
}
|
||||
if h.FinishedButContinuing != other.FinishedButContinuing {
|
||||
t.Errorf("FinishedButContinuing not equal: %t --- %t", h.FinishedButContinuing, other.FinishedButContinuing)
|
||||
}
|
||||
if h.SavingReplay != other.SavingReplay {
|
||||
t.Errorf("SavingReplay not equal: %t --- %t", h.SavingReplay, other.SavingReplay)
|
||||
}
|
||||
if h.AllowNonAdminDebugOptions != other.AllowNonAdminDebugOptions {
|
||||
t.Errorf("AllowNonAdminDebugOptions not equal: %t --- %t", h.AllowNonAdminDebugOptions, other.AllowNonAdminDebugOptions)
|
||||
}
|
||||
if h.LoadedFrom != other.LoadedFrom {
|
||||
t.Errorf("LoadedFrom not equal: %s --- %s", h.LoadedFrom, other.LoadedFrom)
|
||||
}
|
||||
if h.LoadedFromBuild != other.LoadedFromBuild {
|
||||
t.Errorf("LoadedFromBuild not equal: %d --- %d", h.LoadedFromBuild, other.LoadedFromBuild)
|
||||
}
|
||||
if h.AllowedCommands != other.AllowedCommands {
|
||||
t.Errorf("AllowedCommands not equal: %d --- %d", h.AllowedCommands, other.AllowedCommands)
|
||||
}
|
||||
for k := range h.Mods {
|
||||
if h.Mods[k].Name != other.Mods[k].Name {
|
||||
t.Errorf("ModNames not equal: %s --- %s", h.Mods[k].Name, other.Mods[k].Name)
|
||||
} else if h.Mods[k].Version != other.Mods[k].Version {
|
||||
t.Errorf("ModVersions of Mod %s are not equal: %s --- %s", h.Mods[k].Name, h.Mods[k].Version, other.Mods[k].Version)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
package main
|
||||
|
||||
// Stubs for windows-only functions
|
||||
|
||||
func sendCtrlCToPid(pid int) {
|
||||
}
|
||||
|
||||
func setCtrlHandlingIsDisabledForThisProcess(disabled bool) {
|
||||
}
|
@ -1,41 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func sendCtrlCToPid(pid int) {
|
||||
d, e := syscall.LoadDLL("kernel32.dll")
|
||||
if e != nil {
|
||||
log.Fatalf("LoadDLL: %v\n", e)
|
||||
}
|
||||
p, e := d.FindProc("GenerateConsoleCtrlEvent")
|
||||
if e != nil {
|
||||
log.Fatalf("FindProc: %v\n", e)
|
||||
}
|
||||
r, _, e := p.Call(uintptr(syscall.CTRL_C_EVENT), uintptr(pid))
|
||||
if r == 0 {
|
||||
log.Fatalf("GenerateConsoleCtrlEvent: %v\n", e)
|
||||
}
|
||||
}
|
||||
|
||||
func setCtrlHandlingIsDisabledForThisProcess(disabled bool) {
|
||||
disabledInt := 0
|
||||
if disabled {
|
||||
disabledInt = 1
|
||||
}
|
||||
|
||||
d, e := syscall.LoadDLL("kernel32.dll")
|
||||
if e != nil {
|
||||
log.Fatalf("LoadDLL: %v\n", e)
|
||||
}
|
||||
p, e := d.FindProc("SetConsoleCtrlHandler")
|
||||
if e != nil {
|
||||
log.Fatalf("FindProc: %v\n", e)
|
||||
}
|
||||
r, _, e := p.Call(uintptr(0), uintptr(disabledInt))
|
||||
if r == 0 {
|
||||
log.Fatalf("SetConsoleCtrlHandler: %v\n", e)
|
||||
}
|
||||
}
|
BIN
src/factorio_testfiles/belt-balancer_2.1.3.zip
Normal file
BIN
src/factorio_testfiles/belt-balancer_2.1.3.zip
Normal file
Binary file not shown.
1
src/factorio_testfiles/file_usage.txt
Normal file
1
src/factorio_testfiles/file_usage.txt
Normal file
@ -0,0 +1 @@
|
||||
The usage of the belt-balancer mod is allowed without additional licensing by knoxfighter/asdff45 (the creator of the mod and also contributor to this factorio-server-manager project)
|
BIN
src/factorio_testfiles/invalid_mod.zip
Normal file
BIN
src/factorio_testfiles/invalid_mod.zip
Normal file
Binary file not shown.
BIN
src/factorio_testfiles/test_0_18.zip
Normal file
BIN
src/factorio_testfiles/test_0_18.zip
Normal file
Binary file not shown.
@ -10,10 +10,13 @@ require (
|
||||
github.com/gorilla/sessions v1.2.0 // indirect
|
||||
github.com/gorilla/websocket v1.4.1
|
||||
github.com/hpcloud/tail v1.0.0
|
||||
github.com/jessevdk/go-flags v1.4.0
|
||||
github.com/joho/godotenv v1.3.0
|
||||
github.com/lib/pq v1.2.0 // indirect
|
||||
github.com/majormjr/rcon v0.0.0-20120923215419-8fbb8268b60a
|
||||
github.com/mattn/go-sqlite3 v1.11.0 // indirect
|
||||
github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337 // indirect
|
||||
github.com/stretchr/testify v1.6.1
|
||||
github.com/syndtr/goleveldb v1.0.0 // indirect
|
||||
golang.org/x/crypto v0.0.0-20191029031824-8986dd9e96cf // indirect
|
||||
google.golang.org/appengine v1.6.5 // indirect
|
||||
|
13
src/go.sum
13
src/go.sum
@ -1,5 +1,7 @@
|
||||
github.com/apexskier/httpauth v1.3.2 h1:PHwrq/eBRBLIrUthchpbDVTVR/ofBrj2LUcukCRhfXw=
|
||||
github.com/apexskier/httpauth v1.3.2/go.mod h1:aEHd6x648VCocEK0vTsPKkjJ1sBPab3Z4V4MJs9YZAE=
|
||||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/go-ini/ini v1.49.0 h1:ymWFBUkwN3JFPjvjcJJ5TSTwh84M66QrH+8vOytLgRY=
|
||||
@ -24,6 +26,10 @@ github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvK
|
||||
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA=
|
||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
|
||||
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
|
||||
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0=
|
||||
@ -37,10 +43,15 @@ github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs=
|
||||
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU=
|
||||
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337 h1:WN9BUFbdyOsSH/XohnWpXOlq9NBD5sGAB2FciQMUEe8=
|
||||
github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
|
||||
github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
@ -78,3 +89,5 @@ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkep
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
791
src/handlers.go
791
src/handlers.go
@ -1,791 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
type JSONResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Data interface{} `json:"data,string"`
|
||||
}
|
||||
type JSONResponseFileInput struct {
|
||||
Success bool `json:"success"`
|
||||
Data interface{} `json:"data,string"`
|
||||
Error string `json:"error"`
|
||||
ErrorKeys []int `json:"errorkeys"`
|
||||
}
|
||||
|
||||
// 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")
|
||||
|
||||
savesList, err := listSaves(config.FactorioSavesDir)
|
||||
if err != nil {
|
||||
resp.Success = false
|
||||
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
|
||||
}
|
||||
|
||||
loadLatest := Save{Name: "Load Latest"}
|
||||
savesList = append(savesList, loadLatest)
|
||||
|
||||
resp.Data = savesList
|
||||
|
||||
resp.Success = true
|
||||
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
log.Printf("Error listing saves: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func DLSave(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/octet-stream")
|
||||
|
||||
vars := mux.Vars(r)
|
||||
save := vars["save"]
|
||||
saveName := filepath.Join(config.FactorioSavesDir, save)
|
||||
|
||||
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", save))
|
||||
log.Printf("%s downloading: %s", r.Host, saveName)
|
||||
|
||||
http.ServeFile(w, r, saveName)
|
||||
}
|
||||
|
||||
func UploadSave(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
resp := JSONResponse{
|
||||
Success: false,
|
||||
}
|
||||
|
||||
switch r.Method {
|
||||
case "GET":
|
||||
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":
|
||||
log.Println("Uploading save file")
|
||||
|
||||
r.ParseMultipartForm(32 << 20)
|
||||
|
||||
for _, saveFile := range r.MultipartForm.File["savefile"] {
|
||||
file, err := saveFile.Open()
|
||||
if err != nil {
|
||||
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(filepath.Join(config.FactorioSavesDir, saveFile.Filename))
|
||||
if err != nil {
|
||||
resp.Success = false
|
||||
resp.Data = err.Error()
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
log.Printf("Error in out: %s", err)
|
||||
return
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
_, err = io.Copy(out, file)
|
||||
if err != nil {
|
||||
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", saveFile.Filename)
|
||||
resp.Data = "File '" + saveFile.Filename + "' uploaded successfully"
|
||||
resp.Success = true
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
}
|
||||
default:
|
||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||
}
|
||||
}
|
||||
|
||||
// Deletes provided save
|
||||
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)
|
||||
name := vars["save"]
|
||||
|
||||
save, err := findSave(name)
|
||||
if err != nil {
|
||||
resp.Data = fmt.Sprintf("Error removing save: %s", err)
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
log.Printf("Error removing save %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
err = save.remove()
|
||||
if err == nil {
|
||||
// save was removed
|
||||
resp.Data = fmt.Sprintf("Removed save: %s", save.Name)
|
||||
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.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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Launches Factorio server binary with --create flag to create save
|
||||
// Url must include save name for creation of savefile
|
||||
func CreateSaveHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
resp := JSONResponse{
|
||||
Success: false,
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
saveName := vars["save"]
|
||||
|
||||
if saveName == "" {
|
||||
log.Printf("Error creating save, no name provided: %s", err)
|
||||
resp.Data = "No save name provided."
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
log.Printf("Error encoding save handler response: %s", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
saveFile := filepath.Join(config.FactorioSavesDir, saveName)
|
||||
cmdOut, err := createSave(saveFile)
|
||||
if err != nil {
|
||||
log.Printf("Error creating save: %s", err)
|
||||
resp.Data = "Error creating savefile."
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
log.Printf("Error encoding save handler response: %s", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
resp.Success = true
|
||||
resp.Data = fmt.Sprintf("Save %s created successfully. Command output: \n%s", saveName, cmdOut)
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
log.Printf("Error encoding save response: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
// LogTail 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")
|
||||
|
||||
resp.Data, err = tailLog(config.FactorioLog)
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
resp.Success = true
|
||||
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
log.Printf("Error tailing logfile: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
// LoadConfig returns 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.Data = "Error getting config.ini"
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
log.Printf("Error tailing logfile: %s", err)
|
||||
}
|
||||
} else {
|
||||
resp.Data = configContents
|
||||
resp.Success = true
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
log.Printf("Error encoding config file JSON reponse: %s", err)
|
||||
}
|
||||
|
||||
log.Printf("Sent config.ini response")
|
||||
}
|
||||
|
||||
func StartServer(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
resp := JSONResponse{
|
||||
Success: false,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
|
||||
|
||||
if FactorioServ.Running {
|
||||
resp.Data = "Factorio server is already running"
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
log.Printf("Error encoding JSON response: %s", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
switch r.Method {
|
||||
case "GET":
|
||||
log.Printf("GET not supported for startserver handler")
|
||||
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":
|
||||
log.Printf("Starting Factorio server.")
|
||||
|
||||
body, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
log.Printf("Error in starting factorio server handler body: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("Starting Factorio server with settings: %v", string(body))
|
||||
|
||||
err = json.Unmarshal(body, &FactorioServ)
|
||||
if err != nil {
|
||||
log.Printf("Error unmarshaling server settings JSON: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if savefile was submitted with request to start server.
|
||||
if FactorioServ.Savefile == "" {
|
||||
log.Printf("Error starting Factorio server: no save file provided")
|
||||
resp.Success = false
|
||||
resp.Data = fmt.Sprintf("Error starting Factorio server: %s", "No save file provided")
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
log.Printf("Error encoding config file JSON reponse: %s", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
go func() {
|
||||
err = FactorioServ.Run()
|
||||
if err != nil {
|
||||
log.Printf("Error starting Factorio server: %+v", err)
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
timeout := 0
|
||||
for timeout <= 3 {
|
||||
time.Sleep(1 * time.Second)
|
||||
if FactorioServ.Running {
|
||||
resp.Data = fmt.Sprintf("Factorio server with save: %s started on port: %d", FactorioServ.Savefile, FactorioServ.Port)
|
||||
resp.Success = true
|
||||
log.Printf("Factorio server started on port: %v", FactorioServ.Port)
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
log.Printf("Error encoding config file JSON reponse: %s", err)
|
||||
}
|
||||
break
|
||||
} else {
|
||||
log.Printf("Did not detect running Factorio server attempt: %+v", timeout)
|
||||
}
|
||||
|
||||
timeout++
|
||||
}
|
||||
if FactorioServ.Running == false {
|
||||
log.Printf("Error starting Factorio server: %s", err)
|
||||
resp.Data = fmt.Sprintf("Error starting Factorio server: %s", err)
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
log.Printf("Error encoding start server JSON response: %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func StopServer(w http.ResponseWriter, r *http.Request) {
|
||||
resp := JSONResponse{
|
||||
Success: false,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
|
||||
|
||||
if FactorioServ.Running {
|
||||
err := FactorioServ.Stop()
|
||||
if err != nil {
|
||||
log.Printf("Error in stop server handler: %s", err)
|
||||
resp.Data = fmt.Sprintf("Error in stop server handler: %s", err)
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
log.Printf("Error encoding config file JSON reponse: %s", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("Stopped Factorio server.")
|
||||
resp.Success = true
|
||||
resp.Data = fmt.Sprintf("Factorio server stopped")
|
||||
} else {
|
||||
resp.Data = "Factorio server is not running"
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
log.Printf("Error encoding config file JSON reponse: %s", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
log.Printf("Error encoding config file JSON reponse: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func KillServer(w http.ResponseWriter, r *http.Request) {
|
||||
resp := JSONResponse{
|
||||
Success: false,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
|
||||
|
||||
if FactorioServ.Running {
|
||||
err := FactorioServ.Kill()
|
||||
if err != nil {
|
||||
log.Printf("Error in kill server handler: %s", err)
|
||||
resp.Data = fmt.Sprintf("Error in kill server handler: %s", err)
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
log.Printf("Error encoding config file JSON reponse: %s", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("Killed Factorio server.")
|
||||
resp.Success = true
|
||||
resp.Data = fmt.Sprintf("Factorio server killed")
|
||||
} else {
|
||||
resp.Data = "Factorio server is not running"
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
log.Printf("Error encoding config file JSON reponse: %s", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
log.Printf("Error encoding config file JSON reponse: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func CheckServer(w http.ResponseWriter, r *http.Request) {
|
||||
resp := JSONResponse{
|
||||
Success: false,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
|
||||
|
||||
if FactorioServ.Running {
|
||||
resp.Success = true
|
||||
status := map[string]string{}
|
||||
status["status"] = "running"
|
||||
status["port"] = strconv.Itoa(FactorioServ.Port)
|
||||
status["savefile"] = FactorioServ.Savefile
|
||||
status["address"] = FactorioServ.BindIP
|
||||
resp.Data = status
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
log.Printf("Error encoding config file JSON reponse: %s", err)
|
||||
}
|
||||
} else {
|
||||
resp.Success = true
|
||||
status := map[string]string{}
|
||||
status["status"] = "stopped"
|
||||
resp.Data = status
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
log.Printf("Error encoding config file JSON reponse: %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func FactorioVersion(w http.ResponseWriter, r *http.Request) {
|
||||
resp := JSONResponse{
|
||||
Success: true,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
|
||||
|
||||
status := map[string]string{}
|
||||
status["version"] = FactorioServ.Version.String()
|
||||
status["base_mod_version"] = FactorioServ.BaseModVersion
|
||||
|
||||
resp.Data = status
|
||||
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
log.Printf("Error loading Factorio Version: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func LoginUser(w http.ResponseWriter, r *http.Request) {
|
||||
resp := JSONResponse{
|
||||
Success: false,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
|
||||
|
||||
switch r.Method {
|
||||
case "GET":
|
||||
log.Printf("GET not supported for login handler")
|
||||
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":
|
||||
var user User
|
||||
body, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
log.Printf("Error in starting factorio server handler body: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
err = json.Unmarshal(body, &user)
|
||||
if err != nil {
|
||||
log.Printf("Error unmarshaling server settings JSON: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("Logging in user: %s", user.Username)
|
||||
|
||||
err = Auth.aaa.Login(w, r, user.Username, user.Password, "/")
|
||||
if err != nil {
|
||||
log.Printf("Error logging in user: %s, error: %s", user.Username, err)
|
||||
resp.Data = fmt.Sprintf("Error logging in user: %s", user.Username)
|
||||
resp.Success = false
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
log.Printf("Error listing mods: %s", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("User: %s, logged in successfully", user.Username)
|
||||
resp.Data = fmt.Sprintf("User: %s, logged in successfully", user.Username)
|
||||
resp.Success = true
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
log.Printf("Error listing mods: %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func LogoutUser(w http.ResponseWriter, r *http.Request) {
|
||||
resp := JSONResponse{
|
||||
Success: false,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
|
||||
|
||||
if err := Auth.aaa.Logout(w, r); err != nil {
|
||||
log.Printf("Error logging out current user")
|
||||
return
|
||||
}
|
||||
|
||||
resp.Success = true
|
||||
resp.Data = "User logged out successfully."
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
log.Printf("Error logging out: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func GetCurrentLogin(w http.ResponseWriter, r *http.Request) {
|
||||
resp := JSONResponse{
|
||||
Success: false,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
|
||||
|
||||
user, err := Auth.aaa.CurrentUser(w, r)
|
||||
if err != nil {
|
||||
log.Printf("Error getting current user status: %s", err)
|
||||
resp.Data = fmt.Sprintf("Error getting user status: %s", user.Username)
|
||||
resp.Success = false
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
log.Printf("Error listing mods: %s", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
resp.Success = true
|
||||
resp.Data = user
|
||||
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
log.Printf("Error getting user status: %s", err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func ListUsers(w http.ResponseWriter, r *http.Request) {
|
||||
resp := JSONResponse{
|
||||
Success: false,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
|
||||
|
||||
users, err := Auth.listUsers()
|
||||
if err != nil {
|
||||
log.Printf("Error in ListUsers handler: %s", err)
|
||||
resp.Data = fmt.Sprint("Error listing users")
|
||||
resp.Success = false
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
log.Printf("Error listing mods: %s", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
resp.Success = true
|
||||
resp.Data = users
|
||||
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
log.Printf("Error getting user status: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func AddUser(w http.ResponseWriter, r *http.Request) {
|
||||
resp := JSONResponse{
|
||||
Success: false,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
|
||||
|
||||
switch r.Method {
|
||||
case "GET":
|
||||
log.Printf("GET not supported for add user handler")
|
||||
resp.Data = "Unsupported method"
|
||||
resp.Success = false
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
log.Printf("Error adding user: %s", err)
|
||||
}
|
||||
case "POST":
|
||||
user := User{}
|
||||
body, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
log.Printf("Error in reading add user POST: %s", err)
|
||||
resp.Data = fmt.Sprintf("Error in adding user: %s", err)
|
||||
resp.Success = false
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
log.Printf("Error adding user: %s", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("Adding user: %v", string(body))
|
||||
|
||||
err = json.Unmarshal(body, &user)
|
||||
if err != nil {
|
||||
log.Printf("Error unmarshaling user add JSON: %s", err)
|
||||
resp.Data = fmt.Sprintf("Error in adding user: %s", err)
|
||||
resp.Success = false
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
log.Printf("Error adding user: %s", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
err = Auth.addUser(user.Username, user.Password, user.Email, user.Role)
|
||||
if err != nil {
|
||||
log.Printf("Error in adding user: %s", err)
|
||||
resp.Data = fmt.Sprintf("Error in adding user: %s", err)
|
||||
resp.Success = false
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
log.Printf("Error adding user: %s", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
resp.Success = true
|
||||
resp.Data = fmt.Sprintf("User: %s successfully added.", user.Username)
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
log.Printf("Error in returning added user response: %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func RemoveUser(w http.ResponseWriter, r *http.Request) {
|
||||
resp := JSONResponse{
|
||||
Success: false,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
|
||||
|
||||
switch r.Method {
|
||||
case "GET":
|
||||
log.Printf("GET not supported for add user handler")
|
||||
resp.Data = "Unsupported method"
|
||||
resp.Success = false
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
log.Printf("Error adding user: %s", err)
|
||||
}
|
||||
case "POST":
|
||||
user := User{}
|
||||
body, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
log.Printf("Error in reading remove user POST: %s", err)
|
||||
resp.Data = fmt.Sprintf("Error in removing user: %s", err)
|
||||
resp.Success = false
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
log.Printf("Error adding user: %s", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
err = json.Unmarshal(body, &user)
|
||||
if err != nil {
|
||||
log.Printf("Error unmarshaling user remove JSON: %s", err)
|
||||
resp.Data = fmt.Sprintf("Error in removing user: %s", err)
|
||||
resp.Success = false
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
log.Printf("Error removing user: %s", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
err = Auth.removeUser(user.Username)
|
||||
if err != nil {
|
||||
log.Printf("Error in remove user handler: %s", err)
|
||||
resp.Data = fmt.Sprintf("Error in removing user: %s", err)
|
||||
resp.Success = false
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
log.Printf("Error adding user: %s", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
resp.Success = true
|
||||
resp.Data = fmt.Sprintf("User: %s successfully removed.", user.Username)
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
log.Printf("Error in returning remove user response: %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetServerSettings returns JSON response of server-settings.json file
|
||||
func GetServerSettings(w http.ResponseWriter, r *http.Request) {
|
||||
resp := JSONResponse{
|
||||
Success: false,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
|
||||
|
||||
resp.Data = FactorioServ.Settings
|
||||
resp.Success = true
|
||||
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
log.Printf("Error encoding server settings JSON reponse: %s", err)
|
||||
}
|
||||
|
||||
log.Printf("Sent server settings response")
|
||||
}
|
||||
|
||||
func UpdateServerSettings(w http.ResponseWriter, r *http.Request) {
|
||||
resp := JSONResponse{
|
||||
Success: false,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
|
||||
|
||||
switch r.Method {
|
||||
case "GET":
|
||||
log.Printf("GET not supported for add user handler")
|
||||
resp.Data = "Unsupported method"
|
||||
resp.Success = false
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
log.Printf("Error adding user: %s", err)
|
||||
}
|
||||
case "POST":
|
||||
body, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
log.Printf("Error in reading server settings POST: %s", err)
|
||||
resp.Data = fmt.Sprintf("Error in updating settings: %s", err)
|
||||
resp.Success = false
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
log.Printf("Error updating settings: %s", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
log.Printf("Received settings JSON: %s", body)
|
||||
|
||||
err = json.Unmarshal(body, &FactorioServ.Settings)
|
||||
if err != nil {
|
||||
log.Printf("Error unmarshaling server settings JSON: %s", err)
|
||||
resp.Data = fmt.Sprintf("Error in updating settings: %s", err)
|
||||
resp.Success = false
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
log.Printf("Error encoding server settings response: %s", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
settings, err := json.MarshalIndent(&FactorioServ.Settings, "", " ")
|
||||
if err != nil {
|
||||
log.Printf("Failed to marshal server settings: %s", err)
|
||||
return
|
||||
} else {
|
||||
if err = ioutil.WriteFile(filepath.Join(config.FactorioConfigDir, config.SettingsFile), settings, 0644); err != nil {
|
||||
log.Printf("Failed to save server settings: %v\n", err)
|
||||
return
|
||||
}
|
||||
log.Printf("Saved Factorio server settings in server-settings.json")
|
||||
}
|
||||
|
||||
if(FactorioServ.Version.Greater(Version{0,17,0})) {
|
||||
// save admins to adminJson
|
||||
admins, err := json.MarshalIndent(FactorioServ.Settings["admins"], "", " ")
|
||||
if err != nil {
|
||||
log.Printf("Failed to marshal admins-Setting: %s", err)
|
||||
return
|
||||
}
|
||||
err = ioutil.WriteFile(filepath.Join(config.FactorioConfigDir, config.FactorioAdminFile), admins, 0664)
|
||||
if err != nil {
|
||||
log.Printf("Failed to save admins: %s", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
resp.Success = true
|
||||
resp.Data = fmt.Sprintf("Settings successfully saved")
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
log.Printf("Error in sending server settings response: %s", err)
|
||||
}
|
||||
}
|
||||
}
|
128
src/main.go
128
src/main.go
@ -1,136 +1,34 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"github.com/mroote/factorio-server-manager/api"
|
||||
"github.com/mroote/factorio-server-manager/bootstrap"
|
||||
"github.com/mroote/factorio-server-manager/factorio"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
FactorioDir string `json:"factorio_dir"`
|
||||
FactorioSavesDir string `json:"saves_dir"`
|
||||
FactorioModsDir string `json:"mods_dir"`
|
||||
FactorioModPackDir string `json:"mod_pack_dir"`
|
||||
FactorioConfigFile string `json:"config_file"`
|
||||
FactorioConfigDir string `json:"config_directory"`
|
||||
FactorioLog string `json:"logfile"`
|
||||
FactorioBinary string `json:"factorio_binary"`
|
||||
FactorioRconPort int `json:"rcon_port"`
|
||||
FactorioRconPass string `json:"rcon_pass"`
|
||||
FactorioCredentialsFile string `json:"factorio_credentials_file"`
|
||||
FactorioIP string `json:"factorio_ip"`
|
||||
FactorioAdminFile string `json:"-"`
|
||||
ServerIP string `json:"server_ip"`
|
||||
ServerPort string `json:"server_port"`
|
||||
MaxUploadSize int64 `json:"max_upload_size"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
DatabaseFile string `json:"database_file"`
|
||||
CookieEncryptionKey string `json:"cookie_encryption_key"`
|
||||
SettingsFile string `json:"settings_file"`
|
||||
LogFile string `json:"log_file"`
|
||||
ConfFile string
|
||||
glibcCustom string
|
||||
glibcLocation string
|
||||
glibcLibLoc string
|
||||
}
|
||||
|
||||
var (
|
||||
config Config
|
||||
FactorioServ *FactorioServer
|
||||
Auth *AuthHTTP
|
||||
)
|
||||
|
||||
func failOnError(err error, msg string) {
|
||||
if err != nil {
|
||||
log.Printf("%s: %s", msg, err)
|
||||
panic(fmt.Sprintf("%s: %s", msg, err))
|
||||
}
|
||||
}
|
||||
|
||||
// Loads server configuration files
|
||||
// JSON config file contains default values,
|
||||
// config file will overwrite any provided flags
|
||||
func loadServerConfig(f string) {
|
||||
file, err := os.Open(f)
|
||||
failOnError(err, "Error loading config file.")
|
||||
|
||||
decoder := json.NewDecoder(file)
|
||||
err = decoder.Decode(&config)
|
||||
failOnError(err, "Error decoding JSON config file.")
|
||||
|
||||
config.FactorioRconPort = randomPort()
|
||||
}
|
||||
|
||||
func parseFlags() {
|
||||
confFile := flag.String("conf", "./conf.json", "Specify location of Factorio Server Manager config file.")
|
||||
factorioDir := flag.String("dir", "./", "Specify location of Factorio directory.")
|
||||
serverIP := flag.String("host", "0.0.0.0", "Specify IP for webserver to listen on.")
|
||||
factorioIP := flag.String("game-bind-address", "0.0.0.0", "Specify IP for Fcatorio gamer server 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", 1024*1024*20, "Maximum filesize for uploaded files (default 20MB).")
|
||||
factorioBinary := flag.String("bin", "bin/x64/factorio", "Location of Factorio Server binary file")
|
||||
glibcCustom := flag.String("glibc-custom", "false", "By default false, if custom glibc is required set this to true and add glibc-loc and glibc-lib-loc parameters")
|
||||
glibcLocation := flag.String("glibc-loc", "/opt/glibc-2.18/lib/ld-2.18.so", "Location glibc ld.so file if needed (ex. /opt/glibc-2.18/lib/ld-2.18.so)")
|
||||
glibcLibLoc := flag.String("glibc-lib-loc", "/opt/glibc-2.18/lib", "Location of glibc lib folder (ex. /opt/glibc-2.18/lib)")
|
||||
|
||||
flag.Parse()
|
||||
config.glibcCustom = *glibcCustom
|
||||
config.glibcLocation = *glibcLocation
|
||||
config.glibcLibLoc = *glibcLibLoc
|
||||
config.ConfFile = *confFile
|
||||
config.FactorioDir = *factorioDir
|
||||
config.ServerIP = *serverIP
|
||||
config.FactorioIP = *factorioIP
|
||||
config.ServerPort = *factorioPort
|
||||
config.FactorioSavesDir = filepath.Join(config.FactorioDir, "saves")
|
||||
config.FactorioModsDir = filepath.Join(config.FactorioDir, "mods")
|
||||
config.FactorioModPackDir = "./mod_packs"
|
||||
config.FactorioConfigDir = filepath.Join(config.FactorioDir, "config")
|
||||
config.FactorioConfigFile = filepath.Join(config.FactorioDir, *factorioConfigFile)
|
||||
config.FactorioBinary = filepath.Join(config.FactorioDir, *factorioBinary)
|
||||
config.FactorioCredentialsFile = "./factorio.auth"
|
||||
config.FactorioAdminFile = "server-adminlist.json"
|
||||
config.MaxUploadSize = *factorioMaxUpload
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
appdata := os.Getenv("APPDATA")
|
||||
config.FactorioLog = filepath.Join(appdata, "Factorio", "factorio-current.log")
|
||||
} else {
|
||||
config.FactorioLog = filepath.Join(config.FactorioDir, "factorio-current.log")
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
var err error
|
||||
// get the all configs based on the flags
|
||||
config := bootstrap.NewConfig(os.Args)
|
||||
|
||||
// Parse configuration flags
|
||||
parseFlags()
|
||||
// Load server config from file
|
||||
loadServerConfig(config.ConfFile)
|
||||
// create mod-stuff
|
||||
modStartUp()
|
||||
// setup the required files for the mods
|
||||
factorio.ModStartUp()
|
||||
|
||||
// Initialize Factorio Server struct
|
||||
FactorioServ, err = initFactorio()
|
||||
err := factorio.NewFactorioServer()
|
||||
if err != nil {
|
||||
log.Printf("Error occurred during FactorioServer initializaion: %v\n", err)
|
||||
log.Printf("Error occurred during Server initializaion: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Initialize authentication system
|
||||
Auth = initAuth()
|
||||
Auth.CreateAuth(config.DatabaseFile, config.CookieEncryptionKey)
|
||||
Auth.CreateOrUpdateUser(config.Username, config.Password, "admin", "")
|
||||
api.GetAuth()
|
||||
|
||||
// Initialize HTTP router -- also initializes websocket
|
||||
router := api.NewRouter()
|
||||
|
||||
// Initialize HTTP router
|
||||
router := NewRouter()
|
||||
log.Printf("Starting server on: %s:%s", config.ServerIP, config.ServerPort)
|
||||
log.Fatal(http.ListenAndServe(config.ServerIP+":"+config.ServerPort, router))
|
||||
|
||||
|
@ -1,923 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/mroote/factorio-server-manager/lockfile"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
type ModPortalStruct struct {
|
||||
DownloadsCount int `json:"downloads_count"`
|
||||
Name string `json:"name"`
|
||||
Owner string `json:"owner"`
|
||||
Releases []struct {
|
||||
DownloadURL string `json:"download_url"`
|
||||
FileName string `json:"file_name"`
|
||||
InfoJSON struct {
|
||||
FactorioVersion string `json:"factorio_version"`
|
||||
} `json:"info_json"`
|
||||
ReleasedAt time.Time `json:"released_at"`
|
||||
Sha1 string `json:"sha1"`
|
||||
Version Version `json:"version"`
|
||||
} `json:"releases"`
|
||||
Summary string `json:"summary"`
|
||||
Title string `json:"title"`
|
||||
}
|
||||
|
||||
// Returns JSON response of all mods installed in factorio/mods
|
||||
func listInstalledModsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
resp := JSONResponse{
|
||||
Success: false,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
|
||||
|
||||
mods, err := newMods(config.FactorioModsDir)
|
||||
|
||||
if err != nil {
|
||||
w.WriteHeader(500)
|
||||
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
|
||||
}
|
||||
|
||||
resp.Data = mods.listInstalledMods().ModsResult
|
||||
resp.Success = true
|
||||
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
log.Printf("Error in list mods: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
// LoginFactorioModPortal returns JSON response with success or error-message
|
||||
func LoginFactorioModPortal(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
resp := JSONResponse{
|
||||
Success: false,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
|
||||
|
||||
username := r.FormValue("username")
|
||||
password := r.FormValue("password")
|
||||
|
||||
loginStatus, err, statusCode := factorioLogin(username, password)
|
||||
if loginStatus == "" && err == nil {
|
||||
resp.Data = true
|
||||
}
|
||||
|
||||
w.WriteHeader(statusCode)
|
||||
|
||||
if err != nil {
|
||||
resp.Data = fmt.Sprintf("Error trying to login into Factorio: %s", err)
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
log.Printf("Error in Factorio-Login: %s", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
resp.Success = true
|
||||
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
log.Printf("Error in Factorio-Login: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func LoginstatusFactorioModPortal(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
resp := JSONResponse{
|
||||
Success: false,
|
||||
}
|
||||
|
||||
var credentials FactorioCredentials
|
||||
resp.Data, err = credentials.load()
|
||||
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
resp.Data = fmt.Sprintf("Error getting the factorio credentials: %s", err)
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
log.Printf("Error in Factorio-Login: %s", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
resp.Success = true
|
||||
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
log.Printf("Error in Factorio-Login: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func LogoutFactorioModPortalHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
resp := JSONResponse{
|
||||
Success: false,
|
||||
}
|
||||
|
||||
var credentials FactorioCredentials
|
||||
err = credentials.del()
|
||||
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
resp.Data = fmt.Sprintf("Error on logging out of factorio: %s", err)
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
log.Printf("Error in Factorio-Login: %s", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
resp.Data = false
|
||||
resp.Success = true
|
||||
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
log.Printf("Error in Factorio-Login: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
//ModPortalSearchHandler returns JSON response with the found mods
|
||||
func ModPortalSearchHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
resp := JSONResponse{
|
||||
Success: false,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
|
||||
|
||||
//Get Data out of the request
|
||||
searchKeyword := r.FormValue("search")
|
||||
|
||||
var statusCode int
|
||||
resp.Data, err, statusCode = searchModPortal(searchKeyword)
|
||||
|
||||
w.WriteHeader(statusCode)
|
||||
|
||||
if err != nil {
|
||||
w.WriteHeader(500)
|
||||
resp.Data = fmt.Sprintf("Error in searchModPortal: %s", err)
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
log.Printf("Error in searchModPortal: %s", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
resp.Success = true
|
||||
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
log.Printf("Error in ModPortalSearch: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
//ModPortalDetailsHandler returns JSON response with the mod details
|
||||
func ModPortalDetailsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
resp := JSONResponse{
|
||||
Success: false,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
|
||||
|
||||
//Get Data out of the request
|
||||
modId := r.FormValue("modId")
|
||||
|
||||
var statusCode int
|
||||
resp.Data, err, statusCode = getModDetails(modId)
|
||||
|
||||
w.WriteHeader(statusCode)
|
||||
|
||||
if err != nil {
|
||||
resp.Data = fmt.Sprintf("Error in searchModPortal: %s", err)
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
log.Printf("Error in searchModPortal: %s", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
resp.Success = true
|
||||
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
log.Printf("Error in ModPortalSearch: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func ModPortalInstallHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
resp := JSONResponse{
|
||||
Success: false,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
|
||||
|
||||
//Get Data out of the request
|
||||
downloadUrl := r.FormValue("link")
|
||||
filename := r.FormValue("filename")
|
||||
modName := r.FormValue("modName")
|
||||
|
||||
mods, err := newMods(config.FactorioModsDir)
|
||||
if err == nil {
|
||||
err = mods.downloadMod(downloadUrl, filename, modName)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
resp.Data = fmt.Sprintf("Error in installMod: %s", err)
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
log.Printf("Error in installMod: %s", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
resp.Data = mods.listInstalledMods()
|
||||
resp.Success = true
|
||||
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
log.Printf("Error in ModPortalInstallHandler: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func ModPortalInstallMultipleHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
resp := JSONResponse{
|
||||
Success: false,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
|
||||
r.ParseForm()
|
||||
|
||||
var modsList []string
|
||||
var versionsList []Version
|
||||
|
||||
//Parse incoming data
|
||||
for key, values := range r.PostForm {
|
||||
if key == "mod_name" {
|
||||
for _, v := range values {
|
||||
modsList = append(modsList, v)
|
||||
}
|
||||
} else if key == "mod_version" {
|
||||
for _, value := range values {
|
||||
var v Version
|
||||
if err := v.UnmarshalText([]byte(value)); err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
resp.Data = fmt.Sprintf("Error in searchModPortal: %s", err)
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
log.Printf("Error in searchModPortal: %s", err)
|
||||
}
|
||||
}
|
||||
versionsList = append(versionsList, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mods, err := newMods(config.FactorioModsDir)
|
||||
if err != nil {
|
||||
log.Printf("error creating mods: %s", err)
|
||||
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
resp.Data = fmt.Sprintf("Error in searchModPortal: %s", err)
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
log.Printf("Error in searchModPortal: %s", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
for modIndex, mod := range modsList {
|
||||
var err error
|
||||
|
||||
//get details of mod
|
||||
modDetails, err, statusCode := getModDetails(mod)
|
||||
if err != nil {
|
||||
w.WriteHeader(statusCode)
|
||||
resp.Data = fmt.Sprintf("Error in searchModPortal: %s", err)
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
log.Printf("Error in searchModPortal: %s", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
modDetailsArray := []byte(modDetails)
|
||||
var modDetailsStruct ModPortalStruct
|
||||
|
||||
//read mod-data into Struct
|
||||
err = json.Unmarshal(modDetailsArray, &modDetailsStruct)
|
||||
if err != nil {
|
||||
log.Printf("error reading modPortalDetails: %s", err)
|
||||
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
resp.Data = fmt.Sprintf("Error in searchModPortal: %s", err)
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
log.Printf("Error in searchModPortal: %s", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
//find correct mod-version
|
||||
for _, release := range modDetailsStruct.Releases {
|
||||
if release.Version.Equals(versionsList[modIndex]) {
|
||||
mods.downloadMod(release.DownloadURL, release.FileName, modDetailsStruct.Name)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resp.Data = mods.listInstalledMods()
|
||||
|
||||
resp.Success = true
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
log.Printf("Error in ToggleModHandler: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func ToggleModHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
resp := JSONResponse{
|
||||
Success: false,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
|
||||
|
||||
//Get Data out of the request
|
||||
modName := r.FormValue("modName")
|
||||
|
||||
mods, err := newMods(config.FactorioModsDir)
|
||||
if err == nil {
|
||||
err, resp.Data = mods.ModSimpleList.toggleMod(modName)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
resp.Data = fmt.Sprintf("Error in listInstalledModsByFolder: %s", err)
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
log.Printf("Error in listInstalledModsByFolder: %s", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
resp.Success = true
|
||||
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
log.Printf("Error in ToggleModHandler: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func DeleteModHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
resp := JSONResponse{
|
||||
Success: false,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
|
||||
|
||||
//Get Data out of the request
|
||||
modName := r.FormValue("modName")
|
||||
|
||||
mods, err := newMods(config.FactorioModsDir)
|
||||
if err == nil {
|
||||
mods.deleteMod(modName)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
resp.Data = fmt.Sprintf("Error in deleteMod: %s", err)
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
log.Printf("Error in DeleteModHandler: %s", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
resp.Data = modName
|
||||
resp.Success = true
|
||||
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
log.Printf("Error in DeleteModHandler: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func DeleteAllModsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
resp := JSONResponse{
|
||||
Success: false,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
|
||||
|
||||
//delete mods folder
|
||||
err = deleteAllMods()
|
||||
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
resp.Data = fmt.Sprintf("Error in deleteMod: %s", err)
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
log.Printf("Error in DeleteModHandler: %s", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
resp.Data = nil
|
||||
resp.Success = true
|
||||
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
log.Printf("Error in DeleteModHandler: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func UpdateModHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
resp := JSONResponse{
|
||||
Success: false,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
|
||||
|
||||
//Get Data out of the request
|
||||
modName := r.FormValue("modName")
|
||||
downloadUrl := r.FormValue("downloadUrl")
|
||||
fileName := r.FormValue("filename")
|
||||
|
||||
log.Println("--------------------------------------------------------------")
|
||||
|
||||
mods, err := newMods(config.FactorioModsDir)
|
||||
if err == nil {
|
||||
err = mods.updateMod(modName, downloadUrl, fileName)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
resp.Data = fmt.Sprintf("Error in deleteMod: %s", err)
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
log.Printf("Error in DeleteModHandler: %s", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
installedMods := mods.listInstalledMods().ModsResult
|
||||
for _, mod := range installedMods {
|
||||
if mod.Name == modName {
|
||||
resp.Data = mod
|
||||
break
|
||||
}
|
||||
}
|
||||
resp.Success = true
|
||||
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
log.Printf("Error in DeleteModHandler: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func UploadModHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
resp := JSONResponseFileInput{
|
||||
Success: false,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
|
||||
|
||||
r.ParseMultipartForm(32 << 20)
|
||||
|
||||
mods, err := newMods(config.FactorioModsDir)
|
||||
if err == nil {
|
||||
for fileKey, modFile := range r.MultipartForm.File["mod_file"] {
|
||||
err = mods.uploadMod(modFile)
|
||||
if err != nil {
|
||||
resp.ErrorKeys = append(resp.ErrorKeys, fileKey)
|
||||
resp.Error = "An error occurred during upload or saving, pls check manually, if all went well and delete invalid files. (This program also could be crashed)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
w.WriteHeader(500)
|
||||
resp.Data = fmt.Sprintf("Error in uploadMod, listing mods wasn't successful: %s", err)
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
log.Printf("Error in uploadMod, listing mods wasn't successful: %s", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
resp.Data = mods.listInstalledMods()
|
||||
resp.Success = true
|
||||
|
||||
if err = json.NewEncoder(w).Encode(resp); err != nil {
|
||||
log.Printf("Error in UploadModHandler: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func DownloadModsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
|
||||
zipWriter := zip.NewWriter(w)
|
||||
defer zipWriter.Close()
|
||||
|
||||
//iterate over folder and create everything in the zip
|
||||
err = filepath.Walk(config.FactorioModsDir, func(path string, info os.FileInfo, err error) error {
|
||||
if info.IsDir() == false {
|
||||
//Lock the file, that we are want to read
|
||||
err := fileLock.RLock(path)
|
||||
if err != nil {
|
||||
log.Printf("error locking file for reading, something else has locked it")
|
||||
return err
|
||||
}
|
||||
defer fileLock.RUnlock(path)
|
||||
|
||||
writer, err := zipWriter.Create(info.Name())
|
||||
if err != nil {
|
||||
log.Printf("error on creating new file inside zip: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
log.Printf("error on opening modfile: %s", err)
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
_, err = io.Copy(writer, file)
|
||||
if err != nil {
|
||||
log.Printf("error on copying file into zip: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
err = file.Close()
|
||||
if err != nil {
|
||||
log.Printf("error closing file: %s", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err == lockfile.ErrorAlreadyLocked {
|
||||
w.WriteHeader(http.StatusLocked)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
log.Printf("error on walking over the mods: %s", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
writerHeader := w.Header()
|
||||
writerHeader.Set("Content-Type", "application/zip;charset=UTF-8")
|
||||
writerHeader.Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", "all_installed_mods.zip"))
|
||||
}
|
||||
|
||||
//LoadModsFromSaveHandler returns JSON response with the found mods
|
||||
func LoadModsFromSaveHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
resp := JSONResponse{
|
||||
Success: false,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
|
||||
|
||||
//Get Data out of the request
|
||||
SaveFile := r.FormValue("saveFile")
|
||||
|
||||
path := filepath.Join(config.FactorioSavesDir, SaveFile)
|
||||
f, err := OpenArchiveFile(path, "level.dat")
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
log.Printf("cannot open save level file: %v", err)
|
||||
resp.Data = "Error opening save file"
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
log.Printf("Error in loadModsFromSave: %s", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
var header SaveHeader
|
||||
err = header.ReadFrom(f)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
log.Printf("cannot read save header: %v", err)
|
||||
resp.Data = "Error reading save file"
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
log.Printf("Error in loadModsFromSave: %s", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
resp.Data = header
|
||||
resp.Success = true
|
||||
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
log.Printf("Error in LoadModsFromSave: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func ListModPacksHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
resp := JSONResponse{
|
||||
Success: false,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
|
||||
|
||||
modPackMap, err := newModPackMap()
|
||||
|
||||
if err != nil {
|
||||
w.WriteHeader(500)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
resp.Data = fmt.Sprintf("Error listing modpack files: %s", err)
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
log.Printf("Error listing modpacks: %s", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
resp.Data = modPackMap.listInstalledModPacks()
|
||||
resp.Success = true
|
||||
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
log.Printf("Error listing saves: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func CreateModPackHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
resp := JSONResponse{
|
||||
Success: false,
|
||||
}
|
||||
|
||||
name := r.FormValue("name")
|
||||
|
||||
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
|
||||
|
||||
modPackMap, err := newModPackMap()
|
||||
if err == nil {
|
||||
err = modPackMap.createModPack(name)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
resp.Data = fmt.Sprintf("Error creating modpack file: %s", err)
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
log.Printf("Error creating modpack: %s", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
resp.Data = modPackMap.listInstalledModPacks()
|
||||
resp.Success = true
|
||||
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
log.Printf("Error creating modpack response: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func DownloadModPackHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
|
||||
vars := mux.Vars(r)
|
||||
modpack := vars["modpack"]
|
||||
|
||||
modPackMap, err := newModPackMap()
|
||||
if err != nil {
|
||||
log.Printf("error on loading modPacks: %s", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if modPackMap.checkModPackExists(modpack) {
|
||||
zipWriter := zip.NewWriter(w)
|
||||
defer zipWriter.Close()
|
||||
|
||||
//iterate over folder and create everything in the zip
|
||||
err = filepath.Walk(filepath.Join(config.FactorioModPackDir, modpack), func(path string, info os.FileInfo, err error) error {
|
||||
if info.IsDir() == false {
|
||||
writer, err := zipWriter.Create(info.Name())
|
||||
if err != nil {
|
||||
log.Printf("error on creating new file inside zip: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
log.Printf("error on opening modfile: %s", err)
|
||||
return err
|
||||
}
|
||||
// Close file, when function returns
|
||||
defer func() {
|
||||
err2 := file.Close()
|
||||
if err == nil && err2 != nil {
|
||||
log.Printf("Error closing file: %s", err2)
|
||||
err = err2
|
||||
}
|
||||
}()
|
||||
|
||||
_, err = io.Copy(writer, file)
|
||||
if err != nil {
|
||||
log.Printf("error on copying file into zip: %s", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("error on walking over the modpack: %s", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
log.Printf("requested modPack doesnt exist")
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
writerHeader := w.Header()
|
||||
writerHeader.Set("Content-Type", "application/zip;charset=UTF-8")
|
||||
writerHeader.Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", modpack+".zip"))
|
||||
}
|
||||
|
||||
func DeleteModPackHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
resp := JSONResponse{
|
||||
Success: false,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
|
||||
|
||||
name := r.FormValue("name")
|
||||
|
||||
modPackMap, err := newModPackMap()
|
||||
if err == nil {
|
||||
err = modPackMap.deleteModPack(name)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
resp.Data = fmt.Sprintf("Error deleting modpack file: %s", err)
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
log.Printf("Error deleting modpack: %s", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
resp.Data = name
|
||||
resp.Success = true
|
||||
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
log.Printf("Error creating delete modpack response: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func LoadModPackHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
resp := JSONResponse{
|
||||
Success: false,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
|
||||
|
||||
name := r.FormValue("name")
|
||||
|
||||
modPackMap, err := newModPackMap()
|
||||
if err == nil {
|
||||
modPackMap[name].loadModPack()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
resp.Data = fmt.Sprintf("Error loading modpack file: %s", err)
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
log.Printf("Error loading modpack: %s", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
resp.Data = modPackMap[name].Mods.listInstalledMods()
|
||||
resp.Success = true
|
||||
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
log.Printf("Error creating loading modpack response: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func ModPackToggleModHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
resp := JSONResponse{
|
||||
Success: false,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
|
||||
|
||||
modName := r.FormValue("modName")
|
||||
modPackName := r.FormValue("modPack")
|
||||
|
||||
modPackMap, err := newModPackMap()
|
||||
if err == nil {
|
||||
err, resp.Data = modPackMap[modPackName].Mods.ModSimpleList.toggleMod(modName)
|
||||
}
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
resp.Data = fmt.Sprintf("Error loading modpack file: %s", err)
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
log.Printf("Error loading modpack: %s", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
resp.Success = true
|
||||
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
log.Printf("Error creating loading modpack response: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func ModPackDeleteModHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
resp := JSONResponse{
|
||||
Success: false,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
|
||||
|
||||
modName := r.FormValue("modName")
|
||||
modPackName := r.FormValue("modPackName")
|
||||
|
||||
modPackMap, err := newModPackMap()
|
||||
if err == nil {
|
||||
if modPackMap.checkModPackExists(modPackName) {
|
||||
err = modPackMap[modPackName].Mods.deleteMod(modName)
|
||||
} else {
|
||||
err = errors.New("ModPack " + modPackName + " does not exist")
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
resp.Data = fmt.Sprintf("Error loading modpack file: %s", err)
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
log.Printf("Error loading modpack: %s", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
resp.Data = true
|
||||
resp.Success = true
|
||||
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
log.Printf("Error creating loading modpack response: %s", err)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func ModPackUpdateModHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
resp := JSONResponse{
|
||||
Success: false,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
|
||||
|
||||
//Get Data out of the request
|
||||
modName := r.FormValue("modName")
|
||||
downloadUrl := r.FormValue("downloadUrl")
|
||||
fileName := r.FormValue("filename")
|
||||
modPackName := r.FormValue("modPackName")
|
||||
|
||||
modPackMap, err := newModPackMap()
|
||||
if err == nil {
|
||||
if modPackMap.checkModPackExists(modPackName) {
|
||||
err = modPackMap[modPackName].Mods.updateMod(modName, downloadUrl, fileName)
|
||||
} else {
|
||||
err = errors.New("ModPack " + modPackName + "does not exist")
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
resp.Data = fmt.Sprintf("Error in deleteMod: %s", err)
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
log.Printf("Error in DeleteModHandler: %s", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
installedMods := modPackMap[modPackName].Mods.listInstalledMods().ModsResult
|
||||
for _, mod := range installedMods {
|
||||
if mod.Name == modName {
|
||||
resp.Data = mod
|
||||
break
|
||||
}
|
||||
}
|
||||
resp.Success = true
|
||||
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
log.Printf("Error in DeleteModHandler: %s", err)
|
||||
}
|
||||
}
|
@ -1,58 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
type Message struct {
|
||||
Name string `json:"name"`
|
||||
Data interface{} `json:"data"`
|
||||
}
|
||||
|
||||
type FindHandler func(string) (Handler, bool)
|
||||
|
||||
type Client struct {
|
||||
send chan Message
|
||||
socket *websocket.Conn
|
||||
findHandler FindHandler
|
||||
stopChannels map[int]chan bool
|
||||
id string
|
||||
}
|
||||
|
||||
func (client *Client) Read() {
|
||||
var message Message
|
||||
for {
|
||||
if err := client.socket.ReadJSON(&message); err != nil {
|
||||
break
|
||||
}
|
||||
if handler, found := client.findHandler(message.Name); found {
|
||||
handler(client, message.Data)
|
||||
}
|
||||
}
|
||||
client.socket.Close()
|
||||
}
|
||||
|
||||
func (client *Client) Write() {
|
||||
for msg := range client.send {
|
||||
if err := client.socket.WriteJSON(msg); err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
client.socket.Close()
|
||||
}
|
||||
|
||||
func (client *Client) Close() {
|
||||
for _, ch := range client.stopChannels {
|
||||
ch <- true
|
||||
}
|
||||
close(client.send)
|
||||
}
|
||||
|
||||
func NewClient(socket *websocket.Conn, findHandler FindHandler) *Client {
|
||||
return &Client{
|
||||
send: make(chan Message),
|
||||
socket: socket,
|
||||
findHandler: findHandler,
|
||||
stopChannels: make(map[int]chan bool),
|
||||
}
|
||||
}
|
@ -1,41 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/hpcloud/tail"
|
||||
)
|
||||
|
||||
func logSubscribe(client *Client, data interface{}) {
|
||||
go func() {
|
||||
logfile := filepath.Join(config.FactorioDir, "factorio-server-console.log")
|
||||
t, err := tail.TailFile(logfile, tail.Config{Follow: true})
|
||||
if err != nil {
|
||||
log.Printf("Error subscribing to tail log %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
for line := range t.Lines {
|
||||
client.send <- Message{"log update", line.Text}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func commandSend(client *Client, data interface{}) {
|
||||
if FactorioServ.Running {
|
||||
go func() {
|
||||
log.Printf("Received command: %v", data)
|
||||
|
||||
reqId, err := FactorioServ.Rcon.Write(data.(string))
|
||||
if err != nil {
|
||||
log.Printf("Error sending rcon command: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("Command send to Factorio: %s, with rcon request id: %v", data, reqId)
|
||||
|
||||
client.send <- Message{"receive command", data}
|
||||
}()
|
||||
}
|
||||
}
|
59
tailwind.config.js
Normal file
59
tailwind.config.js
Normal file
@ -0,0 +1,59 @@
|
||||
module.exports = {
|
||||
future: {
|
||||
removeDeprecatedGapUtilities: true,
|
||||
purgeLayersByDefault: true,
|
||||
},
|
||||
purge: {
|
||||
mode: 'all',
|
||||
content: [
|
||||
'./ui/**/*.jsx',
|
||||
'./ui/**/*.html',
|
||||
]
|
||||
},
|
||||
theme: {
|
||||
extend: {
|
||||
width: {
|
||||
72: "18rem",
|
||||
80: "20rem",
|
||||
88: "22rem",
|
||||
96: "24rem",
|
||||
},
|
||||
margin: {
|
||||
72: "18rem",
|
||||
80: "20rem",
|
||||
88: "22rem",
|
||||
96: "24rem",
|
||||
}
|
||||
},
|
||||
colors: {
|
||||
"gray": {
|
||||
dark: "#313030",
|
||||
medium: "#403F40",
|
||||
light: "#8E8E8E"
|
||||
},
|
||||
"white": "#F9F9F9",
|
||||
"dirty-white": "#ffe6c0",
|
||||
"green": "#5EB663",
|
||||
"green-light": "#92e897",
|
||||
"blue": "#5C8FFF",
|
||||
"blue-light": "#709DFF",
|
||||
"red": "#FE5A5A",
|
||||
"red-light": "#FF9B9B",
|
||||
"orange": "#E39827",
|
||||
"black": "#1C1C1C"
|
||||
},
|
||||
boxShadow: {
|
||||
default: '0 1px 3px 0 rgba(0, 0, 0, .1), 0 1px 2px 0 rgba(0, 0, 0, .06)',
|
||||
md: '0 4px 6px -1px rgba(0, 0, 0, .1), 0 2px 4px -1px rgba(0, 0, 0, .06)',
|
||||
lg: '0 10px 15px -3px rgba(0, 0, 0, .1), 0 4px 6px -2px rgba(0, 0, 0, .05)',
|
||||
xl: '0 20px 25px -5px rgba(0, 0, 0, .1), 0 10px 10px -5px rgba(0, 0, 0, .04)',
|
||||
"2xl": '0 25px 50px -12px rgba(0, 0, 0, .25)',
|
||||
"3xl": '0 35px 60px -15px rgba(0, 0, 0, .3)',
|
||||
inner: 'inset 0 4px 8px 0 rgba(0, 0, 0, 0.9)',
|
||||
outline: '0 0 0 3px rgba(66, 153, 225, 0.5)',
|
||||
'none': 'none',
|
||||
}
|
||||
},
|
||||
variants: {},
|
||||
plugins: [],
|
||||
}
|
259
ui/App/App.jsx
259
ui/App/App.jsx
@ -1,196 +1,87 @@
|
||||
import React from 'react';
|
||||
import {Switch, Route, withRouter} from 'react-router-dom';
|
||||
import Header from './components/Header.jsx';
|
||||
import Sidebar from './components/Sidebar.jsx';
|
||||
import Footer from './components/Footer.jsx';
|
||||
import Socket from '../socket.js';
|
||||
import Index from "./components/Index";
|
||||
import UsersContent from "./components/UsersContent";
|
||||
import ModsContent from "./components/ModsContent";
|
||||
import LogsContent from "./components/LogsContent";
|
||||
import SavesContent from "./components/SavesContent";
|
||||
import ConfigContent from "./components/ConfigContent";
|
||||
import ConsoleContent from "./components/ConsoleContent";
|
||||
import React, {useCallback, useState} from 'react';
|
||||
|
||||
class App extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.checkLogin = this.checkLogin.bind(this);
|
||||
this.flashMessage = this.flashMessage.bind(this);
|
||||
this.facServStatus = this.facServStatus.bind(this);
|
||||
this.getSaves = this.getSaves.bind(this);
|
||||
this.getStatus = this.getStatus.bind(this);
|
||||
this.connectWebSocket = this.connectWebSocket.bind(this);
|
||||
this.getFactorioVersion = this.getFactorioVersion.bind(this);
|
||||
import user from "../api/resources/user";
|
||||
import Login from "./views/Login";
|
||||
import {Redirect, Route, Switch, useHistory} from "react-router";
|
||||
import Controls from "./views/Controls";
|
||||
import {BrowserRouter} from "react-router-dom";
|
||||
import Logs from "./views/Logs";
|
||||
import Saves from "./views/Saves/Saves";
|
||||
import Layout from "./components/Layout";
|
||||
import server from "../api/resources/server";
|
||||
import Mods from "./views/Mods/Mods";
|
||||
import UserManagement from "./views/UserManagement/UserManagment";
|
||||
import ServerSettings from "./views/ServerSettings";
|
||||
import GameSettings from "./views/GameSettings";
|
||||
import Console from "./views/Console";
|
||||
import Help from "./views/Help";
|
||||
import socket from "../api/socket";
|
||||
import {Flash} from "./components/Flash";
|
||||
|
||||
this.state = {
|
||||
serverRunning: "stopped",
|
||||
serverStatus: {},
|
||||
factorioVersion: "",
|
||||
saves: [],
|
||||
loggedIn: false,
|
||||
username: "",
|
||||
messages: [],
|
||||
showMessage: false,
|
||||
|
||||
const App = () => {
|
||||
|
||||
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
||||
const [serverStatus, setServerStatus] = useState(null);
|
||||
const history = useHistory();
|
||||
|
||||
const updateServerStatus = async () => {
|
||||
const status = await server.status();
|
||||
if (status) {
|
||||
setServerStatus(status)
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.checkLogin();
|
||||
}
|
||||
const handleAuthenticationStatus = useCallback(async () => {
|
||||
const status = await user.status();
|
||||
if (status?.Username) {
|
||||
setIsAuthenticated(true);
|
||||
await updateServerStatus();
|
||||
|
||||
connectWebSocket() {
|
||||
let ws_scheme = window.location.protocol == "https:" ? "wss" : "ws";
|
||||
let ws = new WebSocket(ws_scheme + "://" + window.location.host + "/ws");
|
||||
this.socket = new Socket(ws);
|
||||
}
|
||||
|
||||
flashMessage(message) {
|
||||
var m = this.state.messages;
|
||||
m.push(message);
|
||||
this.setState({messages: m, showMessage: true});
|
||||
}
|
||||
|
||||
checkLogin() {
|
||||
$.ajax({
|
||||
url: "/api/user/status",
|
||||
type: "GET",
|
||||
dataType: "json",
|
||||
success: (data) => {
|
||||
if (data.success === true) {
|
||||
this.setState({
|
||||
loggedIn: true,
|
||||
username: data.data.Username
|
||||
});
|
||||
|
||||
this.connectWebSocket();
|
||||
this.getFactorioVersion(); //Init serverStatus, so i know, which factorio-version is installed
|
||||
} else {
|
||||
this.props.history.push("/login");
|
||||
}
|
||||
},
|
||||
error: () => {
|
||||
this.props.history.push("/login");
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
facServStatus() {
|
||||
$.ajax({
|
||||
url: "/api/server/status",
|
||||
dataType: "json",
|
||||
success: (data) => {
|
||||
this.setState({
|
||||
serverRunning: data.data.status
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
getSaves() {
|
||||
$.ajax({
|
||||
url: "/api/saves/list",
|
||||
dataType: "json",
|
||||
success: (data) => {
|
||||
if (data.success === true) {
|
||||
this.setState({saves: data.data})
|
||||
} else {
|
||||
this.setState({saves: []})
|
||||
}
|
||||
},
|
||||
error: (xhr, status, err) => {
|
||||
console.log('api/saves/list', status, err.toString());
|
||||
}
|
||||
});
|
||||
|
||||
if (!this.state.saves) {
|
||||
this.setState({saves:[]});
|
||||
socket.emit('server status subscribe');
|
||||
socket.on('server_status', updateServerStatus)
|
||||
}
|
||||
}
|
||||
},[]);
|
||||
|
||||
getStatus() {
|
||||
$.ajax({
|
||||
url: "/api/server/status",
|
||||
dataType: "json",
|
||||
success: (data) => {
|
||||
this.setState({
|
||||
serverStatus: data.data
|
||||
})
|
||||
},
|
||||
error: (xhr, status, err) => {
|
||||
console.log('api/server/status', status, err.toString());
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
getFactorioVersion() {
|
||||
$.ajax({
|
||||
url: "/api/server/facVersion",
|
||||
// dataType: "json",
|
||||
success: (data) => {
|
||||
this.setState({
|
||||
factorioVersion: data.data.base_mod_version
|
||||
});
|
||||
},
|
||||
error: (xhr, status, err) => {
|
||||
console.log('api/server/status', status, err.toString());
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
// render main application,
|
||||
// if logged in show application
|
||||
// if not logged in show Not logged in message
|
||||
let appProps = {
|
||||
message: "",
|
||||
messages: this.state.messages,
|
||||
flashMessage: this.flashMessage,
|
||||
facServStatus: this.facServStatus,
|
||||
serverStatus: this.state.serverStatus,
|
||||
factorioVersion: this.state.factorioVersion,
|
||||
getStatus: this.getStatus,
|
||||
saves: this.state.saves,
|
||||
getSaves: this.getSaves,
|
||||
username: this.state.username,
|
||||
socket: this.socket
|
||||
};
|
||||
|
||||
let resp;
|
||||
if (this.state.loggedIn) {
|
||||
resp =
|
||||
<div className="wrapper">
|
||||
<Header
|
||||
username={this.state.username}
|
||||
loggedIn={this.state.loggedIn}
|
||||
messages={this.state.messages}
|
||||
/>
|
||||
|
||||
<Sidebar
|
||||
serverStatus={this.facServStatus}
|
||||
serverRunning={this.state.serverRunning}
|
||||
/>
|
||||
|
||||
{/*Render react-router components and pass in props*/}
|
||||
<Switch>
|
||||
<Route path="/server" render={(props) => {return <Index {...props} {...appProps}/>}}/>
|
||||
<Route path="/settings" render={(props) => {return <UsersContent {...props} {...appProps}/>}}/>
|
||||
<Route path="/mods" render={(props) => {return <ModsContent {...props} {...appProps}/>}}/>
|
||||
<Route path="/logs" render={(props) => {return <LogsContent {...props} {...appProps}/>}}/>
|
||||
<Route path="/saves" render={(props) => {return <SavesContent {...props} {...appProps}/>}}/>
|
||||
<Route path="/config" render={(props) => {return <ConfigContent {...props} {...appProps}/>}}/>
|
||||
<Route path="/console" render={(props) => {return <ConsoleContent {...props} {...appProps}/>}}/>
|
||||
<Route exact path="/" render={(props) => {return <Index {...props} {...appProps} />}}/>
|
||||
</Switch>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
} else {
|
||||
resp = <div><p>Not Logged in</p></div>;
|
||||
const handleLogout = useCallback(async () => {
|
||||
const loggedOut = await user.logout();
|
||||
if (loggedOut) {
|
||||
setIsAuthenticated(false);
|
||||
history.push('/login');
|
||||
}
|
||||
}, []);
|
||||
|
||||
return resp;
|
||||
}
|
||||
const ProtectedRoute = useCallback(({component: Component, ...rest}) => (
|
||||
<Route {...rest} render={(props) => (
|
||||
isAuthenticated && Component
|
||||
? <Component serverStatus={serverStatus} updateServerStatus={updateServerStatus} {...props} />
|
||||
: <Redirect to={{
|
||||
pathname: '/login',
|
||||
state: {from: props.location}
|
||||
}}/>
|
||||
)}/>
|
||||
), [isAuthenticated, serverStatus]);
|
||||
|
||||
return (
|
||||
<BrowserRouter basename="/">
|
||||
<Switch>
|
||||
<Route path="/login" render={() => (<Login handleLogin={handleAuthenticationStatus}/>)}/>
|
||||
|
||||
<Layout handleLogout={handleLogout} serverStatus={serverStatus} updateServerStatus={updateServerStatus}>
|
||||
<ProtectedRoute exact path="/" component={Controls}/>
|
||||
<ProtectedRoute path="/saves" component={Saves}/>
|
||||
<ProtectedRoute path="/mods" component={Mods}/>
|
||||
<ProtectedRoute path="/server-settings" component={ServerSettings}/>
|
||||
<ProtectedRoute path="/game-settings" component={GameSettings}/>
|
||||
<ProtectedRoute path="/console" component={Console}/>
|
||||
<ProtectedRoute path="/logs" component={Logs}/>
|
||||
<ProtectedRoute path="/user-management" component={UserManagement}/>
|
||||
<ProtectedRoute path="/help" component={Help}/>
|
||||
<Flash/>
|
||||
</Layout>
|
||||
</Switch>
|
||||
</BrowserRouter>
|
||||
);
|
||||
}
|
||||
|
||||
export default withRouter(App);
|
||||
export default App;
|
||||
|
37
ui/App/components/Button.jsx
Normal file
37
ui/App/components/Button.jsx
Normal file
@ -0,0 +1,37 @@
|
||||
import React from "react";
|
||||
import {faSpinner} from "@fortawesome/free-solid-svg-icons";
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||
|
||||
const Button = ({ children, type, onClick, isSubmit, className, size, isLoading, isDisabled = false }) => {
|
||||
|
||||
let color = '';
|
||||
let padding = '';
|
||||
|
||||
switch (type) {
|
||||
case 'success':
|
||||
color = `bg-green ${isDisabled || isLoading ? null : "hover:glow-green hover:bg-green-light" }`;
|
||||
break;
|
||||
case 'danger':
|
||||
color = `bg-red ${isDisabled || isLoading ? null : "hover:glow-red hover:bg-red-light"}`;
|
||||
break;
|
||||
default:
|
||||
color = `bg-gray-light ${isDisabled || isLoading ? null : "hover:glow-orange hover:bg-orange"}`
|
||||
}
|
||||
|
||||
switch (size) {
|
||||
case 'sm':
|
||||
padding = 'py-1 px-2';
|
||||
break;
|
||||
default:
|
||||
padding = 'py-2 px-4'
|
||||
}
|
||||
|
||||
return (
|
||||
<button onClick={onClick} disabled={isDisabled || isLoading} className={`${className ? className: null} ${isDisabled || isLoading ? "bg-opacity-50 cursor-not-allowed" : null} ${padding} ${color} inline-block accentuated text-black font-bold`}
|
||||
type={isSubmit ? 'submit' : 'button'}>
|
||||
{children} { isLoading && <FontAwesomeIcon icon={faSpinner} spin={true}/>}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
export default Button;
|
37
ui/App/components/ButtonLink.jsx
Normal file
37
ui/App/components/ButtonLink.jsx
Normal file
@ -0,0 +1,37 @@
|
||||
import React from "react";
|
||||
|
||||
const ButtonLink = ({children, href, type, target, className, size}) => {
|
||||
|
||||
let color = '';
|
||||
let padding = '';
|
||||
|
||||
switch (type) {
|
||||
case 'success':
|
||||
color = 'bg-green hover:glow-green hover:bg-green-light';
|
||||
break;
|
||||
case 'danger':
|
||||
color = 'bg-red hover:glow-red hover:bg-red-light';
|
||||
break;
|
||||
default:
|
||||
color = 'bg-gray-light hover:glow-orange hover:bg-orange'
|
||||
}
|
||||
|
||||
switch (size) {
|
||||
case 'sm':
|
||||
padding = 'py-1 px-2';
|
||||
break;
|
||||
default:
|
||||
padding = 'py-2 px-4'
|
||||
}
|
||||
|
||||
return (
|
||||
<a
|
||||
href={href}
|
||||
target={target ? target : '_self'}
|
||||
className={`${className ? className : null} ${color} ${padding} inline-block accentuated text-black font-bold`}>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
export default ButtonLink;
|
17
ui/App/components/Checkbox.jsx
Normal file
17
ui/App/components/Checkbox.jsx
Normal file
@ -0,0 +1,17 @@
|
||||
import React from "react";
|
||||
|
||||
const Checkbox = ({name, text, inputRef, checked}) => {
|
||||
return (
|
||||
<label className="block text-gray-500 font-bold">
|
||||
<input
|
||||
className="mr-2 leading-tight"
|
||||
ref={inputRef}
|
||||
name={name}
|
||||
id={name}
|
||||
type="checkbox" defaultChecked={checked}/>
|
||||
<span className="text-sm">{text}</span>
|
||||
</label>
|
||||
)
|
||||
}
|
||||
|
||||
export default Checkbox;
|
@ -1,31 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
class Settings extends React.Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
}
|
||||
|
||||
render() {
|
||||
return(
|
||||
<tbody>
|
||||
{Object.keys(this.props.config).map(function(key) {
|
||||
return(
|
||||
<tr key={key}>
|
||||
<td>{key}</td>
|
||||
<td>{this.props.config[key]}</td>
|
||||
</tr>
|
||||
)
|
||||
}, this)}
|
||||
</tbody>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Settings.propTypes = {
|
||||
section: PropTypes.string.isRequired,
|
||||
config: PropTypes.object.isRequired,
|
||||
}
|
||||
|
||||
export default Settings
|
@ -1,362 +0,0 @@
|
||||
import React from 'react';
|
||||
import {Link} from 'react-router-dom';
|
||||
import Settings from './Config/Settings.jsx';
|
||||
import FontAwesomeIcon from "./FontAwesomeIcon";
|
||||
|
||||
//https://stackoverflow.com/a/1414175
|
||||
function stringToBoolean(string) {
|
||||
switch(string.toLowerCase().trim()) {
|
||||
case "true":
|
||||
case "yes":
|
||||
case "1":
|
||||
return true;
|
||||
case "false":
|
||||
case "no":
|
||||
case "0":
|
||||
case null:
|
||||
return false;
|
||||
default:
|
||||
return Boolean(string);
|
||||
}
|
||||
}
|
||||
|
||||
class ConfigContent extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.getConfig = this.getConfig.bind(this);
|
||||
this.getServerSettings = this.getServerSettings.bind(this);
|
||||
this.updateServerSettings = this.updateServerSettings.bind(this);
|
||||
this.handleServerSettingsChange = this.handleServerSettingsChange.bind(this);
|
||||
this.formTypeField = this.formTypeField.bind(this);
|
||||
this.capitalizeFirstLetter = this.capitalizeFirstLetter.bind(this)
|
||||
this.state = {
|
||||
config: {},
|
||||
serverSettings: {}
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.getConfig();
|
||||
this.getServerSettings();
|
||||
}
|
||||
|
||||
capitalizeFirstLetter(string) {
|
||||
return string.charAt(0).toUpperCase() + string.slice(1);
|
||||
}
|
||||
|
||||
handleServerSettingsChange(name, e) {
|
||||
let fieldValue
|
||||
var change = this.state.serverSettings;
|
||||
|
||||
// if true, false and something else can be used, dont switch to boolean type!
|
||||
if(e.target.id == "allow_commands") {
|
||||
fieldValue = e.target.value;
|
||||
} else if(e.target.value === "true" || e.target.value === "false") {
|
||||
// Ensure Boolean type is used if required
|
||||
if(e.target.id === "lan" || e.target.id === "public") {
|
||||
if(e.target.value == "true") {
|
||||
fieldValue = true
|
||||
} else {
|
||||
fieldValue = false
|
||||
}
|
||||
change["visibility"][e.target.id] = fieldValue
|
||||
this.setState({serverSettings: change});
|
||||
return;
|
||||
}
|
||||
fieldValue = stringToBoolean(e.target.value)
|
||||
} else if(e.target.id === "admins" || e.target.id === "tags") {
|
||||
// Split settings values that are stored as arrays
|
||||
fieldValue = e.target.value.split(",")
|
||||
} else {
|
||||
fieldValue = e.target.value
|
||||
}
|
||||
change[name] = fieldValue;
|
||||
|
||||
this.setState({serverSettings: change});
|
||||
}
|
||||
|
||||
getConfig() {
|
||||
$.ajax({
|
||||
url: "/api/config",
|
||||
dataType: "json",
|
||||
success: (resp) => {
|
||||
if(resp.success === true) {
|
||||
this.setState({config: resp.data})
|
||||
}
|
||||
},
|
||||
error: (xhr, status, err) => {
|
||||
console.log('/api/config/get', status, err.toString());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getServerSettings() {
|
||||
$.ajax({
|
||||
url: "/api/settings",
|
||||
dataType: "json",
|
||||
success: (resp) => {
|
||||
if(resp.success === true) {
|
||||
this.setState({serverSettings: resp.data})
|
||||
console.log(this.state)
|
||||
}
|
||||
},
|
||||
error: (xhr, status, err) => {
|
||||
console.log('/api/settings/get', status, err.toString());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
updateServerSettings(e) {
|
||||
e.preventDefault();
|
||||
var serverSettingsJSON = JSON.stringify(this.state.serverSettings)
|
||||
$.ajax({
|
||||
url: "/api/settings/update",
|
||||
datatype: "json",
|
||||
type: "POST",
|
||||
data: serverSettingsJSON,
|
||||
success: (data) => {
|
||||
console.log(data);
|
||||
if(data.success === true) {
|
||||
console.log("settings updated")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
formTypeField(key, setting) {
|
||||
if(key.startsWith("_comment_")) {
|
||||
return (
|
||||
<input
|
||||
key={key}
|
||||
ref={key}
|
||||
id={key}
|
||||
defaultValue={setting}
|
||||
type="hidden"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
switch(typeof setting) {
|
||||
case "number":
|
||||
return (
|
||||
<input
|
||||
ref={key}
|
||||
id={key}
|
||||
className="form-control"
|
||||
defaultValue={setting}
|
||||
type="number"
|
||||
onChange={this.handleServerSettingsChange.bind(this, key)}
|
||||
/>
|
||||
)
|
||||
case "string":
|
||||
if(key.includes("password")) {
|
||||
return (
|
||||
<input
|
||||
ref={key}
|
||||
id={key}
|
||||
className="form-control"
|
||||
defaultValue={setting}
|
||||
type="password"
|
||||
onChange={this.handleServerSettingsChange.bind(this, key)}
|
||||
/>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<input
|
||||
ref={key}
|
||||
id={key}
|
||||
className="form-control"
|
||||
defaultValue={setting}
|
||||
type="text"
|
||||
onChange={this.handleServerSettingsChange.bind(this, key)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
case "boolean":
|
||||
return (
|
||||
<select
|
||||
ref={key}
|
||||
id={key}
|
||||
className="form-control"
|
||||
onChange={this.handleServerSettingsChange.bind(this, key)}
|
||||
>
|
||||
<option value={true}>True</option>
|
||||
<option value={false}>False</option>
|
||||
</select>
|
||||
)
|
||||
case "object":
|
||||
if(Array.isArray(setting)) {
|
||||
return (
|
||||
<input
|
||||
ref={key}
|
||||
id={key}
|
||||
className="form-control"
|
||||
defaultValue={setting}
|
||||
type="text"
|
||||
onChange={this.handleServerSettingsChange.bind(this, key)}
|
||||
/>
|
||||
)
|
||||
} else {
|
||||
if(key.includes("visibility")) {
|
||||
let vis_fields = []
|
||||
for(const key in setting) {
|
||||
const field =
|
||||
<div key={key}>
|
||||
<p>{key}</p>
|
||||
<select
|
||||
label={key}
|
||||
ref={key}
|
||||
id={key}
|
||||
className="form-control"
|
||||
onChange={this.handleServerSettingsChange.bind(this, key)}
|
||||
value={setting[key]}
|
||||
>
|
||||
<option value={true}>True</option>
|
||||
<option value={false}>False</option>
|
||||
</select>
|
||||
</div>
|
||||
vis_fields.push(field)
|
||||
}
|
||||
|
||||
return vis_fields
|
||||
}
|
||||
}
|
||||
default:
|
||||
return (
|
||||
<input
|
||||
ref={key}
|
||||
id={key}
|
||||
className="form-control"
|
||||
defaultValue={setting}
|
||||
type="text"
|
||||
onChange={this.handleServerSettingsChange.bind(this, key)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="content-wrapper">
|
||||
<section className="content-header">
|
||||
<h1>
|
||||
Config
|
||||
<small>Manage game configuration</small>
|
||||
|
||||
<small className="float-sm-right">
|
||||
<ol className="breadcrumb">
|
||||
<li className="breadcrumb-item">
|
||||
<Link to="/"><FontAwesomeIcon icon="tachometer-alt"/>Server Control</Link>
|
||||
</li>
|
||||
<li className="breadcrumb-item active">
|
||||
<FontAwesomeIcon icon="cogs"/>Game configurations
|
||||
</li>
|
||||
</ol>
|
||||
</small>
|
||||
</h1>
|
||||
</section>
|
||||
|
||||
<section className="content">
|
||||
<div className="box">
|
||||
<div className="box-header">
|
||||
<h3 className="box-title">Server Settings</h3>
|
||||
</div>
|
||||
|
||||
<div className="box-body">
|
||||
<div className="row">
|
||||
<div className="col-md-10">
|
||||
<div className="server-settings-section">
|
||||
<div className="table-responsive">
|
||||
<form ref="settingsForm"
|
||||
className="form-horizontal"
|
||||
onSubmit={this.updateServerSettings}
|
||||
>
|
||||
{
|
||||
Object.keys(this.state.serverSettings).map(function(key) {
|
||||
if(key.startsWith("_comment_")) {
|
||||
return (
|
||||
<div key={key}>
|
||||
{this.formTypeField(key, setting)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
var setting = this.state.serverSettings[key]
|
||||
var setting_key = this.capitalizeFirstLetter(key.replace(/_/g, " "))
|
||||
var comment = this.state.serverSettings["_comment_" + key]
|
||||
|
||||
return (
|
||||
<div className="form-group" key={key}>
|
||||
<label htmlFor={key}
|
||||
className="control-label col-md-3"
|
||||
>
|
||||
{setting_key}
|
||||
</label>
|
||||
<div className="col-md-6">
|
||||
{
|
||||
this.formTypeField(key, setting)
|
||||
}
|
||||
<p className="help-block">{comment}</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}, this)
|
||||
}
|
||||
<div className="col-xs-6">
|
||||
<div className="form-group">
|
||||
<input className="form-control btn btn-success" type="submit"
|
||||
ref="button" value="Update Settings"/>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="content">
|
||||
<div className="box">
|
||||
<div className="box-header">
|
||||
<h3 className="box-title">Game Configuration</h3>
|
||||
</div>
|
||||
|
||||
<div className="box-body">
|
||||
<div className="row">
|
||||
<div className="col-md-10">
|
||||
{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>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default ConfigContent
|
33
ui/App/components/ConfirmDialog.jsx
Normal file
33
ui/App/components/ConfirmDialog.jsx
Normal file
@ -0,0 +1,33 @@
|
||||
import React, {useState} from 'react';
|
||||
import Modal from "./Modal";
|
||||
import Button from "./Button";
|
||||
|
||||
function ConfirmDialog({title, content, isOpen, close, onSuccess}) {
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const confirm = () => {
|
||||
setIsLoading(true)
|
||||
onSuccess()
|
||||
.finally(() => {
|
||||
close()
|
||||
setIsLoading(false);
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={title}
|
||||
content={content}
|
||||
actions={
|
||||
<>
|
||||
<Button size="sm" type="danger" className="mr-2" onClick={close}>Cancel</Button>
|
||||
<Button size="sm" isLoading={isLoading} type="success" onClick={confirm}>Confirm</Button>
|
||||
</>
|
||||
}
|
||||
isOpen={isOpen}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default ConfirmDialog;
|
@ -1,133 +0,0 @@
|
||||
import React from 'react';
|
||||
import {Link} from 'react-router-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
import FontAwesomeIcon from "./FontAwesomeIcon";
|
||||
|
||||
class ConsoleContent extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.componentDidMount = this.componentDidMount.bind(this);
|
||||
this.handleInput = this.handleInput.bind(this);
|
||||
this.clearInput = this.clearInput.bind(this);
|
||||
this.clearHistory = this.clearHistory.bind(this);
|
||||
this.addHistory = this.addHistory.bind(this);
|
||||
this.handleClick = this.handleClick.bind(this);
|
||||
this.newLogLine = this.newLogLine.bind(this);
|
||||
this.subscribeLogToSocket = this.subscribeLogToSocket.bind(this);
|
||||
|
||||
this.state = {
|
||||
commands: {},
|
||||
history: [],
|
||||
prompt: '$ ',
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.subscribeLogToSocket();
|
||||
}
|
||||
|
||||
subscribeLogToSocket() {
|
||||
let wsReadyState = this.props.socket.emit("log subscribe");
|
||||
if(wsReadyState != WebSocket.OPEN) {
|
||||
setTimeout(() => {
|
||||
this.subscribeLogToSocket();
|
||||
}, 50);
|
||||
|
||||
return;
|
||||
}
|
||||
this.setState({connected: true});
|
||||
|
||||
this.props.socket.on('log update', this.newLogLine.bind(this));
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
var el = this.refs.output;
|
||||
var container = document.getElementById("console-output");
|
||||
container.scrollTop = this.refs.output.scrollHeight;
|
||||
}
|
||||
|
||||
handleInput(e) {
|
||||
if (e.key === "Enter") {
|
||||
var input_text = this.refs.term.value;
|
||||
this.props.socket.emit("command send", input_text);
|
||||
this.addHistory(this.state.prompt + " " + input_text);
|
||||
this.clearInput();
|
||||
}
|
||||
}
|
||||
|
||||
clearInput() {
|
||||
this.refs.term.value = "";
|
||||
}
|
||||
|
||||
clearHistory() {
|
||||
ths.setState({ history: [] });
|
||||
}
|
||||
|
||||
addHistory(output) {
|
||||
var history = this.state.history;
|
||||
history.push(output);
|
||||
this.setState({
|
||||
'history': history
|
||||
});
|
||||
}
|
||||
|
||||
handleClick() {
|
||||
var term = this.refs.term;
|
||||
term.focus();
|
||||
}
|
||||
|
||||
newLogLine(logline) {
|
||||
var history = this.state.history;
|
||||
history.push(logline);
|
||||
this.setState({
|
||||
'history': history
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
var output = this.state.history.map((op, i) => {
|
||||
return <p key={i}>{op}</p>
|
||||
});
|
||||
|
||||
return(
|
||||
<div className="content-wrapper">
|
||||
<section className="content-header">
|
||||
<h1>
|
||||
Server Console
|
||||
<small>Send commands and messages to the Factorio server</small>
|
||||
|
||||
<small className="float-sm-right">
|
||||
<ol className="breadcrumb">
|
||||
<li className="breadcrumb-item">
|
||||
<Link to="/"><FontAwesomeIcon icon="tachometer-alt"/>Server Control</Link>
|
||||
</li>
|
||||
<li className="breadcrumb-item active">
|
||||
<FontAwesomeIcon icon="terminal"/>Console
|
||||
</li>
|
||||
</ol>
|
||||
</small>
|
||||
</h1>
|
||||
</section>
|
||||
|
||||
<section className="content">
|
||||
<div className="console-box" >
|
||||
<div id='console-output' className='console-container' onClick={this.handleClick} ref="output">
|
||||
{output}
|
||||
</div>
|
||||
<p>
|
||||
<span className="console-prompt-box">{this.state.prompt}
|
||||
<input type="text" onKeyPress={this.handleInput} ref="term" /></span>
|
||||
</p>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ConsoleContent.propTypes = {
|
||||
socket: PropTypes.object.isRequired,
|
||||
}
|
||||
|
||||
export default ConsoleContent;
|
25
ui/App/components/Flash.jsx
Normal file
25
ui/App/components/Flash.jsx
Normal file
@ -0,0 +1,25 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import Bus from '../../notifications';
|
||||
|
||||
export const Flash = () => {
|
||||
let [visibility, setVisibility] = useState(false);
|
||||
let [message, setMessage] = useState('');
|
||||
let [color, setColor] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
Bus.addListener('flash', ({message, color}) => {
|
||||
setVisibility(true);
|
||||
setMessage(message);
|
||||
setColor(color);
|
||||
setTimeout(() => {
|
||||
setVisibility(false);
|
||||
}, 4000);
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
visibility && <div className={`bg-${color} accentuated rounded fixed bottom-0 right-0 mr-8 mb-8 px-4 py-2`}>
|
||||
<p>{message}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -1,27 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
|
||||
class FontAwesomeIcon extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
render() {
|
||||
let classes = classNames(this.props.prefix, {
|
||||
"fas": !this.props.prefix,
|
||||
}, 'fa-' + this.props.icon, this.props.className);
|
||||
|
||||
return (
|
||||
<i className={classes}></i>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
FontAwesomeIcon.propTypes = {
|
||||
icon: PropTypes.string.isRequired,
|
||||
prefix: PropTypes.string,
|
||||
className: PropTypes.string
|
||||
};
|
||||
|
||||
export default FontAwesomeIcon;
|
@ -1,15 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
class Footer extends React.Component {
|
||||
render() {
|
||||
return(
|
||||
<footer className="main-footer">
|
||||
<div className="pull-right hidden-xs">
|
||||
</div>
|
||||
<strong>Copyright © 2019 <a href="https://github.com/MajorMJR/factorio-server-manager">Mitch Roote</a>.</strong> MIT License.
|
||||
</footer>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default Footer
|
@ -1,66 +0,0 @@
|
||||
import React from 'react';
|
||||
import {Link, withRouter} from 'react-router-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
import FontAwesomeIcon from "./FontAwesomeIcon";
|
||||
|
||||
class Header extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.onLogout = this.onLogout.bind(this);
|
||||
}
|
||||
|
||||
onLogout(e) {
|
||||
e.preventDefault();
|
||||
$.ajax({
|
||||
url: "/api/logout",
|
||||
dataType: "json",
|
||||
success: (resp) => {
|
||||
console.log(resp)
|
||||
}
|
||||
});
|
||||
|
||||
// Wait for 1 second for logout callback to complete
|
||||
setTimeout(() => {
|
||||
this.props.history.push("/login")
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
render() {
|
||||
var loginMenu;
|
||||
if (this.props.loggedIn) {
|
||||
loginMenu =
|
||||
<ul className="navbar-nav ml-auto">
|
||||
<li className="nav-item">
|
||||
<Link className="nav-link" to="/settings">
|
||||
<FontAwesomeIcon icon="cogs" className="fa-fw"/>Settings
|
||||
</Link>
|
||||
</li>
|
||||
<li className="nav-item">
|
||||
<a href="javascript:void(0)" onClick={this.onLogout} className="nav-link">
|
||||
<FontAwesomeIcon icon="lock" className="fa-fw"/>Logout
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
}
|
||||
return(
|
||||
<nav className="main-header navbar navbar-expand navbar-light border-bottom">
|
||||
<ul className="navbar-nav">
|
||||
<li className="nav-item">
|
||||
<a className="nav-link" data-widget="pushmenu" href="#">
|
||||
<FontAwesomeIcon icon="bars"/>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
{loginMenu}
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Header.propTypes = {
|
||||
username: PropTypes.string.isRequired,
|
||||
loggedIn: PropTypes.bool.isRequired,
|
||||
}
|
||||
|
||||
export default withRouter(Header);
|
@ -1,59 +0,0 @@
|
||||
import React from 'react';
|
||||
import ServerCtl from './ServerCtl/ServerCtl.jsx';
|
||||
import ServerStatus from './ServerCtl/ServerStatus.jsx';
|
||||
import FontAwesomeIcon from "./FontAwesomeIcon";
|
||||
|
||||
class Index extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.facServStatus();
|
||||
this.props.getSaves();
|
||||
this.props.getStatus();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.facServStatus();
|
||||
}
|
||||
|
||||
render() {
|
||||
return(
|
||||
<div className="content-wrapper" style={{height: "100%"}}>
|
||||
<section className="content-header" style={{height: "100%"}}>
|
||||
<h1>
|
||||
Factorio Server
|
||||
<small>Control your Factorio server</small>
|
||||
|
||||
<small className="float-sm-right">
|
||||
<ol className="breadcrumb">
|
||||
<li className="breadcrumb-item active">
|
||||
<FontAwesomeIcon icon="tachometer-alt"/>Server Control
|
||||
</li>
|
||||
</ol>
|
||||
</small>
|
||||
</h1>
|
||||
</section>
|
||||
|
||||
<section className="content">
|
||||
<ServerStatus
|
||||
serverStatus={this.props.serverStatus}
|
||||
facServStatus={this.props.facServStatus}
|
||||
getStatus={this.props.getStatus}
|
||||
/>
|
||||
|
||||
<ServerCtl
|
||||
getStatus={this.props.getStatus}
|
||||
saves={this.props.saves}
|
||||
getSaves={this.props.getSaves}
|
||||
serverStatus={this.props.serverStatus}
|
||||
facServStatus={this.props.facServStatus}
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default Index
|
19
ui/App/components/Input.jsx
Normal file
19
ui/App/components/Input.jsx
Normal file
@ -0,0 +1,19 @@
|
||||
import React from "react";
|
||||
|
||||
const Input = ({name, inputRef, placeholder = null, type="text", defaultValue=null, hasAutoComplete=true, onKeyDown=() => null}) => {
|
||||
return (
|
||||
<input
|
||||
className="shadow appearance-none border w-full py-2 px-3 text-black"
|
||||
placeholder={placeholder}
|
||||
ref={inputRef}
|
||||
name={name}
|
||||
id={name}
|
||||
type={type}
|
||||
onKeyDown={onKeyDown}
|
||||
autoComplete={hasAutoComplete ? "on" : "off"}
|
||||
defaultValue={defaultValue}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default Input;
|
29
ui/App/components/InputPassword.jsx
Normal file
29
ui/App/components/InputPassword.jsx
Normal file
@ -0,0 +1,29 @@
|
||||
import Input from "./Input";
|
||||
import React, {useState} from "react";
|
||||
import {faEye, faEyeSlash} from "@fortawesome/free-solid-svg-icons";
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||
|
||||
const InputPassword = ({name, inputRef, defaultValue}) => {
|
||||
|
||||
const [type, setType] = useState("password");
|
||||
|
||||
let icon;
|
||||
if (type === "password") {
|
||||
icon = faEye;
|
||||
} else {
|
||||
icon = faEyeSlash
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex">
|
||||
<Input type={type} name={name} defaultValue={defaultValue} inputRef={inputRef} placeholder="*************"/>
|
||||
<div
|
||||
className="accentuated cursor-pointer bg-gray-light flex items-center px-2 text-black"
|
||||
onClick={() => setType(type === "password" ? "text" : "password")}
|
||||
>
|
||||
<FontAwesomeIcon fixedWidth={true} icon={icon} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default InputPassword;
|
12
ui/App/components/Label.jsx
Normal file
12
ui/App/components/Label.jsx
Normal file
@ -0,0 +1,12 @@
|
||||
import React from "react";
|
||||
|
||||
const Label = ({text, htmlFor}) => {
|
||||
return (
|
||||
<label
|
||||
className="block text-white text-sm font-bold mb-1" htmlFor={htmlFor}>
|
||||
{text}
|
||||
</label>
|
||||
)
|
||||
}
|
||||
|
||||
export default Label;
|
108
ui/App/components/Layout.jsx
Normal file
108
ui/App/components/Layout.jsx
Normal file
@ -0,0 +1,108 @@
|
||||
import React, {useEffect, useState} from "react";
|
||||
import {NavLink} from "react-router-dom";
|
||||
import Button from "./Button";
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||
import {faBars} from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
const Layout = ({children, handleLogout, serverStatus, updateServerStatus}) => {
|
||||
|
||||
const [isNavCollapsed, setIsNavCollapsed] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
updateServerStatus()
|
||||
})();
|
||||
}, []);
|
||||
|
||||
const Status = ({info}) => {
|
||||
|
||||
let text = 'Unknown';
|
||||
let color = 'gray-light';
|
||||
|
||||
if (info) {
|
||||
if (info.status === 'running') {
|
||||
text = 'Running';
|
||||
color = 'green';
|
||||
} else if (info.status === 'stopped') {
|
||||
text = 'Stopped';
|
||||
color = 'red';
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`bg-${color} accentuated rounded px-2 py-1 text-black`}>{text}</div>
|
||||
)
|
||||
}
|
||||
|
||||
const Link = ({children, to, last}) => {
|
||||
return (
|
||||
<NavLink
|
||||
onClick={() => setIsNavCollapsed(true)}
|
||||
exact={true}
|
||||
to={to}
|
||||
activeClassName="bg-orange"
|
||||
className={`hover:glow-orange accentuated bg-gray-light hover:bg-orange text-black font-bold py-2 px-4 w-full block${last ? '' : ' mb-1'}`}
|
||||
>{children}</NavLink>)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{/*Sidebar*/}
|
||||
<div className="w-full md:w-88 md:fixed md:top-0 md:left-0 bg-gray-dark md:h-screen overflow-y-auto">
|
||||
<div className="py-4 px-2 accentuated">
|
||||
<div className="mx-4 justify-between flex text-center">
|
||||
<span className="text-dirty-white text-xl">Factorio Server Manager</span>
|
||||
<button
|
||||
className="md:hidden cursor-pointer text-white hover:text-dirty-white"
|
||||
onClick={() => setIsNavCollapsed(!isNavCollapsed)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faBars}/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className={isNavCollapsed ? "hidden md:block" : "block"}>
|
||||
<div className="py-4 px-2 accentuated">
|
||||
<h1 className="text-dirty-white text-lg mb-2 mx-4">Server Status</h1>
|
||||
<div className="mx-4 mb-4 text-center">
|
||||
<Status info={serverStatus}/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="py-4 px-2 accentuated">
|
||||
<h1 className="text-dirty-white text-lg mb-2 mx-4">Server Management</h1>
|
||||
<div className="text-white text-center rounded-sm bg-black shadow-inner mx-4 p-1">
|
||||
<Link to="/">Controls</Link>
|
||||
<Link to="/saves">Saves</Link>
|
||||
<Link to="/mods">Mods</Link>
|
||||
<Link to="/server-settings">Server Settings</Link>
|
||||
<Link to="/game-settings">Game Settings</Link>
|
||||
<Link to="/console">Console</Link>
|
||||
<Link to="/logs" last={true}>Logs</Link>
|
||||
</div>
|
||||
</div>
|
||||
<div className="py-4 px-2 accentuated">
|
||||
<h1 className="text-dirty-white text-lg mb-2 mx-4">FSM Administration</h1>
|
||||
<div className="text-white text-center rounded-sm bg-black shadow-inner mx-4 p-1">
|
||||
<Link to="/user-management">Users</Link>
|
||||
<Link to="/help" last={true}>Help</Link>
|
||||
</div>
|
||||
</div>
|
||||
<div className="py-4 px-2 accentuated">
|
||||
<div className="text-white text-center rounded-sm bg-black shadow-inner mx-4 p-1">
|
||||
<Button type="danger" className="w-full" onClick={handleLogout}>Logout</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="accentuated-t accentuated-x md:block hidden"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/*Main*/}
|
||||
<div className="md:ml-88 min-h-screen">
|
||||
<div className="container mx-auto pt-16 px-6">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default Layout;
|
@ -1,79 +0,0 @@
|
||||
import React from 'react';
|
||||
import {withRouter} from 'react-router-dom';
|
||||
import FontAwesomeIcon from "./FontAwesomeIcon";
|
||||
|
||||
class LoginContent extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.loginUser = this.loginUser.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {}
|
||||
|
||||
loginUser(e) {
|
||||
e.preventDefault();
|
||||
let user = {
|
||||
username: this.refs.username.value,
|
||||
password: this.refs.password.value,
|
||||
};
|
||||
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: "/api/login",
|
||||
dataType: "json",
|
||||
data: JSON.stringify(user),
|
||||
success: (resp) => {
|
||||
console.log(resp);
|
||||
this.props.history.push("/");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return(
|
||||
<div className="container" id="login">
|
||||
<div className="d-flex justify-content-center h-100">
|
||||
<div className="card">
|
||||
<div className="card-header">
|
||||
<h1>
|
||||
<img src="./images/factorio.jpg" className="img-circle" alt="User Image"/>
|
||||
Factorio Server Manager
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div className="car-body">
|
||||
<form onSubmit={this.loginUser}>
|
||||
<label className="input-group form-group">
|
||||
<div className="input-group-prepend">
|
||||
<span className="input-group-text">
|
||||
<FontAwesomeIcon icon="user"/>
|
||||
</span>
|
||||
</div>
|
||||
<input className="form-control" type="text" ref="username" placeholder="Username"/>
|
||||
</label>
|
||||
|
||||
<label className="input-group form-group">
|
||||
<div className="input-group-prepend">
|
||||
<span className="input-group-text">
|
||||
<FontAwesomeIcon icon="lock"/>
|
||||
</span>
|
||||
</div>
|
||||
<input className="form-control" type="password" ref="password" placeholder="Password"/>
|
||||
</label>
|
||||
|
||||
<label className="remember-me">
|
||||
<input type="checkbox"/>
|
||||
Remember me
|
||||
</label>
|
||||
|
||||
<input type="submit" value="Sign In" className="btn btn-primary btn-block btn-flat"/>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default withRouter(LoginContent);
|
@ -1,37 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
class LogLines extends React.Component {
|
||||
updateLog() {
|
||||
this.props.getLastLog();
|
||||
}
|
||||
|
||||
render() {
|
||||
this.props.log.reverse();
|
||||
return(
|
||||
<div id="logLines" className="box">
|
||||
<div className="box-header">
|
||||
<h3 className="box-title">Factorio Log</h3>
|
||||
</div>
|
||||
<div className="box-body">
|
||||
<input className="btn btn-default" type='button' onClick={this.updateLog.bind(this)} value="Refresh" />
|
||||
<h5>Latest log line at the top</h5>
|
||||
<samp>
|
||||
{this.props.log.map ( (line, i) => {
|
||||
return(
|
||||
<p key={i}>{line}</p>
|
||||
)
|
||||
})}
|
||||
</samp>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
LogLines.propTypes = {
|
||||
log: PropTypes.array.isRequired,
|
||||
getLastLog: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
export default LogLines
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user