Added custom cache control headers for static content

This commit is contained in:
Torkel Ödegaard 2015-03-23 18:28:59 -04:00
parent 98c0209976
commit 5f0e7cd52a
6 changed files with 267 additions and 38 deletions

View File

@ -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

View File

@ -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
pkg/api/static/static.go Normal file
View File

@ -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
}
}
}
}

View File

@ -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,
},
))
}

View File

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

View File

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