package main import ( "context" "database/sql" "flag" "fmt" "log/slog" "net/http" "os" "os/signal" "syscall" "time" _ "github.com/jackc/pgx/v5/stdlib" "git.soup.land/soup/lookbook/internal/handlers" "git.soup.land/soup/lookbook/internal/middleware" "git.soup.land/soup/lookbook/internal/migrations" "git.soup.land/soup/lookbook/internal/static" "git.soup.land/soup/sxgo/ssr" ) const defaultAddr = ":8080" func main() { migrate := flag.Bool("migrate", false, "run database migrations and exit") rollback := flag.Bool("rollback", false, "roll back migrations and exit (one step by default)") rollbackTarget := flag.Int64("to", -1, "target version for rollback when using -rollback") dbURLFlag := flag.String("db-url", "", "database connection URL") flag.Parse() logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ Level: slog.LevelInfo, })) dbURL := *dbURLFlag if dbURL == "" { dbURL = os.Getenv("DATABASE_URL") } if dbURL == "" { dbURL = migrations.DefaultURL } switch { case *migrate && *rollback: logger.Error("choose either -migrate or -rollback, not both") os.Exit(1) case *migrate: if err := migrations.Up(context.Background(), dbURL, logger); err != nil { logger.Error("migration failed", slog.Any("err", err)) os.Exit(1) } return case *rollback: if err := migrations.Down(context.Background(), dbURL, *rollbackTarget, logger); err != nil { logger.Error("rollback failed", slog.Any("err", err)) os.Exit(1) } return } // Check for subcommand args := flag.Args() if len(args) < 1 { printUsage() os.Exit(1) } mode := args[0] switch mode { case "web": runWebServer(dbURL, logger) default: logger.Error("unknown mode", slog.String("mode", mode)) printUsage() os.Exit(1) } } func printUsage() { fmt.Fprintln(os.Stderr, "Usage:") fmt.Fprintln(os.Stderr, " lookbook web - Run web server") fmt.Fprintln(os.Stderr, "") fmt.Fprintln(os.Stderr, "Flags:") fmt.Fprintln(os.Stderr, " -migrate Run database migrations") fmt.Fprintln(os.Stderr, " -rollback Roll back migrations") fmt.Fprintln(os.Stderr, " -to Target version for rollback") fmt.Fprintln(os.Stderr, " -db-url Database connection URL") } func runWebServer(dbURL string, logger *slog.Logger) { pending, err := migrations.CheckPending(context.Background(), dbURL, logger) if err != nil { logger.Warn("could not check migration status", slog.Any("err", err)) } else if pending > 0 { logger.Warn("database has pending migrations", slog.Int("pending", pending), slog.String("hint", "run 'make migrate' to apply")) } db, err := sql.Open("pgx", dbURL) if err != nil { logger.Error("failed to open database", slog.Any("err", err)) os.Exit(1) } defer db.Close() if err := db.Ping(); err != nil { logger.Error("failed to ping database", slog.Any("err", err)) os.Exit(1) } rc := &handlers.RequestContext{ DB: db, Logger: logger, TmplCache: ssr.NewTmplCache(handlers.TemplateFuncs), } router := handlers.NewRouter(rc) router.Handle("GET /", handlers.HandleGetGallery) router.Handle("POST /items", handlers.HandlePostItem) router.Handle("GET /items/{id}", handlers.HandleGetItem) router.Handle("POST /items/{id}/tags", handlers.HandlePostItemTags) router.Handle("POST /items/{id}/delete", handlers.HandleDeleteItem) router.Handle("POST /items/{id}/refresh", handlers.HandleRefreshItem) router.Handle("GET /images/{id}", handlers.HandleGetImage) router.Handle("POST /auth/login", handlers.HandlePostAuthLogin) router.Handle("POST /auth/logout", handlers.HandlePostAuthLogout) router.Handle("GET /auth/status", handlers.HandleGetAuthStatus) router.HandleStd("GET /static/{version}/", static.Handler()) addr := defaultAddr if envAddr := os.Getenv("ADDR"); envAddr != "" { addr = envAddr } server := &http.Server{ Addr: addr, Handler: middleware.Logging(logger)(router), ReadHeaderTimeout: 5 * time.Second, } go func() { logger.Info("listening", slog.String("addr", addr)) if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { logger.Error("http server error", slog.Any("err", err)) os.Exit(1) } }() waitForShutdown(server, logger) } func waitForShutdown(server *http.Server, logger *slog.Logger) { quit := make(chan os.Signal, 1) signal.Notify(quit, os.Interrupt, syscall.SIGTERM) <-quit logger.Info("shutting down") ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() if err := server.Shutdown(ctx); err != nil { logger.Error("graceful shutdown failed", slog.Any("err", err)) } }