Browse Source

Added custom cache control headers for static content

Torkel Ödegaard 10 years ago
parent
commit
5f0e7cd52a

+ 3 - 4
conf/defaults.ini

@@ -1,10 +1,9 @@
 app_name = Grafana
 app_mode = production
 
-# Once every 1 hour Grafana will report anonymous data to
-# stats.grafana.org (https). No ip addresses are being tracked.
-# only simple counters to track running instances, dashboard
-# count and errors. It is very helpful to us.
+# Report anonymous usage counters to stats.grafana.org (https).
+# No ip addresses are being tracked, only simple counters to track
+# running instances, dashboard count and errors. It is very helpful to us.
 # Change this option to false to disable reporting.
 reporting-enabled = true
 

+ 1 - 0
pkg/api/dashboard_snapshot.go

@@ -35,5 +35,6 @@ func GetDashboardSnapshot(c *middleware.Context) {
 		Meta:  dtos.DashboardMeta{IsSnapshot: true},
 	}
 
+	c.Resp.Header().Set("Cache-Control", "public max-age: 31536000")
 	c.JSON(200, dto)
 }

+ 218 - 0
pkg/api/static/static.go

@@ -0,0 +1,218 @@
+// Copyright 2013 Martini Authors
+// Copyright 2014 Unknwon
+//
+// Licensed under the Apache License, Version 2.0 (the "License"): you may
+// not use this file except in compliance with the License. You may obtain
+// a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations
+// under the License.
+
+package httpstatic
+
+import (
+	"log"
+	"net/http"
+	"os"
+	"path"
+	"path/filepath"
+	"strings"
+	"sync"
+
+	"github.com/Unknwon/macaron"
+)
+
+var Root string
+
+func init() {
+	var err error
+	Root, err = os.Getwd()
+	if err != nil {
+		panic("error getting work directory: " + err.Error())
+	}
+}
+
+// StaticOptions is a struct for specifying configuration options for the macaron.Static middleware.
+type StaticOptions struct {
+	// Prefix is the optional prefix used to serve the static directory content
+	Prefix string
+	// SkipLogging will disable [Static] log messages when a static file is served.
+	SkipLogging bool
+	// IndexFile defines which file to serve as index if it exists.
+	IndexFile string
+	// Expires defines which user-defined function to use for producing a HTTP Expires Header
+	// https://developers.google.com/speed/docs/insights/LeverageBrowserCaching
+	AddHeaders func(ctx *macaron.Context)
+	// FileSystem is the interface for supporting any implmentation of file system.
+	FileSystem http.FileSystem
+}
+
+// FIXME: to be deleted.
+type staticMap struct {
+	lock sync.RWMutex
+	data map[string]*http.Dir
+}
+
+func (sm *staticMap) Set(dir *http.Dir) {
+	sm.lock.Lock()
+	defer sm.lock.Unlock()
+
+	sm.data[string(*dir)] = dir
+}
+
+func (sm *staticMap) Get(name string) *http.Dir {
+	sm.lock.RLock()
+	defer sm.lock.RUnlock()
+
+	return sm.data[name]
+}
+
+func (sm *staticMap) Delete(name string) {
+	sm.lock.Lock()
+	defer sm.lock.Unlock()
+
+	delete(sm.data, name)
+}
+
+var statics = staticMap{sync.RWMutex{}, map[string]*http.Dir{}}
+
+// staticFileSystem implements http.FileSystem interface.
+type staticFileSystem struct {
+	dir *http.Dir
+}
+
+func newStaticFileSystem(directory string) staticFileSystem {
+	if !filepath.IsAbs(directory) {
+		directory = filepath.Join(Root, directory)
+	}
+	dir := http.Dir(directory)
+	statics.Set(&dir)
+	return staticFileSystem{&dir}
+}
+
+func (fs staticFileSystem) Open(name string) (http.File, error) {
+	return fs.dir.Open(name)
+}
+
+func prepareStaticOption(dir string, opt StaticOptions) StaticOptions {
+	// Defaults
+	if len(opt.IndexFile) == 0 {
+		opt.IndexFile = "index.html"
+	}
+	// Normalize the prefix if provided
+	if opt.Prefix != "" {
+		// Ensure we have a leading '/'
+		if opt.Prefix[0] != '/' {
+			opt.Prefix = "/" + opt.Prefix
+		}
+		// Remove any trailing '/'
+		opt.Prefix = strings.TrimRight(opt.Prefix, "/")
+	}
+	if opt.FileSystem == nil {
+		opt.FileSystem = newStaticFileSystem(dir)
+	}
+	return opt
+}
+
+func prepareStaticOptions(dir string, options []StaticOptions) StaticOptions {
+	var opt StaticOptions
+	if len(options) > 0 {
+		opt = options[0]
+	}
+	return prepareStaticOption(dir, opt)
+}
+
+func staticHandler(ctx *macaron.Context, log *log.Logger, opt StaticOptions) bool {
+	if ctx.Req.Method != "GET" && ctx.Req.Method != "HEAD" {
+		return false
+	}
+
+	file := ctx.Req.URL.Path
+	// if we have a prefix, filter requests by stripping the prefix
+	if opt.Prefix != "" {
+		if !strings.HasPrefix(file, opt.Prefix) {
+			return false
+		}
+		file = file[len(opt.Prefix):]
+		if file != "" && file[0] != '/' {
+			return false
+		}
+	}
+
+	f, err := opt.FileSystem.Open(file)
+	if err != nil {
+		return false
+	}
+	defer f.Close()
+
+	fi, err := f.Stat()
+	if err != nil {
+		return true // File exists but fail to open.
+	}
+
+	// Try to serve index file
+	if fi.IsDir() {
+		// Redirect if missing trailing slash.
+		if !strings.HasSuffix(ctx.Req.URL.Path, "/") {
+			http.Redirect(ctx.Resp, ctx.Req.Request, ctx.Req.URL.Path+"/", http.StatusFound)
+			return true
+		}
+
+		file = path.Join(file, opt.IndexFile)
+		f, err = opt.FileSystem.Open(file)
+		if err != nil {
+			return false // Discard error.
+		}
+		defer f.Close()
+
+		fi, err = f.Stat()
+		if err != nil || fi.IsDir() {
+			return true
+		}
+	}
+
+	if !opt.SkipLogging {
+		log.Println("[Static] Serving " + file)
+	}
+
+	// Add an Expires header to the static content
+	if opt.AddHeaders != nil {
+		opt.AddHeaders(ctx)
+	}
+
+	http.ServeContent(ctx.Resp, ctx.Req.Request, file, fi.ModTime(), f)
+	return true
+}
+
+// Static returns a middleware handler that serves static files in the given directory.
+func Static(directory string, staticOpt ...StaticOptions) macaron.Handler {
+	opt := prepareStaticOptions(directory, staticOpt)
+
+	return func(ctx *macaron.Context, log *log.Logger) {
+		staticHandler(ctx, log, opt)
+	}
+}
+
+// Statics registers multiple static middleware handlers all at once.
+func Statics(opt StaticOptions, dirs ...string) macaron.Handler {
+	if len(dirs) == 0 {
+		panic("no static directory is given")
+	}
+	opts := make([]StaticOptions, len(dirs))
+	for i := range dirs {
+		opts[i] = prepareStaticOption(dirs[i], opt)
+	}
+
+	return func(ctx *macaron.Context, log *log.Logger) {
+		for i := range opts {
+			if staticHandler(ctx, log, opts[i]) {
+				return
+			}
+		}
+	}
+}

