package main import ( "context" "encoding/json" "errors" "flag" "log" "net/http" "os" "os/signal" "path/filepath" "syscall" "time" "estus-shots/internal/server" ) type appConfig struct { MediaDir string `json:"media_dir"` } func main() { cfg := resolveConfig() srv, err := server.New(server.Config{MediaDir: cfg.MediaDir}) if err != nil { log.Fatalf("failed to configure server: %v", err) } httpServer := &http.Server{ Addr: srv.Address(), Handler: srv.Router(), ReadTimeout: 15 * time.Second, WriteTimeout: 15 * time.Second, IdleTimeout: 60 * time.Second, } log.Printf( "Estus Shots web server running on http://127.0.0.1%s (media: %s)", srv.Address(), cfg.MediaDir, ) go func() { if err := httpServer.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { log.Fatalf("server error: %v", err) } }() ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer stop() <-ctx.Done() shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() if err := httpServer.Shutdown(shutdownCtx); err != nil { log.Printf("graceful shutdown failed: %v", err) } else { log.Println("server stopped") } } func resolveConfig() appConfig { mediaDirFlag := flag.String("media-dir", "", "path to the media directory served at /media") configPath := flag.String("config", "", "optional path to a JSON config file") flag.Parse() cfg := appConfig{} if *configPath != "" { fileCfg, err := loadConfig(*configPath) if err != nil { log.Fatalf("failed to read config file: %v", err) } cfg = fileCfg } if *mediaDirFlag != "" { cfg.MediaDir = *mediaDirFlag } if cfg.MediaDir == "" { cfg.MediaDir = defaultMediaDir() } return cfg } func loadConfig(path string) (appConfig, error) { data, err := os.ReadFile(path) if err != nil { return appConfig{}, err } cfg := appConfig{} if err := json.Unmarshal(data, &cfg); err != nil { return appConfig{}, err } return cfg, nil } func defaultMediaDir() string { if cwd, err := os.Getwd(); err == nil { candidate := filepath.Join(cwd, "media") if info, err := os.Stat(candidate); err == nil && info.IsDir() { return candidate } } if exe, err := os.Executable(); err == nil { candidate := filepath.Join(filepath.Dir(exe), "media") if info, err := os.Stat(candidate); err == nil && info.IsDir() { return candidate } } return filepath.Join(".", "media") }