|
|
@@ -7,13 +7,10 @@
|
|
|
package main
|
|
|
|
|
|
import (
|
|
|
- "bytes"
|
|
|
- "compress/gzip"
|
|
|
"crypto/tls"
|
|
|
"encoding/json"
|
|
|
"fmt"
|
|
|
"io/ioutil"
|
|
|
- "mime"
|
|
|
"net"
|
|
|
"net/http"
|
|
|
"os"
|
|
|
@@ -26,7 +23,6 @@ import (
|
|
|
"time"
|
|
|
|
|
|
"github.com/rcrowley/go-metrics"
|
|
|
- "github.com/syncthing/syncthing/lib/auto"
|
|
|
"github.com/syncthing/syncthing/lib/config"
|
|
|
"github.com/syncthing/syncthing/lib/db"
|
|
|
"github.com/syncthing/syncthing/lib/discover"
|
|
|
@@ -54,8 +50,7 @@ type apiService struct {
|
|
|
cfg configIntf
|
|
|
httpsCertFile string
|
|
|
httpsKeyFile string
|
|
|
- assetDir string
|
|
|
- themes []string
|
|
|
+ statics *staticsServer
|
|
|
model modelIntf
|
|
|
eventSub events.BufferedSubscription
|
|
|
discoverer discover.CachingMux
|
|
|
@@ -123,7 +118,7 @@ func newAPIService(id protocol.DeviceID, cfg configIntf, httpsCertFile, httpsKey
|
|
|
cfg: cfg,
|
|
|
httpsCertFile: httpsCertFile,
|
|
|
httpsKeyFile: httpsKeyFile,
|
|
|
- assetDir: assetDir,
|
|
|
+ statics: newStaticsServer(cfg.GUI().Theme, assetDir),
|
|
|
model: m,
|
|
|
eventSub: eventSub,
|
|
|
discoverer: discoverer,
|
|
|
@@ -135,25 +130,6 @@ func newAPIService(id protocol.DeviceID, cfg configIntf, httpsCertFile, httpsKey
|
|
|
systemLog: systemLog,
|
|
|
}
|
|
|
|
|
|
- seen := make(map[string]struct{})
|
|
|
- // Load themes from compiled in assets.
|
|
|
- for file := range auto.Assets() {
|
|
|
- theme := strings.Split(file, "/")[0]
|
|
|
- if _, ok := seen[theme]; !ok {
|
|
|
- seen[theme] = struct{}{}
|
|
|
- service.themes = append(service.themes, theme)
|
|
|
- }
|
|
|
- }
|
|
|
- if assetDir != "" {
|
|
|
- // Load any extra themes from the asset override dir.
|
|
|
- for _, dir := range dirNames(assetDir) {
|
|
|
- if _, ok := seen[dir]; !ok {
|
|
|
- seen[dir] = struct{}{}
|
|
|
- service.themes = append(service.themes, dir)
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
return service
|
|
|
}
|
|
|
|
|
|
@@ -305,20 +281,11 @@ func (s *apiService) Serve() {
|
|
|
mux.HandleFunc("/qr/", s.getQR)
|
|
|
|
|
|
// Serve compiled in assets unless an asset directory was set (for development)
|
|
|
- assets := &embeddedStatic{
|
|
|
- theme: s.cfg.GUI().Theme,
|
|
|
- lastModified: time.Now().Truncate(time.Second), // must truncate, for the wire precision is 1s
|
|
|
- mut: sync.NewRWMutex(),
|
|
|
- assetDir: s.assetDir,
|
|
|
- assets: auto.Assets(),
|
|
|
- }
|
|
|
- mux.Handle("/", assets)
|
|
|
+ mux.Handle("/", s.statics)
|
|
|
|
|
|
// Handle the special meta.js path
|
|
|
mux.HandleFunc("/meta.js", s.getJSMetadata)
|
|
|
|
|
|
- s.cfg.Subscribe(assets)
|
|
|
-
|
|
|
guiCfg := s.cfg.GUI()
|
|
|
|
|
|
// Wrap everything in CSRF protection. The /rest prefix should be
|
|
|
@@ -401,6 +368,10 @@ func (s *apiService) CommitConfiguration(from, to config.Configuration) bool {
|
|
|
return true
|
|
|
}
|
|
|
|
|
|
+ if to.GUI.Theme != from.GUI.Theme {
|
|
|
+ s.statics.setTheme(to.GUI.Theme)
|
|
|
+ }
|
|
|
+
|
|
|
// Tell the serve loop to restart
|
|
|
s.configChanged <- struct{}{}
|
|
|
|
|
|
@@ -842,7 +813,6 @@ func (s *apiService) getSystemStatus(w http.ResponseWriter, r *http.Request) {
|
|
|
res["pathSeparator"] = string(filepath.Separator)
|
|
|
res["uptime"] = int(time.Since(startTime).Seconds())
|
|
|
res["startTime"] = startTime
|
|
|
- res["themes"] = s.themes
|
|
|
|
|
|
sendJSON(w, res)
|
|
|
}
|
|
|
@@ -1192,136 +1162,6 @@ func (s *apiService) getSystemBrowse(w http.ResponseWriter, r *http.Request) {
|
|
|
sendJSON(w, ret)
|
|
|
}
|
|
|
|
|
|
-type embeddedStatic struct {
|
|
|
- theme string
|
|
|
- lastModified time.Time
|
|
|
- mut sync.RWMutex
|
|
|
- assetDir string
|
|
|
- assets map[string][]byte
|
|
|
-}
|
|
|
-
|
|
|
-func (s embeddedStatic) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
|
- file := r.URL.Path
|
|
|
-
|
|
|
- if file[0] == '/' {
|
|
|
- file = file[1:]
|
|
|
- }
|
|
|
-
|
|
|
- if len(file) == 0 {
|
|
|
- file = "index.html"
|
|
|
- }
|
|
|
-
|
|
|
- s.mut.RLock()
|
|
|
- theme := s.theme
|
|
|
- modified := s.lastModified
|
|
|
- s.mut.RUnlock()
|
|
|
-
|
|
|
- // Check for an override for the current theme.
|
|
|
- if s.assetDir != "" {
|
|
|
- p := filepath.Join(s.assetDir, s.theme, filepath.FromSlash(file))
|
|
|
- if _, err := os.Stat(p); err == nil {
|
|
|
- http.ServeFile(w, r, p)
|
|
|
- return
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // Check for a compiled in asset for the current theme.
|
|
|
- bs, ok := s.assets[theme+"/"+file]
|
|
|
- if !ok {
|
|
|
- // Check for an overridden default asset.
|
|
|
- if s.assetDir != "" {
|
|
|
- p := filepath.Join(s.assetDir, config.DefaultTheme, filepath.FromSlash(file))
|
|
|
- if _, err := os.Stat(p); err == nil {
|
|
|
- http.ServeFile(w, r, p)
|
|
|
- return
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // Check for a compiled in default asset.
|
|
|
- bs, ok = s.assets[config.DefaultTheme+"/"+file]
|
|
|
- if !ok {
|
|
|
- http.NotFound(w, r)
|
|
|
- return
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- modifiedSince, err := http.ParseTime(r.Header.Get("If-Modified-Since"))
|
|
|
- if err == nil && !modified.After(modifiedSince) {
|
|
|
- w.WriteHeader(http.StatusNotModified)
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- mtype := s.mimeTypeForFile(file)
|
|
|
- if len(mtype) != 0 {
|
|
|
- w.Header().Set("Content-Type", mtype)
|
|
|
- }
|
|
|
- if strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
|
|
|
- w.Header().Set("Content-Encoding", "gzip")
|
|
|
- } else {
|
|
|
- // ungzip if browser not send gzip accepted header
|
|
|
- var gr *gzip.Reader
|
|
|
- gr, _ = gzip.NewReader(bytes.NewReader(bs))
|
|
|
- bs, _ = ioutil.ReadAll(gr)
|
|
|
- gr.Close()
|
|
|
- }
|
|
|
- w.Header().Set("Content-Length", fmt.Sprintf("%d", len(bs)))
|
|
|
- w.Header().Set("Last-Modified", modified.UTC().Format(http.TimeFormat))
|
|
|
- // Strictly, no-cache means the same as this. However FF and IE treat no-cache as
|
|
|
- // "don't hold a local cache at all", whereas everyone seems to treat this as
|
|
|
- // you can hold a local cache, but you must revalidate it before using it.
|
|
|
- w.Header().Set("Cache-Control", "max-age=0, must-revalidate")
|
|
|
-
|
|
|
- w.Write(bs)
|
|
|
-}
|
|
|
-
|
|
|
-func (s embeddedStatic) mimeTypeForFile(file string) string {
|
|
|
- // We use a built in table of the common types since the system
|
|
|
- // TypeByExtension might be unreliable. But if we don't know, we delegate
|
|
|
- // to the system.
|
|
|
- ext := filepath.Ext(file)
|
|
|
- switch ext {
|
|
|
- case ".htm", ".html":
|
|
|
- return "text/html"
|
|
|
- case ".css":
|
|
|
- return "text/css"
|
|
|
- case ".js":
|
|
|
- return "application/javascript"
|
|
|
- case ".json":
|
|
|
- return "application/json"
|
|
|
- case ".png":
|
|
|
- return "image/png"
|
|
|
- case ".ttf":
|
|
|
- return "application/x-font-ttf"
|
|
|
- case ".woff":
|
|
|
- return "application/x-font-woff"
|
|
|
- case ".svg":
|
|
|
- return "image/svg+xml"
|
|
|
- default:
|
|
|
- return mime.TypeByExtension(ext)
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-// VerifyConfiguration implements the config.Committer interface
|
|
|
-func (s *embeddedStatic) VerifyConfiguration(from, to config.Configuration) error {
|
|
|
- return nil
|
|
|
-}
|
|
|
-
|
|
|
-// CommitConfiguration implements the config.Committer interface
|
|
|
-func (s *embeddedStatic) CommitConfiguration(from, to config.Configuration) bool {
|
|
|
- s.mut.Lock()
|
|
|
- if s.theme != to.GUI.Theme {
|
|
|
- s.theme = to.GUI.Theme
|
|
|
- s.lastModified = time.Now()
|
|
|
- }
|
|
|
- s.mut.Unlock()
|
|
|
-
|
|
|
- return true
|
|
|
-}
|
|
|
-
|
|
|
-func (s *embeddedStatic) String() string {
|
|
|
- return fmt.Sprintf("embeddedStatic@%p", s)
|
|
|
-}
|
|
|
-
|
|
|
func (s *apiService) toNeedSlice(fs []db.FileInfoTruncated) []jsonDBFileInfo {
|
|
|
res := make([]jsonDBFileInfo, len(fs))
|
|
|
for i, f := range fs {
|