198 lines
4.7 KiB
Go
198 lines
4.7 KiB
Go
package routes
|
|
|
|
import (
|
|
"bakonpancakz/homepage/include"
|
|
"bytes"
|
|
"compress/gzip"
|
|
"crypto/md5"
|
|
"fmt"
|
|
"io/fs"
|
|
"log"
|
|
"mime"
|
|
"net/http"
|
|
"os"
|
|
"path"
|
|
"strings"
|
|
"sync"
|
|
"text/template"
|
|
)
|
|
|
|
type HttpHandler func(w http.ResponseWriter, r *http.Request)
|
|
|
|
var hashTable sync.Map
|
|
|
|
type ServeTemplateInfo struct {
|
|
FileSystem fs.FS
|
|
FilePaths []string
|
|
Filename string
|
|
Literals map[string]any
|
|
StatusCode int
|
|
ContentType string
|
|
}
|
|
|
|
func prepareStaticTemplate(info *ServeTemplateInfo, ds *[]byte, dc *[]byte, dh *string) error {
|
|
|
|
// Render Document
|
|
tmpl, err := template.ParseFS(info.FileSystem, info.FilePaths...)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var b bytes.Buffer
|
|
if err := tmpl.ExecuteTemplate(&b, info.Filename, info.Literals); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Compress Document
|
|
var c bytes.Buffer
|
|
g := gzip.NewWriter(&c)
|
|
g.Write(b.Bytes())
|
|
g.Close()
|
|
|
|
// Store Document
|
|
*ds = b.Bytes()
|
|
*dc = c.Bytes()
|
|
*dh = fmt.Sprintf("%X", md5.Sum(b.Bytes()))
|
|
|
|
return nil
|
|
}
|
|
|
|
func ServeStaticTemplate(info *ServeTemplateInfo) func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
// Defaults
|
|
if info.StatusCode == 0 {
|
|
info.StatusCode = http.StatusOK
|
|
}
|
|
if info.ContentType == "" {
|
|
info.ContentType = "text/html"
|
|
}
|
|
for i, p := range info.FilePaths {
|
|
info.FilePaths[i] = include.PreparePath(info.FileSystem, p)
|
|
}
|
|
|
|
// Render Document
|
|
var (
|
|
documentStandard []byte
|
|
documentCompress []byte
|
|
documentHash string
|
|
)
|
|
if err := prepareStaticTemplate(info, &documentStandard, &documentCompress, &documentHash); err != nil {
|
|
log.Fatalln("[routes/template] Cannot Prepare Template:", err)
|
|
}
|
|
|
|
// Serve Function
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodGet {
|
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
if !include.Embedded {
|
|
if err := prepareStaticTemplate(info, &documentStandard, &documentCompress, &documentHash); err != nil {
|
|
log.Println("[routes/template] Refresh Error:", err)
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
return
|
|
}
|
|
}
|
|
|
|
// Check Browser Cache
|
|
if r.Header.Get("If-None-Match") == documentHash {
|
|
w.WriteHeader(http.StatusNotModified)
|
|
return
|
|
}
|
|
|
|
// Send Document
|
|
w.Header().Set("ETag", documentHash)
|
|
w.Header().Set("Content-Type", info.ContentType)
|
|
w.Header().Set("Cache-Control", "public, max-age=3600, must-revalidate")
|
|
if strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
|
|
w.Header().Set("Content-Encoding", "gzip")
|
|
w.Write(documentCompress)
|
|
} else {
|
|
w.Write(documentStandard)
|
|
}
|
|
}
|
|
}
|
|
|
|
func ServeStaticFile(filepath string) HttpHandler {
|
|
var (
|
|
fileHash string
|
|
fileData []byte
|
|
fileMime string
|
|
)
|
|
|
|
// Prepare File
|
|
b, err := include.ReadFile(include.Public, filepath)
|
|
if err != nil {
|
|
log.Fatalln("[routes/static] Cannot Prepare File:", filepath, err)
|
|
}
|
|
fileMime = mime.TypeByExtension(path.Ext(filepath))
|
|
fileHash = fmt.Sprintf("%X", md5.Sum(b))
|
|
fileData = b
|
|
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodGet {
|
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
if r.Header.Get("If-None-Match") == fileHash {
|
|
w.WriteHeader(http.StatusNotModified)
|
|
return
|
|
}
|
|
w.Header().Add("ETag", fileHash)
|
|
w.Header().Add("X-Robots-Tag", "noindex")
|
|
w.Header().Add("Content-Type", fileMime)
|
|
w.Header().Add("Cache-Control", "public, max-age=604800, immutable")
|
|
w.Write(fileData)
|
|
}
|
|
}
|
|
|
|
func ServePublicFile(w http.ResponseWriter, r *http.Request, filepath string) {
|
|
if r.Method != http.MethodGet {
|
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
// Check Cache Hash
|
|
if hash, ok := hashTable.Load(filepath); ok {
|
|
if r.Header.Get("If-None-Match") == hash.(string) {
|
|
w.WriteHeader(http.StatusNotModified)
|
|
return
|
|
}
|
|
}
|
|
|
|
// Read File Contents
|
|
filedata, err := include.ReadFile(include.Public, filepath)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
w.WriteHeader(http.StatusNotFound)
|
|
return
|
|
}
|
|
log.Println("[http] Read Asset Error:", err)
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// Generate Cache Hash
|
|
filehash := fmt.Sprintf("%X", md5.Sum(filedata))
|
|
hashTable.Store(filepath, filehash)
|
|
fileMime := mime.TypeByExtension(path.Ext(filepath))
|
|
|
|
// Send Contents
|
|
w.Header().Add("ETag", filehash)
|
|
w.Header().Add("X-Robots-Tag", "noindex")
|
|
w.Header().Add("Content-Type", fileMime)
|
|
w.Header().Add("Cache-Control", "public, max-age=604800, immutable")
|
|
|
|
if len(filedata) < (1<<20) &&
|
|
!strings.Contains(fileMime, "image") &&
|
|
!strings.Contains(fileMime, "audio") &&
|
|
!strings.Contains(fileMime, "video") &&
|
|
strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
|
|
w.Header().Set("Content-Encoding", "gzip")
|
|
g := gzip.NewWriter(w)
|
|
g.Write(filedata)
|
|
g.Close()
|
|
} else {
|
|
w.Write(filedata)
|
|
}
|
|
}
|