Explorar o código

Merge branch 'master' of github.com:torkelo/grafana-pro

Conflicts:
	grafana
Torkel Ödegaard %!s(int64=11) %!d(string=hai) anos
pai
achega
f133a9de79

+ 1 - 1
grafana

@@ -1 +1 @@
-Subproject commit 3944c37627326a14c8d39d8f2f25617681e3f074
+Subproject commit 9b2476451ef341285e1387c6eefe97c7995e300a

+ 2 - 0
install_dependencies.sh

@@ -0,0 +1,2 @@
+go get code.google.com/p/goprotobuf/{proto,protoc-gen-go}
+

+ 2 - 2
pkg/api/api.go

@@ -51,8 +51,8 @@ func (self *HttpServer) ListenAndServe() {
 	}
 
 	// register default route
-	self.router.GET("/", self.authMiddleware(), self.index)
-	self.router.GET("/dashboard/*_", self.authMiddleware(), self.index)
+	self.router.GET("/", self.auth(), self.index)
+	self.router.GET("/dashboard/*_", self.auth(), self.index)
 
 	self.router.Run(":" + self.port)
 }

+ 19 - 6
pkg/api/api_dashboard.go

@@ -1,22 +1,24 @@
 package api
 
 import (
+	log "github.com/alecthomas/log4go"
 	"github.com/gin-gonic/gin"
 	"github.com/torkelo/grafana-pro/pkg/models"
 )
 
 func init() {
 	addRoutes(func(self *HttpServer) {
-		self.router.GET("/api/dashboards/:id", self.getDashboard)
-		self.router.GET("/api/search/", self.search)
-		self.router.POST("/api/dashboard", self.postDashboard)
+		self.router.GET("/api/dashboards/:id", self.auth(), self.getDashboard)
+		self.router.GET("/api/search/", self.auth(), self.search)
+		self.router.POST("/api/dashboard", self.auth(), self.postDashboard)
 	})
 }
 
 func (self *HttpServer) getDashboard(c *gin.Context) {
 	id := c.Params.ByName("id")
+	accountId, err := c.Get("accountId")
 
-	dash, err := self.store.GetById(id)
+	dash, err := self.store.GetDashboard(id, accountId.(int))
 	if err != nil {
 		c.JSON(404, newErrorResponse("Dashboard not found"))
 		return
@@ -30,6 +32,7 @@ func (self *HttpServer) search(c *gin.Context) {
 
 	results, err := self.store.Query(query)
 	if err != nil {
+		log.Error("Store query error: %v", err)
 		c.JSON(500, newErrorResponse("Failed"))
 		return
 	}
@@ -41,9 +44,19 @@ func (self *HttpServer) postDashboard(c *gin.Context) {
 	var command saveDashboardCommand
 
 	if c.EnsureBody(&command) {
-		err := self.store.Save(&models.Dashboard{Data: command.Dashboard})
+		dashboard := models.NewDashboard("test")
+		dashboard.Data = command.Dashboard
+		dashboard.Title = dashboard.Data["title"].(string)
+		dashboard.AccountId = 1
+		dashboard.UpdateSlug()
+
+		if dashboard.Data["id"] != nil {
+			dashboard.Id = dashboard.Data["id"].(string)
+		}
+
+		err := self.store.SaveDashboard(dashboard)
 		if err == nil {
-			c.JSON(200, gin.H{"status": "saved"})
+			c.JSON(200, gin.H{"status": "success", "slug": dashboard.Slug})
 			return
 		}
 	}

+ 23 - 10
pkg/api/api_login.go

@@ -19,18 +19,28 @@ type loginJsonModel struct {
 func (self *HttpServer) loginPost(c *gin.Context) {
 	var loginModel loginJsonModel
 
-	if c.EnsureBody(&loginModel) {
-		if loginModel.Email == "manu" && loginModel.Password == "123" {
+	if !c.EnsureBody(&loginModel) {
+		c.JSON(400, gin.H{"status": "bad request"})
+		return
+	}
 
-			session, _ := sessionStore.Get(c.Request, "grafana-session")
-			session.Values["login"] = true
-			session.Save(c.Request, c.Writer)
+	account, err := self.store.GetUserAccountLogin(loginModel.Email)
+	if err != nil {
+		c.JSON(400, gin.H{"status": "some error"})
+	}
 
-			c.JSON(200, gin.H{"status": "you are logged in"})
-		} else {
-			c.JSON(401, gin.H{"status": "unauthorized"})
-		}
+	if loginModel.Password != account.Password {
+		c.JSON(401, gin.H{"status": "unauthorized"})
+		return
 	}
+
+	session, _ := sessionStore.Get(c.Request, "grafana-session")
+	session.Values["login"] = true
+	session.Values["accountId"] = account.DatabaseId
+
+	session.Save(c.Request, c.Writer)
+
+	c.JSON(200, gin.H{"status": "you are logged in"})
 }
 
 func (self *HttpServer) logoutPost(c *gin.Context) {
@@ -41,15 +51,18 @@ func (self *HttpServer) logoutPost(c *gin.Context) {
 	c.JSON(200, gin.H{"status": "logged out"})
 }
 
-func (self *HttpServer) authMiddleware() gin.HandlerFunc {
+func (self *HttpServer) auth() gin.HandlerFunc {
 	return func(c *gin.Context) {
 		session, _ := sessionStore.Get(c.Request, "grafana-session")
 
 		if c.Request.URL.Path != "/login" && session.Values["login"] == nil {
 			c.Writer.Header().Set("Location", "/login")
 			c.Abort(302)
+			return
 		}
 
+		c.Set("accountId", session.Values["accountId"])
+
 		session.Save(c.Request, c.Writer)
 	}
 }

+ 45 - 0
pkg/api/api_register.go

@@ -0,0 +1,45 @@
+package api
+
+import (
+	log "github.com/alecthomas/log4go"
+	"github.com/gin-gonic/gin"
+	"github.com/torkelo/grafana-pro/pkg/models"
+)
+
+func init() {
+	addRoutes(func(self *HttpServer) {
+		self.router.GET("/register/*_", self.index)
+		self.router.POST("/api/register/user", self.registerUserPost)
+	})
+}
+
+type registerAccountJsonModel struct {
+	Email     string `json:"email" binding:"required"`
+	Password  string `json:"password" binding:"required"`
+	Password2 bool   `json:"remember2"`
+}
+
+func (self *HttpServer) registerUserPost(c *gin.Context) {
+	var registerModel registerAccountJsonModel
+
+	if !c.EnsureBody(&registerModel) {
+		c.JSON(400, gin.H{"status": "bad request"})
+		return
+	}
+
+	account := models.UserAccount{
+		UserName: registerModel.Email,
+		Login:    registerModel.Email,
+		Email:    registerModel.Email,
+		Password: registerModel.Password,
+	}
+
+	err := self.store.SaveUserAccount(&account)
+	if err != nil {
+		log.Error("Failed to create user account, email: %v, error: %v", registerModel.Email, err)
+		c.JSON(500, gin.H{"status": "failed to create account"})
+		return
+	}
+
+	c.JSON(200, gin.H{"status": "ok"})
+}

+ 18 - 0
pkg/models/dashboard_test.go

@@ -0,0 +1,18 @@
+package models
+
+import (
+	"testing"
+
+	. "github.com/smartystreets/goconvey/convey"
+)
+
+func TestDashboardModel(t *testing.T) {
+
+	Convey("When generating slug", t, func() {
+		dashboard := NewDashboard("Grafana Play Home")
+		dashboard.UpdateSlug()
+
+		So(dashboard.Slug, ShouldEqual, "grafana-play-home")
+	})
+
+}

+ 50 - 14
pkg/models/dashboards.go

@@ -3,22 +3,65 @@ package models
 import (
 	"encoding/json"
 	"io"
+	"regexp"
+	"strings"
+	"time"
 )
 
 type Dashboard struct {
-	Data map[string]interface{}
+	Id                   string `gorethink:"id,omitempty"`
+	Slug                 string
+	AccountId            int
+	LastModifiedByUserId string
+	LastModifiedByDate   time.Time
+	CreatedDate          time.Time
+
+	Title string
+	Tags  []string
+	Data  map[string]interface{}
+}
+
+type UserAccountLink struct {
+	UserId     int
+	Role       string
+	ModifiedOn time.Time
+	CreatedOn  time.Time
+}
+
+type UserAccount struct {
+	DatabaseId      int `gorethink:"id"`
+	UserName        string
+	Login           string
+	Email           string
+	Password        string
+	NextDashboardId int
+	UsingAccountId  int
+	GrantedAccess   []UserAccountLink
+	CreatedOn       time.Time
+	ModifiedOn      time.Time
+}
+
+type UserContext struct {
+	UserId    string
+	AccountId string
 }
 
 type SearchResult struct {
-	Type  string `json:"title"`
 	Id    string `json:"id"`
 	Title string `json:"title"`
+	Slug  string `json:"slug"`
 }
 
 func NewDashboard(title string) *Dashboard {
 	dash := &Dashboard{}
+	dash.Id = ""
+	dash.LastModifiedByDate = time.Now()
+	dash.CreatedDate = time.Now()
+	dash.LastModifiedByUserId = "123"
 	dash.Data = make(map[string]interface{})
 	dash.Data["title"] = title
+	dash.Title = title
+	dash.UpdateSlug()
 
 	return dash
 }
@@ -34,20 +77,13 @@ func NewFromJson(reader io.Reader) (*Dashboard, error) {
 	return dash, nil
 }
 
-/*type DashboardServices struct {
-}
-
-type DashboardServicesFilter struct {
-}
-
-type DashboardServicesFilterTime struct {
-	From string 	To	string
-}*/
-
 func (dash *Dashboard) GetString(prop string) string {
 	return dash.Data[prop].(string)
 }
 
-func (dash *Dashboard) Title() string {
-	return dash.GetString("title")
+func (dash *Dashboard) UpdateSlug() {
+	title := strings.ToLower(dash.Data["title"].(string))
+	re := regexp.MustCompile("[^\\w ]+")
+	re2 := regexp.MustCompile("\\s")
+	dash.Slug = re2.ReplaceAllString(re.ReplaceAllString(title, ""), "-")
 }

+ 154 - 153
pkg/stores/file_store.go

@@ -1,155 +1,156 @@
 package stores
 
-import (
-	"encoding/json"
-	"io"
-	"os"
-	"path/filepath"
-	"strings"
-
-	log "github.com/alecthomas/log4go"
-	"github.com/torkelo/grafana-pro/pkg/models"
-)
-
-type fileStore struct {
-	dataDir string
-	dashDir string
-	cache   map[string]*models.Dashboard
-}
-
-func NewFileStore(dataDir string) *fileStore {
-
-	if dirDoesNotExist(dataDir) {
-		log.Crashf("FileStore failed to initialize, dataDir does not exist %v", dataDir)
-	}
-
-	dashDir := filepath.Join(dataDir, "dashboards")
-
-	if dirDoesNotExist(dashDir) {
-		log.Debug("Did not find dashboard dir, creating...")
-		err := os.Mkdir(dashDir, 0777)
-		if err != nil {
-			log.Crashf("FileStore failed to initialize, could not create directory %v, error: %v", dashDir, err)
-		}
-	}
-
-	store := &fileStore{}
-	store.dataDir = dataDir
-	store.dashDir = dashDir
-	store.cache = make(map[string]*models.Dashboard)
-	go store.scanFiles()
-
-	return store
-}
-
-func (store *fileStore) scanFiles() {
-	visitor := func(path string, f os.FileInfo, err error) error {
-		if err != nil {
-			return err
-		}
-		if f.IsDir() {
-			return nil
-		}
-		if strings.HasSuffix(f.Name(), ".json") {
-			err = store.loadDashboardIntoCache(path)
-			if err != nil {
-				return err
-			}
-		}
-		return nil
-	}
-
-	err := filepath.Walk(store.dashDir, visitor)
-	if err != nil {
-		log.Error("FileStore::updateCache failed %v", err)
-	}
-}
-
-func (store fileStore) loadDashboardIntoCache(filename string) error {
-	log.Info("Loading dashboard file %v into cache", filename)
-	dash, err := loadDashboardFromFile(filename)
-	if err != nil {
-		return err
-	}
-
-	store.cache[dash.Title()] = dash
-
-	return nil
-}
-
-func (store *fileStore) Close() {
-
-}
-
-func (store *fileStore) GetById(id string) (*models.Dashboard, error) {
-	log.Debug("FileStore::GetById id = %v", id)
-	filename := store.getFilePathForDashboard(id)
-
-	return loadDashboardFromFile(filename)
-}
-
-func (store *fileStore) Save(dash *models.Dashboard) error {
-	filename := store.getFilePathForDashboard(dash.Title())
-
-	log.Debug("Saving dashboard %v to %v", dash.Title(), filename)
-
-	var err error
-	var data []byte
-	if data, err = json.Marshal(dash.Data); err != nil {
-		return err
-	}
-
-	return writeFile(filename, data)
-}
-
-func (store *fileStore) Query(query string) ([]*models.SearchResult, error) {
-	results := make([]*models.SearchResult, 0, 50)
-
-	for _, dash := range store.cache {
-		item := &models.SearchResult{
-			Id:   dash.Title(),
-			Type: "dashboard",
-		}
-		results = append(results, item)
-	}
-
-	return results, nil
-}
-
-func loadDashboardFromFile(filename string) (*models.Dashboard, error) {
-	log.Debug("FileStore::loading dashboard from file %v", filename)
-
-	configFile, err := os.Open(filename)
-	if err != nil {
-		return nil, err
-	}
-
-	return models.NewFromJson(configFile)
-}
-
-func (store *fileStore) getFilePathForDashboard(id string) string {
-	id = strings.ToLower(id)
-	id = strings.Replace(id, " ", "-", -1)
-	return filepath.Join(store.dashDir, id) + ".json"
-}
-
-func dirDoesNotExist(dir string) bool {
-	_, err := os.Stat(dir)
-	return os.IsNotExist(err)
-}
-
-func writeFile(filename string, data []byte) error {
-	f, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
-	if err != nil {
-		return err
-	}
-	n, err := f.Write(data)
-	if err == nil && n < len(data) {
-		err = io.ErrShortWrite
-	}
-	if err1 := f.Close(); err == nil {
-		err = err1
-	}
-
-	return err
-}
+//
+// import (
+// 	"encoding/json"
+// 	"io"
+// 	"os"
+// 	"path/filepath"
+// 	"strings"
+//
+// 	log "github.com/alecthomas/log4go"
+// 	"github.com/torkelo/grafana-pro/pkg/models"
+// )
+//
+// type fileStore struct {
+// 	dataDir string
+// 	dashDir string
+// 	cache   map[string]*models.Dashboard
+// }
+//
+// func NewFileStore(dataDir string) *fileStore {
+//
+// 	if dirDoesNotExist(dataDir) {
+// 		log.Crashf("FileStore failed to initialize, dataDir does not exist %v", dataDir)
+// 	}
+//
+// 	dashDir := filepath.Join(dataDir, "dashboards")
+//
+// 	if dirDoesNotExist(dashDir) {
+// 		log.Debug("Did not find dashboard dir, creating...")
+// 		err := os.Mkdir(dashDir, 0777)
+// 		if err != nil {
+// 			log.Crashf("FileStore failed to initialize, could not create directory %v, error: %v", dashDir, err)
+// 		}
+// 	}
+//
+// 	store := &fileStore{}
+// 	store.dataDir = dataDir
+// 	store.dashDir = dashDir
+// 	store.cache = make(map[string]*models.Dashboard)
+// 	store.scanFiles()
+//
+// 	return store
+// }
+//
+// func (store *fileStore) scanFiles() {
+// 	visitor := func(path string, f os.FileInfo, err error) error {
+// 		if err != nil {
+// 			return err
+// 		}
+// 		if f.IsDir() {
+// 			return nil
+// 		}
+// 		if strings.HasSuffix(f.Name(), ".json") {
+// 			err = store.loadDashboardIntoCache(path)
+// 			if err != nil {
+// 				return err
+// 			}
+// 		}
+// 		return nil
+// 	}
+//
+// 	err := filepath.Walk(store.dashDir, visitor)
+// 	if err != nil {
+// 		log.Error("FileStore::updateCache failed %v", err)
+// 	}
+// }
+//
+// func (store fileStore) loadDashboardIntoCache(filename string) error {
+// 	log.Info("Loading dashboard file %v into cache", filename)
+// 	dash, err := loadDashboardFromFile(filename)
+// 	if err != nil {
+// 		return err
+// 	}
+//
+// 	store.cache[dash.Title] = dash
+//
+// 	return nil
+// }
+//
+// func (store *fileStore) Close() {
+//
+// }
+//
+// func (store *fileStore) GetById(id string) (*models.Dashboard, error) {
+// 	log.Debug("FileStore::GetById id = %v", id)
+// 	filename := store.getFilePathForDashboard(id)
+//
+// 	return loadDashboardFromFile(filename)
+// }
+//
+// func (store *fileStore) Save(dash *models.Dashboard) error {
+// 	filename := store.getFilePathForDashboard(dash.Title)
+//
+// 	log.Debug("Saving dashboard %v to %v", dash.Title, filename)
+//
+// 	var err error
+// 	var data []byte
+// 	if data, err = json.Marshal(dash.Data); err != nil {
+// 		return err
+// 	}
+//
+// 	return writeFile(filename, data)
+// }
+//
+// func (store *fileStore) Query(query string) ([]*models.SearchResult, error) {
+// 	results := make([]*models.SearchResult, 0, 50)
+//
+// 	for _, dash := range store.cache {
+// 		item := &models.SearchResult{
+// 			Id:   dash.Title,
+// 			Type: "dashboard",
+// 		}
+// 		results = append(results, item)
+// 	}
+//
+// 	return results, nil
+// }
+//
+// func loadDashboardFromFile(filename string) (*models.Dashboard, error) {
+// 	log.Debug("FileStore::loading dashboard from file %v", filename)
+//
+// 	configFile, err := os.Open(filename)
+// 	if err != nil {
+// 		return nil, err
+// 	}
+//
+// 	return models.NewFromJson(configFile)
+// }
+//
+// func (store *fileStore) getFilePathForDashboard(id string) string {
+// 	id = strings.ToLower(id)
+// 	id = strings.Replace(id, " ", "-", -1)
+// 	return filepath.Join(store.dashDir, id) + ".json"
+// }
+//
+// func dirDoesNotExist(dir string) bool {
+// 	_, err := os.Stat(dir)
+// 	return os.IsNotExist(err)
+// }
+//
+// func writeFile(filename string, data []byte) error {
+// 	f, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
+// 	if err != nil {
+// 		return err
+// 	}
+// 	n, err := f.Write(data)
+// 	if err == nil && n < len(data) {
+// 		err = io.ErrShortWrite
+// 	}
+// 	if err1 := f.Close(); err == nil {
+// 		err = err1
+// 	}
+//
+// 	return err
+// }

+ 111 - 110
pkg/stores/file_store_test.go

@@ -1,112 +1,113 @@
 package stores
 
-import (
-	"fmt"
-	"io"
-	"io/ioutil"
-	"os"
-	"path/filepath"
-	"testing"
-
-	. "github.com/smartystreets/goconvey/convey"
-	"github.com/torkelo/grafana-pro/pkg/models"
-)
-
-func TestFileStore(t *testing.T) {
-
-	GivenFileStore("When saving a dashboard", t, func(store *fileStore) {
-		dashboard := models.NewDashboard("hello")
-
-		err := store.Save(dashboard)
-
-		Convey("should be saved to disk", func() {
-			So(err, ShouldBeNil)
-
-			_, err = os.Stat(store.getFilePathForDashboard("hello"))
-			So(err, ShouldBeNil)
-		})
-	})
-
-	GivenFileStore("When getting a saved dashboard", t, func(store *fileStore) {
-		copyDashboardToTempData("default.json", "", store.dashDir)
-		dash, err := store.GetById("default")
-
-		Convey("should be read from disk", func() {
-			So(err, ShouldBeNil)
-			So(dash, ShouldNotBeNil)
-
-			So(dash.Title(), ShouldEqual, "Grafana Play Home")
-		})
-	})
-
-	GivenFileStore("when getting dashboard with capital letters", t, func(store *fileStore) {
-		copyDashboardToTempData("annotations.json", "", store.dashDir)
-		dash, err := store.GetById("AnnoTations")
-
-		Convey("should be read from disk", func() {
-			So(err, ShouldBeNil)
-			So(dash, ShouldNotBeNil)
-
-			So(dash.Title(), ShouldEqual, "Annotations")
-		})
-	})
-
-	GivenFileStore("When copying dashboards into data dir", t, func(store *fileStore) {
-		copyDashboardToTempData("annotations.json", "", store.dashDir)
-		copyDashboardToTempData("default.json", "", store.dashDir)
-		copyDashboardToTempData("graph-styles.json", "", store.dashDir)
-		store.scanFiles()
-
-		Convey("scan should generate index of all dashboards", func() {
-
-			result, err := store.Query("*")
-			So(err, ShouldBeNil)
-			So(len(result), ShouldEqual, 3)
-		})
-	})
-}
-
-func copyDashboardToTempData(name string, destName string, dir string) {
-	if destName == "" {
-		destName = name
-	}
-	source, _ := filepath.Abs("../../data/dashboards/" + name)
-	dest := filepath.Join(dir, destName)
-	err := copyFile(dest, source)
-	if err != nil {
-		panic(fmt.Sprintf("failed to copy file %v", name))
-	}
-}
-
-func GivenFileStore(desc string, t *testing.T, f func(store *fileStore)) {
-	Convey(desc, t, func() {
-		tempDir, _ := ioutil.TempDir("", "store")
-
-		store := NewFileStore(tempDir)
-
-		f(store)
-
-		Reset(func() {
-			os.RemoveAll(tempDir)
-		})
-	})
-}
-
-func copyFile(dst, src string) error {
-	in, err := os.Open(src)
-	if err != nil {
-		return err
-	}
-	defer in.Close()
-	out, err := os.Create(dst)
-	if err != nil {
-		return err
-	}
-	defer out.Close()
-	_, err = io.Copy(out, in)
-	cerr := out.Close()
-	if err != nil {
-		return err
-	}
-	return cerr
-}
+//
+// import (
+// 	"fmt"
+// 	"io"
+// 	"io/ioutil"
+// 	"os"
+// 	"path/filepath"
+// 	"testing"
+//
+// 	. "github.com/smartystreets/goconvey/convey"
+// 	"github.com/torkelo/grafana-pro/pkg/models"
+// )
+//
+// func TestFileStore(t *testing.T) {
+//
+// 	GivenFileStore("When saving a dashboard", t, func(store *fileStore) {
+// 		dashboard := models.NewDashboard("hello")
+//
+// 		err := store.Save(dashboard)
+//
+// 		Convey("should be saved to disk", func() {
+// 			So(err, ShouldBeNil)
+//
+// 			_, err = os.Stat(store.getFilePathForDashboard("hello"))
+// 			So(err, ShouldBeNil)
+// 		})
+// 	})
+//
+// 	GivenFileStore("When getting a saved dashboard", t, func(store *fileStore) {
+// 		copyDashboardToTempData("default.json", "", store.dashDir)
+// 		dash, err := store.GetById("default")
+//
+// 		Convey("should be read from disk", func() {
+// 			So(err, ShouldBeNil)
+// 			So(dash, ShouldNotBeNil)
+//
+// 			So(dash.Title, ShouldEqual, "Grafana Play Home")
+// 		})
+// 	})
+//
+// 	GivenFileStore("when getting dashboard with capital letters", t, func(store *fileStore) {
+// 		copyDashboardToTempData("annotations.json", "", store.dashDir)
+// 		dash, err := store.GetById("AnnoTations")
+//
+// 		Convey("should be read from disk", func() {
+// 			So(err, ShouldBeNil)
+// 			So(dash, ShouldNotBeNil)
+//
+// 			So(dash.Title, ShouldEqual, "Annotations")
+// 		})
+// 	})
+//
+// 	GivenFileStore("When copying dashboards into data dir", t, func(store *fileStore) {
+// 		copyDashboardToTempData("annotations.json", "", store.dashDir)
+// 		copyDashboardToTempData("default.json", "", store.dashDir)
+// 		copyDashboardToTempData("graph-styles.json", "", store.dashDir)
+// 		store.scanFiles()
+//
+// 		Convey("scan should generate index of all dashboards", func() {
+//
+// 			result, err := store.Query("*")
+// 			So(err, ShouldBeNil)
+// 			So(len(result), ShouldEqual, 3)
+// 		})
+// 	})
+// }
+//
+// func copyDashboardToTempData(name string, destName string, dir string) {
+// 	if destName == "" {
+// 		destName = name
+// 	}
+// 	source, _ := filepath.Abs("../../data/dashboards/" + name)
+// 	dest := filepath.Join(dir, destName)
+// 	err := copyFile(dest, source)
+// 	if err != nil {
+// 		panic(fmt.Sprintf("failed to copy file %v", name))
+// 	}
+// }
+//
+// func GivenFileStore(desc string, t *testing.T, f func(store *fileStore)) {
+// 	Convey(desc, t, func() {
+// 		tempDir, _ := ioutil.TempDir("", "store")
+//
+// 		store := NewFileStore(tempDir)
+//
+// 		f(store)
+//
+// 		Reset(func() {
+// 			os.RemoveAll(tempDir)
+// 		})
+// 	})
+// }
+//
+// func copyFile(dst, src string) error {
+// 	in, err := os.Open(src)
+// 	if err != nil {
+// 		return err
+// 	}
+// 	defer in.Close()
+// 	out, err := os.Create(dst)
+// 	if err != nil {
+// 		return err
+// 	}
+// 	defer out.Close()
+// 	_, err = io.Copy(out, in)
+// 	cerr := out.Close()
+// 	if err != nil {
+// 		return err
+// 	}
+// 	return cerr
+// }

+ 110 - 0
pkg/stores/rethinkdb.go

@@ -0,0 +1,110 @@
+package stores
+
+import (
+	"time"
+
+	log "github.com/alecthomas/log4go"
+	r "github.com/dancannon/gorethink"
+	"github.com/torkelo/grafana-pro/pkg/models"
+)
+
+type rethinkStore struct {
+	session *r.Session
+}
+
+type RethinkCfg struct {
+	DatabaseName string
+}
+
+type Account struct {
+	Id              int `gorethink:"id"`
+	NextDashboardId int
+}
+
+func NewRethinkStore(config *RethinkCfg) *rethinkStore {
+	log.Info("Initializing rethink storage")
+
+	session, err := r.Connect(r.ConnectOpts{
+		Address:     "localhost:28015",
+		Database:    config.DatabaseName,
+		MaxIdle:     10,
+		IdleTimeout: time.Second * 10,
+	})
+
+	if err != nil {
+		log.Crash("Failed to connect to rethink database %v", err)
+	}
+
+	r.DbCreate(config.DatabaseName).Exec(session)
+	r.Db(config.DatabaseName).TableCreate("dashboards").Exec(session)
+	r.Db(config.DatabaseName).TableCreate("accounts").Exec(session)
+	r.Db(config.DatabaseName).TableCreate("master").Exec(session)
+
+	r.Db(config.DatabaseName).Table("dashboards").IndexCreateFunc("AccountIdSlug", func(row r.Term) interface{} {
+		return []interface{}{row.Field("AccountId"), row.Field("Slug")}
+	}).Exec(session)
+
+	r.Db(config.DatabaseName).Table("accounts").IndexCreateFunc("AccountLogin", func(row r.Term) interface{} {
+		return []interface{}{row.Field("Login")}
+	}).Exec(session)
+
+	_, err = r.Table("master").Insert(map[string]interface{}{"id": "ids", "NextAccountId": 0}).RunWrite(session)
+	if err != nil {
+		log.Error("Failed to insert master ids row", err)
+	}
+
+	return &rethinkStore{
+		session: session,
+	}
+}
+
+func (self *rethinkStore) SaveDashboard(dash *models.Dashboard) error {
+	resp, err := r.Table("dashboards").Insert(dash, r.InsertOpts{Upsert: true}).RunWrite(self.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 (self *rethinkStore) GetDashboard(slug string, accountId int) (*models.Dashboard, error) {
+	resp, err := r.Table("dashboards").GetAllByIndex("AccountIdSlug", []interface{}{accountId, slug}).Run(self.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 (self *rethinkStore) Query(query string) ([]*models.SearchResult, error) {
+
+	docs, err := r.Table("dashboards").Filter(r.Row.Field("Title").Match(".*")).Run(self.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
+}
+
+func (self *rethinkStore) Close() {}

+ 76 - 0
pkg/stores/rethinkdb_accounts.go

@@ -0,0 +1,76 @@
+package stores
+
+import (
+	"errors"
+
+	r "github.com/dancannon/gorethink"
+	"github.com/torkelo/grafana-pro/pkg/models"
+)
+
+func (self *rethinkStore) getNextAccountId() (int, error) {
+	resp, err := r.Table("master").Get("ids").Update(map[string]interface{}{
+		"NextAccountId": r.Row.Field("NextAccountId").Add(1),
+	}, r.UpdateOpts{ReturnVals: true}).RunWrite(self.session)
+
+	if err != nil {
+		return 0, err
+	}
+
+	if resp.NewValue == nil {
+		return 0, errors.New("Failed to get new value after incrementing account id")
+	}
+
+	return int(resp.NewValue.(map[string]interface{})["NextAccountId"].(float64)), nil
+}
+
+func (self *rethinkStore) SaveUserAccount(account *models.UserAccount) error {
+	accountId, err := self.getNextAccountId()
+	if err != nil {
+		return err
+	}
+
+	account.DatabaseId = accountId
+
+	resp, err := r.Table("accounts").Insert(account).RunWrite(self.session)
+	if err != nil {
+		return err
+	}
+
+	if resp.Inserted == 0 {
+		return errors.New("Failed to insert acccount")
+	}
+
+	return nil
+}
+
+func (self *rethinkStore) GetUserAccountLogin(emailOrName string) (*models.UserAccount, error) {
+	resp, err := r.Table("accounts").GetAllByIndex("AccountLogin", []interface{}{emailOrName}).Run(self.session)
+
+	if err != nil {
+		return nil, err
+	}
+
+	var account models.UserAccount
+	err = resp.One(&account)
+	if err != nil {
+		return nil, errors.New("Not found")
+	}
+
+	return &account, nil
+}
+
+func (self *rethinkStore) getNextDashboardNumber(accountId int) (int, error) {
+	resp, err := r.Table("accounts").Get(accountId).Update(map[string]interface{}{
+		"NextDashboardId": r.Row.Field("NextDashboardId").Add(1),
+	}, r.UpdateOpts{ReturnVals: true}).RunWrite(self.session)
+
+	if err != nil {
+		return 0, err
+	}
+
+	if resp.NewValue == nil {
+		return 0, errors.New("Failed to get next dashboard id, no new value after update")
+	}
+
+	return int(resp.NewValue.(map[string]interface{})["NextDashboardId"].(float64)), nil
+}

+ 56 - 0
pkg/stores/rethinkdb_test.go

@@ -0,0 +1,56 @@
+package stores
+
+import (
+	"testing"
+
+	"github.com/dancannon/gorethink"
+	. "github.com/smartystreets/goconvey/convey"
+	"github.com/torkelo/grafana-pro/pkg/models"
+)
+
+func TestRethinkStore(t *testing.T) {
+	store := NewRethinkStore(&RethinkCfg{DatabaseName: "tests"})
+	defer gorethink.DbDrop("tests").Exec(store.session)
+
+	Convey("Insert dashboard", t, func() {
+		dashboard := models.NewDashboard("test")
+		dashboard.AccountId = 1
+
+		err := store.SaveDashboard(dashboard)
+		So(err, ShouldBeNil)
+		So(dashboard.Id, ShouldNotBeEmpty)
+
+		read, err := store.GetDashboard("test", 1)
+		So(err, ShouldBeNil)
+		So(read, ShouldNotBeNil)
+	})
+
+	Convey("can get next account id", t, func() {
+		id, err := store.getNextAccountId()
+		So(err, ShouldBeNil)
+		So(id, ShouldNotEqual, 0)
+
+		id2, err := store.getNextAccountId()
+		So(id2, ShouldEqual, id+1)
+	})
+
+	Convey("can create account", t, func() {
+		account := &models.UserAccount{UserName: "torkelo", Email: "mupp", Login: "test@test.com"}
+		err := store.SaveUserAccount(account)
+		So(err, ShouldBeNil)
+		So(account.DatabaseId, ShouldNotEqual, 0)
+
+		read, err := store.GetUserAccountLogin("test@test.com")
+		So(err, ShouldBeNil)
+		So(read.DatabaseId, ShouldEqual, account.DatabaseId)
+	})
+
+	Convey("can get next dashboard id", t, func() {
+		account := &models.UserAccount{UserName: "torkelo", Email: "mupp"}
+		err := store.SaveUserAccount(account)
+		dashId, err := store.getNextDashboardNumber(account.DatabaseId)
+		So(err, ShouldBeNil)
+		So(dashId, ShouldEqual, 1)
+	})
+
+}

+ 5 - 3
pkg/stores/store.go

@@ -5,12 +5,14 @@ import (
 )
 
 type Store interface {
-	GetById(id string) (*models.Dashboard, error)
-	Save(dash *models.Dashboard) error
+	GetDashboard(title string, accountId int) (*models.Dashboard, error)
+	SaveDashboard(dash *models.Dashboard) error
 	Query(query string) ([]*models.SearchResult, error)
+	SaveUserAccount(acccount *models.UserAccount) error
+	GetUserAccountLogin(emailOrName string) (*models.UserAccount, error)
 	Close()
 }
 
 func New() Store {
-	return NewFileStore("data")
+	return NewRethinkStore(&RethinkCfg{DatabaseName: "grafana"})
 }

+ 7 - 0
start_dependencies.sh

@@ -0,0 +1,7 @@
+docker kill gfdev
+docker rm gfdev
+
+docker run -d -p 8180:8080 -p 28015:28015 -p 29015:29015 \
+  --name rethinkdb \
+  -v /var/docker/grafana-pro-rethinkdb:/data \
+  dockerfile/rethinkdb