verstak-sync-server/cmd/server/main.go

148 lines
4.0 KiB
Go

package main
import (
"context"
"errors"
"flag"
"fmt"
"log"
"net"
"net/http"
"os"
"os/signal"
"path/filepath"
"strings"
"syscall"
"time"
"github.com/verstak/verstak-sync-server/internal/server"
)
func main() {
dataDir := flag.String("data", "./server-data", "Data directory (db, blobs, config)")
listen := flag.String("listen", "", "HTTP listen address (default 127.0.0.1:47732)")
port := flag.Int("port", 0, "Deprecated compatibility override for the loopback port")
adminUser := flag.String("admin-user", "", "Create admin user (first run)")
adminPassFile := flag.String("admin-pass-file", "", "Read initial admin password from a 0600 file")
adminPassStdin := flag.Bool("admin-pass-stdin", false, "Read initial admin password from stdin")
showVersion := flag.Bool("version", false, "Print build version and exit")
flag.Parse()
if *showVersion {
fmt.Printf("verstak-sync-server %s (%s)\n", server.Version, server.BuildCommit)
return
}
absData, err := filepath.Abs(*dataDir)
if err != nil {
log.Fatalf("data dir: %v", err)
}
if err := os.MkdirAll(absData, 0750); err != nil {
log.Fatalf("create data dir: %v", err)
}
cfg, err := server.LoadConfig(absData)
if err != nil {
log.Fatalf("config: %v", err)
}
if envListen := strings.TrimSpace(os.Getenv("VERSTAK_LISTEN")); envListen != "" {
cfg.Listen = envListen
}
if *listen != "" {
cfg.Listen = *listen
}
if *port != 0 && *listen == "" {
cfg.Listen = fmt.Sprintf("127.0.0.1:%d", *port)
}
if publicURL := strings.TrimSpace(os.Getenv("VERSTAK_PUBLIC_URL")); publicURL != "" {
cfg.PublicURL = publicURL
}
if trusted := strings.TrimSpace(os.Getenv("VERSTAK_TRUSTED_PROXIES")); trusted != "" {
cfg.TrustedProxies = strings.Split(trusted, ",")
}
if err := cfg.Normalize(); err != nil {
log.Fatalf("config: %v", err)
}
adminPass, err := initialAdminPassword(*adminPassFile, *adminPassStdin)
if err != nil {
log.Fatalf("admin password: %v", err)
}
if (*adminUser == "") != (adminPass == "") {
log.Fatal("admin-user and one admin password source must be supplied together")
}
if *adminUser != "" {
if err := cfg.SetAdmin(*adminUser, adminPass); err != nil {
log.Fatalf("set admin: %v", err)
}
log.Printf("initial admin user %q configured", *adminUser)
}
dbPath := filepath.Join(absData, "server.db")
srv, err := server.NewServer(dbPath, absData, cfg)
if err != nil {
log.Fatalf("server: %v", err)
}
srv.SetupRoutes()
if err := srv.CleanupRetention(time.Now().UTC()); err != nil {
_ = srv.Close()
log.Fatalf("retention cleanup: %v", err)
}
addr := cfg.ListenAddress()
listener, err := net.Listen("tcp", addr)
if err != nil {
_ = srv.Close()
log.Fatalf("listen %s: %v", addr, err)
}
httpServer := srv.HTTPServer(addr)
serveDone := make(chan error, 1)
go func() { serveDone <- httpServer.Serve(listener) }()
log.Printf("Verstak Sync Server %s (%s) listening on %s", server.Version, server.BuildCommit, addr)
signals := make(chan os.Signal, 1)
signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM)
select {
case sig := <-signals:
log.Printf("received %s; shutting down", sig)
shutdownCtx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
err = httpServer.Shutdown(shutdownCtx)
cancel()
if err != nil {
log.Printf("graceful shutdown: %v", err)
}
case err = <-serveDone:
if err != nil && !errors.Is(err, http.ErrServerClosed) {
_ = srv.Close()
log.Fatalf("serve: %v", err)
}
}
if err := srv.Close(); err != nil {
log.Printf("close database: %v", err)
}
}
func initialAdminPassword(path string, useStdin bool) (string, error) {
if path != "" && useStdin {
return "", fmt.Errorf("choose either --admin-pass-file or --admin-pass-stdin")
}
if path == "" && !useStdin {
return "", nil
}
var data []byte
var err error
if path != "" {
data, err = os.ReadFile(path)
} else {
data, err = os.ReadFile("/dev/stdin")
}
if err != nil {
return "", err
}
password := strings.TrimSpace(string(data))
if password == "" {
return "", fmt.Errorf("password source is empty")
}
return password, nil
}