package main
import (
"bytes"
"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 posts() []string {
postFiles, err := blogPostFiles.ReadDir("templates/blog/posts")
if err != nil {
log.Fatalf("blog posts: %s", err)
}
posts := make([]string, len(postFiles))
for i, f := range postFiles {
posts[i] = strings.TrimSuffix(f.Name(), ".html.template")
}
return posts
}
func postName(post string) string {
postBytes, err := blogPostFiles.ReadFile("templates/blog/posts/" + post + ".html.template")
if err != nil {
log.Fatalf("blog post %q: %s", post, err)
}
return string(bytes.TrimSuffix(bytes.TrimPrefix(postBytes[:bytes.IndexByte(postBytes, '\n')], []byte("
")), []byte("
")))
}
func preloadBlogPosts() map[string][]byte {
base, err := outlineTemplate.Clone()
if err != nil {
log.Fatalf("outlineTemplate.Clone: %s", err)
}
base, err = base.New("body").Parse(postString)
if err != nil {
log.Fatalf("post.html.template: invalid template: %s", err)
}
postSlugs := posts()
posts := make(map[string][]byte, len(postSlugs))
for _, post := range postSlugs {
postTemplate, err := base.Clone()
if err != nil {
log.Fatalf("base.Clone: %s", err)
}
postString, err := blogPostFiles.ReadFile("templates/blog/posts/" + post + ".html.template")
if err != nil {
log.Fatalf("somehow I fucked the paths up: %s", err)
}
postTemplate, err = postTemplate.New("post").Parse(string(postString))
if err != nil {
log.Fatalf("%q: invalid template: %s", post, err)
}
type args struct {
Title string
}
buf := new(bytes.Buffer)
if err := postTemplate.ExecuteTemplate(buf, "outline", args{Title: postName(post)}); err != nil {
log.Fatalf("%q: error: %s", post, err)
}
posts[post] = buf.Bytes()
}
return posts
}
var blogPostsHTML = preloadBlogPosts()
func blog(w http.ResponseWriter, r *http.Request) {
post, ok := blogPostsHTML[strings.TrimPrefix(r.URL.Path, "/blog/")]
if !ok {
notFound(w, r)
return
}
http.ServeContent(w, r, "post.html", serverStartTime, bytes.NewReader(post))
}
//go:embed templates/index.html.template
var indexString string
func preloadIndex() []byte {
base, err := outlineTemplate.Clone()
if err != nil {
log.Fatalf("outlineTemplate.Clone: %s", err)
}
base.Funcs(template.FuncMap{
"name": func(post string) string {
return post[:strings.IndexByte(post, '_')] + ": " + postName(post)
},
})
indexTemplate, err := base.New("body").Parse(indexString)
if err != nil {
log.Fatalf("index.html.template: invalid template: %s", err)
}
posts := posts()
slices.SortFunc(posts, func(x, y string) int { return strings.Compare(y, x) })
type args struct {
Title string
Posts []string
}
buf := new(bytes.Buffer)
if err := indexTemplate.ExecuteTemplate(buf, "outline", args{Title: "roseh.moe", Posts: posts}); err != nil {
log.Fatalf("index.html.template: error: %s", err)
}
return buf.Bytes()
}
var indexHTML = preloadIndex()
func index(w http.ResponseWriter, r *http.Request) {
http.ServeContent(w, r, "index.html", serverStartTime, bytes.NewReader(indexHTML))
}
//go:embed templates/pong.html.template
var pongString string
func preloadPong() []byte {
pongTemplate, err := template.New("pong").Parse(pongString)
if err != nil {
log.Fatalf("pong.html.template: invalid template: %s", err)
}
buf := new(bytes.Buffer)
if err := pongTemplate.Execute(buf, nil); err != nil {
log.Fatalf("pong.html.template: error: %s", err)
}
return buf.Bytes()
}
var pongHTML = preloadPong()
func pong(w http.ResponseWriter, r *http.Request) {
http.ServeContent(w, r, "pong.html", serverStartTime, bytes.NewReader(pongHTML))
}
//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())
}
}