Compare commits

...

16 Commits

Author SHA1 Message Date
283ffb66eb rebuilt command flag system 2026-05-07 17:38:28 -06:00
3d24198ad3 Hotfix
my method of extracting archives does not preserve permission
this has be fixed with a workaround by using chmod to make factorio
executable
2026-05-04 13:11:29 -06:00
9e7bd21602 Added default config if config.yml is missing
Default config:
server:
  serverFolder: "factorio"
  worldFile: "factorio/saves/newworld.zip"
  serverSettings: "factorio/data/server-settings.json"
  serverExec: "factorio/bin/x64/factorio"
  port: 34197

factoryman:
  screen: True
  screenName: "Factorio"
  backupDir: "factorio/backups"
  username: ""
  apitoken: ""
2026-05-04 12:41:18 -06:00
e051d2d6f6 Changed GoConfig to UsrConfig
Changed GoConfig struct to UsrConfig
2026-05-04 11:50:08 -06:00
6ef8f280a8 Added README 2026-05-04 10:02:18 -06: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
aef97fe613 Added path checks and updated config
- Cleaned up func main()
- Separated config into two sections server and factoryman
- Updated backup.go to reflect changes to config file
- Updated config.go to reflect changes to config.yml as well as added a check to paths in config.yml
- Updated launch.go to reflect changes to config
move cli handler from main to cli.go
2026-04-29 11:39:55 -06:00
bdf636b0d3 updated gitignore and added new file to begin new feature 2026-04-28 15:54:41 -06:00
e7f967c6bd Reverted changes that used bubbletea lib
Fixed some minor issues with backup func
added stdout to backup functions
added stdout to launchserver functions
changed tar execs to add verbos for stdout
2026-01-14 15:13:59 -07:00
86ccf302ea Working on adding a TUI 2026-01-14 02:18:47 -07:00
b2c534f29b Fixes Changes and new additions
Fixed an issue where launching server w/o screen failed
New: added an output showing screen sessions after launching
New: added stdout and stdin to screenless launch allowing interacting with server cli
Change: Broke up source code to make it easier to read and make changes
Change: Updated .gitignore
2026-01-13 10:51:18 -07:00
14 changed files with 638 additions and 135 deletions

3
.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
.vscode
factorio-headless_linux_2.0.72
factorio

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

36
backup.go Normal file
View File

@@ -0,0 +1,36 @@
package main
import (
"fmt"
"os"
"os/exec"
"time"
)
func backUp(cmd string, c UsrConfig) {
switch cmd {
case "full":
fmt.Println("Starting full server backup")
t := time.Now()
timeStamp := t.Format(time.RFC3339)
fullBackupName := fmt.Sprintf("%s/ServerBackup.%s.tgz", c.Factoryman.BackupDir, timeStamp)
fullBackup := exec.Command("tar", "-czvf", fullBackupName, c.Server.ServDir)
fullBackup.Stdout = os.Stdout
err := fullBackup.Run()
if err != nil {
fmt.Printf("Backup Failed: %s\n", err)
}
case "saves":
fmt.Println("Backing up saves")
t := time.Now()
timeStamp := t.Format(time.RFC3339)
savesBackupName := fmt.Sprintf("%s/SaveBackup.%s.tgz", c.Factoryman.BackupDir, timeStamp)
saveDir := fmt.Sprintf("%s/saves", c.Server.ServDir)
savesBackup := exec.Command("tar", "-czvf", savesBackupName, saveDir)
savesBackup.Stdout = os.Stdout
err := savesBackup.Run()
if err != nil {
fmt.Printf("Backup Failed: %s\n", err)
}
}
}

43
config.go Normal file
View File

