vitrine

main.go

// Command vitrine is a tiny read-only web viewer for local source trees.
//
// Each configured exhibit is served under its name as a URL prefix,
// e.g. "/moonshot-1/crates/iri/filetests/verify-errors.md".
// The default view is rendered (markdown becomes HTML, directories become listings);
// appending "?fmt=raw" serves the file bytes directly.
//
// Exhibits come from a YAML config file given via the required -config flag;
// see config.go for the schema and README.md for examples.
// The config can also declare forwards: URL prefixes reverse-proxied
// to other http services (websockets and SSE pass through); see serve.go.
package main

import (
	"flag"
	"log"
	"net/http"
	"os"
	"time"
)

func main() {
	addr := flag.String("addr", ":8000", "listen address")
	configPath := flag.String("config", "", "path to config file (required)")
	themeSeed := flag.String("theme-seed", "", "theme name override (default: first dot-segment of the hostname)")
	flag.Parse()

	if *configPath == "" {
		log.Fatal("the -config flag is required; there is no default config location")
	}
	seed := *themeSeed
	if seed == "" {
		hostname, err := os.Hostname()
		if err != nil {
			log.Fatalf("cannot determine hostname for theming (use -theme-seed to bypass): %v", err)
		}
		seed = hostLabel(hostname)
	}
	cfg, err := loadConfig(*configPath)
	if err != nil {
		log.Fatal(err)
	}
	srv := newServer(cfg, newTheme(seed))

	// Config changes apply live: reloads are fail-stable
	// (a malformed config logs loudly and the old exhibits keep serving).
	// A well-formed config naming an absent directory is not an error at all:
	// that exhibit is skipped with a warning, at startup and on reload alike.
	watchConfig(*configPath, 5*time.Second, func() {
		reloadConfig(srv, *configPath)
	})

	log.Printf("vitrine listening on %s", *addr)
	log.Fatal(http.ListenAndServe(*addr, srv))
}

// reloadConfig loads the config file and swaps the server's exhibits to it.
// A malformed config (parse or validation failure) logs loudly and leaves the
// previously served exhibits untouched: a fleet of vitrines must not go dark
// from one typo mid-edit.  A well-formed config whose directories are partly
// absent applies normally; the missing ones are skipped with a warning.
func reloadConfig(srv *server, path string) {
	cfg, err := loadConfig(path)
	if err != nil {
		log.Printf("config reload REJECTED, keeping previous exhibits: %v", err)
		return
	}
	srv.setMounts(cfg)
	log.Printf("config reloaded: %d exhibits", len(cfg.Exhibits))
}

main.go · 2.5 KiB · 964f50d4cb