Initial Release
This commit is contained in:
@@ -0,0 +1,197 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user