148 lines
4.0 KiB
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
|
|
}
|