mirror of
https://github.com/glanceapp/glance.git
synced 2025-07-24 01:15:40 +02:00
270 lines
5.9 KiB
Go
270 lines
5.9 KiB
Go
package glance
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"html/template"
|
|
"log"
|
|
"net/http"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
var (
|
|
pageTemplate = mustParseTemplate("page.html", "document.html")
|
|
pageContentTemplate = mustParseTemplate("page-content.html")
|
|
pageThemeStyleTemplate = mustParseTemplate("theme-style.gotmpl")
|
|
)
|
|
|
|
type application struct {
|
|
Version string
|
|
Config config
|
|
ParsedThemeStyle template.HTML
|
|
|
|
slugToPage map[string]*page
|
|
widgetByID map[uint64]widget
|
|
}
|
|
|
|
func newApplication(config *config) (*application, error) {
|
|
app := &application{
|
|
Version: buildVersion,
|
|
Config: *config,
|
|
slugToPage: make(map[string]*page),
|
|
widgetByID: make(map[uint64]widget),
|
|
}
|
|
|
|
app.slugToPage[""] = &config.Pages[0]
|
|
|
|
providers := &widgetProviders{
|
|
assetResolver: app.AssetPath,
|
|
}
|
|
|
|
var err error
|
|
app.ParsedThemeStyle, err = executeTemplateToHTML(pageThemeStyleTemplate, &app.Config.Theme)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("parsing theme style: %v", err)
|
|
}
|
|
|
|
for p := range config.Pages {
|
|
page := &config.Pages[p]
|
|
page.PrimaryColumnIndex = -1
|
|
|
|
if page.Slug == "" {
|
|
page.Slug = titleToSlug(page.Title)
|
|
}
|
|
|
|
app.slugToPage[page.Slug] = page
|
|
|
|
for c := range page.Columns {
|
|
column := &page.Columns[c]
|
|
|
|
if page.PrimaryColumnIndex == -1 && column.Size == "full" {
|
|
page.PrimaryColumnIndex = int8(c)
|
|
}
|
|
|
|
for w := range column.Widgets {
|
|
widget := column.Widgets[w]
|
|
app.widgetByID[widget.id()] = widget
|
|
|
|
widget.setProviders(providers)
|
|
}
|
|
}
|
|
}
|
|
|
|
config = &app.Config
|
|
|
|
config.Server.BaseURL = strings.TrimRight(config.Server.BaseURL, "/")
|
|
config.Theme.CustomCSSFile = app.transformUserDefinedAssetPath(config.Theme.CustomCSSFile)
|
|
|
|
if config.Branding.FaviconURL == "" {
|
|
config.Branding.FaviconURL = app.AssetPath("favicon.png")
|
|
} else {
|
|
config.Branding.FaviconURL = app.transformUserDefinedAssetPath(config.Branding.FaviconURL)
|
|
}
|
|
|
|
config.Branding.LogoURL = app.transformUserDefinedAssetPath(config.Branding.LogoURL)
|
|
|
|
return app, nil
|
|
}
|
|
|
|
func (p *page) updateOutdatedWidgets() {
|
|
now := time.Now()
|
|
|
|
var wg sync.WaitGroup
|
|
context := context.Background()
|
|
|
|
for c := range p.Columns {
|
|
for w := range p.Columns[c].Widgets {
|
|
widget := p.Columns[c].Widgets[w]
|
|
|
|
if !widget.requiresUpdate(&now) {
|
|
continue
|
|
}
|
|
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
widget.update(context)
|
|
}()
|
|
}
|
|
}
|
|
|
|
wg.Wait()
|
|
}
|
|
|
|
func (a *application) transformUserDefinedAssetPath(path string) string {
|
|
if strings.HasPrefix(path, "/assets/") {
|
|
return a.Config.Server.BaseURL + path
|
|
}
|
|
|
|
return path
|
|
}
|
|
|
|
type pageTemplateData struct {
|
|
App *application
|
|
Page *page
|
|
}
|
|
|
|
func (a *application) handlePageRequest(w http.ResponseWriter, r *http.Request) {
|
|
page, exists := a.slugToPage[r.PathValue("page")]
|
|
|
|
if !exists {
|
|
a.handleNotFound(w, r)
|
|
return
|
|
}
|
|
|
|
pageData := pageTemplateData{
|
|
Page: page,
|
|
App: a,
|
|
}
|
|
|
|
var responseBytes bytes.Buffer
|
|
err := pageTemplate.Execute(&responseBytes, pageData)
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
w.Write([]byte(err.Error()))
|
|
return
|
|
}
|
|
|
|
w.Write(responseBytes.Bytes())
|
|
}
|
|
|
|
func (a *application) handlePageContentRequest(w http.ResponseWriter, r *http.Request) {
|
|
page, exists := a.slugToPage[r.PathValue("page")]
|
|
|
|
if !exists {
|
|
a.handleNotFound(w, r)
|
|
return
|
|
}
|
|
|
|
pageData := pageTemplateData{
|
|
Page: page,
|
|
}
|
|
|
|
var err error
|
|
var responseBytes bytes.Buffer
|
|
|
|
func() {
|
|
page.mu.Lock()
|
|
defer page.mu.Unlock()
|
|
|
|
page.updateOutdatedWidgets()
|
|
err = pageContentTemplate.Execute(&responseBytes, pageData)
|
|
}()
|
|
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
w.Write([]byte(err.Error()))
|
|
return
|
|
}
|
|
|
|
w.Write(responseBytes.Bytes())
|
|
}
|
|
|
|
func (a *application) handleNotFound(w http.ResponseWriter, _ *http.Request) {
|
|
// TODO: add proper not found page
|
|
w.WriteHeader(http.StatusNotFound)
|
|
w.Write([]byte("Page not found"))
|
|
}
|
|
|
|
func (a *application) handleWidgetRequest(w http.ResponseWriter, r *http.Request) {
|
|
widgetValue := r.PathValue("widget")
|
|
|
|
widgetID, err := strconv.ParseUint(widgetValue, 10, 64)
|
|
if err != nil {
|
|
a.handleNotFound(w, r)
|
|
return
|
|
}
|
|
|
|
widget, exists := a.widgetByID[widgetID]
|
|
|
|
if !exists {
|
|
a.handleNotFound(w, r)
|
|
return
|
|
}
|
|
|
|
widget.handleRequest(w, r)
|
|
}
|
|
|
|
func (a *application) AssetPath(asset string) string {
|
|
return a.Config.Server.BaseURL + "/static/" + staticFSHash + "/" + asset
|
|
}
|
|
|
|
func (a *application) server() (func() error, func() error) {
|
|
// TODO: add gzip support, static files must have their gzipped contents cached
|
|
// TODO: add HTTPS support
|
|
mux := http.NewServeMux()
|
|
|
|
mux.HandleFunc("GET /{$}", a.handlePageRequest)
|
|
mux.HandleFunc("GET /{page}", a.handlePageRequest)
|
|
|
|
mux.HandleFunc("GET /api/pages/{page}/content/{$}", a.handlePageContentRequest)
|
|
mux.HandleFunc("/api/widgets/{widget}/{path...}", a.handleWidgetRequest)
|
|
mux.HandleFunc("GET /api/healthz", func(w http.ResponseWriter, _ *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
})
|
|
|
|
mux.Handle(
|
|
fmt.Sprintf("GET /static/%s/{path...}", staticFSHash),
|
|
http.StripPrefix("/static/"+staticFSHash, fileServerWithCache(http.FS(staticFS), 24*time.Hour)),
|
|
)
|
|
|
|
var absAssetsPath string
|
|
if a.Config.Server.AssetsPath != "" {
|
|
absAssetsPath, _ = filepath.Abs(a.Config.Server.AssetsPath)
|
|
assetsFS := fileServerWithCache(http.Dir(a.Config.Server.AssetsPath), 2*time.Hour)
|
|
mux.Handle("/assets/{path...}", http.StripPrefix("/assets/", assetsFS))
|
|
}
|
|
|
|
server := http.Server{
|
|
Addr: fmt.Sprintf("%s:%d", a.Config.Server.Host, a.Config.Server.Port),
|
|
Handler: mux,
|
|
}
|
|
|
|
start := func() error {
|
|
a.Config.Server.StartedAt = time.Now()
|
|
log.Printf("Starting server on %s:%d (base-url: \"%s\", assets-path: \"%s\")\n",
|
|
a.Config.Server.Host,
|
|
a.Config.Server.Port,
|
|
a.Config.Server.BaseURL,
|
|
absAssetsPath,
|
|
)
|
|
|
|
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
stop := func() error {
|
|
return server.Close()
|
|
}
|
|
|
|
return start, stop
|
|
}
|