import (
"context"
"errors"
+ "fmt"
+ "io"
"log/slog"
"net"
"net/http"
"os"
+ "path"
+ "path/filepath"
"strings"
"sync"
"sync/atomic"
+
+ "git.earlybird.gay/today-engine/include"
+ "git.earlybird.gay/today-engine/page"
)
+func init() {
+ include.SetNotEligible("git.earlybird.gay/today-app/app")
+}
+
type App struct {
http.Server
http.ServeMux
Logger *slog.Logger
+ static map[string]http.Handler
+ explicitPages []*page.Page
+
running atomic.Bool
wg sync.WaitGroup
shutdown, stop chan struct{}
}()
}
+// Static serves the files in rootDir at rootPath.
+//
+// - .html files have their extensions stripped, and are served as their
+// filenames, following a directory structure. If there is an index.html
+// in a directory, it is served under the directory name, with a trailing
+// slash, so /foo/bar/index.html is served at /foo/bar/.
+// - Other non-excluded files are served with their extensions intact.
+// Currently, .go files and anything in a hidden directory are excluded.
+// MIME types are chosen by Go's standard HTTP library behavior until that
+// becomes a pain.
+// - If using today-engine, page.New and page.Static will *override* the
+// default behavior of Static. *This is only possible when using
+// today-engine.*
+// - If static is called twice for the same rootPath, the second call will
+// override the first.
+func (app *App) Static(rootPath, rootDir string) error {
+ if app.static == nil {
+ app.static = make(map[string]http.Handler)
+ }
+ var f func(fpath string) error
+ f = func(fpath string) error {
+ stat, err := os.Stat(fpath)
+ if err != nil {
+ return err
+ }
+ if stat.IsDir() {
+ // Recurse to subdirectories
+ entries, err := os.ReadDir(fpath)
+ if err != nil {
+ return err
+ }
+ for _, entry := range entries {
+ err = errors.Join(err, f(filepath.Join(fpath, entry.Name())))
+ }
+ return err
+ } else {
+ // Serve file
+ fileName := filepath.Base(fpath)
+ fileExt := filepath.Ext(fileName)
+ servePath := filepath.Join(rootPath, strings.TrimPrefix(fpath, rootDir))
+ var handler http.Handler
+ switch fileExt {
+ case ".go":
+ // explicit ignore .go
+ handler = nil
+ case ".html":
+ if fileName == "index.html" {
+ servePath = path.Clean(path.Dir(servePath) + "/{$}")
+ } else {
+ servePath = strings.TrimSuffix(servePath, ".html")
+ }
+ handler = page.Static(fpath)
+ default:
+ handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ f, err := os.Open(fpath)
+ if err != nil {
+ w.WriteHeader(http.StatusInternalServerError)
+ fmt.Fprintln(w, "failed to open file")
+ return
+ }
+ w.WriteHeader(http.StatusOK)
+ io.Copy(w, f)
+ })
+ }
+
+ // If not ignoring the file, handle it.
+ if handler != nil {
+ serveExpr := "GET " + servePath
+ app.static[serveExpr] = handler
+ }
+ return nil
+ }
+ }
+ return f(rootDir)
+}
+
+func (app *App) Handle(expr string, handler http.Handler) {
+ // Check page handlers against static
+ if todayPage, ok := handler.(*page.Page); ok {
+ app.explicitPages = append(app.explicitPages, todayPage)
+ }
+ app.ServeMux.Handle(expr, handler)
+}
+
+func (app *App) registerStatic() {
+registerStaticHandlers:
+ for expr, staticHandler := range app.static {
+ // If the staticHandler is a *page.Page,
+ if staticPage, ok := staticHandler.(*page.Page); ok {
+ // and the source file can be identified,
+ var staticPath string
+ if fopener, ok := staticPage.File().(include.FileOpener); !ok {
+ continue
+ } else {
+ staticPath = fopener.FileName()
+ }
+ // Make sure it isn't overridden by an explicitly handled *page.Page.
+ for _, expPage := range app.explicitPages {
+ var expPath string
+ if fopener, ok := expPage.File().(include.FileOpener); !ok {
+ continue
+ } else {
+ expPath = fopener.FileName()
+ }
+
+ // If paths are equal, continue to the next static handler.
+ if staticPath == expPath {
+ continue registerStaticHandlers
+ }
+ }
+ }
+ app.Handle(expr, staticHandler)
+ }
+}
+
func (app *App) ListenAndServe() error {
app.setDefaults()
+ app.registerStatic()
app.Logger.Info("application starting", "host", app.Addr)
var listener net.Listener