file_reader.go 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227
  1. package dashboards
  2. import (
  3. "context"
  4. "errors"
  5. "fmt"
  6. "os"
  7. "path/filepath"
  8. "strings"
  9. "time"
  10. "github.com/grafana/grafana/pkg/services/dashboards"
  11. "github.com/grafana/grafana/pkg/bus"
  12. "github.com/grafana/grafana/pkg/components/simplejson"
  13. "github.com/grafana/grafana/pkg/log"
  14. "github.com/grafana/grafana/pkg/models"
  15. )
  16. var (
  17. checkDiskForChangesInterval time.Duration = time.Second * 3
  18. ErrFolderNameMissing error = errors.New("Folder name missing")
  19. )
  20. type fileReader struct {
  21. Cfg *DashboardsAsConfig
  22. Path string
  23. log log.Logger
  24. dashboardRepo dashboards.Repository
  25. cache *dashboardCache
  26. createWalk func(fr *fileReader, folderId int64) filepath.WalkFunc
  27. }
  28. func NewDashboardFileReader(cfg *DashboardsAsConfig, log log.Logger) (*fileReader, error) {
  29. path, ok := cfg.Options["folder"].(string)
  30. if !ok {
  31. return nil, fmt.Errorf("Failed to load dashboards. folder param is not a string")
  32. }
  33. if _, err := os.Stat(path); os.IsNotExist(err) {
  34. log.Error("Cannot read directory", "error", err)
  35. }
  36. return &fileReader{
  37. Cfg: cfg,
  38. Path: path,
  39. log: log,
  40. dashboardRepo: dashboards.GetRepository(),
  41. cache: NewDashboardCache(),
  42. createWalk: createWalkFn,
  43. }, nil
  44. }
  45. func (fr *fileReader) ReadAndListen(ctx context.Context) error {
  46. ticker := time.NewTicker(checkDiskForChangesInterval)
  47. if err := fr.startWalkingDisk(); err != nil {
  48. fr.log.Error("failed to search for dashboards", "error", err)
  49. }
  50. running := false
  51. for {
  52. select {
  53. case <-ticker.C:
  54. if !running { // avoid walking the filesystem in parallel. incase fs is very slow.
  55. running = true
  56. go func() {
  57. if err := fr.startWalkingDisk(); err != nil {
  58. fr.log.Error("failed to search for dashboards", "error", err)
  59. }
  60. running = false
  61. }()
  62. }
  63. case <-ctx.Done():
  64. return nil
  65. }
  66. }
  67. }
  68. func (fr *fileReader) startWalkingDisk() error {
  69. if _, err := os.Stat(fr.Path); err != nil {
  70. if os.IsNotExist(err) {
  71. return err
  72. }
  73. }
  74. folderId, err := getOrCreateFolderId(fr.Cfg, fr.dashboardRepo)
  75. if err != nil && err != ErrFolderNameMissing {
  76. return err
  77. }
  78. return filepath.Walk(fr.Path, fr.createWalk(fr, folderId))
  79. }
  80. func getOrCreateFolderId(cfg *DashboardsAsConfig, repo dashboards.Repository) (int64, error) {
  81. if cfg.Folder == "" {
  82. return 0, ErrFolderNameMissing
  83. }
  84. cmd := &models.GetDashboardQuery{Slug: models.SlugifyTitle(cfg.Folder), OrgId: cfg.OrgId}
  85. err := bus.Dispatch(cmd)
  86. if err != nil && err != models.ErrDashboardNotFound {
  87. return 0, err
  88. }
  89. // dashboard folder not found. create one.
  90. if err == models.ErrDashboardNotFound {
  91. dash := &dashboards.SaveDashboardItem{}
  92. dash.Dashboard = models.NewDashboard(cfg.Folder)
  93. dash.Dashboard.IsFolder = true
  94. dash.Overwrite = true
  95. dash.OrgId = cfg.OrgId
  96. dbDash, err := repo.SaveDashboard(dash)
  97. if err != nil {
  98. return 0, err
  99. }
  100. return dbDash.Id, nil
  101. }
  102. if !cmd.Result.IsFolder {
  103. return 0, fmt.Errorf("Got invalid response. Expected folder, found dashboard")
  104. }
  105. return cmd.Result.Id, nil
  106. }
  107. func createWalkFn(fr *fileReader, folderId int64) filepath.WalkFunc {
  108. return func(path string, fileInfo os.FileInfo, err error) error {
  109. if err != nil {
  110. return err
  111. }
  112. if fileInfo.IsDir() {
  113. if strings.HasPrefix(fileInfo.Name(), ".") {
  114. return filepath.SkipDir
  115. }
  116. return nil
  117. }
  118. if !strings.HasSuffix(fileInfo.Name(), ".json") {
  119. return nil
  120. }
  121. checkFilepath, err := filepath.EvalSymlinks(path)
  122. if path != checkFilepath {
  123. path = checkFilepath
  124. fi, err := os.Lstat(checkFilepath)
  125. if err != nil {
  126. return err
  127. }
  128. fileInfo = fi
  129. }
  130. cachedDashboard, exist := fr.cache.getCache(path)
  131. if exist && cachedDashboard.UpdatedAt == fileInfo.ModTime() {
  132. return nil
  133. }
  134. dash, err := fr.readDashboardFromFile(path, folderId)
  135. if err != nil {
  136. fr.log.Error("failed to load dashboard from ", "file", path, "error", err)
  137. return nil
  138. }
  139. if dash.Dashboard.Id != 0 {
  140. fr.log.Error("Cannot provision dashboard. Please remove the id property from the json file")
  141. return nil
  142. }
  143. cmd := &models.GetDashboardQuery{Slug: dash.Dashboard.Slug}
  144. err = bus.Dispatch(cmd)
  145. // if we don't have the dashboard in the db, save it!
  146. if err == models.ErrDashboardNotFound {
  147. fr.log.Debug("saving new dashboard", "file", path)
  148. _, err = fr.dashboardRepo.SaveDashboard(dash)
  149. return err
  150. }
  151. if err != nil {
  152. fr.log.Error("failed to query for dashboard", "slug", dash.Dashboard.Slug, "error", err)
  153. return nil
  154. }
  155. // break if db version is newer then fil version
  156. if cmd.Result.Updated.Unix() >= fileInfo.ModTime().Unix() {
  157. return nil
  158. }
  159. fr.log.Debug("loading dashboard from disk into database.", "file", path)
  160. _, err = fr.dashboardRepo.SaveDashboard(dash)
  161. return err
  162. }
  163. }
  164. func (fr *fileReader) readDashboardFromFile(path string, folderId int64) (*dashboards.SaveDashboardItem, error) {
  165. reader, err := os.Open(path)
  166. if err != nil {
  167. return nil, err
  168. }
  169. defer reader.Close()
  170. data, err := simplejson.NewFromReader(reader)
  171. if err != nil {
  172. return nil, err
  173. }
  174. stat, err := os.Stat(path)
  175. if err != nil {
  176. return nil, err
  177. }
  178. dash, err := createDashboardJson(data, stat.ModTime(), fr.Cfg, folderId)
  179. if err != nil {
  180. return nil, err
  181. }
  182. fr.cache.addDashboardCache(path, dash)
  183. return dash, nil
  184. }