prefer_precompressed

This commit is contained in:
3v0k4 2024-04-30 11:15:39 +02:00
parent 87c7127c28
commit 188dbc4e8b

View file

@ -15,6 +15,8 @@
package fileserver
import (
"bytes"
"compress/gzip"
"errors"
"fmt"
"io"
@ -36,6 +38,29 @@ import (
"github.com/caddyserver/caddy/v2/modules/caddyhttp/encode"
)
type VirtualFile struct {
content *bytes.Reader
fileInfo fs.FileInfo
}
func (vf VirtualFile) Close() error {
return nil
}
func (vf VirtualFile) Read(p []byte) (n int, err error) {
n, err = vf.content.Read(p)
return
}
func (vf VirtualFile) Stat() (fs.FileInfo, error) {
return vf.fileInfo, nil
}
func (vf VirtualFile) Seek(offset int64, whence int) (n int64, err error) {
n, err = vf.content.Seek(offset, whence)
return
}
func init() {
caddy.RegisterModule(FileServer{})
}
@ -273,8 +298,138 @@ func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request, next c
zap.String("request_path", r.URL.Path),
zap.String("result", filename))
var file fs.File
var info fs.FileInfo
var err error
respHeader := w.Header()
// etag is usually unset, but if the user knows what they're doing, let them override it
etag := respHeader.Get("Etag")
// static file responses are often compressed, either on-the-fly
// or with precompressed sidecar files; in any case, the headers
// should contain "Vary: Accept-Encoding" even when not compressed
// so caches can craft a reliable key (according to REDbot results)
// see #5849
respHeader.Add("Vary", "Accept-Encoding")
preferPrecompressed := true // TODO: move to config
if preferPrecompressed {
fsrv.logger.Debug("prefer_precompressed: seeking precompressed file according to accepted encodings")
for _, ae := range encode.AcceptedEncodings(r, fsrv.PrecompressedOrder) {
precompress, ok := fsrv.precompressors[ae]
if !ok {
continue
}
compressedFilename := filename + precompress.Suffix()
compressedInfo, err := fs.Stat(fileSystem, compressedFilename)
if err != nil || compressedInfo.IsDir() {
fsrv.logger.Debug("precompressed file not accessible", zap.String("filename", compressedFilename), zap.Error(err))
continue
}
fsrv.logger.Debug("opening compressed file", zap.String("filename", compressedFilename), zap.Error(err))
file, err = fsrv.openFile(fileSystem, compressedFilename, w)
if err != nil {
fsrv.logger.Warn("opening precompressed file failed", zap.String("filename", compressedFilename), zap.Error(err))
if caddyErr, ok := err.(caddyhttp.HandlerError); ok && caddyErr.StatusCode == http.StatusServiceUnavailable {
return err
}
file = nil
continue
}
defer file.Close()
respHeader.Set("Content-Encoding", ae)
respHeader.Del("Accept-Ranges")
// try to get the etag from pre computed files if an etag suffix list was provided
if etag == "" && fsrv.EtagFileExtensions != nil {
etag, err = fsrv.getEtagFromFile(fileSystem, compressedFilename)
if err != nil {
return err
}
}
info = compressedInfo
// Set the Etag:
// https://caddy.community/t/gzipped-sidecar-file-wrong-same-etag/16793
if etag == "" {
etag = calculateEtag(compressedInfo)
}
break
}
}
if preferPrecompressed && file == nil {
fsrv.logger.Debug("prefer_precompressed: seeking precompressed file to decompress")
for _, e := range fsrv.PrecompressedOrder {
precompress, ok := fsrv.precompressors[e]
if !ok {
continue
}
compressedFilename := filename + precompress.Suffix()
compressedInfo, err := fs.Stat(fileSystem, compressedFilename)
if err != nil || compressedInfo.IsDir() {
fsrv.logger.Debug("precompressed file not accessible", zap.String("filename", compressedFilename), zap.Error(err))
continue
}
fsrv.logger.Debug("opening compressed file", zap.String("filename", compressedFilename), zap.Error(err))
file, err = fsrv.openFile(fileSystem, compressedFilename, w)
if err != nil {
fsrv.logger.Warn("opening precompressed file failed", zap.String("filename", compressedFilename), zap.Error(err))
if caddyErr, ok := err.(caddyhttp.HandlerError); ok && caddyErr.StatusCode == http.StatusServiceUnavailable {
return err
}
file = nil
continue
}
defer file.Close()
// TODO: handle all compressed formats, not only gzip
r, err := gzip.NewReader(file)
if err != nil {
fsrv.logger.Warn("instantiating new gzip reader for file failed", zap.String("filename", compressedFilename), zap.Error(err))
return err
}
defer r.Close()
var decomp bytes.Buffer
_, err = decomp.ReadFrom(r)
if err != nil {
fsrv.logger.Warn("reading from gzip reader failed", zap.String("filename", compressedFilename), zap.Error(err))
return err
}
file = VirtualFile{bytes.NewReader(decomp.Bytes()), compressedInfo}
respHeader.Del("Accept-Ranges")
// try to get the etag from pre computed files if an etag suffix list was provided
if etag == "" && fsrv.EtagFileExtensions != nil {
etag, err = fsrv.getEtagFromFile(fileSystem, compressedFilename)
if err != nil {
return err
}
}
info = compressedInfo
// Set the Etag:
// https://caddy.community/t/gzipped-sidecar-file-wrong-same-etag/16793
if etag == "" {
etag = calculateEtag(compressedInfo)
}
break
}
}
if file == nil {
// get information about the file
info, err := fs.Stat(fileSystem, filename)
info, err = fs.Stat(fileSystem, filename)
if err != nil {
err = fsrv.mapDirOpenError(fileSystem, err, filename)
if errors.Is(err, fs.ErrNotExist) || errors.Is(err, fs.ErrInvalid) {
@ -369,20 +524,9 @@ func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request, next c
}
}
}
}
var file fs.File
respHeader := w.Header()
// etag is usually unset, but if the user knows what they're doing, let them override it
etag := respHeader.Get("Etag")
// static file responses are often compressed, either on-the-fly
// or with precompressed sidecar files; in any case, the headers
// should contain "Vary: Accept-Encoding" even when not compressed
// so caches can craft a reliable key (according to REDbot results)
// see #5849
respHeader.Add("Vary", "Accept-Encoding")
if file == nil {
// check for precompressed files
for _, ae := range encode.AcceptedEncodings(r, fsrv.PrecompressedOrder) {
precompress, ok := fsrv.precompressors[ae]
@ -426,6 +570,7 @@ func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request, next c
break
}
}
// no precompressed file found, use the actual file
if file == nil {