2021-05-02 12:29:43 +00:00
package main
import (
"embed"
"flag"
"fmt"
"io/fs"
2022-01-27 15:49:18 +00:00
"log"
2021-09-18 20:06:22 +00:00
"math/rand"
2022-01-02 10:45:09 +00:00
"os"
2021-05-02 19:33:37 +00:00
"runtime"
2021-05-02 12:29:43 +00:00
"time"
2022-01-27 15:49:18 +00:00
"go.uber.org/zap/zapcore"
2022-01-02 10:45:09 +00:00
jsoniter "github.com/json-iterator/go"
2022-01-27 15:49:18 +00:00
"go.uber.org/zap"
2022-01-02 10:45:09 +00:00
2021-05-02 12:29:43 +00:00
"github.com/strimertul/strimertul/modules"
2021-11-23 10:34:02 +00:00
"github.com/strimertul/strimertul/modules/database"
"github.com/strimertul/strimertul/modules/http"
2021-05-02 12:29:43 +00:00
"github.com/strimertul/strimertul/modules/loyalty"
2021-05-14 14:37:54 +00:00
"github.com/strimertul/strimertul/modules/stulbe"
2021-05-14 11:15:38 +00:00
"github.com/strimertul/strimertul/modules/twitch"
2021-05-02 12:29:43 +00:00
"github.com/dgraph-io/badger/v3"
2021-11-23 10:34:02 +00:00
"github.com/pkg/browser"
2021-06-05 23:18:31 +00:00
_ "net/http/pprof"
2021-05-02 12:29:43 +00:00
)
const AppHeader = `
_ _ _ O O _
__ | | _ _ _ ( _ ) _ __ ___ _ _ | | _ _ _ | |
( _ - < _ | ' _ | | ' \ / - _ ) ' _ | _ | || | |
/ __ / \ __ | _ | | _ | _ | _ | _ \ ___ | _ | \ __ | \ _ , _ | _ | `
2021-12-06 13:47:38 +00:00
var appVersion = "v0.0.0-UNKNOWN"
2021-05-02 12:29:43 +00:00
//go:embed frontend/dist/*
var frontend embed . FS
2022-01-27 15:49:18 +00:00
var logger * zap . Logger
2021-05-02 12:29:43 +00:00
2021-12-09 10:45:10 +00:00
type ModuleConstructor = func ( manager * modules . Manager ) error
var moduleList = map [ modules . ModuleID ] ModuleConstructor {
modules . ModuleStulbe : stulbe . Register ,
modules . ModuleLoyalty : loyalty . Register ,
2022-01-12 11:02:54 +00:00
modules . ModuleTwitch : twitch . Register ,
2021-12-09 10:45:10 +00:00
}
2021-05-02 12:29:43 +00:00
func main ( ) {
2021-05-02 19:33:37 +00:00
// Get cmd line parameters
2022-01-02 10:45:09 +00:00
noHeader := flag . Bool ( "no-header" , false , "Do not print the app header" )
dbDir := flag . String ( "database-dir" , "data" , "Path to strimertül database dir" )
2022-01-27 15:49:18 +00:00
debug := flag . Bool ( "debug" , false , "Start in debug mode (more logging)" )
json := flag . Bool ( "json" , false , "Print logging in JSON format" )
2021-12-09 10:45:10 +00:00
cleanup := flag . Bool ( "run-gc" , false , "Run garbage collection and exit immediately after" )
2022-01-02 10:45:09 +00:00
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" )
2021-05-02 12:29:43 +00:00
flag . Parse ( )
2021-09-18 20:06:22 +00:00
rand . Seed ( time . Now ( ) . UnixNano ( ) )
2022-01-27 15:49:18 +00:00
if * debug {
cfg := zap . NewDevelopmentConfig ( )
if * json {
cfg . Encoding = "json"
}
logger , _ = cfg . Build ( )
2021-12-07 00:22:45 +00:00
} else {
2022-01-27 15:49:18 +00:00
cfg := zap . NewProductionConfig ( )
if ! * json {
cfg . Encoding = "console"
cfg . EncoderConfig . EncodeTime = zapcore . ISO8601TimeEncoder
cfg . EncoderConfig . CallerKey = zapcore . OmitKey
}
logger , _ = cfg . Build ( )
2021-05-02 19:33:37 +00:00
}
2022-01-27 15:49:18 +00:00
defer logger . Sync ( )
undo := zap . RedirectStdLog ( logger )
defer undo ( )
2021-05-02 19:33:37 +00:00
2022-01-02 10:45:09 +00:00
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 ( ) )
2021-11-23 10:34:02 +00:00
}
// Create module manager
2022-01-27 15:49:18 +00:00
manager := modules . NewManager ( logger )
2021-05-02 12:29:43 +00:00
// Loading routine
2022-01-02 10:45:09 +00:00
db , err := database . Open ( badger . DefaultOptions ( * dbDir ) , manager )
2021-05-02 12:29:43 +00:00
failOnError ( err , "Could not open DB" )
2021-12-09 12:32:58 +00:00
defer func ( ) {
if err := db . Close ( ) ; err != nil {
2022-01-27 15:49:18 +00:00
logger . Error ( "Could not close DB" , zap . Error ( err ) )
2021-12-09 12:32:58 +00:00
}
} ( )
2021-05-02 12:29:43 +00:00
2021-12-09 10:45:10 +00:00
if * cleanup {
// Run DB garbage collection until it's done
var err error
for err == nil {
err = db . Client ( ) . RunValueLogGC ( 0.5 )
2021-05-02 12:29:43 +00:00
}
2021-12-09 10:45:10 +00:00
return
2021-05-02 12:29:43 +00:00
}
2022-01-02 10:45:09 +00:00
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 {
2022-01-27 15:49:18 +00:00
logger . Error ( "Could not import entry" , zap . String ( "key" , key ) , zap . Error ( err ) )
2022-01-02 10:45:09 +00:00
errors += 1
} else {
imported += 1
}
}
2022-01-07 08:42:06 +00:00
_ = db . Client ( ) . Sync ( )
2022-01-27 15:49:18 +00:00
logger . Info ( "Imported database from file" , zap . Int ( "imported" , imported ) , zap . Int ( "errors" , errors ) )
2022-01-02 10:45:09 +00:00
}
if * restoreDB != "" {
file , err := os . Open ( * restoreDB )
failOnError ( err , "Could not open backup" )
2022-01-07 08:42:06 +00:00
err = db . RestoreOverwrite ( file )
2022-01-02 10:45:09 +00:00
failOnError ( err , "Could not restore database" )
2022-01-07 08:42:06 +00:00
_ = db . Client ( ) . Sync ( )
2022-01-27 15:49:18 +00:00
logger . Info ( "Restored database from backup" )
2022-01-02 10:45:09 +00:00
}
2021-12-09 10:45:10 +00:00
// Set meta keys
_ = db . PutKey ( "stul-meta/version" , [ ] byte ( appVersion ) )
2021-05-02 12:29:43 +00:00
2021-12-09 10:45:10 +00:00
runMigrations ( db )
2021-05-02 12:29:43 +00:00
2021-12-09 10:45:10 +00:00
for module , constructor := range moduleList {
err := constructor ( manager )
2021-11-19 18:37:42 +00:00
if err != nil {
2022-01-27 15:49:18 +00:00
logger . Error ( "Could not register module" , zap . String ( "module" , string ( module ) ) )
2021-11-19 18:37:42 +00:00
} else {
2021-12-09 10:45:10 +00:00
//goland:noinspection GoDeferInLoop
defer func ( ) {
if err := manager . Modules [ module ] . Close ( ) ; err != nil {
2022-01-27 15:49:18 +00:00
logger . Error ( "Could not close module" , zap . String ( "module" , string ( module ) ) , zap . Error ( err ) )
2021-12-09 10:45:10 +00:00
}
} ( )
2021-05-02 12:29:43 +00:00
}
}
// Create logger and endpoints
2021-11-23 10:34:02 +00:00
httpServer , err := http . NewServer ( manager )
2021-11-19 18:37:42 +00:00
failOnError ( err , "Could not initialize http server" )
2021-12-09 12:32:58 +00:00
defer func ( ) {
if err := httpServer . Close ( ) ; err != nil {
2022-01-27 15:49:18 +00:00
logger . Error ( "Could not close DB" , zap . Error ( err ) )
2021-12-09 12:32:58 +00:00
}
} ( )
2021-05-02 12:29:43 +00:00
fedir , _ := fs . Sub ( frontend , "frontend/dist" )
2021-11-19 18:37:42 +00:00
httpServer . SetFrontend ( fedir )
2021-05-02 12:29:43 +00:00
go func ( ) {
time . Sleep ( time . Second ) // THIS IS STUPID
2021-11-19 18:37:42 +00:00
dashboardURL := fmt . Sprintf ( "http://%s/ui" , httpServer . Config . Bind )
2021-10-28 09:01:52 +00:00
err := browser . OpenURL ( dashboardURL )
if err != nil {
2022-01-27 15:49:18 +00:00
logger . Warn ( fmt . Sprintf ( "could not open browser, dashboard URL available at: %s" , dashboardURL ) , zap . Error ( err ) )
2021-10-28 09:01:52 +00:00
}
2021-05-02 12:29:43 +00:00
} ( )
2021-12-05 21:17:52 +00:00
// Run garbage collection every once in a while
2021-12-06 11:22:54 +00:00
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 )
}
2021-12-05 21:17:52 +00:00
}
2021-12-06 11:22:54 +00:00
} ( )
2021-12-05 21:17:52 +00:00
2022-01-02 10:45:09 +00:00
// Backup database periodically
go func ( ) {
if * backupDir == "" {
2022-01-27 15:49:18 +00:00
logger . Warn ( "Backup directory not set, database backups are disabled (this is dangerous, power loss will result in your database being potentially wiped!)" )
2022-01-02 10:45:09 +00:00
return
}
err := os . MkdirAll ( * backupDir , 0755 )
if err != nil {
2022-01-27 15:49:18 +00:00
logger . Error ( "Could not create backup directory, moving to a temporary folder" , zap . Error ( err ) )
2022-01-02 10:45:09 +00:00
* backupDir = os . TempDir ( )
2022-01-27 15:49:18 +00:00
logger . Info ( "Using temporary directory" , zap . String ( "backup-dir" , * backupDir ) )
2022-01-02 10:45:09 +00:00
return
}
ticker := time . NewTicker ( time . Duration ( * backupInterval ) * time . Minute )
defer ticker . Stop ( )
for range ticker . C {
// Run backup procedure
2022-01-22 10:08:00 +00:00
file , err := os . Create ( fmt . Sprintf ( "%s/%s.db" , * backupDir , time . Now ( ) . Format ( "20060102-150405" ) ) )
2022-01-02 10:45:09 +00:00
if err != nil {
2022-01-27 15:49:18 +00:00
logger . Error ( "Could not create backup file" , zap . Error ( err ) )
2022-01-02 10:45:09 +00:00
continue
}
_ , err = db . Client ( ) . Backup ( file , 0 )
if err != nil {
2022-01-27 15:49:18 +00:00
logger . Error ( "Could not backup database" , zap . Error ( err ) )
2022-01-02 10:45:09 +00:00
}
_ = file . Close ( )
2022-01-27 15:49:18 +00:00
logger . Info ( "Database backed up" , zap . String ( "backup-file" , file . Name ( ) ) )
2022-01-02 10:45:09 +00:00
}
} ( )
2021-05-02 12:29:43 +00:00
// Start HTTP server
2021-11-19 18:37:42 +00:00
failOnError ( httpServer . Listen ( ) , "HTTP server stopped" )
2021-05-02 12:29:43 +00:00
}
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 ( ) )
}