mirror of
https://github.com/octoleo/syncthing.git
synced 2025-02-02 03:48:26 +00:00
cmd/syncthing: Refactor out staticsServer (prev. embeddedStatic) a bit
The purpose of this operation is to separate the serving of GUI assets a bit from the serving of the REST API. It's by no means complete. The end goal is something like a combined server type that embeds a statics server and an API server and wraps it in authentication and HTTPS and stuff, plus possibly a named pipe server that only provides the API and does not wrap in the same authentication etc. GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3273
This commit is contained in:
parent
b7e186b370
commit
03a8027efc
@ -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 {
|
||||
|
176
cmd/syncthing/gui_statics.go
Normal file
176
cmd/syncthing/gui_statics.go
Normal file
@ -0,0 +1,176 @@
|
||||
// Copyright (C) 2014 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"mime"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/auto"
|
||||
"github.com/syncthing/syncthing/lib/config"
|
||||
"github.com/syncthing/syncthing/lib/sync"
|
||||
)
|
||||
|
||||
type staticsServer struct {
|
||||
assetDir string
|
||||
assets map[string][]byte
|
||||
availableThemes []string
|
||||
|
||||
mut sync.RWMutex
|
||||
theme string
|
||||
}
|
||||
|
||||
func newStaticsServer(theme, assetDir string) *staticsServer {
|
||||
s := &staticsServer{
|
||||
assetDir: assetDir,
|
||||
assets: auto.Assets(),
|
||||
mut: sync.NewRWMutex(),
|
||||
theme: theme,
|
||||
}
|
||||
|
||||
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{}{}
|
||||
s.availableThemes = append(s.availableThemes, 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{}{}
|
||||
s.availableThemes = append(s.availableThemes, dir)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *staticsServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/themes.json":
|
||||
s.serveThemes(w, r)
|
||||
default:
|
||||
s.serveAsset(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *staticsServer) serveAsset(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
|
||||
s.mut.RUnlock()
|
||||
|
||||
// Check for an override for the current theme.
|
||||
if s.assetDir != "" {
|
||||
p := filepath.Join(s.assetDir, 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
|
||||
}
|
||||
}
|
||||
|
||||
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.Write(bs)
|
||||
}
|
||||
|
||||
func (s *staticsServer) serveThemes(w http.ResponseWriter, r *http.Request) {
|
||||
sendJSON(w, map[string][]string{
|
||||
"themes": s.availableThemes,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *staticsServer) 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)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *staticsServer) setTheme(theme string) {
|
||||
s.mut.Lock()
|
||||
s.theme = theme
|
||||
s.mut.Unlock()
|
||||
}
|
||||
|
||||
func (s *staticsServer) String() string {
|
||||
return fmt.Sprintf("staticsServer@%p", s)
|
||||
}
|
@ -116,7 +116,7 @@ func TestAssetsDir(t *testing.T) {
|
||||
gw.Close()
|
||||
foo := buf.Bytes()
|
||||
|
||||
e := embeddedStatic{
|
||||
e := &staticsServer{
|
||||
theme: "foo",
|
||||
mut: sync.NewRWMutex(),
|
||||
assetDir: "testdata",
|
||||
|
@ -49,6 +49,7 @@ angular.module('syncthing.core')
|
||||
$scope.failedCurrentFolder = undefined;
|
||||
$scope.failedPageSize = 10;
|
||||
$scope.scanProgress = {};
|
||||
$scope.themes = [];
|
||||
|
||||
$scope.localStateTotal = {
|
||||
bytes: 0,
|
||||
@ -88,6 +89,7 @@ angular.module('syncthing.core')
|
||||
refreshConnectionStats();
|
||||
refreshDeviceStats();
|
||||
refreshFolderStats();
|
||||
refreshThemes();
|
||||
|
||||
$http.get(urlbase + '/system/version').success(function (data) {
|
||||
if ($scope.version.version && $scope.version.version !== data.version) {
|
||||
@ -599,6 +601,12 @@ angular.module('syncthing.core')
|
||||
}).error($scope.emitHTTPError);
|
||||
}, 2500);
|
||||
|
||||
var refreshThemes = debounce(function () {
|
||||
$http.get("themes.json").success(function (data) { // no urlbase here as this is served by the asset handler
|
||||
$scope.themes = data.themes;
|
||||
}).error($scope.emitHTTPError);
|
||||
}, 2500);
|
||||
|
||||
$scope.refresh = function () {
|
||||
refreshSystem();
|
||||
refreshConnectionStats();
|
||||
@ -627,7 +635,7 @@ angular.module('syncthing.core')
|
||||
return 'outofsync';
|
||||
}
|
||||
if (state === 'scanning') {
|
||||
return state;
|
||||
return state;
|
||||
}
|
||||
|
||||
if (folderCfg.devices.length <= 1) {
|
||||
|
@ -141,10 +141,10 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-if="system.themes.length > 1">
|
||||
<div class="form-group" ng-if="themes.length > 1">
|
||||
<label>GUI Theme</label>
|
||||
<select class="form-control" ng-model="tmpGUI.theme">
|
||||
<option ng-repeat="theme in system.themes.sort()" value="{{ theme }}">
|
||||
<option ng-repeat="theme in themes.sort()" value="{{ theme }}">
|
||||
{{ themeName(theme) }}
|
||||
</option>
|
||||
</select>
|
||||
|
Loading…
x
Reference in New Issue
Block a user