diff --git a/conf/defaults.ini b/conf/defaults.ini index d35f71fa4ce..2e4ea89b3f2 100644 --- a/conf/defaults.ini +++ b/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 diff --git a/pkg/api/dashboard_snapshot.go b/pkg/api/dashboard_snapshot.go index 979a3aa86f2..e4841074901 100644 --- a/pkg/api/dashboard_snapshot.go +++ b/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) } diff --git a/pkg/api/static/static.go b/pkg/api/static/static.go new file mode 100644 index 00000000000..43ba6a32b20 --- /dev/null +++ b/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 + } + } + } +} diff --git a/pkg/cmd/web.go b/pkg/cmd/web.go index e5516fb52d9..a8482185e78 100644 --- a/pkg/cmd/web.go +++ b/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, }, )) } diff --git a/src/app/features/dashboard/shareSnapshotCtrl.js b/src/app/features/dashboard/shareSnapshotCtrl.js index 6a787cee752..fc7973a676c 100644 --- a/src/app/features/dashboard/shareSnapshotCtrl.js +++ b/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; + $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; + }); }; }); diff --git a/tasks/options/requirejs.js b/tasks/options/requirejs.js index 947553f1266..d9edf76ada4 100644 --- a/tasks/options/requirejs.js +++ b/tasks/options/requirejs.js @@ -61,6 +61,7 @@ module.exports = function(config,grunt) { 'controllers/all', 'routes/all', 'components/partials', + 'plugins/datasource/grafana/datasource', ] } ];