gosora/extend/plugin_hyperdrive.go

262 lines
7.5 KiB
Go

// Highly experimental plugin for caching rendered pages for guests
package extend
import (
//"log"
"bytes"
"errors"
"net/http"
"net/http/httptest"
"strconv"
"strings"
"sync/atomic"
"time"
c "github.com/Azareal/Gosora/common"
"github.com/Azareal/Gosora/routes"
)
var hyperspace *Hyperspace
func init() {
c.Plugins.Add(&c.Plugin{UName: "hyperdrive", Name: "Hyperdrive", Author: "Azareal", Init: initHdrive, Deactivate: deactivateHdrive})
}
func initHdrive(pl *c.Plugin) error {
hyperspace = newHyperspace()
pl.AddHook("tasks_tick_topic_list", tickHdrive)
pl.AddHook("tasks_tick_widget_wol", tickHdriveWol)
pl.AddHook("route_topic_list_start", jumpHdriveTopicList)
pl.AddHook("route_forum_list_start", jumpHdriveForumList)
tickHdrive()
return nil
}
func deactivateHdrive(pl *c.Plugin) {
pl.RemoveHook("tasks_tick_topic_list", tickHdrive)
pl.RemoveHook("tasks_tick_widget_wol", tickHdriveWol)
pl.RemoveHook("route_topic_list_start", jumpHdriveTopicList)
pl.RemoveHook("route_forum_list_start", jumpHdriveForumList)
hyperspace = nil
}
type Hyperspace struct {
topicList atomic.Value
forumList atomic.Value
lastTopicListUpdate atomic.Value
}
func newHyperspace() *Hyperspace {
pageCache := new(Hyperspace)
blank := make(map[string][3][]byte, len(c.Themes))
pageCache.topicList.Store(blank)
pageCache.forumList.Store(blank)
pageCache.lastTopicListUpdate.Store(int64(0))
return pageCache
}
func tickHdriveWol(args ...interface{}) (skip bool, rerr c.RouteError) {
c.DebugLog("docking at wol")
return tickHdrive(args)
}
// TODO: Find a better way of doing this
func tickHdrive(args ...interface{}) (skip bool, rerr c.RouteError) {
c.DebugLog("Refueling...")
// Avoid accidentally caching already cached content
blank := make(map[string][3][]byte, len(c.Themes))
hyperspace.topicList.Store(blank)
hyperspace.forumList.Store(blank)
tListMap := make(map[string][3][]byte)
fListMap := make(map[string][3][]byte)
cacheTheme := func(tname string) (skip, fail bool, rerr c.RouteError) {
themeCookie := http.Cookie{Name: "current_theme", Value: tname, Path: "/", MaxAge: c.Year}
w := httptest.NewRecorder()
req := httptest.NewRequest("get", "/topics/", bytes.NewReader(nil))
req.AddCookie(&themeCookie)
user := c.GuestUser
head, rerr := c.UserCheck(w, req, &user)
if rerr != nil {
return true, true, rerr
}
rerr = routes.TopicList(w, req, &user, head)
if rerr != nil {
return true, true, rerr
}
if w.Code != 200 {
c.LogWarning(errors.New("not 200 for topic list in hyperdrive"))
return false, true, nil
}
buf := new(bytes.Buffer)
buf.ReadFrom(w.Result().Body)
gbuf, err := c.CompressBytesGzip(buf.Bytes())
if err != nil {
c.LogWarning(err)
return false, true, nil
}
bbuf, err := c.CompressBytesBrotli(buf.Bytes())
if err != nil {
c.LogWarning(err)
return false, true, nil
}
tListMap[tname] = [3][]byte{buf.Bytes(), gbuf, bbuf}
w = httptest.NewRecorder()
req = httptest.NewRequest("get", "/forums/", bytes.NewReader(nil))
user = c.GuestUser
head, rerr = c.UserCheck(w, req, &user)
if rerr != nil {
return true, true, rerr
}
rerr = routes.ForumList(w, req, &user, head)
if rerr != nil {
return true, true, rerr
}
if w.Code != 200 {
c.LogWarning(errors.New("not 200 for forum list in hyperdrive"))
return false, true, nil
}
buf = new(bytes.Buffer)
buf.ReadFrom(w.Result().Body)
gbuf, err = c.CompressBytesGzip(buf.Bytes())
if err != nil {
c.LogWarning(err)
return false, true, nil
}
bbuf, err = c.CompressBytesBrotli(buf.Bytes())
if err != nil {
c.LogWarning(err)
return false, true, nil
}
fListMap[tname] = [3][]byte{buf.Bytes(), gbuf, bbuf}
return false, false, nil
}
for tname, _ := range c.Themes {
skip, fail, rerr := cacheTheme(tname)
if fail || rerr != nil {
return skip, rerr
}
}
hyperspace.topicList.Store(tListMap)
hyperspace.forumList.Store(fListMap)
hyperspace.lastTopicListUpdate.Store(time.Now().Unix())
return false, nil
}
func jumpHdriveTopicList(args ...interface{}) (skip bool, rerr c.RouteError) {
theme := c.GetThemeByReq(args[1].(*http.Request))
p := hyperspace.topicList.Load().(map[string][3][]byte)
return jumpHdrive(p[theme.Name], args)
}
func jumpHdriveForumList(args ...interface{}) (skip bool, rerr c.RouteError) {
theme := c.GetThemeByReq(args[1].(*http.Request))
p := hyperspace.forumList.Load().(map[string][3][]byte)
return jumpHdrive(p[theme.Name], args)
}
func jumpHdrive( /*pg, */ p [3][]byte, args []interface{}) (skip bool, rerr c.RouteError) {
var tList []byte
w := args[0].(http.ResponseWriter)
r := args[1].(*http.Request)
var iw http.ResponseWriter
gzw, ok := w.(c.GzipResponseWriter)
//bzw, ok2 := w.(c.BrResponseWriter)
// !temp until global brotli
br := strings.Contains(r.Header.Get("Accept-Encoding"), "br")
if br && ok {
tList = p[2]
iw = gzw.ResponseWriter
} else if br {
tList = p[2]
iw = w
} else if ok {
tList = p[1]
iw = gzw.ResponseWriter
/*} else if ok2 {
tList = p[2]
iw = bzw.ResponseWriter
*/
} else {
tList = p[0]
iw = w
}
if len(tList) == 0 {
c.DebugLog("no itemlist in hyperspace")
return false, nil
}
//c.DebugLog("tList: ", tList)
// Avoid intercepting user requests as we only have guests in cache right now
user := args[2].(*c.User)
if user.ID != 0 {
c.DebugLog("not guest")
return false, nil
}
// Avoid intercepting search requests and filters as we don't have those in cache
//c.DebugLog("r.URL.Path:",r.URL.Path)
//c.DebugLog("r.URL.RawQuery:",r.URL.RawQuery)
if r.URL.RawQuery != "" {
return false, nil
}
if r.FormValue("js") == "1" || r.FormValue("i") == "1" {
return false, nil
}
c.DebugLog("Successful jump")
var etag string
lastUpdate := hyperspace.lastTopicListUpdate.Load().(int64)
c.DebugLog("lastUpdate:", lastUpdate)
if br {
h := iw.Header()
h.Set("X-I", "1")
h.Set("Content-Encoding", "br")
etag = "\"" + strconv.FormatInt(lastUpdate, 10) + "-b\""
} else if ok {
iw.Header().Set("X-I", "1")
etag = "\"" + strconv.FormatInt(lastUpdate, 10) + "-g\""
/*} else if ok2 {
iw.Header().Set("X-I", "1")
etag = "\"" + strconv.FormatInt(lastUpdate, 10) + "-b\""
*/
} else {
etag = "\"" + strconv.FormatInt(lastUpdate, 10) + "\""
}
if lastUpdate != 0 {
iw.Header().Set("ETag", etag)
if match := r.Header.Get("If-None-Match"); match != "" {
if strings.Contains(match, etag) {
iw.WriteHeader(http.StatusNotModified)
return true, nil
}
}
}
header := args[3].(*c.Header)
if br || ok /*ok2*/ {
iw.Header().Set("Content-Type", "text/html;charset=utf-8")
}
routes.FootHeaders(w, header)
iw.Write(tList)
return true, nil
}