package main import ( "embed" "flag" "fmt" "html/template" "io" "log" "net/http" "net/http/httputil" "net/url" "slices" "strings" "time" ) var ( port = flag.Int("port", 42069, "port to listen on") https = flag.Bool("https", false, "use https") serverStartTime = time.Now() ) var ( //go:embed templates/404.html.template notFoundString string notFoundTemplate = template.Must(template.New("notFound").Parse(notFoundString)) ) func notFound(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNotFound) type args struct { Path string } if err := notFoundTemplate.Execute(w, args{Path: r.URL.Path}); err != nil { log.Printf("Warning: notFound: %s", err) } } var ( //go:embed templates/outline.html.template outlineString string outlineTemplate = template.Must(template.New("outline").Parse(outlineString)) ) var ( //go:embed templates/blog/post.html.template postString string //go:embed templates/blog/posts blogPostFiles embed.FS ) func mustParseBlogPostTemplates() map[string]*template.Template { base, err := outlineTemplate.Clone() if err != nil { log.Fatalf("outlineTemplate.Clone: %s", err) } if _, err := base.New("body").Parse(postString); err != nil { log.Fatalf("post.html.template: %s", err) } postFiles, err := blogPostFiles.ReadDir("templates/blog/posts") if err != nil { log.Fatalf("read blog post templates: %s", err) } posts := make(map[string]*template.Template, len(postFiles)) for _, file := range postFiles { fileContent, err := blogPostFiles.ReadFile("templates/blog/posts/" + file.Name()) if err != nil { log.Fatalf("read blog post template %q: %s", file.Name(), err) } tmpl, err := base.Clone() if err != nil { log.Fatalf("base.Clone: %s", err) } if _, err := tmpl.New("post").Parse(string(fileContent)); err != nil { log.Fatalf("%q: %s", file.Name(), err) } posts[strings.TrimSuffix(file.Name(), ".html.template")] = tmpl } return posts } var blogPostTemplates = mustParseBlogPostTemplates() func blog(w http.ResponseWriter, r *http.Request) { tmpl, ok := blogPostTemplates[strings.TrimPrefix(r.URL.Path, "/blog/")] if !ok { notFound(w, r) return } if err := tmpl.ExecuteTemplate(w, "outline", nil); err != nil { log.Printf("Warning: blog: %s", err) } } type blogPost struct { Name string Slug string } func mustLoadBlogPosts() []blogPost { buf := new(strings.Builder) posts := make([]blogPost, 0, len(blogPostTemplates)) for slug, tmpl := range blogPostTemplates { buf.Reset() if err := tmpl.ExecuteTemplate(buf, "title", nil); err != nil { log.Fatalf("blog post %q: title: %s", slug, err) } posts = append(posts, blogPost{ Name: buf.String(), Slug: slug, }) } slices.SortFunc(posts, func(x, y blogPost) int { return strings.Compare(y.Slug, x.Slug) }) return posts } var ( //go:embed templates/index.html.template indexString string indexTemplate = template.Must(template.Must(outlineTemplate.Clone()).New("body").Parse(indexString)) blogPostsList = mustLoadBlogPosts() ) func index(w http.ResponseWriter, _ *http.Request) { if err := indexTemplate.ExecuteTemplate(w, "outline", blogPostsList); err != nil { log.Printf("Warning: index: %s", err) } } var ( //go:embed templates/pong.html.template pongString string pongTemplate = template.Must(template.New("pong").Parse(pongString)) ) func pong(w http.ResponseWriter, _ *http.Request) { if err := pongTemplate.Execute(w, nil); err != nil { log.Printf("Warning: pong: %s", err) } } //go:embed static var staticFiles embed.FS func static(w http.ResponseWriter, r *http.Request) { fileName := strings.TrimPrefix(r.URL.Path, "/") f, err := staticFiles.Open(fileName) if err != nil { notFound(w, r) return } defer f.Close() http.ServeContent(w, r, fileName, serverStartTime, f.(io.ReadSeeker)) } func main() { flag.Parse() mux := http.NewServeMux() mux.HandleFunc("/blog/", blog) mux.HandleFunc("/pong", pong) jellyfinURL, err := url.Parse("http://localhost:8096") if err != nil { log.Fatalf("Jellyfin URL: %s", err) } mux.Handle("/cinema/", httputil.NewSingleHostReverseProxy(jellyfinURL)) mux.HandleFunc("/static/", static) mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/" { index(w, r) } else { notFound(w, r) } }) httpServer := http.Server{ Addr: fmt.Sprintf(":%d", *port), Handler: mux, ReadTimeout: 5 * time.Second, WriteTimeout: 5 * time.Second, } log.Printf("Listening on %q", httpServer.Addr) if *https { log.Fatal(httpServer.ListenAndServeTLS("/var/lib/acme/roseh.moe/cert.pem", "/var/lib/acme/roseh.moe/key.pem")) } else { log.Fatal(httpServer.ListenAndServe()) } }