Initial lookbook implementation
Pinterest-style visual bookmarking app with: - URL metadata extraction (OG/Twitter meta, oEmbed fallback) - Image caching in Postgres with 480px thumbnails - Multi-tag filtering with Ctrl/Cmd for OR mode - Fuzzy tag suggestions and inline tag editing - Browser console auth() with first-use password setup - Brutalist UI with Commit Mono font and Pico CSS - Light/dark mode via browser preference
This commit is contained in:
commit
fc625fb9cf
486 changed files with 195373 additions and 0 deletions
168
cmd/server/main.go
Normal file
168
cmd/server/main.go
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
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 <version> Target version for rollback")
|
||||
fmt.Fprintln(os.Stderr, " -db-url <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))
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue