Files
RenewCMS/cmd/server/main.go
T
2026-10-08 02:33:32 +02:00

199 lines
5.3 KiB
Go

package main
import (
"RenewCMS/api"
validator "RenewCMS/api/pkg/validator"
"RenewCMS/internal/infrastructure/config"
"context"
"errors"
"fmt"
"log"
"net"
"net/http"
"os"
"os/signal"
"slices"
"syscall"
"time"
"github.com/joho/godotenv"
)
var version = "dev"
const (
readHeaderTimeout = 10 * time.Second
readTimeout = 30 * time.Second
writeTimeout = 60 * time.Second
idleTimeout = 120 * time.Second
shutdownTimeout = 10 * time.Second
)
var possibleEnvFileLocations = []string{".env", "../.env"}
var requiredEnvVars = []string{
"PUBLIC_URL",
"JWT_SECRET",
"SMTP_LOGIN",
"SMTP_SENDER",
"SMTP_PASSWORD",
"SMTP_HOST",
"SMTP_PORT",
}
var envDefaults = map[string]string{
"ENVIRONMENT": "production",
"PORT": "8080",
"DB_FILE": "./data/renewcms.db",
config.EnvDatabaseDriver: config.DefaultDatabaseDriver,
"DB_TLS": config.DefaultDatabaseTLS,
"DB_SSLMODE": config.DefaultDatabaseSSLMode,
"DB_MAX_OPEN_CONNS": config.DefaultMaxOpenConns,
"DB_MAX_IDLE_CONNS": config.DefaultMaxIdleConns,
"DB_CONN_MAX_LIFETIME_SECONDS": config.DefaultConnMaxLifetimeSeconds,
"UPLOAD_DIR": "./data/uploads",
config.EnvStorageDriver: config.DefaultStorageDriver,
"S3_REGION": config.DefaultS3Region,
"S3_PATH_STYLE": config.DefaultS3PathStyle,
"S3_TLS": config.DefaultS3TLS,
"AUTH_RATE_LIMIT_MAX_ATTEMPTS": "5",
"AUTH_RATE_LIMIT_WINDOW_SECONDS": "60",
"TRUSTED_PROXIES": "",
"UPDATE_FEED_URL": "https://git.floriansylvain.fr",
"RENEWCMS_CONTAINER": "false",
config.EnvImageVariantMaxSourceBytes: config.DefaultImageVariantMaxSourceBytes,
config.EnvImageVariantMaxSourcePixels: config.DefaultImageVariantMaxSourcePixels,
config.EnvImageVariantMaxConcurrentRenders: config.DefaultImageVariantMaxConcurrentRenders,
}
const versionFlag = "--version"
func main() {
if len(os.Args) == 2 && os.Args[1] == versionFlag {
fmt.Println(version)
return
}
if len(os.Args) > 1 {
os.Exit(runCommand(os.Args[1:], os.Stdout, os.Stderr))
}
if err := run(); err != nil {
log.Fatal(err)
}
}
func run() error {
initEnvVariables()
settings, err := config.NewSettingsFromEnv()
if err != nil {
return err
}
settings.Version = version
container, closeDb := api.InitContainer(settings)
defer closeDatabase(closeDb)
validator.InitValidator()
server := newServer(api.InitRoutes(container, settings), settings)
return serveUntilStopped(server, settings, container.RecordRunningInfoUseCase.Record, container.RunSchedulers)
}
func initEnvVariables() {
loadEnvFile()
applyEnvDefaults()
ensureRequiredEnvVars()
}
func loadEnvFile() {
for _, envLocation := range possibleEnvFileLocations {
if godotenv.Load(envLocation) == nil {
return
}
}
log.Println("WARNING: Could not load any .env file")
}
func applyEnvDefaults() {
for envVar, value := range envDefaults {
if _, isSet := os.LookupEnv(envVar); !isSet {
_ = os.Setenv(envVar, value)
}
}
}
func ensureRequiredEnvVars() {
required := slices.Concat(
requiredEnvVars,
config.DatabaseRequiredEnvVars(os.Getenv(config.EnvDatabaseDriver)),
config.StorageRequiredEnvVars(os.Getenv(config.EnvStorageDriver)),
)
if err := config.MissingEnvVarError(required, os.LookupEnv); err != nil {
panic(err.Error())
}
}
func closeDatabase(closeDb func() error) {
if err := closeDb(); err != nil {
log.Println("error closing database:", err)
}
}
func newServer(handler http.Handler, settings config.Settings) *http.Server {
return &http.Server{
Addr: ":" + settings.Port,
Handler: handler,
ReadHeaderTimeout: readHeaderTimeout,
ReadTimeout: readTimeout,
WriteTimeout: writeTimeout,
IdleTimeout: idleTimeout,
}
}
func serveUntilStopped(server *http.Server, settings config.Settings, recordRunning func() error, runScheduler func(context.Context)) error {
stopSignal, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
schedulerDone := make(chan struct{})
go func() {
defer close(schedulerDone)
runScheduler(stopSignal)
}()
defer func() {
stop()
<-schedulerDone
}()
serverErr := make(chan error, 1)
go listen(server, serverErr, settings, recordRunning)
select {
case err := <-serverErr:
return err
case <-stopSignal.Done():
return shutdown(server)
}
}
func listen(server *http.Server, serverErr chan<- error, settings config.Settings, recordRunning func() error) {
listener, err := net.Listen("tcp", server.Addr)
if err != nil {
serverErr <- err
return
}
if err := recordRunning(); err != nil {
log.Printf("WARNING: could not write the running-version file: %v", err)
}
log.Printf("RenewCMS %s (%s) listening on %s, public URL %s", version, settings.Environment, server.Addr, settings.PublicURL)
err = server.Serve(listener)
if errors.Is(err, http.ErrServerClosed) {
err = nil
}
serverErr <- err
}
func shutdown(server *http.Server) error {
log.Println("shutting down...")
ctx, cancel := context.WithTimeout(context.Background(), shutdownTimeout)
defer cancel()
return server.Shutdown(ctx)
}