penguin/flatgit

flatgit showcase

internal/server/server.go

raw ยท 2420 bytes

package server

import (
	"context"
	"errors"
	"log/slog"
	"net/http"
	"path"
	"strings"
	"time"
)

type Options struct {
	Addr       string
	Root       string
	Logger     *slog.Logger
	WebhookMux func(*http.ServeMux)
}

func withCacheHeaders(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		ext := path.Ext(r.URL.Path)

		switch ext {
		case ".html", ".json", ".xml", ".txt":
			w.Header().Set("Cache-Control", "no-cache")
		case ".css", ".js", ".png", ".jpg", ".jpeg", ".gif", ".svg", ".ico", ".woff", ".woff2":
			w.Header().Set("Cache-Control", "public, max-age=31536000, immutable")
		default:
			// Raw files are tricky.
			// For now, keep them revalidating unless we make commit-addressed raw URLs.
			w.Header().Set("Cache-Control", "no-cache")
		}

		next.ServeHTTP(w, r)
	})
}

func ListenAndServe(ctx context.Context, opts Options) error {
	if opts.Addr == "" {
		opts.Addr = ":8080"
	}
	if opts.Logger == nil {
		opts.Logger = slog.Default()
	}

	mux := http.NewServeMux()
	mux.HandleFunc("/healthz", func(w http.ResponseWriter, _ *http.Request) {
		w.Header().Set("Content-Type", "text/plain; charset=utf-8")
		_, _ = w.Write([]byte("ok\n"))
	})
	if opts.WebhookMux != nil {
		opts.WebhookMux(mux)
	}
	fs := http.FileServer(http.Dir(opts.Root))
	mux.Handle("/", withCacheHeaders(fs))

	srv := &http.Server{
		Addr:              opts.Addr,
		Handler:           logRequests(opts.Logger, mux),
		ReadHeaderTimeout: 5 * time.Second,
	}

	errCh := make(chan error, 1)
	opts.Logger.Info("flatgit serving", "addr", opts.Addr, "root", opts.Root)

	go func() {
		errCh <- srv.ListenAndServe()
	}()

	select {
	case err := <-errCh:
		if errors.Is(err, http.ErrServerClosed) {
			return nil
		}
		return err
	case <-ctx.Done():
		opts.Logger.Info("flatgit shutting down")

		shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
		defer cancel()

		if err := srv.Shutdown(shutdownCtx); err != nil {
			return err
		}
		return nil
	}
}

func logRequests(log *slog.Logger, next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if strings.HasPrefix(r.URL.Path, "/healthz") {
			next.ServeHTTP(w, r)
			return
		}
		start := time.Now()
		next.ServeHTTP(w, r)
		log.Info("http request", "method", r.Method, "path", r.URL.Path, "remote", r.RemoteAddr, "dur", time.Since(start))
	})
}