@@ -0,0 +1,43 @@
package main
import (
"fmt"
"log"
"os"
"gopkg.in/yaml.v3"
)
func DefCfg() *DefConfig {
c := &DefConfig{}
// server settings
c.Server.ServDir = "factorio"
c.Server.ServPort = 34197
c.Server.ServCfg = "factorio/data/server-settings.json"
c.Server.ServExec = "factorio/bin/x64/factorio"
c.Server.WorldFile = "factorio/saves/newworld.zip"
// factoryman settings
c.Factoryman.UseScreen = true
c.Factoryman.ScreenName = "factorio"
c.Factoryman.BackupDir = "factorio/backups"
c.Factoryman.UserName = ""
c.Factoryman.ApiToken = ""
return c
}
func readCfg(factCfg string) UsrConfig {
//read config file (YAML)
fileBytes, err := os.ReadFile(factCfg)
if err != nil {
fmt.Printf("Error reading config.yml file, using defaults\n")
return UsrConfig(*DefCfg())
}
// return Struct
var config UsrConfig
err = yaml.Unmarshal(fileBytes, &config)
if err != nil {
log.Fatalf("Error unmarshalling YAML file: %v", err)
}
return config
}

View File

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

Binary file not shown.

14
go.mod
View File

@@ -1,5 +1,15 @@
module gitlab.com/Raum0x2A/factoryman module gitlab.com/Raum0x2A/factoryman
go 1.22.2 go 1.25.7
require gopkg.in/yaml.v3 v3.0.1 require (
github.com/spf13/pflag v1.0.10
github.com/therootcompany/xz v1.0.1
gopkg.in/yaml.v3 v3.0.1
)
require (
github.com/kr/pretty v0.3.1 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
)

19
go.sum
View File

@@ -1,4 +1,21 @@
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
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/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/therootcompany/xz v1.0.1 h1:CmOtsn1CbtmyYiusbfmhmkpAAETj0wBIH6kCYaX+xzw=
github.com/therootcompany/xz v1.0.1/go.mod h1:3K3UH1yCKgBneZYhuQUvJ9HPD19UEXEI0BWbMn8qNMY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

42
launchserver.go Normal file
View File

@@ -0,0 +1,42 @@
package main
import (
"fmt"
"log"
"os"
"os/exec"
)
func startStopServer(cmd string, con UsrConfig) {
switch cmd {
case "start":
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
startScreenCmd.Stderr = os.Stderr
err := startScreenCmd.Run()
if err != nil {
log.Fatalf("Failed to start server: %s", err)
}
} else {
startSrvCmd := exec.Command("bash", "-c", x)
startSrvCmd.Stdout = os.Stdout
startSrvCmd.Stderr = os.Stdout
startSrvCmd.Stdin = os.Stdin
err := startSrvCmd.Run()
if err != nil {
log.Fatalf("Failed to start server: %s", err)
}
}
case "stop":
quitServerCmd := exec.Command("screen", "-S", con.Factoryman.ScreenName, "-p", "0", "-X", "stuff", "/quit\n")
err := quitServerCmd.Run()
if err != nil {
log.Fatalf("Command failed: %s, Error: %v", quitServerCmd.Args, err)
} else {
fmt.Println("Server Stopped")
}
}
}

213
main.go
View File

