lib/assets: Allow assets to remain uncompressed (#6661)

This commit is contained in:
greatroar 2020-05-25 08:51:27 +02:00 committed by GitHub
parent c3b5eba205
commit baa38eea7a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 104 additions and 49 deletions

View File

@ -268,17 +268,13 @@ func handleAssets(w http.ResponseWriter, r *http.Request) {
path = "index.html" path = "index.html"
} }
content, ok := auto.Assets()[path] as, ok := auto.Assets()[path]
if !ok { if !ok {
w.WriteHeader(http.StatusNotFound) w.WriteHeader(http.StatusNotFound)
return return
} }
assets.Serve(w, r, assets.Asset{ assets.Serve(w, r, as)
ContentGz: content,
Filename: path,
Modified: time.Unix(auto.Generated, 0).UTC(),
})
} }
func handleRequest(w http.ResponseWriter, r *http.Request) { func handleRequest(w http.ResponseWriter, r *http.Request) {

View File

@ -1267,7 +1267,7 @@ func (s *service) getEventSub(mask events.EventType) events.BufferedSubscription
func (s *service) getSystemUpgrade(w http.ResponseWriter, r *http.Request) { func (s *service) getSystemUpgrade(w http.ResponseWriter, r *http.Request) {
if s.noUpgrade { if s.noUpgrade {
http.Error(w, upgrade.ErrUpgradeUnsupported.Error(), 500) http.Error(w, upgrade.ErrUpgradeUnsupported.Error(), http.StatusServiceUnavailable)
return return
} }
opts := s.cfg.Options() opts := s.cfg.Options()

View File

@ -24,7 +24,7 @@ const themePrefix = "theme-assets/"
type staticsServer struct { type staticsServer struct {
assetDir string assetDir string
assets map[string]string assets map[string]assets.Asset
availableThemes []string availableThemes []string
mut sync.RWMutex mut sync.RWMutex
@ -118,7 +118,7 @@ func (s *staticsServer) serveAsset(w http.ResponseWriter, r *http.Request) {
} }
// Check for a compiled in asset for the current theme. // Check for a compiled in asset for the current theme.
bs, ok := s.assets[theme+"/"+file] as, ok := s.assets[theme+"/"+file]
if !ok { if !ok {
// Check for an overridden default asset. // Check for an overridden default asset.
if s.assetDir != "" { if s.assetDir != "" {
@ -134,18 +134,15 @@ func (s *staticsServer) serveAsset(w http.ResponseWriter, r *http.Request) {
} }
// Check for a compiled in default asset. // Check for a compiled in default asset.
bs, ok = s.assets[config.DefaultTheme+"/"+file] as, ok = s.assets[config.DefaultTheme+"/"+file]
if !ok { if !ok {
http.NotFound(w, r) http.NotFound(w, r)
return return
} }
} }
assets.Serve(w, r, assets.Asset{ as.Modified = modificationTime
ContentGz: bs, assets.Serve(w, r, as)
Filename: file,
Modified: modificationTime,
})
} }
func (s *staticsServer) serveThemes(w http.ResponseWriter, r *http.Request) { func (s *staticsServer) serveThemes(w http.ResponseWriter, r *http.Request) {

View File

@ -25,6 +25,7 @@ import (
"time" "time"
"github.com/d4l3k/messagediff" "github.com/d4l3k/messagediff"
"github.com/syncthing/syncthing/lib/assets"
"github.com/syncthing/syncthing/lib/config" "github.com/syncthing/syncthing/lib/config"
"github.com/syncthing/syncthing/lib/events" "github.com/syncthing/syncthing/lib/events"
"github.com/syncthing/syncthing/lib/fs" "github.com/syncthing/syncthing/lib/fs"
@ -152,19 +153,25 @@ func TestAssetsDir(t *testing.T) {
gw := gzip.NewWriter(buf) gw := gzip.NewWriter(buf)
gw.Write([]byte("default")) gw.Write([]byte("default"))
gw.Close() gw.Close()
def := buf.String() def := assets.Asset{
Content: buf.String(),
Gzipped: true,
}
buf = new(bytes.Buffer) buf = new(bytes.Buffer)
gw = gzip.NewWriter(buf) gw = gzip.NewWriter(buf)
gw.Write([]byte("foo")) gw.Write([]byte("foo"))
gw.Close() gw.Close()
foo := buf.String() foo := assets.Asset{
Content: buf.String(),
Gzipped: true,
}
e := &staticsServer{ e := &staticsServer{
theme: "foo", theme: "foo",
mut: sync.NewRWMutex(), mut: sync.NewRWMutex(),
assetDir: "testdata", assetDir: "testdata",
assets: map[string]string{ assets: map[string]assets.Asset{
"foo/a": foo, // overridden in foo/a "foo/a": foo, // overridden in foo/a
"foo/b": foo, "foo/b": foo,
"default/a": def, // overridden in default/a (but foo/a takes precedence) "default/a": def, // overridden in default/a (but foo/a takes precedence)

View File

@ -22,9 +22,12 @@ func TestAssets(t *testing.T) {
if !ok { if !ok {
t.Fatal("No index.html in compiled in assets") t.Fatal("No index.html in compiled in assets")
} }
if !idx.Gzipped {
t.Fatal("default/index.html should be compressed")
}
var gr *gzip.Reader var gr *gzip.Reader
gr, _ = gzip.NewReader(strings.NewReader(idx)) gr, _ = gzip.NewReader(strings.NewReader(idx.Content))
html, _ := ioutil.ReadAll(gr) html, _ := ioutil.ReadAll(gr)
if !bytes.Contains(html, []byte("<html")) { if !bytes.Contains(html, []byte("<html")) {

View File

@ -22,11 +22,13 @@ import (
"time" "time"
) )
// Asset is the type of arguments to Serve. // An Asset is an embedded file to be served over HTTP.
type Asset struct { type Asset struct {
ContentGz string // gzipped contents of asset. Content string // Contents of asset, possibly gzipped.
Filename string // Original filename, determines Content-Type. Gzipped bool
Modified time.Time // Determines ETag and Last-Modified. Length int // Length of (decompressed) Content.
Filename string // Original filename, determines Content-Type.
Modified time.Time // Determines ETag and Last-Modified.
} }
// Serve writes a gzipped asset to w. // Serve writes a gzipped asset to w.
@ -53,14 +55,19 @@ func Serve(w http.ResponseWriter, r *http.Request, asset Asset) {
return return
} }
if strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") { switch {
case !asset.Gzipped:
header.Set("Content-Length", strconv.Itoa(len(asset.Content)))
io.WriteString(w, asset.Content)
case strings.Contains(r.Header.Get("Accept-Encoding"), "gzip"):
header.Set("Content-Encoding", "gzip") header.Set("Content-Encoding", "gzip")
header.Set("Content-Length", strconv.Itoa(len(asset.ContentGz))) header.Set("Content-Length", strconv.Itoa(len(asset.Content)))
io.WriteString(w, asset.ContentGz) io.WriteString(w, asset.Content)
} else { default:
header.Set("Content-Length", strconv.Itoa(asset.Length))
// gunzip for browsers that don't want gzip. // gunzip for browsers that don't want gzip.
var gr *gzip.Reader var gr *gzip.Reader
gr, _ = gzip.NewReader(strings.NewReader(asset.ContentGz)) gr, _ = gzip.NewReader(strings.NewReader(asset.Content))
io.Copy(w, gr) io.Copy(w, gr)
gr.Close() gr.Close()
} }

View File

@ -13,6 +13,7 @@ import (
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"strconv"
"strings" "strings"
"testing" "testing"
"time" "time"
@ -38,15 +39,23 @@ func decompress(p []byte) (out []byte) {
return out return out
} }
func TestServe(t *testing.T) { func TestServe(t *testing.T) { testServe(t, false) }
indexHTML := `<html>Hello, world!</html>` func TestServeGzip(t *testing.T) { testServe(t, true) }
indexGz := compress(indexHTML)
func testServe(t *testing.T, gzip bool) {
const indexHTML = `<html>Hello, world!</html>`
content := indexHTML
if gzip {
content = compress(indexHTML)
}
handler := func(w http.ResponseWriter, r *http.Request) { handler := func(w http.ResponseWriter, r *http.Request) {
Serve(w, r, Asset{ Serve(w, r, Asset{
ContentGz: indexGz, Content: content,
Filename: r.URL.Path[1:], Gzipped: gzip,
Modified: time.Unix(0, 0), Length: len(indexHTML),
Filename: r.URL.Path[1:],
Modified: time.Unix(0, 0),
}) })
} }
@ -73,7 +82,17 @@ func TestServe(t *testing.T) {
} }
body, _ := ioutil.ReadAll(res.Body) body, _ := ioutil.ReadAll(res.Body)
if acceptGzip {
// Content-Length is the number of bytes in the encoded (compressed) body
// (https://stackoverflow.com/a/3819303).
n, err := strconv.Atoi(res.Header.Get("Content-Length"))
if err != nil {
t.Errorf("malformed Content-Length %q", res.Header.Get("Content-Length"))
} else if n != len(body) {
t.Errorf("wrong Content-Length %d, should be %d", n, len(body))
}
if gzip && acceptGzip {
body = decompress(body) body = decompress(body)
} }
if string(body) != indexHTML { if string(body) != indexHTML {

View File

@ -15,6 +15,7 @@ import (
"fmt" "fmt"
"go/format" "go/format"
"io" "io"
"io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
"strconv" "strconv"
@ -27,20 +28,35 @@ var tpl = template.Must(template.New("assets").Parse(`// Code generated by genas
package auto package auto
const Generated int64 = {{.Generated}} import (
"time"
"github.com/syncthing/syncthing/lib/assets"
)
func Assets() map[string]assets.Asset {
var ret = make(map[string]assets.Asset, {{.Assets | len}})
t := time.Unix({{.Generated}}, 0)
func Assets() map[string]string {
var assets = make(map[string]string, {{.Assets | len}})
{{range $asset := .Assets}} {{range $asset := .Assets}}
assets["{{$asset.Name}}"] = {{$asset.Data}}{{end}} ret["{{$asset.Name}}"] = assets.Asset{
return assets Content: {{$asset.Data}},
Gzipped: {{$asset.Gzipped}},
Length: {{$asset.Length}},
Filename: {{$asset.Name | printf "%q"}},
Modified: t,
}
{{end}}
return ret
} }
`)) `))
type asset struct { type asset struct {
Name string Name string
Data string Data string
Length int
Gzipped bool
} }
var assets []asset var assets []asset
@ -57,22 +73,32 @@ func walkerFor(basePath string) filepath.WalkFunc {
} }
if info.Mode().IsRegular() { if info.Mode().IsRegular() {
fd, err := os.Open(name) data, err := ioutil.ReadFile(name)
if err != nil { if err != nil {
return err return err
} }
length := len(data)
var buf bytes.Buffer var buf bytes.Buffer
gw := gzip.NewWriter(&buf) gw, _ := gzip.NewWriterLevel(&buf, gzip.BestCompression)
io.Copy(gw, fd) gw.Write(data)
fd.Close()
gw.Flush()
gw.Close() gw.Close()
// Only replace asset by gzipped version if it is smaller.
// In practice, this means HTML, CSS, SVG etc. get compressed,
// while PNG and WOFF files are left uncompressed.
// lib/assets detects gzip and sets headers/decompresses.
gzipped := buf.Len() < len(data)
if gzipped {
data = buf.Bytes()
}
name, _ = filepath.Rel(basePath, name) name, _ = filepath.Rel(basePath, name)
assets = append(assets, asset{ assets = append(assets, asset{
Name: filepath.ToSlash(name), Name: filepath.ToSlash(name),
Data: fmt.Sprintf("%q", buf.String()), Data: fmt.Sprintf("%q", string(data)),
Length: length,
Gzipped: gzipped,
}) })
} }