فهرست منبع

more macaroon stuff

Torkel Ödegaard 11 سال پیش
والد
کامیت
e84f06b503

BIN
data/sessions/5/a/5a7a6d798450f878d373110c50ed07ae3bc99d63


BIN
data/sessions/7/a/7ad60c89b1bc7a310c66e59570df698fc75d28b3


BIN
data/sessions/7/b/7b786a2d47bb26f2fce2d9aa874615c6428c55a3


BIN
data/sessions/b/7/b724e1a2a6d52de49c11d1d62d6e0d83cba2911a


BIN
grafana-pro


+ 1 - 8
pkg/cmd/web.go

@@ -15,7 +15,6 @@ import (
 	"github.com/torkelo/grafana-pro/pkg/log"
 	"github.com/torkelo/grafana-pro/pkg/middleware"
 	"github.com/torkelo/grafana-pro/pkg/routes"
-	"github.com/torkelo/grafana-pro/pkg/routes/login"
 	"github.com/torkelo/grafana-pro/pkg/setting"
 	"github.com/torkelo/grafana-pro/pkg/stores/rethink"
 )
@@ -70,13 +69,7 @@ func runWeb(*cli.Context) {
 	log.Info("Starting Grafana-Pro v.1-alpha")
 
 	m := newMacaron()
-
-	auth := middleware.Auth()
-
-	// index
-	m.Get("/", auth, routes.Index)
-	m.Get("/login", routes.Index)
-	m.Post("/login", login.LoginPost)
+	routes.Register(m)
 
 	var err error
 	listenAddr := fmt.Sprintf("%s:%s", setting.HttpAddr, setting.HttpPort)

+ 69 - 0
pkg/components/renderer/renderer.go

@@ -0,0 +1,69 @@
+package renderer
+
+import (
+	"crypto/md5"
+	"encoding/hex"
+	"io"
+	"os"
+	"os/exec"
+	"path/filepath"
+	"time"
+
+	"github.com/torkelo/grafana-pro/pkg/log"
+	"github.com/torkelo/grafana-pro/pkg/setting"
+)
+
+type RenderOpts struct {
+	Url    string
+	Width  string
+	Height string
+}
+
+func RenderToPng(params *RenderOpts) (string, error) {
+	log.Info("PhantomRenderer::renderToPng url %v", params.Url)
+	binPath, _ := filepath.Abs(filepath.Join(setting.PhantomDir, "phantomjs"))
+	scriptPath, _ := filepath.Abs(filepath.Join(setting.PhantomDir, "render.js"))
+	pngPath, _ := filepath.Abs(filepath.Join(setting.ImagesDir, getHash(params.Url)))
+	pngPath = pngPath + ".png"
+
+	cmd := exec.Command(binPath, scriptPath, "url="+params.Url, "width="+params.Width, "height="+params.Height, "png="+pngPath)
+	stdout, err := cmd.StdoutPipe()
+
+	if err != nil {
+		return "", err
+	}
+	stderr, err := cmd.StderrPipe()
+	if err != nil {
+		return "", err
+	}
+
+	err = cmd.Start()
+	if err != nil {
+		return "", err
+	}
+
+	go io.Copy(os.Stdout, stdout)
+	go io.Copy(os.Stdout, stderr)
+
+	done := make(chan error)
+	go func() {
+		cmd.Wait()
+		close(done)
+	}()
+
+	select {
+	case <-time.After(10 * time.Second):
+		if err := cmd.Process.Kill(); err != nil {
+			log.Error(4, "failed to kill: %v", err)
+		}
+	case <-done:
+	}
+
+	return pngPath, nil
+}
+
+func getHash(text string) string {
+	hasher := md5.New()
+	hasher.Write([]byte(text))
+	return hex.EncodeToString(hasher.Sum(nil))
+}

+ 2 - 3
pkg/components/phantom_renderer_test.go → pkg/components/renderer/renderer_test.go

@@ -1,4 +1,4 @@
-package components
+package renderer
 
 import (
 	"io/ioutil"
@@ -12,8 +12,7 @@ func TestPhantomRender(t *testing.T) {
 
 	Convey("Can render url", t, func() {
 		tempDir, _ := ioutil.TempDir("", "img")
-		renderer := &PhantomRenderer{ImagesDir: tempDir, PhantomDir: "../../_vendor/phantomjs/"}
-		png, err := renderer.RenderToPng("http://www.google.com")
+		png, err := RenderToPng("http://www.google.com")
 		So(err, ShouldBeNil)
 		So(exists(png), ShouldEqual, true)
 

+ 28 - 0
pkg/middleware/middleware.go

@@ -21,6 +21,10 @@ type Context struct {
 	IsSigned bool
 }
 
+func (c *Context) GetAccountId() int {
+	return c.Account.Id
+}
+
 func GetContextHandler() macaron.Handler {
 	return func(c *macaron.Context, sess session.Store) {
 		ctx := &Context{
@@ -51,6 +55,30 @@ func (ctx *Context) Handle(status int, title string, err error) {
 	ctx.HTML(status, "index")
 }
 
+func (ctx *Context) ApiError(status int, message string, err error) {
+	resp := make(map[string]interface{})
+
+	if err != nil {
+		log.Error(4, "%s: %v", message, err)
+		if macaron.Env != macaron.PROD {
+			resp["error"] = err
+		}
+	}
+
+	switch status {
+	case 404:
+		resp["message"] = "Not Found"
+	case 500:
+		resp["message"] = "Internal Server Error"
+	}
+
+	if message != "" {
+		resp["message"] = message
+	}
+
+	ctx.HTML(status, "index")
+}
+
 func (ctx *Context) JsonBody(model interface{}) bool {
 	b, _ := ioutil.ReadAll(ctx.Req.Body)
 	err := json.Unmarshal(b, &model)

+ 7 - 0
pkg/models/dashboards.go

@@ -8,6 +8,13 @@ import (
 	"time"
 )
 
+var (
+	GetDashboard    func(slug string, accountId int) (*Dashboard, error)
+	SaveDashboard   func(dash *Dashboard) error
+	DeleteDashboard func(slug string, accountId int) error
+	SearchQuery     func(query string, acccountId int) ([]*SearchResult, error)
+)
+
 type Dashboard struct {
 	Id                   string `gorethink:"id,omitempty"`
 	Slug                 string

+ 82 - 0
pkg/routes/api/api_dashboard.go

@@ -0,0 +1,82 @@
+package api
+
+import (
+	"github.com/gin-gonic/gin"
+
+	"github.com/torkelo/grafana-pro/pkg/middleware"
+	"github.com/torkelo/grafana-pro/pkg/models"
+	"github.com/torkelo/grafana-pro/pkg/routes/apimodel"
+)
+
+func GetDashboard(c *middleware.Context) {
+	slug := c.Params(":slug")
+
+	dash, err := models.GetDashboard(slug, c.GetAccountId())
+	if err != nil {
+		c.ApiError(404, "Dashboard not found", nil)
+		return
+	}
+
+	dash.Data["id"] = dash.Id
+
+	c.JSON(200, dash.Data)
+}
+
+func DeleteDashboard(c *middleware.Context) {
+	slug := c.Params(":slug")
+
+	dash, err := models.GetDashboard(slug, c.GetAccountId())
+	if err != nil {
+		c.ApiError(404, "Dashboard not found", nil)
+		return
+	}
+
+	err = models.DeleteDashboard(slug, c.GetAccountId())
+	if err != nil {
+		c.ApiError(500, "Failed to delete dashboard", err)
+		return
+	}
+
+	var resp = map[string]interface{}{"title": dash.Title}
+
+	c.JSON(200, resp)
+}
+
+func Search(c *middleware.Context) {
+	query := c.Query("q")
+
+	results, err := models.SearchQuery(query, c.GetAccountId())
+	if err != nil {
+		c.ApiError(500, "Search failed", err)
+		return
+	}
+
+	c.JSON(200, results)
+}
+
+func PostDashboard(c *middleware.Context) {
+	var command apimodel.SaveDashboardCommand
+
+	if !c.JsonBody(&command) {
+		c.ApiError(400, "bad request", nil)
+		return
+	}
+
+	dashboard := models.NewDashboard("test")
+	dashboard.Data = command.Dashboard
+	dashboard.Title = dashboard.Data["title"].(string)
+	dashboard.AccountId = c.GetAccountId()
+	dashboard.UpdateSlug()
+
+	if dashboard.Data["id"] != nil {
+		dashboard.Id = dashboard.Data["id"].(string)
+	}
+
+	err := models.SaveDashboard(dashboard)
+	if err != nil {
+		c.ApiError(500, "Failed to save dashboard", err)
+		return
+	}
+
+	c.JSON(200, gin.H{"status": "success", "slug": dashboard.Slug})
+}

+ 30 - 0
pkg/routes/api/api_render.go

@@ -0,0 +1,30 @@
+package api
+
+import (
+	"strconv"
+
+	"github.com/torkelo/grafana-pro/pkg/components/renderer"
+	"github.com/torkelo/grafana-pro/pkg/middleware"
+	"github.com/torkelo/grafana-pro/pkg/utils"
+)
+
+func RenderToPng(c *middleware.Context) {
+	accountId := c.GetAccountId()
+	queryReader := utils.NewUrlQueryReader(c.Req.URL)
+	queryParams := "?render&accountId=" + strconv.Itoa(accountId) + "&" + c.Req.URL.RawQuery
+
+	renderOpts := &renderer.RenderOpts{
+		Url:    c.Params("url") + queryParams,
+		Width:  queryReader.Get("width", "800"),
+		Height: queryReader.Get("height", "400"),
+	}
+
+	renderOpts.Url = "http://localhost:3000" + renderOpts.Url
+
+	pngPath, err := renderer.RenderToPng(renderOpts)
+	if err != nil {
+		c.HTML(500, "error.html", nil)
+	}
+
+	c.ServeFile(pngPath)
+}

+ 6 - 0
pkg/routes/apimodel/models.go

@@ -34,3 +34,9 @@ func getGravatarUrl(text string) string {
 	hasher.Write([]byte(strings.ToLower(text)))
 	return fmt.Sprintf("https://secure.gravatar.com/avatar/%x?s=90&default=mm", hasher.Sum(nil))
 }
+
+type SaveDashboardCommand struct {
+	Id        string                 `json:"id"`
+	Title     string                 `json:"title"`
+	Dashboard map[string]interface{} `json:"dashboard"`
+}

+ 25 - 0
pkg/routes/index.go

@@ -1,10 +1,35 @@
 package routes
 
 import (
+	"github.com/Unknwon/macaron"
 	"github.com/torkelo/grafana-pro/pkg/middleware"
+	"github.com/torkelo/grafana-pro/pkg/routes/api"
 	"github.com/torkelo/grafana-pro/pkg/routes/apimodel"
+	"github.com/torkelo/grafana-pro/pkg/routes/login"
 )
 
+func Register(m *macaron.Macaron) {
+	auth := middleware.Auth()
+
+	// index
+	m.Get("/", auth, Index)
+	m.Post("/logout", login.LogoutPost)
+	m.Post("/login", login.LoginPost)
+
+	// no auth
+	m.Get("/login", Index)
+
+	// dashboards
+	m.Get("/dashboard/*", auth, Index)
+	m.Get("/api/dashboards/:slug", auth, api.GetDashboard)
+	m.Get("/api/search/", auth, api.Search)
+	m.Post("/api/dashboard/", auth, api.PostDashboard)
+	m.Delete("/api/dashboard/:slug", auth, api.DeleteDashboard)
+
+	// rendering
+	m.Get("/render/*url", auth, api.RenderToPng)
+}
+
 func Index(ctx *middleware.Context) {
 	ctx.Data["User"] = apimodel.NewCurrentUserDto(ctx.UserAccount)
 	ctx.HTML(200, "index")

+ 8 - 0
pkg/setting/setting.go

@@ -58,6 +58,10 @@ var (
 	ProdMode     bool
 	RunUser      string
 	IsWindows    bool
+
+	// PhantomJs Rendering
+	ImagesDir  string
+	PhantomDir string
 )
 
 func init() {
@@ -140,6 +144,10 @@ func NewConfigContext() {
 
 	StaticRootPath = Cfg.MustValue("server", "static_root_path", workDir)
 	RouterLogging = Cfg.MustBool("server", "router_logging", false)
+
+	// PhantomJS rendering
+	ImagesDir = "data/png"
+	PhantomDir = "_vendor/phantomjs"
 }
 
 func initSessionService() {

+ 5 - 0
pkg/stores/rethink/rethink.go

@@ -34,6 +34,11 @@ func Init() {
 
 	models.GetAccount = GetAccount
 	models.GetAccountByLogin = GetAccountByLogin
+
+	models.GetDashboard = GetDashboard
+	models.SearchQuery = SearchQuery
+	models.DeleteDashboard = DeleteDashboard
+	models.SaveDashboard = SaveDashboard
 }
 
 func createRethinkDBTablesAndIndices() {

+ 80 - 0
pkg/stores/rethink/rethink_dashboards.go

@@ -0,0 +1,80 @@
+package rethink
+
+import (
+	"errors"
+
+	r "github.com/dancannon/gorethink"
+
+	"github.com/torkelo/grafana-pro/pkg/log"
+	"github.com/torkelo/grafana-pro/pkg/models"
+)
+
+func SaveDashboard(dash *models.Dashboard) error {
+	resp, err := r.Table("dashboards").Insert(dash, r.InsertOpts{Conflict: "update"}).RunWrite(session)
+	if err != nil {
+		return err
+	}
+
+	log.Info("Inserted: %v, Errors: %v, Updated: %v", resp.Inserted, resp.Errors, resp.Updated)
+	log.Info("First error:", resp.FirstError)
+	if len(resp.GeneratedKeys) > 0 {
+		dash.Id = resp.GeneratedKeys[0]
+	}
+
+	return nil
+}
+
+func GetDashboard(slug string, accountId int) (*models.Dashboard, error) {
+	resp, err := r.Table("dashboards").
+		GetAllByIndex("AccountIdSlug", []interface{}{accountId, slug}).
+		Run(session)
+
+	if err != nil {
+		return nil, err
+	}
+
+	var dashboard models.Dashboard
+	err = resp.One(&dashboard)
+	if err != nil {
+		return nil, err
+	}
+
+	return &dashboard, nil
+}
+
+func DeleteDashboard(slug string, accountId int) error {
+	resp, err := r.Table("dashboards").
+		GetAllByIndex("AccountIdSlug", []interface{}{accountId, slug}).
+		Delete().RunWrite(session)
+
+	if err != nil {
+		return err
+	}
+
+	if resp.Deleted != 1 {
+		return errors.New("Did not find dashboard to delete")
+	}
+
+	return nil
+}
+
+func SearchQuery(query string, accountId int) ([]*models.SearchResult, error) {
+	docs, err := r.Table("dashboards").
+		GetAllByIndex("AccountId", []interface{}{accountId}).
+		Filter(r.Row.Field("Title").Match(".*")).Run(session)
+
+	if err != nil {
+		return nil, err
+	}
+
+	results := make([]*models.SearchResult, 0, 50)
+	var dashboard models.Dashboard
+	for docs.Next(&dashboard) {
+		results = append(results, &models.SearchResult{
+			Title: dashboard.Title,
+			Id:    dashboard.Slug,
+		})
+	}
+
+	return results, nil
+}