mirror of
https://github.com/octoleo/syncthing.git
synced 2025-04-02 07:41:51 +00:00
lib/assets: Allow assets to remain uncompressed (#6661)
This commit is contained in:
parent
c3b5eba205
commit
baa38eea7a
@ -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) {
|
||||||
|
@ -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()
|
||||||
|
@ -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) {
|
||||||
|
@ -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)
|
||||||
|
@ -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")) {
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -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,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user