[feature] Implement cache-control and etags for static assets (#711)

* start working on etag stuff

* add + use cache middleware

* generate etags on the fly

* remove unused field

* clean up filepath

* add license headers to cache files

* add attachgroup function to router interface

* move cache into web module

* rename a couple things

* remove attachStaticFS function from router

* rename + tidy up a few things

* mount assets filesystem

* create assetsFileInfoCache

* update comment

* simplify hash

* fix string fmt

* skip last mod chk, prefer strong etags w/long cache

* move base handler to its own file
this matches the modules in the api folder

* generate new etag if file was modified

* wrap strong etag in quotation marks as per spec

* clarify logic in avatar search

* make hashing a little niftier
This commit is contained in:
tobi
2022-07-18 12:55:06 +02:00
committed by GitHub
parent ab03318b7a
commit c3b6a5b0f9
11 changed files with 466 additions and 164 deletions

View File

@ -19,89 +19,14 @@
package web
import (
"errors"
"fmt"
"io/ioutil"
"net/http"
"path/filepath"
"strings"
"github.com/gin-gonic/gin"
"github.com/superseriousbusiness/gotosocial/internal/api"
"github.com/superseriousbusiness/gotosocial/internal/config"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/processing"
"github.com/superseriousbusiness/gotosocial/internal/router"
"github.com/superseriousbusiness/gotosocial/internal/uris"
)
const (
confirmEmailPath = "/" + uris.ConfirmEmailPath
tokenParam = "token"
usernameKey = "username"
statusIDKey = "status"
profilePath = "/@:" + usernameKey
statusPath = profilePath + "/statuses/:" + statusIDKey
)
// Module implements the api.ClientModule interface for web pages.
type Module struct {
processor processing.Processor
assetsPath string
adminPath string
defaultAvatars []string
}
// New returns a new api.ClientModule for web pages.
func New(processor processing.Processor) (api.ClientModule, error) {
assetsBaseDir := config.GetWebAssetBaseDir()
if assetsBaseDir == "" {
return nil, fmt.Errorf("%s cannot be empty and must be a relative or absolute path", config.WebAssetBaseDirFlag())
}
assetsPath, err := filepath.Abs(assetsBaseDir)
if err != nil {
return nil, fmt.Errorf("error getting absolute path of %s: %s", assetsBaseDir, err)
}
defaultAvatarsPath := filepath.Join(assetsPath, "default_avatars")
defaultAvatarFiles, err := ioutil.ReadDir(defaultAvatarsPath)
if err != nil {
return nil, fmt.Errorf("error reading default avatars at %s: %s", defaultAvatarsPath, err)
}
defaultAvatars := []string{}
for _, f := range defaultAvatarFiles {
// ignore directories
if f.IsDir() {
continue
}
// ignore files bigger than 50kb
if f.Size() > 50000 {
continue
}
extension := strings.TrimPrefix(strings.ToLower(filepath.Ext(f.Name())), ".")
// take only files with simple extensions
switch extension {
case "svg", "jpeg", "jpg", "gif", "png":
defaultAvatarPath := fmt.Sprintf("/assets/default_avatars/%s", f.Name())
defaultAvatars = append(defaultAvatars, defaultAvatarPath)
default:
continue
}
}
return &Module{
processor: processor,
assetsPath: assetsPath,
adminPath: filepath.Join(assetsPath, "admin"),
defaultAvatars: defaultAvatars,
}, nil
}
func (m *Module) baseHandler(c *gin.Context) {
host := config.GetHost()
instance, err := m.processor.InstanceGet(c.Request.Context(), host)
@ -114,85 +39,3 @@ func (m *Module) baseHandler(c *gin.Context) {
"instance": instance,
})
}
// TODO: abstract the {admin, user}panel handlers in some way
func (m *Module) AdminPanelHandler(c *gin.Context) {
host := config.GetHost()
instance, err := m.processor.InstanceGet(c.Request.Context(), host)
if err != nil {
api.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet)
return
}
c.HTML(http.StatusOK, "frontend.tmpl", gin.H{
"instance": instance,
"stylesheets": []string{
"/assets/Fork-Awesome/css/fork-awesome.min.css",
"/assets/dist/panels-admin-style.css",
},
"javascript": []string{
"/assets/dist/bundle.js",
"/assets/dist/admin-panel.js",
},
})
}
func (m *Module) UserPanelHandler(c *gin.Context) {
host := config.GetHost()
instance, err := m.processor.InstanceGet(c.Request.Context(), host)
if err != nil {
api.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet)
return
}
c.HTML(http.StatusOK, "frontend.tmpl", gin.H{
"instance": instance,
"stylesheets": []string{
"/assets/Fork-Awesome/css/fork-awesome.min.css",
"/assets/dist/_colors.css",
"/assets/dist/base.css",
"/assets/dist/panels-user-style.css",
},
"javascript": []string{
"/assets/dist/bundle.js",
"/assets/dist/user-panel.js",
},
})
}
// Route satisfies the RESTAPIModule interface
func (m *Module) Route(s router.Router) error {
// serve static files from assets dir at /assets
s.AttachStaticFS("/assets", fileSystem{http.Dir(m.assetsPath)})
s.AttachHandler(http.MethodGet, "/admin", m.AdminPanelHandler)
// redirect /admin/ to /admin
s.AttachHandler(http.MethodGet, "/admin/", func(c *gin.Context) {
c.Redirect(http.StatusMovedPermanently, "/admin")
})
s.AttachHandler(http.MethodGet, "/user", m.UserPanelHandler)
// redirect /settings/ to /settings
s.AttachHandler(http.MethodGet, "/user/", func(c *gin.Context) {
c.Redirect(http.StatusMovedPermanently, "/user")
})
// serve front-page
s.AttachHandler(http.MethodGet, "/", m.baseHandler)
// serve profile pages at /@username
s.AttachHandler(http.MethodGet, profilePath, m.profileGETHandler)
// serve statuses
s.AttachHandler(http.MethodGet, statusPath, m.threadGETHandler)
// serve email confirmation page at /confirm_email?token=whatever
s.AttachHandler(http.MethodGet, confirmEmailPath, m.confirmEmailGETHandler)
// 404 handler
s.AttachNoRouteHandler(func(c *gin.Context) {
api.ErrorHandler(c, gtserror.NewErrorNotFound(errors.New(http.StatusText(http.StatusNotFound))), m.processor.InstanceGet)
})
return nil
}