cachefs/pkg/srv/srv.go

167 lines
3.5 KiB
Go

// Copyright (C) 2022 Marius Schellenberger
package srv
import (
"errors"
"io"
stdfs "io/fs"
"math"
"net/http"
"path"
"strings"
"cachefs/pkg/fs"
"github.com/go-logr/logr"
)
const (
csp = "Content-Security-Policy"
indexCSP = "default-src 'none';style-src 'unsafe-inline';frame-ancestors 'none'"
videoCSP = indexCSP + ";script-src 'sha256-ZLSc/s5/US9uVMMZOJ/yWiS1tj9nEoi+5Qoohy3QetU=';media-src 'self'"
gib = 1024 * 1024 * 1024
)
type data struct {
QuotaCur float64
QuotaMax float64
Paths dirContents
PathAnchor string
}
type FileServer struct {
log logr.Logger
fs *fs.FS
h http.Handler
}
func NewFileServer(fs *fs.FS, log logr.Logger) http.Handler {
return &FileServer{log, fs, http.FileServer(fs)}
}
func skipLog(path string) bool {
return path == "/?o=preloads" ||
strings.HasSuffix(path, "favicon.ico")
}
func (fs *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
s := &statusInterceptor{w: w}
w = s
if !skipLog(r.URL.Path) {
defer func() {
if s.Status() == http.StatusPartialContent {
return
}
fs.log.Info("access",
"client", r.RemoteAddr,
"method", r.Method,
"status", s.Status(),
"uri", r.URL.Path,
)
}()
}
upath := r.URL.Path
if !strings.HasPrefix(upath, "/") {
upath = "/" + upath
r.URL.Path = upath
}
p := path.Clean(upath)
option := r.FormValue("o")
d, err := fs.fs.Stat(p)
if err != nil {
msg, code := toHTTPError(err)
http.Error(w, msg, code)
return
}
h := w.Header()
if option == "v" {
h.Set(csp, videoCSP)
err = video.Execute(w, data{Paths: dirContents{Base: r.URL.Path}})
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
return
}
if !d.IsDir() {
if option == "p" || option == "i" || option == "d" || option == "s" {
switch option {
case "p":
fs.fs.Preload(p, 0)
case "i":
fs.fs.Preload(p, 1)
case "d":
fs.fs.Preload(p, -1)
case "s":
fs.fs.RemovePreload(p)
}
rdir := "/"
if r.FormValue("r") == "preloads" {
http.Redirect(w, r, rdir+"?o=preloads", http.StatusFound)
return
}
if i := strings.LastIndex(p, "/"); i > 0 {
rdir = p[:i]
}
http.Redirect(w, r, rdir+"#"+anchor(p), http.StatusFound)
return
}
if option == "n" {
fs.fs.NoCache.ServeHTTP(w, r)
return
}
fs.h.ServeHTTP(w, r)
return
}
if option == "preloads" {
h.Set(csp, indexCSP)
cur, max := fs.fs.QuotaUsage()
err = preloads.Execute(w, data{
QuotaCur: math.Round(float64(cur)/gib*100) / 100,
QuotaMax: float64(max) / gib,
Paths: getPreloads(p, fs.fs),
})
if err != nil {
fs.log.Error(err, "error rendering preloads")
http.Error(w, err.Error(), http.StatusInternalServerError)
}
return
}
i := &responseInterceptor{w: w}
r.Header.Del("If-Modified-Since")
r.Header.Del("Cache-Control")
fs.h.ServeHTTP(i, r)
h.Del("Last-Modified")
paths, pathAnchor, err := i.GetPaths(p, fs.fs, false)
if err == io.EOF {
w.WriteHeader(i.Status())
return
}
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
h.Set(csp, indexCSP)
w.WriteHeader(i.Status())
err = index.Execute(w, data{Paths: paths, PathAnchor: pathAnchor})
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
func toHTTPError(err error) (string, int) {
if errors.Is(err, stdfs.ErrNotExist) {
return "404 page not found", http.StatusNotFound
}
if errors.Is(err, stdfs.ErrPermission) {
return "403 Forbidden", http.StatusForbidden
}
return "500 Internal Server Error", http.StatusInternalServerError
}