@@ -2,142 +2,97 @@ package main
import ( import (
"fmt" "fmt"
"log"
"os" "os"
"os/exec"
"time"
"gopkg.in/yaml.v3" flag "github.com/spf13/pflag"
) )
const fmConfig = "config.yml"
type GoConfig struct {
Config struct {
ServDir string `yaml:"serverFolder"`
ServPort int `yaml:"port"`
WorldFile string `yaml:"worldFile"`
ServCfg string `yaml:"serverSettings"`
ServExec string `yaml:"serverExec"`
BackupDir string `yaml:"backupDir"`
UseScreen bool `yaml:"screen"`
ScreenName string `yaml:"screenName"`
} `yaml:"server"`
}
func readCfg(factCfg string) GoConfig {
//read config file (YAML)
fileBytes, err := os.ReadFile(factCfg)
if err != nil {
log.Fatalf("Error reading config file: %v", err)
}
// return Struct
var config GoConfig
err = yaml.Unmarshal(fileBytes, &config)
if err != nil {
log.Fatalf("Error unmarshalling YAML file: %v", err)
}
return config
}
func startStopServer(cmd string, con GoConfig) {
switch cmd {
case "start":
x := fmt.Sprintf("%s --port %d --server-settings %s --start-server %s", con.Config.ServExec, con.Config.ServPort, con.Config.ServCfg, con.Config.WorldFile)
//x := fmt.Sprintf("%s --port %d --server-settings %s --start-server %s; exec sh", con.Config.ServExec, con.Config.ServPort, con.Config.ServCfg, con.Config.WorldFile)
///home/raum/factorio-serverfmt.Println(x)
if con.Config.UseScreen {
fmt.Println("Starting factorio server in screen session")
startScreenCmd := exec.Command("screen", "-dmS", con.Config.ScreenName, "bash", "-c", x, "; exec sh")
err := startScreenCmd.Run()
if err != nil {
log.Fatalf("Failed to start server: %s", err)
} else {
fmt.Printf("Started server on port %d, in screen named %s\n", con.Config.ServPort, con.Config.ScreenName)
}
} else {
startSrvCmd := exec.Command(x)
err := startSrvCmd.Run()
if err != nil {
log.Fatalf("Failed to start server: %s", err)
}
}
case "stop":
quitServerCmd := exec.Command("screen", "-S", con.Config.ScreenName, "-p", "0", "-X", "stuff", "/quit\n")
err := quitServerCmd.Run()
if err != nil {
log.Fatalf("Command failed: %s, Error: %v", quitServerCmd.Args, err)
} else {
fmt.Printf("Server in screen %s stopped\n", con.Config.ScreenName)
}
fmt.Println("Waiting for server to shutdown\r")
//time.Sleep(20 * time.Second)
//fmt.Println("Closing screen session ", con.Config.ScreenName)
//stopScreenCmd := exec.Command("screen", "-S", con.Config.ScreenName, "-p", "0", "-X", "stuff", "exit\n")
//err = stopScreenCmd.Run()
//if err != nil {
// log.Fatalf("Command failed: %s, Error: %v", stopScreenCmd.Args, err)
//} else {
// fmt.Printf("Screen \"%s\" closed\n", con.Config.ScreenName)
//}
}
}
func backUp(cmd string, c GoConfig) {
switch cmd {
case "full":
fmt.Println("Starting full server backup")
t := time.Now()
timeStamp := t.Format(time.RFC3339)
fullBackupName := fmt.Sprintf("%s/ServerBackup.%s.tgz", c.Config.BackupDir, timeStamp)
fullBackup := exec.Command("tar", "-czf", fullBackupName, c.Config.ServDir)
err := fullBackup.Run()
if err != nil {
fmt.Printf("Backup Failed: %s\n", err)
}
case "saves":
fmt.Println("Backing up saves")
t := time.Now()
timeStamp := t.Format(time.RFC3339)
savesBackupName := fmt.Sprintf("%s/SaveBackup.%s.tgz", c.Config.BackupDir, timeStamp)
saveDir := fmt.Sprintf("%s/saves", c.Config.ServDir)
savesBackup := exec.Command("tar", "-czf", savesBackupName, saveDir)
err := savesBackup.Run()
if err != nil {
fmt.Printf("Backup Failed: %s\n", err)
}
}
}
func main() { func main() {
c := readCfg(fmConfig) var c = readCfg("config.yml")
if len(os.Args) > 1 { //start command config flags
switch os.Args[1] { startCmd := flag.NewFlagSet("start", flag.ExitOnError)
case "start": startScreen := startCmd.BoolP("screen", "s", c.Factoryman.UseScreen, "Start server in screen session")
startStopServer("start", c) startServerExec := startCmd.StringP("exec", "e", c.Server.ServExec, "Path to server executable")
case "stop": startSettings := startCmd.StringP("config", "c", c.Server.ServCfg, "Server config json")
startStopServer("stop", c) startPort := startCmd.IntP("port", "p", c.Server.ServPort, "Port to start server on")
case "backup": startScrName := startCmd.StringP("name", "n", c.Factoryman.ScreenName, "Name for screen session")
if len(os.Args) > 2 { startWorld := startCmd.StringP("world", "w", c.Server.WorldFile, "World file")
switch os.Args[2] {
case "full": // stop command config flags
backUp("full", c) stopCmd := flag.NewFlagSet("stop", flag.ExitOnError)
case "saves": stopScrName := stopCmd.StringP("name", "n", c.Factoryman.ScreenName, "Screen session name")
backUp("saves", c)
default: // download command flags
fmt.Println("Invalid backup type: use 'full' or 'saves'") downloadCmd := flag.NewFlagSet("download", flag.ExitOnError)
} downloadSrvLoc := downloadCmd.StringP("path", "d", c.Server.ServDir, "Server Directory path")
} else { downloadAPIKey := downloadCmd.StringP("token", "t", c.Factoryman.ApiToken, "API token")
fmt.Println("Missing backup type: use 'full' or 'saves'") downloadUserName := downloadCmd.StringP("username", "u", c.Factoryman.UserName, "Factorio username")
}
// backup command flags
backupCmd := flag.NewFlagSet("backup", flag.ExitOnError)
backupDir := backupCmd.StringP("path", "d", c.Factoryman.BackupDir, "Path to backup directory")
if len(os.Args) < 2 {
fmt.Println("Expected subcommand. \n\tex.) start, stop, download")
os.Exit(1)
}
switch os.Args[1] {
case "start":
fmt.Println("starting server")
startCmd.Parse(os.Args[2:])
//map user input settings to config struct
c.Factoryman.UseScreen = *startScreen
c.Server.ServExec = *startServerExec
c.Server.ServCfg = *startSettings
c.Server.ServPort = *startPort
c.Factoryman.ScreenName = *startScrName
c.Server.WorldFile = *startWorld
startStopServer("start", c)
case "stop":
fmt.Println("stopping server")
stopCmd.Parse(os.Args[2:])
c.Factoryman.ScreenName = *stopScrName
startStopServer("stop", c)
case "download":
downloadCmd.Parse(os.Args[2:])
c.Server.ServDir = *downloadSrvLoc
c.Factoryman.ApiToken = *downloadAPIKey
c.Factoryman.UserName = *downloadUserName
switch os.Args[2] {
case "mods":
fmt.Println("Processing mod-list.json")
downloadMods(c)
case "server":
fmt.Println("starting download")
downloadHeadless(c)
default: default:
fmt.Printf("Unknown command: %s. Use 'start', 'stop', or 'backup'.\n", os.Args[1]) fmt.Println("mods or server")
os.Exit(1)
} }
} else { case "backup":
fmt.Println("Use 'start', 'stop', or 'backup' command") backupCmd.Parse(os.Args[2:])
fmt.Println("ex. factoryman start") c.Factoryman.BackupDir = *backupDir
fmt.Println("To configure edit 'conifg.yml'")
switch os.Args[2] {
case "saves":
backUp("saves", c)
case "full":
backUp("full", c)
default:
fmt.Println("saves or full")
os.Exit(1)
}
case "dev":
fmt.Println("=)")
default:
flag.Usage()
os.Exit(1)
} }
} }

84
mods.go Normal file
View File

@@ -0,0 +1,84 @@
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"os"
)
func downloadMods(c UsrConfig) {
//var c = readCfg("config.yml")
modlist := string(c.Server.ServDir) + "/mods/mod-list.json"
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)
}
}
}
}

49
serv-install.go Normal file
View File

@@ -0,0 +1,49 @@
package main
import (
"archive/tar"
"io"
"log"
"net/http"
"os"
"os/exec"
"path/filepath"
"github.com/therootcompany/xz"
)
func downloadHeadless(c UsrConfig) {
//var c = readCfg("config.yml")
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()
}
}
// zip does not preserve permissions correctly so i have to do this
exec.Command("chmod", "+x", c.Server.ServExec)
}

76
structs.go Normal file
View File

@@ -0,0 +1,76 @@
package main
// config struct
type UsrConfig 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"`
}
type DefConfig struct {
Server struct { // server specific settings
ServDir string
WorldFile string
ServCfg string
ServExec string
ServPort int
}
Factoryman struct { // factoryman settings
BackupDir string
UseScreen bool
ScreenName string
UserName string
ApiToken string
}
}
// 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
}