diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9f11b75 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.idea/ diff --git a/go.mod b/go.mod index 43397a3..4af84ad 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,8 @@ module github.com/Kugelschieber/schnittfest go 1.13 require ( - github.com/emvi/logbuch v0.0.0-20191002134629-fd76a46de20c // indirect + github.com/emvi/iso-639-1 v0.0.0-20190602002026-5ad2c26993cd + github.com/emvi/logbuch v0.0.0-20191002134629-fd76a46de20c github.com/gorilla/mux v1.7.3 // indirect github.com/rs/cors v1.7.0 // indirect ) diff --git a/go.sum b/go.sum index a8bb12c..a2450d4 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/emvi/iso-639-1 v0.0.0-20190602002026-5ad2c26993cd h1:x5d+Ikfu2nrNXdC8WThAdCcdajxG4JKqCkLpqAFqmpc= +github.com/emvi/iso-639-1 v0.0.0-20190602002026-5ad2c26993cd/go.mod h1:mghC4MDFyszxzH98ujf/K5whvB6B0nV4qCa5u94dP84= github.com/emvi/logbuch v0.0.0-20191002134629-fd76a46de20c h1:LsG8/aichRG7oGqTz77PQJuArY1id0kQttxNBuC/C3Q= github.com/emvi/logbuch v0.0.0-20191002134629-fd76a46de20c/go.mod h1:J2Wgbr3BuSc1JO+D2MBVh6q3WPVSK5GzktwWz8pvkKw= github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw= diff --git a/main.go b/main.go index 8d8a47f..8de9d72 100644 --- a/main.go +++ b/main.go @@ -1,6 +1,7 @@ package main import ( + "github.com/Kugelschieber/schnittfest/pages" "github.com/emvi/logbuch" "github.com/gorilla/mux" "github.com/rs/cors" @@ -23,6 +24,7 @@ const ( func configureLog() { logbuch.Info("Configure logging...") + logbuch.SetFormatter(logbuch.NewFieldFormatter("", "\t")) level := strings.ToLower(os.Getenv("SCHNITTFEST_LOGLEVEL")) if level == "debug" { @@ -45,20 +47,21 @@ func logEnvConfig() { func setupRouter() *mux.Router { router := mux.NewRouter() - router.PathPrefix(staticDirPrefix).Handler(http.StripPrefix(staticDirPrefix, http.FileServer(http.Dir(staticDir)))) + router.PathPrefix(staticDirPrefix).Handler(http.StripPrefix(staticDirPrefix, http.FileServer(http.Dir(staticDir)))).Methods("GET") + router.HandleFunc("/", pages.LandingPageHandler).Methods("GET") return router } func configureCors(router *mux.Router) http.Handler { logbuch.Info("Configuring CORS...") - origins := strings.Split(os.Getenv("ACCWEB_ALLOWED_ORIGINS"), ",") + origins := strings.Split(os.Getenv("SCHNITTFEST_ALLOWED_ORIGINS"), ",") c := cors.New(cors.Options{ AllowedOrigins: origins, AllowedMethods: []string{"GET", "POST", "PUT", "DELETE"}, AllowedHeaders: []string{"*"}, AllowCredentials: true, - Debug: strings.ToLower(os.Getenv("ACCWEB_CORS_LOGLEVEL")) == "debug", + Debug: strings.ToLower(os.Getenv("SCHNITTFEST_CORS_LOGLEVEL")) == "debug", }) return c.Handler(router) } @@ -70,16 +73,16 @@ func start(handler http.Handler) { readTimeout := defaultHttpReadTimeout var err error - if os.Getenv("ACCWEB_HTTP_WRITE_TIMEOUT") != "" { - writeTimeout, err = strconv.Atoi(os.Getenv("ACCWEB_HTTP_WRITE_TIMEOUT")) + if os.Getenv("SCHNITTFEST_HTTP_WRITE_TIMEOUT") != "" { + writeTimeout, err = strconv.Atoi(os.Getenv("SCHNITTFEST_HTTP_WRITE_TIMEOUT")) if err != nil { logbuch.Fatal(err.Error()) } } - if os.Getenv("ACCWEB_HTTP_READ_TIMEOUT") != "" { - readTimeout, err = strconv.Atoi(os.Getenv("ACCWEB_HTTP_READ_TIMEOUT")) + if os.Getenv("SCHNITTFEST_HTTP_READ_TIMEOUT") != "" { + readTimeout, err = strconv.Atoi(os.Getenv("SCHNITTFEST_HTTP_READ_TIMEOUT")) if err != nil { logbuch.Fatal(err.Error()) @@ -90,15 +93,15 @@ func start(handler http.Handler) { server := &http.Server{ Handler: handler, - Addr: os.Getenv("ACCWEB_HOST"), + Addr: os.Getenv("SCHNITTFEST_HOST"), WriteTimeout: time.Duration(writeTimeout) * time.Second, ReadTimeout: time.Duration(readTimeout) * time.Second, } // TODO certmagic - if strings.ToLower(os.Getenv("ACCWEB_TLS_ENABLE")) == "true" { + if strings.ToLower(os.Getenv("SCHNITTFEST_TLS_ENABLE")) == "true" { logbuch.Info("TLS enabled") - logbuch.Fatal("Error starting server", server.ListenAndServeTLS(os.Getenv("ACCWEB_TLS_CERT"), os.Getenv("ACCWEB_TLS_PKEY"))) + logbuch.Fatal("Error starting server", server.ListenAndServeTLS(os.Getenv("SCHNITTFEST_TLS_CERT"), os.Getenv("SCHNITTFEST_TLS_PKEY"))) } else { logbuch.Fatal("Error starting server", server.ListenAndServe()) } @@ -107,6 +110,7 @@ func start(handler http.Handler) { func main() { configureLog() logEnvConfig() + pages.LoadTemplates() router := setupRouter() corsConfig := configureCors(router) start(corsConfig) diff --git a/pages/footer.go b/pages/footer.go new file mode 100644 index 0000000..8929c4b --- /dev/null +++ b/pages/footer.go @@ -0,0 +1,5 @@ +package pages + +var footerComponentI18n = map[string]map[string]string{ + "de": {}, +} diff --git a/pages/landing_page.go b/pages/landing_page.go index 76e382d..ea39943 100644 --- a/pages/landing_page.go +++ b/pages/landing_page.go @@ -1 +1,38 @@ package pages + +import ( + "github.com/Kugelschieber/schnittfest/util" + "github.com/emvi/logbuch" + "net/http" +) + +var landingPageI18n = map[string]map[string]string{ + "de": { + "test": "Hello World", + }, +} + +func LandingPageHandler(w http.ResponseWriter, r *http.Request) { + tpl := tplCache.GetTemplate(landingPageTemplate) + + if tpl == nil { + w.WriteHeader(http.StatusNotFound) + return + } + + langCode := util.GetLangCode(r) + data := struct { + Vars map[string]string + NavbarVars map[string]string + FooterVars map[string]string + }{ + landingPageI18n[langCode], + navbarComponentI18n[langCode], + footerComponentI18n[langCode], + } + + if err := tpl.Execute(w, &data); err != nil { + logbuch.Error("Error rendering landing page", logbuch.Fields{"err": err}) + w.WriteHeader(http.StatusInternalServerError) + } +} diff --git a/pages/navbar.go b/pages/navbar.go new file mode 100644 index 0000000..012bd7f --- /dev/null +++ b/pages/navbar.go @@ -0,0 +1,5 @@ +package pages + +var navbarComponentI18n = map[string]map[string]string{ + "de": {}, +} diff --git a/pages/template.go b/pages/template.go new file mode 100644 index 0000000..4764309 --- /dev/null +++ b/pages/template.go @@ -0,0 +1,43 @@ +package pages + +import ( + "github.com/Kugelschieber/schnittfest/util" + "github.com/emvi/logbuch" + "os" + "path/filepath" +) + +const ( + defaultTemplateBase = "template" + landingPageTemplate = "landing_page" + notfoundPageTemplate = "notfound_page" +) + +var ( + tplCache *util.TemplateCache +) + +func LoadTemplates() { + tplCache = util.NewTemplateCache() + templateBase := os.Getenv("SCHNITTFEST_TEMPLATE_BASE") + + if templateBase == "" { + templateBase = defaultTemplateBase + } + + if _, err := tplCache.ParseFiles(landingPageTemplate, filepath.Join(templateBase, "landing_page.html"), + filepath.Join(templateBase, "head.html"), + filepath.Join(templateBase, "end.html"), + filepath.Join(templateBase, "navbar.html"), + filepath.Join(templateBase, "footer.html")); err != nil { + logbuch.Fatal("Error loading landing page template", logbuch.Fields{"err": err}) + } + + if _, err := tplCache.ParseFiles(notfoundPageTemplate, filepath.Join(templateBase, "404_page.html"), + filepath.Join(templateBase, "head.html"), + filepath.Join(templateBase, "end.html"), + filepath.Join(templateBase, "navbar.html"), + filepath.Join(templateBase, "footer.html")); err != nil { + logbuch.Fatal("Error loading 404 page template", logbuch.Fields{"err": err}) + } +} diff --git a/run_dev.sh b/run_dev.sh new file mode 100755 index 0000000..1555ad8 --- /dev/null +++ b/run_dev.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +export SCHNITTFEST_HOST=localhost:8080 +export SCHNITTFEST_LOGLEVEL=debug +export SCHNITTFEST_ALLOWED_ORIGINS=* +export SCHNITTFEST_TLS_ENABLE=false + +go run main.go diff --git a/template/404_page.html b/template/404_page.html new file mode 100644 index 0000000..e69de29 diff --git a/template/end.html b/template/end.html new file mode 100644 index 0000000..e69de29 diff --git a/template/footer.html b/template/footer.html new file mode 100644 index 0000000..e69de29 diff --git a/template/head.html b/template/head.html new file mode 100644 index 0000000..e69de29 diff --git a/template/landing_page.html b/template/landing_page.html index e69de29..1a92b1e 100644 --- a/template/landing_page.html +++ b/template/landing_page.html @@ -0,0 +1 @@ +{{index .Vars "test"}} diff --git a/template/navbar.html b/template/navbar.html new file mode 100644 index 0000000..e69de29 diff --git a/util/cache.go b/util/cache.go new file mode 100644 index 0000000..29da355 --- /dev/null +++ b/util/cache.go @@ -0,0 +1,55 @@ +package util + +import ( + "github.com/emvi/logbuch" + "html/template" + "sync" +) + +// TemplateCache caches templates. +type TemplateCache struct { + templates map[string]*template.Template + mutex sync.RWMutex +} + +// NewTemplateCache creates a new template cache. +func NewTemplateCache() *TemplateCache { + return &TemplateCache{templates: make(map[string]*template.Template)} +} + +// ParseFiles parses the given template files and stores them as one template called name. +func (tplcache *TemplateCache) ParseFiles(name string, files ...string) (*template.Template, error) { + if tplcache.templates[name] != nil { + tplcache.mutex.RLock() + defer tplcache.mutex.RUnlock() + return tplcache.templates[name], nil + } + + tplcache.mutex.Lock() + defer tplcache.mutex.Unlock() + logbuch.Debug("Caching template", logbuch.Fields{"name": name}) + tpl, err := template.ParseFiles(files...) + + if err != nil { + return nil, err + } + + tplcache.templates[name] = tpl + return tpl, nil +} + +// GetTemplate returns a cached template by name or nil if not found. +func (tplcache *TemplateCache) GetTemplate(name string) *template.Template { + if tplcache.templates[name] != nil { + tplcache.mutex.RLock() + defer tplcache.mutex.RUnlock() + return tplcache.templates[name] + } + + return nil +} + +// Clear clears the template cache. +func (tplcache *TemplateCache) Clear() { + tplcache.templates = make(map[string]*template.Template) +} diff --git a/util/lang.go b/util/lang.go new file mode 100644 index 0000000..f34f25b --- /dev/null +++ b/util/lang.go @@ -0,0 +1,39 @@ +package util + +import ( + iso6391 "github.com/emvi/iso-639-1" + "net/http" + "strings" +) + +var ( + supportedLangCodes = map[string]bool{"de": true} + defaultLangCode = "de" +) + +func getLangCodeFromHeader(r *http.Request) string { + header := r.Header.Get("Accept-Language") + parts := strings.Split(header, ";") + + if len(parts) == 0 || len(parts[0]) < 2 { + return defaultLangCode + } + + code := strings.ToLower(parts[0][:2]) + + if iso6391.ValidCode(code) { + return code + } + + return defaultLangCode +} + +func GetLangCode(r *http.Request) string { + langCode := getLangCodeFromHeader(r) + + if _, ok := supportedLangCodes[langCode]; ok { + return langCode + } + + return defaultLangCode +}