Compare commits

...

9 Commits

Author SHA1 Message Date
7aa0793075 Merge pull request 'Added README' (#2) from develop into main
Reviewed-on: #2
2026-05-04 16:05:49 +00:00
6ef8f280a8 Added README 2026-05-04 10:02:18 -06:00
783008bfb6 Merge pull request 'Added Mod and server downloader' (#1) from develop into main
Reviewed-on: #1
2026-05-04 13:47:15 +00:00
cf71d956d2 Minor changes
Spelling error
modified headlessQuery string removing Sprint
2026-05-04 07:35:57 -06:00
bb65f409b4 Better way to download and extract server 2026-05-03 14:25:45 -06:00
8fb2c2aeb2 Download mods and headless server
till working on improvements to code this more like
a savepoint.
2026-05-03 14:00:16 -06:00
aecc2162e7 Added download and extract mod from modlist 2026-04-30 21:36:36 -06:00
2548261d66 Started work on adding mod downloader
Read from mod-list.json in mod dir and download each mod in list.
This requires reading from factorio mod portal api, and extracting download url and calling it with username and api token.

new field in config.yml username and apitoken, is needed to download mod files from mod.factorio.com
2026-04-30 12:06:49 -06:00
b2f485269b Fix issues with config handling 2026-04-29 11:47:29 -06:00
13 changed files with 569 additions and 65 deletions

91
README.md Normal file
View File

@@ -0,0 +1,91 @@
# FactoryMan
A simple Factorio server manager for linux systems.
* Download and install headless factorio server (requires api key)
* Download and install mods directly from mod-list.json
* Start and stop factorio in a screen session, great for remote servers
* Backup saves and server
### Commands
```shell
$> ./factoryman download server
```
Run this command to download factorio-headless_linux_latest.
It will install to path in serverFolder in ``config.yml``
*NOTE: username and apitoken are required in ``config.yml``>factoryman*
---
```shell
$> ./factoryman download mod
```
Run this to download mods directly from ``$serverFolder/mods/mod-list.json``
*NOTE: username and apitoken are required in ``config.yml``>factoryman*
---
```shell
$> ./factoryman start
```
Start factorio server (in screen session by default)
---
```shell
$> ./factoryman stop
```
Stop factorio server (in screen session)
---
```shell
$> ./factoryman backup saves
```
Backup saves to path in ``config.yml``>factoryman>backupDir
---
```shell
$> ./factoryman backup full
```
Backup Full serverDir to backupDir
---
*Note:*
use ``$> screen -LS`` to view server terminal
### Simple Config
```config.yml```
```yaml
server:
serverFolder: "factorio"
worldFile: ""
serverSettings: "factorio/data/server-settings.json"
serverExec: "factorio/bin/x64/factorio"
port: 34197
factoryman:
screen: True
screenName: "Factorio"
backupDir: "factorio/backups"
username: ""
apitoken: ""
```
Default config assumes you have used factoryman to download the server

77
cli.go
View File

@@ -6,36 +6,57 @@ import (
)
func cliToolMode() {
var c = readCfg(fmConfig)
if verifyConfig(c) {
if len(os.Args) > 1 {
switch os.Args[1] {
case "start":
var c = readCfg("config.yml")
modlist := string(c.Server.ServDir) + "/mods/mod-list.json"
if len(os.Args) > 1 {
switch os.Args[1] {
case "start":
if verifyConfig(c) {
startStopServer("start", c)
case "stop":
startStopServer("stop", c)
case "help", "h", "--help", "-h":
fmt.Printf("Start Server: %s start\nStop Server: %s stop\n", os.Args[0], os.Args[0])
fmt.Printf("Run backup\n\tFull backup: %s backup full", os.Args[0])
fmt.Printf("\n\tBackup saves: %s backup saves\n", os.Args[0])
case "backup":
if len(os.Args) > 2 {
switch os.Args[2] {
case "full":
backUp("full", c)
case "saves":
backUp("saves", c)
default:
fmt.Println("Invalid backup type: use 'full' or 'saves'")
}
} else {
fmt.Println("Missing backup type: use 'full' or 'saves'")
}
default:
fmt.Printf("Unknown command: %s. Use 'start', 'stop', or 'backup'.\n", os.Args[1])
}
} else {
fmt.Println("Use 'start', 'stop', or 'backup' command\nex. factoryman start\nTo configure edit 'conifg.yml'")
case "stop":
if verifyConfig(c) {
startStopServer("stop", c)
}
case "help", "h", "--help", "-h":
fmt.Printf("Start Server: %s start\nStop Server: %s stop\n", os.Args[0], os.Args[0])
fmt.Printf("Run backup\n\tFull backup: %s backup full", os.Args[0])
fmt.Printf("\n\tBackup saves: %s backup saves\n", os.Args[0])
case "download":
if len(os.Args) > 2 {
switch os.Args[2] {
case "mods":
if !findmodlist(modlist) {
fmt.Printf("FAILED TO FIND MOD LIST")
} else {
fmt.Printf("found %s\n", modlist)
downloadMods(modlist)
}
case "server":
downloadHeadless()
default:
fmt.Println("Invalid mod option: use 'update'")
}
} else {
fmt.Println("Missing mod option: use 'update'")
}
case "backup":
if len(os.Args) > 2 {
switch os.Args[2] {
case "full":
backUp("full", c)
case "saves":
backUp("saves", c)
default:
fmt.Println("Invalid backup type: use 'full' or 'saves'")
}
} else {
fmt.Println("Missing backup type: use 'full' or 'saves'")
}
default:
fmt.Printf("Unknown command: %s. Use 'start', 'stop', or 'backup'.\n", os.Args[1])
}
} else {
fmt.Println("Use 'start', 'stop', or 'backup' command\nex. factoryman start\nTo configure edit 'config.yml'")
}
}

View File

@@ -9,24 +9,6 @@ import (
"gopkg.in/yaml.v3"
)
const fmConfig = "config.yml"
type GoConfig struct {
Server struct {
ServDir string `yaml:"serverFolder"`
WorldFile string `yaml:"worldFile"`
ServCfg string `yaml:"serverSettings"`
ServExec string `yaml:"serverExec"`
} `yaml:"server"`
Factoryman struct {
ServPort int `yaml:"port"`
BackupDir string `yaml:"backupDir"`
UseScreen bool `yaml:"screen"`
ScreenName string `yaml:"screenName"`
} `yaml:"factoryman"`
}
func readCfg(factCfg string) GoConfig {
//read config file (YAML)
fileBytes, err := os.ReadFile(factCfg)
@@ -41,7 +23,7 @@ func readCfg(factCfg string) GoConfig {
}
return config
}
func isItReal(path string) bool {
func isItReal(path string) bool { //check if path exists
if _, err := os.Stat(path); err == nil {
return true
} else if errors.Is(err, os.ErrNotExist) {
@@ -50,27 +32,31 @@ func isItReal(path string) bool {
return false
}
}
func verifyConfig(config GoConfig) bool {
if !isItReal(config.Server.ServDir) {
func verifyConfig(config GoConfig) bool { // check each file/dir to see if it exists
if !isItReal(config.Server.ServDir) { //check for Server directory
fmt.Printf("PATH NOT FOUND: %s", config.Server.ServDir)
return false
}
if !isItReal(config.Server.WorldFile) {
if !isItReal(config.Server.WorldFile) { // check for world file
fmt.Printf("PATH NOT FOUND: %s", config.Server.WorldFile)
return false
}
if !isItReal(config.Server.ServCfg) {
if !isItReal(config.Server.ServCfg) { // check for server config
fmt.Printf("PATH NOT FOUND: %s", config.Server.ServCfg)
return false
}
if !isItReal(config.Server.ServExec) {
if !isItReal(config.Server.ServExec) { // check for server exec file to lanch server
fmt.Printf("PATH NOT FOUND: %s", config.Server.ServExec)
return false
}
if !isItReal(config.Factoryman.BackupDir) {
if !isItReal(config.Factoryman.BackupDir) { // check for backup directory
fmt.Printf("PATH NOT FOUND: %s", config.Factoryman.BackupDir)
return false
}
if config.Factoryman.ApiToken == "" || config.Factoryman.UserName == "" {
fmt.Printf("API Token/Username not set (to download mods add api token and username to config)")
}
return true
}

View File

@@ -1,12 +1,13 @@
server:
serverFolder: "factorio"
worldFile: "factorio/data/saves/newworld.zip"
worldFile: ""
serverSettings: "factorio/data/server-settings.json"
serverExec: "factorio/bin/x64/factorio"
factoryman:
port: 34197
factoryman:
screen: True
screenName: "Factorio"
backupDir: "factorio/backups"
backupDir: "factorio/backups"
username: ""
apitoken: ""

Binary file not shown.

37
go.mod
View File

@@ -1,7 +1,38 @@
module gitlab.com/Raum0x2A/factoryman
go 1.24.0
go 1.25.7
toolchain go1.24.4
require (
golift.io/xtractr v0.3.1
gopkg.in/yaml.v3 v3.0.1
)
require gopkg.in/yaml.v3 v3.0.1
require (
github.com/Unpackerr/iso9660 v0.0.3 // indirect
github.com/andybalholm/brotli v1.2.0 // indirect
github.com/bodgit/plumbing v1.3.0 // indirect
github.com/bodgit/sevenzip v1.6.1 // indirect
github.com/bodgit/windows v1.0.1 // indirect
github.com/cavaliergopher/cpio v1.0.1 // indirect
github.com/cavaliergopher/rpm v1.3.0 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/icza/bitio v1.1.0 // indirect
github.com/klauspost/compress v1.18.4 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/mewkiz/flac v1.0.13 // indirect
github.com/mewkiz/pkg v0.0.0-20250417130911-3f050ff8c56d // indirect
github.com/mewpkg/term v0.0.0-20241026122259-37a80af23985 // indirect
github.com/nwaples/rardecode/v2 v2.2.2 // indirect
github.com/peterebden/ar v0.0.0-20241106141004-20dc11b778e8 // indirect
github.com/pierrec/lz4/v4 v4.1.26 // indirect
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d // indirect
github.com/spf13/afero v1.15.0 // indirect
github.com/sshaman1101/dcompress v0.0.0-20200109162717-50436a6332de // indirect
github.com/therootcompany/xz v1.0.1 // indirect
github.com/ulikunitz/xz v0.5.15 // indirect
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
go4.org v0.0.0-20260112195520-a5071408f32f // indirect
golang.org/x/crypto v0.48.0 // indirect
golang.org/x/text v0.34.0 // indirect
golift.io/udf v0.0.1 // indirect
)

90
go.sum
View File

@@ -1,4 +1,92 @@
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
github.com/Unpackerr/iso9660 v0.0.3 h1:WXXFIcmDLhnsKhXjPg2moUmHxhoUmIX7FLxrtqHJ7yQ=
github.com/Unpackerr/iso9660 v0.0.3/go.mod h1:4Py6ZWQ+sUVo4BmmzZaFgOLcS3to5BMvH39TlOYNxhA=
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/bodgit/plumbing v1.3.0 h1:pf9Itz1JOQgn7vEOE7v7nlEfBykYqvUYioC61TwWCFU=
github.com/bodgit/plumbing v1.3.0/go.mod h1:JOTb4XiRu5xfnmdnDJo6GmSbSbtSyufrsyZFByMtKEs=
github.com/bodgit/sevenzip v1.6.1 h1:kikg2pUMYC9ljU7W9SaqHXhym5HyKm8/M/jd31fYan4=
github.com/bodgit/sevenzip v1.6.1/go.mod h1:GVoYQbEVbOGT8n2pfqCIMRUaRjQ8F9oSqoBEqZh5fQ8=
github.com/bodgit/windows v1.0.1 h1:tF7K6KOluPYygXa3Z2594zxlkbKPAOvqr97etrGNIz4=
github.com/bodgit/windows v1.0.1/go.mod h1:a6JLwrB4KrTR5hBpp8FI9/9W9jJfeQ2h4XDXU74ZCdM=
github.com/cavaliergopher/cpio v1.0.1 h1:KQFSeKmZhv0cr+kawA3a0xTQCU4QxXF1vhU7P7av2KM=
github.com/cavaliergopher/cpio v1.0.1/go.mod h1:pBdaqQjnvXxdS/6CvNDwIANIFSP0xRKI16PX4xejRQc=
github.com/cavaliergopher/rpm v1.3.0 h1:UHX46sasX8MesUXXQ+UbkFLUX4eUWTlEcX8jcnRBIgI=
github.com/cavaliergopher/rpm v1.3.0/go.mod h1:vEumo1vvtrHM1Ov86f6+k8j7zNKOxQfHDCAIcR/36ZI=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dsnet/compress v0.0.1 h1:PlZu0n3Tuv04TzpfPbrnI0HW/YwodEXDS+oPKahKF0Q=
github.com/dsnet/compress v0.0.1/go.mod h1:Aw8dCMJ7RioblQeTqt88akK31OvO8Dhf5JflhBbQEHo=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/icza/bitio v1.1.0 h1:ysX4vtldjdi3Ygai5m1cWy4oLkhWTAi+SyO6HC8L9T0=
github.com/icza/bitio v1.1.0/go.mod h1:0jGnlLAx8MKMr9VGnn/4YrvZiprkvBelsVIbA9Jjr9A=
github.com/icza/mighty v0.0.0-20180919140131-cfd07d671de6 h1:8UsGZ2rr2ksmEru6lToqnXgA8Mz1DP11X4zSJ159C3k=
github.com/icza/mighty v0.0.0-20180919140131-cfd07d671de6/go.mod h1:xQig96I1VNBDIWGCdTt54nHt6EeI639SmHycLYL7FkA=
github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mewkiz/flac v1.0.13 h1:6wF8rRQKBFW159Daqx6Ro7K5ZnlVhHUKfS5aTsC4oXs=
github.com/mewkiz/flac v1.0.13/go.mod h1:HfPYDA+oxjyuqMu2V+cyKcxF51KM6incpw5eZXmfA6k=
github.com/mewkiz/pkg v0.0.0-20250417130911-3f050ff8c56d h1:IL2tii4jXLdhCeQN69HNzYYW1kl0meSG0wt5+sLwszU=
github.com/mewkiz/pkg v0.0.0-20250417130911-3f050ff8c56d/go.mod h1:SIpumAnUWSy0q9RzKD3pyH3g1t5vdawUAPcW5tQrUtI=
github.com/mewpkg/term v0.0.0-20241026122259-37a80af23985 h1:h8O1byDZ1uk6RUXMhj1QJU3VXFKXHDZxr4TXRPGeBa8=
github.com/mewpkg/term v0.0.0-20241026122259-37a80af23985/go.mod h1:uiPmbdUbdt1NkGApKl7htQjZ8S7XaGUAVulJUJ9v6q4=
github.com/nwaples/rardecode/v2 v2.2.2 h1:/5oL8dzYivRM/tqX9VcTSWfbpwcbwKG1QtSJr3b3KcU=
github.com/nwaples/rardecode/v2 v2.2.2/go.mod h1:7uz379lSxPe6j9nvzxUZ+n7mnJNgjsRNb6IbvGVHRmw=
github.com/peterebden/ar v0.0.0-20241106141004-20dc11b778e8 h1:27L3dHkYbeWGU3/5NasAzVDgXG9QzlfKCvcl4cdNW6c=
github.com/peterebden/ar v0.0.0-20241106141004-20dc11b778e8/go.mod h1:hpFkyhCgB5Rm8FK+ISypOE+9UyrCuL6MNcjPMB1s1ec=
github.com/pierrec/lz4/v4 v4.1.26 h1:GrpZw1gZttORinvzBdXPUXATeqlJjqUG/D87TKMnhjY=
github.com/pierrec/lz4/v4 v4.1.26/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d h1:hrujxIzL1woJ7AwssoOcM/tq5JjjG2yYOc8odClEiXA=
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU=
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
github.com/sshaman1101/dcompress v0.0.0-20200109162717-50436a6332de h1:uIeuAon/xwRdiZaCmEd5mocquesYkWCf71WBO7obTmA=
github.com/sshaman1101/dcompress v0.0.0-20200109162717-50436a6332de/go.mod h1:XIUpD+1rteMazWrMFjNSpM6TocSHxDYXk6UEgBb5+F0=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4=
github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/therootcompany/xz v1.0.1 h1:CmOtsn1CbtmyYiusbfmhmkpAAETj0wBIH6kCYaX+xzw=
github.com/therootcompany/xz v1.0.1/go.mod h1:3K3UH1yCKgBneZYhuQUvJ9HPD19UEXEI0BWbMn8qNMY=
github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY=
github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo=
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
go4.org v0.0.0-20260112195520-a5071408f32f h1:ziUVAjmTPwQMBmYR1tbdRFJPtTcQUI12fH9QQjfb0Sw=
go4.org v0.0.0-20260112195520-a5071408f32f/go.mod h1:ZRJnO5ZI4zAwMFp+dS1+V6J6MSyAowhRqAE+DPa1Xp0=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
golift.io/udf v0.0.1 h1:kEcJVzqqR+IEWGMuPjuVPT9DzXRDukEgsizKAKn1LF8=
golift.io/udf v0.0.1/go.mod h1:ndK7AlWOh+u+nW9tNsQR95dfHsfASG5Y3dMyzVqmPjw=
golift.io/xtractr v0.3.1 h1:91ZU0DBL3SSfrVYbrENSLUQG4BdHT91rTqkod7TfOUI=
golift.io/xtractr v0.3.1/go.mod h1:X3IQQIiYgwDmAuqeRSBfQqvo6KIWrFzCADTx3yFmt7o=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -10,8 +10,8 @@ import (
func startStopServer(cmd string, con GoConfig) {
switch cmd {
case "start":
x := fmt.Sprintf("%s --port %d --server-settings %s --start-server %s", con.Server.ServExec, con.Factoryman.ServPort, con.Server.ServCfg, con.Server.WorldFile)
if con.Factoryman.UseScreen { // if screen enabled in confing.yml
x := fmt.Sprintf("%s --port %d --server-settings %s --start-server %s", con.Server.ServExec, con.Server.ServPort, con.Server.ServCfg, con.Server.WorldFile)
if con.Factoryman.UseScreen { // if screen enabled in config.yml
fmt.Println("Starting factorio server in screen session")
startScreenCmd := exec.Command("screen", "-dmS", con.Factoryman.ScreenName, "bash", "-c", x, "; exec sh")
startScreenCmd.Stdout = os.Stdout

View File

@@ -7,7 +7,7 @@ import (
func main() {
if len(os.Args) == 1 {
fmt.Println("Use 'start', 'stop', or 'backup' command\nex. factoryman start\nTo configure edit 'conifg.yml'")
fmt.Println("Use 'start', 'stop', or 'backup' command\nex. factoryman start\nTo configure edit 'config.yml'")
} else {
cliToolMode()
}

91
mods.go Normal file
View File

@@ -0,0 +1,91 @@
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"os"
)
func findmodlist(modlist string) bool {
if !isItReal(modlist) {
return false
} else {
return true
}
}
func downloadMods(modlist string) {
var c = readCfg("config.yml")
fmt.Printf("Updating mods from %s\n", modlist)
//read from modlist
file, err := os.Open(modlist)
if err != nil {
log.Fatalf("Error reading modlist: %v", err)
}
defer file.Close()
// create temp directory
tempDir, err := os.MkdirTemp("", "factoryman-*")
if err != nil {
log.Fatalln("Failed to create temporary directory")
}
defer os.RemoveAll(tempDir)
//decode json response from api
var modList ModList
decoder := json.NewDecoder(file)
if err := decoder.Decode(&modList); err != nil {
log.Fatalf("Error reading JSON: %v", err)
}
// Iterate over modlist.json
for _, mod := range modList.Mods {
switch mod.Name {
case "base":
fmt.Println("Skipping base...")
case "elevated-rails":
fmt.Println("Skipping elevated-rails...")
case "quality":
fmt.Println("Skipping quality...")
case "space-age":
fmt.Println("Skipping space-age...")
default:
// query mod on modportal api
modportalurl := fmt.Sprintf("https://mods.factorio.com/api/mods/%s", mod.Name)
resp, err := http.Get(modportalurl)
if err != nil {
log.Fatalf("Error reading JSON: %v", err)
}
defer resp.Body.Close()
// decode json response
var moddata ModPortal
if err := json.NewDecoder(resp.Body).Decode(&moddata); err != nil {
log.Fatalf("Error reading JSON: %v", err)
}
// query download url for mod
accessToken := fmt.Sprintf("?username=%s&token=%s", c.Factoryman.UserName, c.Factoryman.ApiToken)
modDownloadUrl := fmt.Sprintf("https://mods.factorio.com%s%s", moddata.Releases[len(moddata.Releases)-1].DownloadUrl, accessToken)
fileName := string(tempDir + "/" + moddata.Releases[len(moddata.Releases)-1].Filename)
//download mod archive
downloadErr := download(fileName, modDownloadUrl)
if downloadErr != nil {
log.Fatalf("Error downloading: %v", downloadErr)
}
fmt.Printf("Downloaded: %s\n", fileName)
fmt.Printf("extracting files to %s\n", c.Server.ServDir+"/mods/")
// extract archive to mods folder
//exec.Command("unzip", fileName, "-d", c.Server.ServDir+"/mods/")
_, extracterr := unzip(fileName, c.Server.ServDir+"/mods/")
if extracterr != nil {
log.Fatalf("Error extracting archive: %v", extracterr)
}
}
}
}

View File

@@ -1 +1,45 @@
package main
import (
"archive/tar"
"io"
"log"
"net/http"
"os"
"path/filepath"
"github.com/therootcompany/xz"
)
func downloadHeadless() {
headlessQuery := "https://www.factorio.com/get-download/latest/headless/linux64"
url := headlessQuery
resp, err := http.Get(url)
if err != nil || resp.StatusCode != http.StatusOK {
log.Fatal("Download failed")
}
defer resp.Body.Close()
// Wrap stream in XZ and Tar readers
xzReader, _ := xz.NewReader(resp.Body, 0)
tr := tar.NewReader(xzReader)
for {
header, err := tr.Next()
if err == io.EOF {
break
}
target := filepath.Join(".", header.Name)
switch header.Typeflag {
case tar.TypeDir:
os.MkdirAll(target, 0755)
case tar.TypeReg:
os.MkdirAll(filepath.Dir(target), 0755)
outFile, _ := os.Create(target)
io.Copy(outFile, tr)
outFile.Close()
}
}
}

58
structs.go Normal file
View File

@@ -0,0 +1,58 @@
package main
// config struct
type GoConfig struct {
Server struct { // server specific settings
ServDir string `yaml:"serverFolder"`
WorldFile string `yaml:"worldFile"`
ServCfg string `yaml:"serverSettings"`
ServExec string `yaml:"serverExec"`
ServPort int `yaml:"port"`
} `yaml:"server"`
Factoryman struct { // factoryman settings
BackupDir string `yaml:"backupDir"`
UseScreen bool `yaml:"screen"`
ScreenName string `yaml:"screenName"`
UserName string `yaml:"username"`
ApiToken string `yaml:"apitoken"`
} `yaml:"factoryman"`
}
// JSON file from config
type Mod struct {
Name string `json:"name"`
Enabled bool `json:"enabled"`
Version string `json:"version,omitempty"`
}
type ModList struct {
Mods []Mod `json:"mods"`
}
// JSON response from mod poartal api
type ModPortal struct {
Category string `json:"category"`
DownloadCount int `json:"downloads_count"`
Name string `json:"name"`
Owner string `json:"owner"`
Releases []Release `json:"releases"`
Score float64 `json:"score"`
Summary string `json:"summary"`
Thumbnail string `json:"thumbnail"`
Title string `json:"title"`
}
type Release struct {
DownloadUrl string `json:"download_url"`
Filename string `json:"file_name"`
InfoJson InfoJSON `json:"info_json"`
ReleaseTime string `json:"released_at"`
Sha1 string `json:"sha1"`
Version string `json:"version"`
}
type InfoJSON struct {
Dependencies []string `json:"dependencies"`
FactorioVersion string `json:"factorio_version"`
}

93
utils.go Normal file
View File

@@ -0,0 +1,93 @@
package main
import (
"archive/zip"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strings"
)
//Utilities
func download(filedest string, url string) error {
resp, err := http.Get(url)
if err != nil {
return err
}
defer resp.Body.Close()
out, err := os.Create(filedest)
if err != nil {
return err
}
defer out.Close()
_, err = io.Copy(out, resp.Body)
return err
}
func unzip(src, dest string) ([]string, error) {
//store file names available in a array of strings
var filenames []string
r, err := zip.OpenReader(src)
if err != nil {
return filenames, err
}
defer r.Close()
for _, f := range r.File {
// Skip folders to prevent permission issues
if strings.Contains(f.Name, "__MACOSX") {
continue
}
fpath := filepath.Join(dest, f.Name)
// Checking for any invalid file paths
if !strings.HasPrefix(fpath, filepath.Clean(dest)+string(os.PathSeparator)) {
return filenames, fmt.Errorf("%s is an illegal filepath", fpath)
}
filenames = append(filenames, fpath)
if f.FileInfo().IsDir() {
os.MkdirAll(fpath, os.ModePerm)
continue
}
// Creating the files in the target directory
if err = os.MkdirAll(filepath.Dir(fpath), os.ModePerm); err != nil {
return filenames, err
}
outFile, err := os.OpenFile(fpath,
os.O_WRONLY|os.O_CREATE|os.O_TRUNC,
f.Mode())
if err != nil {
return filenames, err
}
rc, err := f.Open()
if err != nil {
return filenames, err
}
_, err = io.Copy(outFile, rc)
outFile.Close()
rc.Close()
if err != nil {
return filenames, err
}
}
return filenames, nil
}