- FileFromFS with http.FS(embedFS) returned 301 redirects due to Gin setting c.Request.URL.Path to a relative path for http.FileServer - Replace with direct fs.ReadFile + Gin c.Data, determining content type via mime.TypeByExtension - Supports all asset types (JS, CSS, SVG, HTML) and SPA fallback
176 lines
4.7 KiB
Go
176 lines
4.7 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"io/fs"
|
|
"log"
|
|
"mime"
|
|
"net/http"
|
|
"os"
|
|
"os/signal"
|
|
"path/filepath"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
|
|
"arma3-web-server/embed"
|
|
"arma3-web-server/internal/api"
|
|
"arma3-web-server/internal/services"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
func main() {
|
|
devDeploy := getEnv("DEV_DEPLOY_DIR", filepath.Join("..", "dev-deploy"))
|
|
dataDir := mustAbs(getEnv("DATA_DIR", filepath.Join(devDeploy, "data")))
|
|
serverfileDir := mustAbs(getEnv("SERVERFILE_DIR", filepath.Join(devDeploy, "serverfiles")))
|
|
modsDir := mustAbs(getEnv("MODS_DIR", filepath.Join(devDeploy, "serverfiles", "mods")))
|
|
cfgDir := mustAbs(getEnv("CFG_DIR", filepath.Join(devDeploy, "serverfiles", "cfg")))
|
|
profilesDir := mustAbs(getEnv("PROFILES_DIR", filepath.Join(devDeploy, "serverfiles", "profiles")))
|
|
listen := getEnv("LISTEN", ":8080")
|
|
|
|
log.Printf("data dir: %s", dataDir)
|
|
log.Printf("serverfile dir: %s", serverfileDir)
|
|
log.Printf("mods dir: %s", modsDir)
|
|
log.Printf("cfg dir: %s", cfgDir)
|
|
log.Printf("profiles dir: %s", profilesDir)
|
|
log.Printf("listening on %s", listen)
|
|
|
|
dirs := []string{
|
|
dataDir, filepath.Join(dataDir, "modlists"),
|
|
serverfileDir, modsDir, cfgDir, profilesDir,
|
|
filepath.Join(serverfileDir, "steamapps", "workshop", "content", "107410"),
|
|
}
|
|
for _, dir := range dirs {
|
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
|
log.Fatalf("mkdir %s: %v", dir, err)
|
|
}
|
|
}
|
|
|
|
settings := services.NewSettingsManager(dataDir)
|
|
|
|
streamer := services.NewLogStreamer()
|
|
configMgr := services.NewConfigManager(cfgDir)
|
|
modlistMgr := services.NewModlistManager(dataDir)
|
|
process := services.NewProcessManager(serverfileDir, modsDir, cfgDir, profilesDir, settings, modlistMgr, configMgr, streamer)
|
|
steamcmd := services.NewSteamCmdManager(serverfileDir, streamer)
|
|
scheduler := services.NewScheduler(settings, modlistMgr, steamcmd)
|
|
|
|
r := gin.Default()
|
|
|
|
subFS, subErr := fs.Sub(embed.Frontend, "dist")
|
|
if subErr == nil {
|
|
if _, err := subFS.Open("index.html"); err == nil {
|
|
r.NoRoute(func(c *gin.Context) {
|
|
path := strings.TrimPrefix(c.Request.URL.Path, "/")
|
|
if path == "" {
|
|
path = "index.html"
|
|
}
|
|
data, err := fs.ReadFile(subFS, path)
|
|
if err == nil {
|
|
ct := mime.TypeByExtension(filepath.Ext(path))
|
|
if ct == "" {
|
|
ct = "text/html; charset=utf-8"
|
|
}
|
|
c.Data(http.StatusOK, ct, data)
|
|
return
|
|
}
|
|
data, _ = fs.ReadFile(subFS, "index.html")
|
|
c.Data(http.StatusOK, "text/html; charset=utf-8", data)
|
|
})
|
|
log.Print("serving embedded frontend")
|
|
}
|
|
}
|
|
|
|
handler := api.New(settings, configMgr, modlistMgr, process, steamcmd, scheduler, streamer, dataDir, serverfileDir, modsDir, cfgDir, profilesDir)
|
|
handler.SetupRoutes(r)
|
|
|
|
// Startup auto-tasks
|
|
s, err := settings.Load()
|
|
if err != nil {
|
|
log.Printf("startup: load settings: %v", err)
|
|
} else {
|
|
if s.AutoUpdateOnStartup && s.SteamUser != "" {
|
|
go func() {
|
|
log.Print("startup: auto-updating gameserver")
|
|
if err := steamcmd.UpdateGame(s.SteamBranch, s.SteamUser); err != nil {
|
|
log.Printf("startup: auto-update game failed: %v", err)
|
|
}
|
|
}()
|
|
}
|
|
|
|
if s.AutoUpdateModsOnStartup && s.ActiveModlist != "" {
|
|
go func() {
|
|
log.Print("startup: auto-updating mods")
|
|
ml, err := modlistMgr.Get(s.ActiveModlist)
|
|
if err != nil {
|
|
log.Printf("startup: load modlist for auto-update: %v", err)
|
|
return
|
|
}
|
|
var modIDs []string
|
|
for _, m := range ml.Mods {
|
|
if m.Enabled {
|
|
modIDs = append(modIDs, m.ID)
|
|
}
|
|
}
|
|
if len(modIDs) > 0 {
|
|
if err := steamcmd.DownloadMods(modIDs); err != nil {
|
|
log.Printf("startup: auto-update mods failed: %v", err)
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
|
|
if s.AutoStartOnStartup && s.WasRunning {
|
|
go func() {
|
|
log.Print("startup: auto-restarting server (was running before)")
|
|
if err := process.Start(); err != nil {
|
|
log.Printf("startup: auto-start failed: %v", err)
|
|
}
|
|
}()
|
|
}
|
|
}
|
|
|
|
// Start the scheduled updater
|
|
scheduler.Start()
|
|
|
|
log.Printf("starting server on %s", listen)
|
|
|
|
srv := &http.Server{Addr: listen, Handler: r}
|
|
|
|
go func() {
|
|
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
|
log.Fatalf("server: %v", err)
|
|
}
|
|
}()
|
|
|
|
quit := make(chan os.Signal, 1)
|
|
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
|
<-quit
|
|
|
|
log.Print("shutting down server...")
|
|
scheduler.Stop()
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
defer cancel()
|
|
if err := srv.Shutdown(ctx); err != nil {
|
|
log.Fatalf("server forced shutdown: %v", err)
|
|
}
|
|
log.Print("server exited")
|
|
}
|
|
|
|
func mustAbs(p string) string {
|
|
a, err := filepath.Abs(p)
|
|
if err != nil {
|
|
log.Fatalf("abs %s: %v", p, err)
|
|
}
|
|
return a
|
|
}
|
|
|
|
func getEnv(key, fallback string) string {
|
|
if v := os.Getenv(key); v != "" {
|
|
return v
|
|
}
|
|
return fallback
|
|
}
|