+ 14 - 6
pkg/cmd/web.go

@@ -11,7 +11,6 @@ import (
 	"path"
 	"path/filepath"
 	"strconv"
-	"time"
 
 	"github.com/Unknwon/macaron"
 	"github.com/codegangsta/cli"
@@ -20,6 +19,7 @@ import (
 	_ "github.com/macaron-contrib/session/postgres"
 
 	"github.com/grafana/grafana/pkg/api"
+	"github.com/grafana/grafana/pkg/api/static"
 	"github.com/grafana/grafana/pkg/log"
 	"github.com/grafana/grafana/pkg/metrics"
 	"github.com/grafana/grafana/pkg/middleware"
@@ -65,14 +65,22 @@ func newMacaron() *macaron.Macaron {
 }
 
 func mapStatic(m *macaron.Macaron, dir string, prefix string) {
-	m.Use(macaron.Static(
+	headers := func(c *macaron.Context) {
+		c.Resp.Header().Set("Cache-Control", "public max-age: 3600")
+	}
+
+	if setting.Env == setting.DEV {
+		headers = func(c *macaron.Context) {
+			c.Resp.Header().Set("Cache-Control", "max-age: 0")
+		}
+	}
+
+	m.Use(httpstatic.Static(
 		path.Join(setting.StaticRootPath, dir),
-		macaron.StaticOptions{
+		httpstatic.StaticOptions{
 			SkipLogging: true,
 			Prefix:      prefix,
-			Expires: func() string {
-				return time.Now().UTC().Format(http.TimeFormat)
-			},
+			AddHeaders:  headers,
 		},
 	))
 }

+ 29 - 27
src/app/features/dashboard/shareSnapshotCtrl.js

@@ -18,42 +18,44 @@ function (angular) {
       $rootScope.$broadcast('refresh');
 
       $timeout(function() {
-        var dash = angular.copy($scope.dashboard);
-        dash.title = $scope.snapshot.name;
-
-        dash.forEachPanel(function(panel) {
-          panel.targets = [];
-          panel.links = [];
-        });
+        $scope.saveSnapshot(makePublic);
+      }, 2000);
+    };
 
-        // cleanup snapshotData
-        $scope.dashboard.snapshot = false;
-        $scope.dashboard.forEachPanel(function(panel) {
-          delete panel.snapshotData;
-        });
+    $scope.saveSnapshot = function(makePublic) {
+      var dash = angular.copy($scope.dashboard);
+      dash.title = $scope.snapshot.name;
 
-        var apiUrl = '/api/snapshots';
+      dash.forEachPanel(function(panel) {
+        panel.targets = [];
+        panel.links = [];
+      });
 
-        if (makePublic) {
-          apiUrl = 'http://snapshots.raintank.io/api/snapshots';
-        }
+      // cleanup snapshotData
+      $scope.dashboard.snapshot = false;
+      $scope.dashboard.forEachPanel(function(panel) {
+        delete panel.snapshotData;
+      });
 
-        backendSrv.post(apiUrl, {dashboard: dash}).then(function(results) {
-          $scope.loading = false;
+      var apiUrl = '/api/snapshots';
 
-          var baseUrl = $location.absUrl().replace($location.url(), "");
-          if (makePublic) {
-            baseUrl = 'http://snapshots.raintank.io';
-          }
+      if (makePublic) {
+        apiUrl = 'http://snapshots.raintank.io/api/snapshots';
+      }
 
-          $scope.snapshotUrl = baseUrl + '/dashboard/snapshots/' + results.key;
+      backendSrv.post(apiUrl, {dashboard: dash}).then(function(results) {
+        $scope.loading = false;
 
-        }, function() {
-          $scope.loading = false;
-        });
+        var baseUrl = $location.absUrl().replace($location.url(), "");
+        if (makePublic) {
+          baseUrl = 'http://snapshots.raintank.io';
+        }
 
+        $scope.snapshotUrl = baseUrl + '/dashboard/snapshots/' + results.key;
 
-      }, 2000);
+      }, function() {
+        $scope.loading = false;
+      });
     };
 
   });

+ 1 - 0
tasks/options/requirejs.js

@@ -61,6 +61,7 @@ module.exports = function(config,grunt) {
           'controllers/all',
           'routes/all',
           'components/partials',
+          'plugins/datasource/grafana/datasource',
         ]
       }
     ];