main.go 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709
  1. package main
  2. import (
  3. "bytes"
  4. "crypto/tls"
  5. "database/sql"
  6. "encoding/json"
  7. "fmt"
  8. "html/template"
  9. "io"
  10. "io/ioutil"
  11. "log"
  12. "net/http"
  13. "os"
  14. "regexp"
  15. "sort"
  16. "strings"
  17. "sync"
  18. "time"
  19. _ "github.com/lib/pq"
  20. )
  21. var (
  22. keyFile = getEnvDefault("UR_KEY_FILE", "key.pem")
  23. certFile = getEnvDefault("UR_CRT_FILE", "crt.pem")
  24. dbConn = getEnvDefault("UR_DB_URL", "postgres://user:password@localhost/ur?sslmode=disable")
  25. listenAddr = getEnvDefault("UR_LISTEN", "0.0.0.0:8443")
  26. tpl *template.Template
  27. compilerRe = regexp.MustCompile(`\(([A-Za-z0-9()., -]+) [\w-]+ \w+\) ([\w@-]+)`)
  28. aggregateVersions = []string{"v0.7", "v0.8", "v0.9", "v0.10"}
  29. )
  30. var funcs = map[string]interface{}{
  31. "commatize": commatize,
  32. "number": number,
  33. }
  34. func getEnvDefault(key, def string) string {
  35. if val := os.Getenv(key); val != "" {
  36. return val
  37. }
  38. return def
  39. }
  40. type report struct {
  41. Received time.Time // Only from DB
  42. UniqueID string
  43. Version string
  44. LongVersion string
  45. Platform string
  46. NumFolders int
  47. NumDevices int
  48. TotFiles int
  49. FolderMaxFiles int
  50. TotMiB int
  51. FolderMaxMiB int
  52. MemoryUsageMiB int
  53. SHA256Perf float64
  54. MemorySize int
  55. // v2 fields
  56. URVersion int
  57. NumCPU int
  58. FolderUses struct {
  59. ReadOnly int
  60. IgnorePerms int
  61. IgnoreDelete int
  62. AutoNormalize int
  63. }
  64. DeviceUses struct {
  65. Introducer int
  66. CustomCertName int
  67. CompressAlways int
  68. CompressMetadata int
  69. CompressNever int
  70. DynamicAddr int
  71. StaticAddr int
  72. }
  73. Announce struct {
  74. GlobalEnabled bool
  75. LocalEnabled bool
  76. DefaultServersDNS int
  77. DefaultServersIP int
  78. OtherServers int
  79. }
  80. Relays struct {
  81. Enabled bool
  82. DefaultServers int
  83. OtherServers int
  84. }
  85. UsesRateLimit bool
  86. UpgradeAllowedManual bool
  87. UpgradeAllowedAuto bool
  88. // Generated
  89. Date string
  90. }
  91. func (r *report) Validate() error {
  92. if r.UniqueID == "" || r.Version == "" || r.Platform == "" {
  93. return fmt.Errorf("missing required field")
  94. }
  95. if len(r.Date) != 8 {
  96. return fmt.Errorf("date not initialized")
  97. }
  98. return nil
  99. }
  100. func (r *report) FieldsInDBOrder() []interface{} {
  101. return []interface{}{r.UniqueID, r.Version, r.LongVersion, r.Platform, r.NumFolders,
  102. r.NumDevices, r.TotFiles, r.FolderMaxFiles, r.TotMiB, r.FolderMaxMiB,
  103. r.MemoryUsageMiB, r.SHA256Perf, r.MemorySize, r.Date,
  104. r.URVersion, r.NumCPU,
  105. r.FolderUses.ReadOnly, r.FolderUses.IgnorePerms, r.FolderUses.IgnoreDelete, r.FolderUses.AutoNormalize,
  106. r.DeviceUses.Introducer, r.DeviceUses.CustomCertName, r.DeviceUses.CompressAlways, r.DeviceUses.CompressMetadata, r.DeviceUses.CompressNever,
  107. r.DeviceUses.DynamicAddr, r.DeviceUses.StaticAddr,
  108. r.Announce.GlobalEnabled, r.Announce.LocalEnabled, r.Announce.DefaultServersDNS, r.Announce.DefaultServersIP, r.Announce.OtherServers,
  109. r.Relays.Enabled, r.Relays.DefaultServers, r.Relays.OtherServers,
  110. r.UsesRateLimit, r.UpgradeAllowedManual, r.UpgradeAllowedAuto}
  111. }
  112. func setupDB(db *sql.DB) error {
  113. _, err := db.Exec(`CREATE TABLE IF NOT EXISTS Reports (
  114. Received TIMESTAMP NOT NULL,
  115. UniqueID VARCHAR(32) NOT NULL,
  116. Version VARCHAR(32) NOT NULL,
  117. LongVersion VARCHAR(256) NOT NULL,
  118. Platform VARCHAR(32) NOT NULL,
  119. NumFolders INTEGER NOT NULL,
  120. NumDevices INTEGER NOT NULL,
  121. TotFiles INTEGER NOT NULL,
  122. FolderMaxFiles INTEGER NOT NULL,
  123. TotMiB INTEGER NOT NULL,
  124. FolderMaxMiB INTEGER NOT NULL,
  125. MemoryUsageMiB INTEGER NOT NULL,
  126. SHA256Perf DOUBLE PRECISION NOT NULL,
  127. MemorySize INTEGER NOT NULL,
  128. Date VARCHAR(8) NOT NULL
  129. )`)
  130. if err != nil {
  131. return err
  132. }
  133. var t string
  134. row := db.QueryRow(`SELECT 'UniqueIDIndex'::regclass`)
  135. if err := row.Scan(&t); err != nil {
  136. log.Println(err)
  137. if _, err = db.Exec(`CREATE UNIQUE INDEX UniqueIDIndex ON Reports (Date, UniqueID)`); err != nil {
  138. return err
  139. }
  140. }
  141. row = db.QueryRow(`SELECT 'ReceivedIndex'::regclass`)
  142. if err := row.Scan(&t); err != nil {
  143. if _, err = db.Exec(`CREATE INDEX ReceivedIndex ON Reports (Received)`); err != nil {
  144. return err
  145. }
  146. }
  147. row = db.QueryRow(`SELECT attname FROM pg_attribute WHERE attrelid = (SELECT oid FROM pg_class WHERE relname = 'reports') AND attname = 'reportversion'`)
  148. if err := row.Scan(&t); err != nil {
  149. // The ReportVersion column doesn't exist; add the new columns.
  150. _, err = db.Exec(`ALTER TABLE Reports
  151. ADD COLUMN ReportVersion INTEGER NOT NULL DEFAULT 0,
  152. ADD COLUMN NumCPU INTEGER NOT NULL DEFAULT 0,
  153. ADD COLUMN FolderRO INTEGER NOT NULL DEFAULT 0,
  154. ADD COLUMN FolderIgnorePerms INTEGER NOT NULL DEFAULT 0,
  155. ADD COLUMN FolderIgnoreDelete INTEGER NOT NULL DEFAULT 0,
  156. ADD COLUMN FolderAutoNormalize INTEGER NOT NULL DEFAULT 0,
  157. ADD COLUMN DeviceIntroducer INTEGER NOT NULL DEFAULT 0,
  158. ADD COLUMN DeviceCustomCertName INTEGER NOT NULL DEFAULT 0,
  159. ADD COLUMN DeviceCompressAlways INTEGER NOT NULL DEFAULT 0,
  160. ADD COLUMN DeviceCompressMetadata INTEGER NOT NULL DEFAULT 0,
  161. ADD COLUMN DeviceCompressNever INTEGER NOT NULL DEFAULT 0,
  162. ADD COLUMN DeviceDynamicAddr INTEGER NOT NULL DEFAULT 0,
  163. ADD COLUMN DeviceStaticAddr INTEGER NOT NULL DEFAULT 0,
  164. ADD COLUMN AnnounceGlobalEnabled BOOLEAN NOT NULL DEFAULT FALSE,
  165. ADD COLUMN AnnounceLocalEnabled BOOLEAN NOT NULL DEFAULT FALSE,
  166. ADD COLUMN AnnounceDefaultServersDNS INTEGER NOT NULL DEFAULT 0,
  167. ADD COLUMN AnnounceDefaultServersIP INTEGER NOT NULL DEFAULT 0,
  168. ADD COLUMN AnnounceOtherServers INTEGER NOT NULL DEFAULT 0,
  169. ADD COLUMN RelayEnabled BOOLEAN NOT NULL DEFAULT FALSE,
  170. ADD COLUMN RelayDefaultServers INTEGER NOT NULL DEFAULT 0,
  171. ADD COLUMN RelayOtherServers INTEGER NOT NULL DEFAULT 0,
  172. ADD COLUMN RateLimitEnabled BOOLEAN NOT NULL DEFAULT FALSE,
  173. ADD COLUMN UpgradeAllowedManual BOOLEAN NOT NULL DEFAULT FALSE,
  174. ADD COLUMN UpgradeAllowedAuto BOOLEAN NOT NULL DEFAULT FALSE
  175. `)
  176. if err != nil {
  177. return err
  178. }
  179. }
  180. row = db.QueryRow(`SELECT 'ReportVersionIndex'::regclass`)
  181. if err := row.Scan(&t); err != nil {
  182. if _, err = db.Exec(`CREATE INDEX ReportVersionIndex ON Reports (ReportVersion)`); err != nil {
  183. return err
  184. }
  185. }
  186. return nil
  187. }
  188. func insertReport(db *sql.DB, r report) error {
  189. fields := r.FieldsInDBOrder()
  190. params := make([]string, len(fields))
  191. for i := range params {
  192. params[i] = fmt.Sprintf("$%d", i+1)
  193. }
  194. query := "INSERT INTO Reports VALUES (TIMEZONE('UTC', NOW()), " + strings.Join(params, ", ") + ")"
  195. _, err := db.Exec(query, fields...)
  196. return err
  197. }
  198. type withDBFunc func(*sql.DB, http.ResponseWriter, *http.Request)
  199. func withDB(db *sql.DB, f withDBFunc) http.HandlerFunc {
  200. return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  201. f(db, w, r)
  202. })
  203. }
  204. func main() {
  205. log.SetFlags(log.Ltime | log.Ldate)
  206. log.SetOutput(os.Stdout)
  207. // Template
  208. fd, err := os.Open("static/index.html")
  209. if err != nil {
  210. log.Fatalln("template:", err)
  211. }
  212. bs, err := ioutil.ReadAll(fd)
  213. if err != nil {
  214. log.Fatalln("template:", err)
  215. }
  216. fd.Close()
  217. tpl = template.Must(template.New("index.html").Funcs(funcs).Parse(string(bs)))
  218. // DB
  219. db, err := sql.Open("postgres", dbConn)
  220. if err != nil {
  221. log.Fatalln("database:", err)
  222. }
  223. err = setupDB(db)
  224. if err != nil {
  225. log.Fatalln("database:", err)
  226. }
  227. // TLS
  228. cert, err := tls.LoadX509KeyPair(certFile, keyFile)
  229. if err != nil {
  230. log.Fatalln("tls:", err)
  231. }
  232. cfg := &tls.Config{
  233. Certificates: []tls.Certificate{cert},
  234. SessionTicketsDisabled: true,
  235. }
  236. // HTTPS
  237. listener, err := tls.Listen("tcp", listenAddr, cfg)
  238. if err != nil {
  239. log.Fatalln("https:", err)
  240. }
  241. srv := http.Server{
  242. ReadTimeout: 5 * time.Second,
  243. WriteTimeout: 5 * time.Second,
  244. }
  245. http.HandleFunc("/", withDB(db, rootHandler))
  246. http.HandleFunc("/newdata", withDB(db, newDataHandler))
  247. http.HandleFunc("/summary.json", withDB(db, summaryHandler))
  248. http.HandleFunc("/movement.json", withDB(db, movementHandler))
  249. http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static"))))
  250. err = srv.Serve(listener)
  251. if err != nil {
  252. log.Fatalln("https:", err)
  253. }
  254. }
  255. var (
  256. cacheData []byte
  257. cacheTime time.Time
  258. cacheMut sync.Mutex
  259. )
  260. const maxCacheTime = 5 * 60 * time.Second
  261. func rootHandler(db *sql.DB, w http.ResponseWriter, r *http.Request) {
  262. if r.URL.Path == "/" || r.URL.Path == "/index.html" {
  263. cacheMut.Lock()
  264. defer cacheMut.Unlock()
  265. if time.Since(cacheTime) > maxCacheTime {
  266. rep := getReport(db)
  267. buf := new(bytes.Buffer)
  268. err := tpl.Execute(buf, rep)
  269. if err != nil {
  270. log.Println(err)
  271. http.Error(w, "Template Error", http.StatusInternalServerError)
  272. return
  273. }
  274. cacheData = buf.Bytes()
  275. cacheTime = time.Now()
  276. }
  277. w.Header().Set("Content-Type", "text/html; charset=utf-8")
  278. w.Write(cacheData)
  279. } else {
  280. http.Error(w, "Not found", 404)
  281. return
  282. }
  283. }
  284. func newDataHandler(db *sql.DB, w http.ResponseWriter, r *http.Request) {
  285. defer r.Body.Close()
  286. var rep report
  287. rep.Date = time.Now().UTC().Format("20060102")
  288. lr := &io.LimitedReader{R: r.Body, N: 10240}
  289. if err := json.NewDecoder(lr).Decode(&rep); err != nil {
  290. log.Println("json decode:", err)
  291. http.Error(w, "JSON Decode Error", http.StatusInternalServerError)
  292. return
  293. }
  294. if err := rep.Validate(); err != nil {
  295. log.Println("validate:", err)
  296. log.Printf("%#v", rep)
  297. http.Error(w, "Validation Error", http.StatusInternalServerError)
  298. return
  299. }
  300. if err := insertReport(db, rep); err != nil {
  301. log.Println("insert:", err)
  302. log.Printf("%#v", rep)
  303. http.Error(w, "Database Error", http.StatusInternalServerError)
  304. return
  305. }
  306. }
  307. func summaryHandler(db *sql.DB, w http.ResponseWriter, r *http.Request) {
  308. s, err := getSummary(db)
  309. if err != nil {
  310. log.Println("summaryHandler:", err)
  311. http.Error(w, "Database Error", http.StatusInternalServerError)
  312. return
  313. }
  314. bs, err := s.MarshalJSON()
  315. if err != nil {
  316. log.Println("summaryHandler:", err)
  317. http.Error(w, "JSON Encode Error", http.StatusInternalServerError)
  318. return
  319. }
  320. w.Header().Set("Content-Type", "application/json")
  321. w.Write(bs)
  322. }
  323. func movementHandler(db *sql.DB, w http.ResponseWriter, r *http.Request) {
  324. s, err := getMovement(db)
  325. if err != nil {
  326. log.Println("movementHandler:", err)
  327. http.Error(w, "Database Error", http.StatusInternalServerError)
  328. return
  329. }
  330. bs, err := json.Marshal(s)
  331. if err != nil {
  332. log.Println("movementHandler:", err)
  333. http.Error(w, "JSON Encode Error", http.StatusInternalServerError)
  334. return
  335. }
  336. w.Header().Set("Content-Type", "application/json")
  337. w.Write(bs)
  338. }
  339. type category struct {
  340. Values [4]float64
  341. Key string
  342. Descr string
  343. Unit string
  344. Binary bool
  345. }
  346. func getReport(db *sql.DB) map[string]interface{} {
  347. nodes := 0
  348. var versions []string
  349. var platforms []string
  350. var oses []string
  351. var numFolders []int
  352. var numDevices []int
  353. var totFiles []int
  354. var maxFiles []int
  355. var totMiB []int
  356. var maxMiB []int
  357. var memoryUsage []int
  358. var sha256Perf []float64
  359. var memorySize []int
  360. var compilers []string
  361. var builders []string
  362. rows, err := db.Query(`SELECT * FROM Reports WHERE Received > now() - '1 day'::INTERVAL`)
  363. if err != nil {
  364. log.Println("sql:", err)
  365. return nil
  366. }
  367. defer rows.Close()
  368. for rows.Next() {
  369. var rep report
  370. err := rows.Scan(&rep.Received, &rep.UniqueID, &rep.Version,
  371. &rep.LongVersion, &rep.Platform, &rep.NumFolders, &rep.NumDevices,
  372. &rep.TotFiles, &rep.FolderMaxFiles, &rep.TotMiB, &rep.FolderMaxMiB,
  373. &rep.MemoryUsageMiB, &rep.SHA256Perf, &rep.MemorySize, &rep.Date)
  374. if err != nil {
  375. log.Println("sql:", err)
  376. return nil
  377. }
  378. nodes++
  379. versions = append(versions, transformVersion(rep.Version))
  380. platforms = append(platforms, rep.Platform)
  381. ps := strings.Split(rep.Platform, "-")
  382. oses = append(oses, ps[0])
  383. if m := compilerRe.FindStringSubmatch(rep.LongVersion); len(m) == 3 {
  384. compilers = append(compilers, m[1])
  385. builders = append(builders, m[2])
  386. }
  387. if rep.NumFolders > 0 {
  388. numFolders = append(numFolders, rep.NumFolders)
  389. }
  390. if rep.NumDevices > 0 {
  391. numDevices = append(numDevices, rep.NumDevices)
  392. }
  393. if rep.TotFiles > 0 {
  394. totFiles = append(totFiles, rep.TotFiles)
  395. }
  396. if rep.FolderMaxFiles > 0 {
  397. maxFiles = append(maxFiles, rep.FolderMaxFiles)
  398. }
  399. if rep.TotMiB > 0 {
  400. totMiB = append(totMiB, rep.TotMiB*(1<<20))
  401. }
  402. if rep.FolderMaxMiB > 0 {
  403. maxMiB = append(maxMiB, rep.FolderMaxMiB*(1<<20))
  404. }
  405. if rep.MemoryUsageMiB > 0 {
  406. memoryUsage = append(memoryUsage, rep.MemoryUsageMiB*(1<<20))
  407. }
  408. if rep.SHA256Perf > 0 {
  409. sha256Perf = append(sha256Perf, rep.SHA256Perf*(1<<20))
  410. }
  411. if rep.MemorySize > 0 {
  412. memorySize = append(memorySize, rep.MemorySize*(1<<20))
  413. }
  414. }
  415. var categories []category
  416. categories = append(categories, category{
  417. Values: statsForInts(totFiles),
  418. Descr: "Files Managed per Device",
  419. })
  420. categories = append(categories, category{
  421. Values: statsForInts(maxFiles),
  422. Descr: "Files in Largest Folder",
  423. })
  424. categories = append(categories, category{
  425. Values: statsForInts(totMiB),
  426. Descr: "Data Managed per Device",
  427. Unit: "B",
  428. Binary: true,
  429. })
  430. categories = append(categories, category{
  431. Values: statsForInts(maxMiB),
  432. Descr: "Data in Largest Folder",
  433. Unit: "B",
  434. Binary: true,
  435. })
  436. categories = append(categories, category{
  437. Values: statsForInts(numDevices),
  438. Descr: "Number of Devices in Cluster",
  439. })
  440. categories = append(categories, category{
  441. Values: statsForInts(numFolders),
  442. Descr: "Number of Folders Configured",
  443. })
  444. categories = append(categories, category{
  445. Values: statsForInts(memoryUsage),
  446. Descr: "Memory Usage",
  447. Unit: "B",
  448. Binary: true,
  449. })
  450. categories = append(categories, category{
  451. Values: statsForInts(memorySize),
  452. Descr: "System Memory",
  453. Unit: "B",
  454. Binary: true,
  455. })
  456. categories = append(categories, category{
  457. Values: statsForFloats(sha256Perf),
  458. Descr: "SHA-256 Hashing Performance",
  459. Unit: "B/s",
  460. Binary: true,
  461. })
  462. r := make(map[string]interface{})
  463. r["nodes"] = nodes
  464. r["categories"] = categories
  465. r["versions"] = analyticsFor(versions, 10)
  466. r["platforms"] = analyticsFor(platforms, 0)
  467. r["os"] = analyticsFor(oses, 0)
  468. r["compilers"] = analyticsFor(compilers, 12)
  469. r["builders"] = analyticsFor(builders, 12)
  470. return r
  471. }
  472. func ensureDir(dir string, mode int) {
  473. fi, err := os.Stat(dir)
  474. if os.IsNotExist(err) {
  475. os.MkdirAll(dir, 0700)
  476. } else if mode >= 0 && err == nil && int(fi.Mode()&0777) != mode {
  477. os.Chmod(dir, os.FileMode(mode))
  478. }
  479. }
  480. var vRe = regexp.MustCompile(`^(v\d+\.\d+\.\d+(?:-[a-z]\w+)?)[+\.-]`)
  481. // transformVersion returns a version number formatted correctly, with all
  482. // development versions aggregated into one.
  483. func transformVersion(v string) string {
  484. if v == "unknown-dev" {
  485. return v
  486. }
  487. if !strings.HasPrefix(v, "v") {
  488. v = "v" + v
  489. }
  490. if m := vRe.FindStringSubmatch(v); len(m) > 0 {
  491. return m[1] + " (+dev)"
  492. }
  493. // Truncate old versions to just the generation part
  494. for _, agg := range aggregateVersions {
  495. if strings.HasPrefix(v, agg) {
  496. return agg + ".x"
  497. }
  498. }
  499. return v
  500. }
  501. type summary struct {
  502. versions map[string]int // version string to count index
  503. rows map[string][]int // date to list of counts
  504. }
  505. func newSummary() summary {
  506. return summary{
  507. versions: make(map[string]int),
  508. rows: make(map[string][]int),
  509. }
  510. }
  511. func (s *summary) setCount(date, version string, count int) {
  512. idx, ok := s.versions[version]
  513. if !ok {
  514. idx = len(s.versions)
  515. s.versions[version] = idx
  516. }
  517. row := s.rows[date]
  518. if len(row) <= idx {
  519. old := row
  520. row = make([]int, idx+1)
  521. copy(row, old)
  522. s.rows[date] = row
  523. }
  524. row[idx] = count
  525. }
  526. func (s *summary) MarshalJSON() ([]byte, error) {
  527. var versions []string
  528. for v := range s.versions {
  529. versions = append(versions, v)
  530. }
  531. sort.Strings(versions)
  532. headerRow := []interface{}{"Day"}
  533. for _, v := range versions {
  534. headerRow = append(headerRow, v)
  535. }
  536. var table [][]interface{}
  537. table = append(table, headerRow)
  538. var dates []string
  539. for k := range s.rows {
  540. dates = append(dates, k)
  541. }
  542. sort.Strings(dates)
  543. for _, date := range dates {
  544. row := []interface{}{date}
  545. for _, ver := range versions {
  546. idx := s.versions[ver]
  547. if len(s.rows[date]) > idx && s.rows[date][idx] > 0 {
  548. row = append(row, s.rows[date][idx])
  549. } else {
  550. row = append(row, nil)
  551. }
  552. }
  553. table = append(table, row)
  554. }
  555. return json.Marshal(table)
  556. }
  557. func getSummary(db *sql.DB) (summary, error) {
  558. s := newSummary()
  559. rows, err := db.Query(`SELECT Day, Version, Count FROM VersionSummary WHERE Day > now() - '1 year'::INTERVAL;`)
  560. if err != nil {
  561. return summary{}, err
  562. }
  563. defer rows.Close()
  564. for rows.Next() {
  565. var day time.Time
  566. var ver string
  567. var num int
  568. err := rows.Scan(&day, &ver, &num)
  569. if err != nil {
  570. return summary{}, err
  571. }
  572. if ver == "v0.0" {
  573. // ?
  574. continue
  575. }
  576. // SUPER UGLY HACK to avoid having to do sorting properly
  577. if len(ver) == 4 { // v0.x
  578. ver = ver[:3] + "0" + ver[3:] // now v0.0x
  579. }
  580. s.setCount(day.Format("2006-01-02"), ver, num)
  581. }
  582. return s, nil
  583. }
  584. func getMovement(db *sql.DB) ([][]interface{}, error) {
  585. rows, err := db.Query(`SELECT Day, Added, Removed, Bounced FROM UserMovement WHERE Day > now() - '1 year'::INTERVAL ORDER BY Day`)
  586. if err != nil {
  587. return nil, err
  588. }
  589. defer rows.Close()
  590. res := [][]interface{}{
  591. {"Day", "Joined", "Left", "Bounced"},
  592. }
  593. for rows.Next() {
  594. var day time.Time
  595. var added, removed, bounced int
  596. err := rows.Scan(&day, &added, &removed, &bounced)
  597. if err != nil {
  598. return nil, err
  599. }
  600. row := []interface{}{day.Format("2006-01-02"), added, -removed, bounced}
  601. if removed == 0 {
  602. row[2] = nil
  603. }
  604. if bounced == 0 {
  605. row[3] = nil
  606. }
  607. res = append(res, row)
  608. }
  609. return res, nil
  610. }