1
0
Fork 0
mirror of https://git.sr.ht/~ashkeel/strimertul synced 2024-09-18 01:50:50 +00:00
strimertul/main.go
2022-01-27 16:49:18 +01:00

251 lines
6.9 KiB
Go

package main
import (
"embed"
"flag"
"fmt"
"io/fs"
"log"
"math/rand"
"os"
"runtime"
"time"
"go.uber.org/zap/zapcore"
jsoniter "github.com/json-iterator/go"
"go.uber.org/zap"
"github.com/strimertul/strimertul/modules"
"github.com/strimertul/strimertul/modules/database"
"github.com/strimertul/strimertul/modules/http"
"github.com/strimertul/strimertul/modules/loyalty"
"github.com/strimertul/strimertul/modules/stulbe"
"github.com/strimertul/strimertul/modules/twitch"
"github.com/dgraph-io/badger/v3"
"github.com/pkg/browser"
_ "net/http/pprof"
)
const AppHeader = `
_ _ _ O O _
__| |_ _ _(_)_ __ ___ _ _| |_ _ _| |
(_-< _| '_| | ' \/ -_) '_| _| || | |
/__/\__|_| |_|_|_|_\___|_| \__|\_,_|_| `
var appVersion = "v0.0.0-UNKNOWN"
//go:embed frontend/dist/*
var frontend embed.FS
var logger *zap.Logger
type ModuleConstructor = func(manager *modules.Manager) error
var moduleList = map[modules.ModuleID]ModuleConstructor{
modules.ModuleStulbe: stulbe.Register,
modules.ModuleLoyalty: loyalty.Register,
modules.ModuleTwitch: twitch.Register,
}
func main() {
// Get cmd line parameters
noHeader := flag.Bool("no-header", false, "Do not print the app header")
dbDir := flag.String("database-dir", "data", "Path to strimertül database dir")
debug := flag.Bool("debug", false, "Start in debug mode (more logging)")
json := flag.Bool("json", false, "Print logging in JSON format")
cleanup := flag.Bool("run-gc", false, "Run garbage collection and exit immediately after")
exportDB := flag.Bool("export", false, "Export database as JSON")
importDB := flag.String("import", "", "Import database from JSON file")
restoreDB := flag.String("restore", "", "Restore database from backup file")
backupDir := flag.String("backup-dir", "backups", "Path to directory with database backups")
backupInterval := flag.Int("backup-interval", 60, "Backup database every X minutes, 0 to disable")
flag.Parse()
rand.Seed(time.Now().UnixNano())
if *debug {
cfg := zap.NewDevelopmentConfig()
if *json {
cfg.Encoding = "json"
}
logger, _ = cfg.Build()
} else {
cfg := zap.NewProductionConfig()
if !*json {
cfg.Encoding = "console"
cfg.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
cfg.EncoderConfig.CallerKey = zapcore.OmitKey
}
logger, _ = cfg.Build()
}
defer logger.Sync()
undo := zap.RedirectStdLog(logger)
defer undo()
if !*noHeader {
// Print the app header and version info
_, _ = fmt.Fprintf(os.Stderr, "%s\n\n %s - %s/%s (%s)\n\n", AppHeader, appVersion, runtime.GOOS, runtime.GOARCH, runtime.Version())
}
// Create module manager
manager := modules.NewManager(logger)
// Loading routine
db, err := database.Open(badger.DefaultOptions(*dbDir), manager)
failOnError(err, "Could not open DB")
defer func() {
if err := db.Close(); err != nil {
logger.Error("Could not close DB", zap.Error(err))
}
}()
if *cleanup {
// Run DB garbage collection until it's done
var err error
for err == nil {
err = db.Client().RunValueLogGC(0.5)
}
return
}
if *exportDB {
// Export database to stdout
data, err := db.GetAll("")
failOnError(err, "Could not export database")
failOnError(jsoniter.ConfigFastest.NewEncoder(os.Stdout).Encode(data), "Could not encode database")
return
}
if *importDB != "" {
file, err := os.Open(*importDB)
failOnError(err, "Could not open import file")
var entries map[string]string
err = jsoniter.ConfigFastest.NewDecoder(file).Decode(&entries)
failOnError(err, "Could not decode import file")
errors := 0
imported := 0
for key, value := range entries {
err = db.PutKey(key, []byte(value))
if err != nil {
logger.Error("Could not import entry", zap.String("key", key), zap.Error(err))
errors += 1
} else {
imported += 1
}
}
_ = db.Client().Sync()
logger.Info("Imported database from file", zap.Int("imported", imported), zap.Int("errors", errors))
}
if *restoreDB != "" {
file, err := os.Open(*restoreDB)
failOnError(err, "Could not open backup")
err = db.RestoreOverwrite(file)
failOnError(err, "Could not restore database")
_ = db.Client().Sync()
logger.Info("Restored database from backup")
}
// Set meta keys
_ = db.PutKey("stul-meta/version", []byte(appVersion))
runMigrations(db)
for module, constructor := range moduleList {
err := constructor(manager)
if err != nil {
logger.Error("Could not register module", zap.String("module", string(module)))
} else {
//goland:noinspection GoDeferInLoop
defer func() {
if err := manager.Modules[module].Close(); err != nil {
logger.Error("Could not close module", zap.String("module", string(module)), zap.Error(err))
}
}()
}
}
// Create logger and endpoints
httpServer, err := http.NewServer(manager)
failOnError(err, "Could not initialize http server")
defer func() {
if err := httpServer.Close(); err != nil {
logger.Error("Could not close DB", zap.Error(err))
}
}()
fedir, _ := fs.Sub(frontend, "frontend/dist")
httpServer.SetFrontend(fedir)
go func() {
time.Sleep(time.Second) // THIS IS STUPID
dashboardURL := fmt.Sprintf("http://%s/ui", httpServer.Config.Bind)
err := browser.OpenURL(dashboardURL)
if err != nil {
logger.Warn(fmt.Sprintf("could not open browser, dashboard URL available at: %s", dashboardURL), zap.Error(err))
}
}()
// Run garbage collection every once in a while
go func() {
ticker := time.NewTicker(15 * time.Minute)
defer ticker.Stop()
for range ticker.C {
// Run DB garbage collection until it's done
var err error
for err == nil {
err = db.Client().RunValueLogGC(0.5)
}
}
}()
// Backup database periodically
go func() {
if *backupDir == "" {
logger.Warn("Backup directory not set, database backups are disabled (this is dangerous, power loss will result in your database being potentially wiped!)")
return
}
err := os.MkdirAll(*backupDir, 0755)
if err != nil {
logger.Error("Could not create backup directory, moving to a temporary folder", zap.Error(err))
*backupDir = os.TempDir()
logger.Info("Using temporary directory", zap.String("backup-dir", *backupDir))
return
}
ticker := time.NewTicker(time.Duration(*backupInterval) * time.Minute)
defer ticker.Stop()
for range ticker.C {
// Run backup procedure
file, err := os.Create(fmt.Sprintf("%s/%s.db", *backupDir, time.Now().Format("20060102-150405")))
if err != nil {
logger.Error("Could not create backup file", zap.Error(err))
continue
}
_, err = db.Client().Backup(file, 0)
if err != nil {
logger.Error("Could not backup database", zap.Error(err))
}
_ = file.Close()
logger.Info("Database backed up", zap.String("backup-file", file.Name()))
}
}()
// Start HTTP server
failOnError(httpServer.Listen(), "HTTP server stopped")
}
func failOnError(err error, text string) {
if err != nil {
fatalError(err, text)
}
}
func fatalError(err error, text string) {
log.Fatalf("FATAL ERROR OCCURRED: %s\n\n%s", text, err.Error())
}