mirror of
https://github.com/octoleo/syncthing.git
synced 2025-02-02 20:08:27 +00:00
e2febf246e
Storing assets as []byte requires every compiled-in asset to be copied into writable memory at program startup. That currently takes up 1.6MB per syncthing process. Strings stay in the RODATA section and should be shared between processes running the same binary.
225 lines
5.2 KiB
Go
225 lines
5.2 KiB
Go
// 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 https://mozilla.org/MPL/2.0/.
|
|
|
|
package api
|
|
|
|
import (
|
|
"compress/gzip"
|
|
"fmt"
|
|
"io"
|
|
"mime"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/syncthing/syncthing/lib/auto"
|
|
"github.com/syncthing/syncthing/lib/config"
|
|
"github.com/syncthing/syncthing/lib/sync"
|
|
)
|
|
|
|
const themePrefix = "theme-assets/"
|
|
|
|
type staticsServer struct {
|
|
assetDir string
|
|
assets map[string]string
|
|
availableThemes []string
|
|
|
|
mut sync.RWMutex
|
|
theme string
|
|
lastThemeChange time.Time
|
|
}
|
|
|
|
func newStaticsServer(theme, assetDir string) *staticsServer {
|
|
s := &staticsServer{
|
|
assetDir: assetDir,
|
|
assets: auto.Assets(),
|
|
mut: sync.NewRWMutex(),
|
|
theme: theme,
|
|
lastThemeChange: time.Now().UTC(),
|
|
}
|
|
|
|
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) {
|
|
w.Header().Set("Cache-Control", "no-cache, must-revalidate")
|
|
|
|
file := r.URL.Path
|
|
|
|
if file[0] == '/' {
|
|
file = file[1:]
|
|
}
|
|
|
|
if len(file) == 0 {
|
|
file = "index.html"
|
|
}
|
|
|
|
s.mut.RLock()
|
|
theme := s.theme
|
|
modificationTime := s.lastThemeChange
|
|
s.mut.RUnlock()
|
|
|
|
// If path starts with special prefix, get theme and file from path
|
|
if strings.HasPrefix(file, themePrefix) {
|
|
path := file[len(themePrefix):]
|
|
i := strings.IndexRune(path, '/')
|
|
|
|
if i == -1 {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
|
|
theme = path[:i]
|
|
file = path[i+1:]
|
|
}
|
|
|
|
// 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 {
|
|
mtype := s.mimeTypeForFile(file)
|
|
if len(mtype) != 0 {
|
|
w.Header().Set("Content-Type", mtype)
|
|
}
|
|
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 {
|
|
mtype := s.mimeTypeForFile(file)
|
|
if len(mtype) != 0 {
|
|
w.Header().Set("Content-Type", mtype)
|
|
}
|
|
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
|
|
}
|
|
}
|
|
|
|
etag := fmt.Sprintf("%d", modificationTime.Unix())
|
|
w.Header().Set("Last-Modified", modificationTime.Format(http.TimeFormat))
|
|
w.Header().Set("Etag", etag)
|
|
|
|
if t, err := time.Parse(http.TimeFormat, r.Header.Get("If-Modified-Since")); err == nil {
|
|
if modificationTime.Equal(t) || modificationTime.Before(t) {
|
|
w.WriteHeader(http.StatusNotModified)
|
|
return
|
|
}
|
|
}
|
|
|
|
if match := r.Header.Get("If-None-Match"); match != "" {
|
|
if strings.Contains(match, etag) {
|
|
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")
|
|
w.Header().Set("Content-Length", strconv.Itoa(len(bs)))
|
|
io.WriteString(w, bs)
|
|
} else {
|
|
// ungzip if browser not send gzip accepted header
|
|
var gr *gzip.Reader
|
|
gr, _ = gzip.NewReader(strings.NewReader(bs))
|
|
io.Copy(w, gr)
|
|
gr.Close()
|
|
}
|
|
}
|
|
|
|
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. All our files are UTF-8.
|
|
ext := filepath.Ext(file)
|
|
switch ext {
|
|
case ".htm", ".html":
|
|
return "text/html; charset=utf-8"
|
|
case ".css":
|
|
return "text/css; charset=utf-8"
|
|
case ".js":
|
|
return "application/javascript; charset=utf-8"
|
|
case ".json":
|
|
return "application/json; charset=utf-8"
|
|
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; charset=utf-8"
|
|
default:
|
|
return mime.TypeByExtension(ext)
|
|
}
|
|
}
|
|
|
|
func (s *staticsServer) setTheme(theme string) {
|
|
s.mut.Lock()
|
|
s.theme = theme
|
|
s.lastThemeChange = time.Now().UTC()
|
|
s.mut.Unlock()
|
|
}
|
|
|
|
func (s *staticsServer) String() string {
|
|
return fmt.Sprintf("staticsServer@%p", s)
|
|
}
|