main.go 18 KB

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