main.go 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239
  1. package main
  2. import (
  3. "bytes"
  4. "crypto/tls"
  5. "database/sql"
  6. "database/sql/driver"
  7. "encoding/json"
  8. "errors"
  9. "fmt"
  10. "html/template"
  11. "io"
  12. "io/ioutil"
  13. "log"
  14. "net/http"
  15. "os"
  16. "regexp"
  17. "sort"
  18. "strings"
  19. "sync"
  20. "time"
  21. _ "github.com/lib/pq"
  22. )
  23. var (
  24. keyFile = getEnvDefault("UR_KEY_FILE", "key.pem")
  25. certFile = getEnvDefault("UR_CRT_FILE", "crt.pem")
  26. dbConn = getEnvDefault("UR_DB_URL", "postgres://user:password@localhost/ur?sslmode=disable")
  27. listenAddr = getEnvDefault("UR_LISTEN", "0.0.0.0:8443")
  28. tpl *template.Template
  29. compilerRe = regexp.MustCompile(`\(([A-Za-z0-9()., -]+) \w+-\w+(?:| android| default)\) ([\[email protected]]+)`)
  30. )
  31. var funcs = map[string]interface{}{
  32. "commatize": commatize,
  33. "number": number,
  34. }
  35. func getEnvDefault(key, def string) string {
  36. if val := os.Getenv(key); val != "" {
  37. return val
  38. }
  39. return def
  40. }
  41. type IntMap map[string]int
  42. func (p IntMap) Value() (driver.Value, error) {
  43. return json.Marshal(p)
  44. }
  45. func (p *IntMap) Scan(src interface{}) error {
  46. source, ok := src.([]byte)
  47. if !ok {
  48. return errors.New("Type assertion .([]byte) failed.")
  49. }
  50. var i interface{}
  51. err := json.Unmarshal(source, &i)
  52. if err != nil {
  53. return err
  54. }
  55. *p, ok = i.(map[string]int)
  56. if !ok {
  57. return errors.New("Type assertion .(map[string]int) failed.")
  58. }
  59. return nil
  60. }
  61. type report struct {
  62. Received time.Time // Only from DB
  63. UniqueID string
  64. Version string
  65. LongVersion string
  66. Platform string
  67. NumFolders int
  68. NumDevices int
  69. TotFiles int
  70. FolderMaxFiles int
  71. TotMiB int
  72. FolderMaxMiB int
  73. MemoryUsageMiB int
  74. SHA256Perf float64
  75. MemorySize int
  76. // v2 fields
  77. URVersion int
  78. NumCPU int
  79. FolderUses struct {
  80. ReadOnly int
  81. IgnorePerms int
  82. IgnoreDelete int
  83. AutoNormalize int
  84. SimpleVersioning int
  85. ExternalVersioning int
  86. StaggeredVersioning int
  87. TrashcanVersioning int
  88. }
  89. DeviceUses struct {
  90. Introducer int
  91. CustomCertName int
  92. CompressAlways int
  93. CompressMetadata int
  94. CompressNever int
  95. DynamicAddr int
  96. StaticAddr int
  97. }
  98. Announce struct {
  99. GlobalEnabled bool
  100. LocalEnabled bool
  101. DefaultServersDNS int
  102. DefaultServersIP int
  103. OtherServers int
  104. }
  105. Relays struct {
  106. Enabled bool
  107. DefaultServers int
  108. OtherServers int
  109. }
  110. UsesRateLimit bool
  111. UpgradeAllowedManual bool
  112. UpgradeAllowedAuto bool
  113. // v3 fields
  114. Uptime int
  115. NATType string
  116. AlwaysLocalNets bool
  117. CacheIgnoredFiles bool
  118. OverwriteRemoteDeviceNames bool
  119. ProgressEmitterEnabled bool
  120. CustomDefaultFolderPath bool
  121. WeakHashSelection string
  122. CustomTrafficClass bool
  123. CustomTempIndexMinBlocks bool
  124. TemporariesDisabled bool
  125. TemporariesCustom bool
  126. LimitBandwidthInLan bool
  127. CustomReleaseURL bool
  128. RestartOnWakeup bool
  129. CustomStunServers bool
  130. FolderUsesV3 struct {
  131. ScanProgressDisabled int
  132. ConflictsDisabled int
  133. ConflictsUnlimited int
  134. ConflictsOther int
  135. DisableSparseFiles int
  136. DisableTempIndexes int
  137. AlwaysWeakHash int
  138. CustomWeakHashThreshold int
  139. PullOrder IntMap
  140. FilesystemType IntMap
  141. }
  142. GUIStats struct {
  143. Enabled int
  144. UseTLS int
  145. UseAuth int
  146. UseAPIKey int
  147. InsecureAdminAccess int
  148. Debugging int
  149. InsecureSkipHostCheck int
  150. InsecureAllowFrameLoading int
  151. ListenLocal int
  152. ListenUnspecified int
  153. Theme IntMap
  154. }
  155. BlockStats struct {
  156. Total int
  157. Renamed int
  158. Reused int
  159. Pulled int
  160. CopyOrigin int
  161. CopyOriginShifted int
  162. CopyElsewhere int
  163. }
  164. TransportStats IntMap
  165. IgnoreStats struct {
  166. Lines int
  167. Inverts int
  168. Folded int
  169. Deletable int
  170. Rooted int
  171. Includes int
  172. EscapedIncludes int
  173. DoubleStars int
  174. Stars int
  175. }
  176. // Generated
  177. Date string
  178. }
  179. func (r *report) Validate() error {
  180. if r.UniqueID == "" || r.Version == "" || r.Platform == "" {
  181. return fmt.Errorf("missing required field")
  182. }
  183. if len(r.Date) != 8 {
  184. return fmt.Errorf("date not initialized")
  185. }
  186. return nil
  187. }
  188. func (r *report) FieldPointers() []interface{} {
  189. // All the fields of the report, in the same order as the database fields.
  190. return []interface{}{
  191. &r.Received, &r.UniqueID, &r.Version, &r.LongVersion, &r.Platform,
  192. &r.NumFolders, &r.NumDevices, &r.TotFiles, &r.FolderMaxFiles,
  193. &r.TotMiB, &r.FolderMaxMiB, &r.MemoryUsageMiB, &r.SHA256Perf,
  194. &r.MemorySize, &r.Date,
  195. // V2
  196. &r.URVersion, &r.NumCPU, &r.FolderUses.ReadOnly, &r.FolderUses.IgnorePerms,
  197. &r.FolderUses.IgnoreDelete, &r.FolderUses.AutoNormalize, &r.DeviceUses.Introducer,
  198. &r.DeviceUses.CustomCertName, &r.DeviceUses.CompressAlways,
  199. &r.DeviceUses.CompressMetadata, &r.DeviceUses.CompressNever,
  200. &r.DeviceUses.DynamicAddr, &r.DeviceUses.StaticAddr,
  201. &r.Announce.GlobalEnabled, &r.Announce.LocalEnabled,
  202. &r.Announce.DefaultServersDNS, &r.Announce.DefaultServersIP,
  203. &r.Announce.OtherServers, &r.Relays.Enabled, &r.Relays.DefaultServers,
  204. &r.Relays.OtherServers, &r.UsesRateLimit, &r.UpgradeAllowedManual,
  205. &r.UpgradeAllowedAuto, &r.FolderUses.SimpleVersioning,
  206. &r.FolderUses.ExternalVersioning, &r.FolderUses.StaggeredVersioning,
  207. &r.FolderUses.TrashcanVersioning,
  208. // V3
  209. &r.Uptime, &r.NATType, &r.AlwaysLocalNets, &r.CacheIgnoredFiles,
  210. &r.OverwriteRemoteDeviceNames, &r.ProgressEmitterEnabled, &r.CustomDefaultFolderPath,
  211. &r.WeakHashSelection, &r.CustomTrafficClass, &r.CustomTempIndexMinBlocks,
  212. &r.TemporariesDisabled, &r.TemporariesCustom, &r.LimitBandwidthInLan,
  213. &r.CustomReleaseURL, &r.RestartOnWakeup, &r.CustomStunServers,
  214. &r.FolderUsesV3.ScanProgressDisabled, &r.FolderUsesV3.ConflictsDisabled,
  215. &r.FolderUsesV3.ConflictsUnlimited, &r.FolderUsesV3.ConflictsOther,
  216. &r.FolderUsesV3.DisableSparseFiles, &r.FolderUsesV3.DisableTempIndexes,
  217. &r.FolderUsesV3.AlwaysWeakHash, &r.FolderUsesV3.CustomWeakHashThreshold,
  218. &r.FolderUsesV3.PullOrder, &r.FolderUsesV3.FilesystemType,
  219. &r.GUIStats.Enabled, &r.GUIStats.UseTLS, &r.GUIStats.UseAuth,
  220. &r.GUIStats.UseAPIKey, &r.GUIStats.InsecureAdminAccess,
  221. &r.GUIStats.Debugging, &r.GUIStats.InsecureSkipHostCheck,
  222. &r.GUIStats.InsecureAllowFrameLoading, &r.GUIStats.ListenLocal,
  223. &r.GUIStats.ListenUnspecified, &r.GUIStats.Theme,
  224. &r.BlockStats.Total, &r.BlockStats.Renamed,
  225. &r.BlockStats.Reused, &r.BlockStats.Pulled, &r.BlockStats.CopyOrigin,
  226. &r.BlockStats.CopyOriginShifted, &r.BlockStats.CopyElsewhere,
  227. &r.TransportStats,
  228. &r.IgnoreStats.Lines, &r.IgnoreStats.Inverts, &r.IgnoreStats.Folded,
  229. &r.IgnoreStats.Deletable, &r.IgnoreStats.Rooted, &r.IgnoreStats.Includes,
  230. &r.IgnoreStats.EscapedIncludes, &r.IgnoreStats.DoubleStars, &r.IgnoreStats.Stars,
  231. }
  232. }
  233. func (r *report) FieldNames() []string {
  234. // The database fields that back this struct in PostgreSQL
  235. return []string{
  236. // V1
  237. "Received",
  238. "UniqueID",
  239. "Version",
  240. "LongVersion",
  241. "Platform",
  242. "NumFolders",
  243. "NumDevices",
  244. "TotFiles",
  245. "FolderMaxFiles",
  246. "TotMiB",
  247. "FolderMaxMiB",
  248. "MemoryUsageMiB",
  249. "SHA256Perf",
  250. "MemorySize",
  251. "Date",
  252. // V2
  253. "ReportVersion",
  254. "NumCPU",
  255. "FolderRO",
  256. "FolderIgnorePerms",
  257. "FolderIgnoreDelete",
  258. "FolderAutoNormalize",
  259. "DeviceIntroducer",
  260. "DeviceCustomCertName",
  261. "DeviceCompressAlways",
  262. "DeviceCompressMetadata",
  263. "DeviceCompressNever",
  264. "DeviceDynamicAddr",
  265. "DeviceStaticAddr",
  266. "AnnounceGlobalEnabled",
  267. "AnnounceLocalEnabled",
  268. "AnnounceDefaultServersDNS",
  269. "AnnounceDefaultServersIP",
  270. "AnnounceOtherServers",
  271. "RelayEnabled",
  272. "RelayDefaultServers",
  273. "RelayOtherServers",
  274. "RateLimitEnabled",
  275. "UpgradeAllowedManual",
  276. "UpgradeAllowedAuto",
  277. // v0.12.19+
  278. "FolderSimpleVersioning",
  279. "FolderExternalVersioning",
  280. "FolderStaggeredVersioning",
  281. "FolderTrashcanVersioning",
  282. // V3
  283. "Uptime",
  284. "NATType",
  285. "AlwaysLocalNets",
  286. "CacheIgnoredFiles",
  287. "OverwriteRemoteDeviceNames",
  288. "ProgressEmitterEnabled",
  289. "CustomDefaultFolderPath",
  290. "WeakHashSelection",
  291. "CustomTrafficClass",
  292. "CustomTempIndexMinBlocks",
  293. "TemporariesDisabled",
  294. "TemporariesCustom",
  295. "LimitBandwidthInLan",
  296. "CustomReleaseURL",
  297. "RestartOnWakeup",
  298. "CustomStunServers",
  299. "FolderScanProgressDisabled",
  300. "FolderConflictsDisabled",
  301. "FolderConflictsUnlimited",
  302. "FolderConflictsOther",
  303. "FolderDisableSparseFiles",
  304. "FolderDisableTempIndexes",
  305. "FolderAlwaysWeakHash",
  306. "FolderCustomWeakHashThreshold",
  307. "FolderPullOrder",
  308. "FolderFilesystemType",
  309. "GUIEnabled",
  310. "GUIUseTLS",
  311. "GUIUseAuth",
  312. "GUIUseAPIKey",
  313. "GUIInsecureAdminAccess",
  314. "GUIDebugging",
  315. "GUIInsecureSkipHostCheck",
  316. "GUIInsecureAllowFrameLoading",
  317. "GUIListenLocal",
  318. "GUIListenUnspecified",
  319. "GUITheme",
  320. "BlocksTotal",
  321. "BlocksRenamed",
  322. "BlocksReused",
  323. "BlocksPulled",
  324. "BlocksCopyOrigin",
  325. "BlocksCopyOriginShifted",
  326. "BlocksCopyElsewhere",
  327. "Transport",
  328. "IgnoreLines",
  329. "IgnoreInverts",
  330. "IgnoreFolded",
  331. "IgnoreDeletable",
  332. "IgnoreRooted",
  333. "IgnoreIncludes",
  334. "IgnoreEscapedIncludes",
  335. "IgnoreDoubleStars",
  336. "IgnoreStars",
  337. }
  338. }
  339. func setupDB(db *sql.DB) error {
  340. _, err := db.Exec(`CREATE TABLE IF NOT EXISTS Reports (
  341. Received TIMESTAMP NOT NULL,
  342. UniqueID VARCHAR(32) NOT NULL,
  343. Version VARCHAR(32) NOT NULL,
  344. LongVersion VARCHAR(256) NOT NULL,
  345. Platform VARCHAR(32) NOT NULL,
  346. NumFolders INTEGER NOT NULL,
  347. NumDevices INTEGER NOT NULL,
  348. TotFiles INTEGER NOT NULL,
  349. FolderMaxFiles INTEGER NOT NULL,
  350. TotMiB INTEGER NOT NULL,
  351. FolderMaxMiB INTEGER NOT NULL,
  352. MemoryUsageMiB INTEGER NOT NULL,
  353. SHA256Perf DOUBLE PRECISION NOT NULL,
  354. MemorySize INTEGER NOT NULL,
  355. Date VARCHAR(8) NOT NULL
  356. )`)
  357. if err != nil {
  358. return err
  359. }
  360. var t string
  361. row := db.QueryRow(`SELECT 'UniqueIDIndex'::regclass`)
  362. if err := row.Scan(&t); err != nil {
  363. if _, err = db.Exec(`CREATE UNIQUE INDEX UniqueIDIndex ON Reports (Date, UniqueID)`); err != nil {
  364. return err
  365. }
  366. }
  367. row = db.QueryRow(`SELECT 'ReceivedIndex'::regclass`)
  368. if err := row.Scan(&t); err != nil {
  369. if _, err = db.Exec(`CREATE INDEX ReceivedIndex ON Reports (Received)`); err != nil {
  370. return err
  371. }
  372. }
  373. // V2
  374. row = db.QueryRow(`SELECT attname FROM pg_attribute WHERE attrelid = (SELECT oid FROM pg_class WHERE relname = 'reports') AND attname = 'reportversion'`)
  375. if err := row.Scan(&t); err != nil {
  376. // The ReportVersion column doesn't exist; add the new columns.
  377. _, err = db.Exec(`ALTER TABLE Reports
  378. ADD COLUMN ReportVersion INTEGER NOT NULL DEFAULT 0,
  379. ADD COLUMN NumCPU INTEGER NOT NULL DEFAULT 0,
  380. ADD COLUMN FolderRO INTEGER NOT NULL DEFAULT 0,
  381. ADD COLUMN FolderIgnorePerms INTEGER NOT NULL DEFAULT 0,
  382. ADD COLUMN FolderIgnoreDelete INTEGER NOT NULL DEFAULT 0,
  383. ADD COLUMN FolderAutoNormalize INTEGER NOT NULL DEFAULT 0,
  384. ADD COLUMN DeviceIntroducer INTEGER NOT NULL DEFAULT 0,
  385. ADD COLUMN DeviceCustomCertName INTEGER NOT NULL DEFAULT 0,
  386. ADD COLUMN DeviceCompressAlways INTEGER NOT NULL DEFAULT 0,
  387. ADD COLUMN DeviceCompressMetadata INTEGER NOT NULL DEFAULT 0,
  388. ADD COLUMN DeviceCompressNever INTEGER NOT NULL DEFAULT 0,
  389. ADD COLUMN DeviceDynamicAddr INTEGER NOT NULL DEFAULT 0,
  390. ADD COLUMN DeviceStaticAddr INTEGER NOT NULL DEFAULT 0,
  391. ADD COLUMN AnnounceGlobalEnabled BOOLEAN NOT NULL DEFAULT FALSE,
  392. ADD COLUMN AnnounceLocalEnabled BOOLEAN NOT NULL DEFAULT FALSE,
  393. ADD COLUMN AnnounceDefaultServersDNS INTEGER NOT NULL DEFAULT 0,
  394. ADD COLUMN AnnounceDefaultServersIP INTEGER NOT NULL DEFAULT 0,
  395. ADD COLUMN AnnounceOtherServers INTEGER NOT NULL DEFAULT 0,
  396. ADD COLUMN RelayEnabled BOOLEAN NOT NULL DEFAULT FALSE,
  397. ADD COLUMN RelayDefaultServers INTEGER NOT NULL DEFAULT 0,
  398. ADD COLUMN RelayOtherServers INTEGER NOT NULL DEFAULT 0,
  399. ADD COLUMN RateLimitEnabled BOOLEAN NOT NULL DEFAULT FALSE,
  400. ADD COLUMN UpgradeAllowedManual BOOLEAN NOT NULL DEFAULT FALSE,
  401. ADD COLUMN UpgradeAllowedAuto BOOLEAN NOT NULL DEFAULT FALSE,
  402. ADD COLUMN FolderSimpleVersioning INTEGER NOT NULL DEFAULT 0,
  403. ADD COLUMN FolderExternalVersioning INTEGER NOT NULL DEFAULT 0,
  404. ADD COLUMN FolderStaggeredVersioning INTEGER NOT NULL DEFAULT 0,
  405. ADD COLUMN FolderTrashcanVersioning INTEGER NOT NULL DEFAULT 0
  406. `)
  407. if err != nil {
  408. return err
  409. }
  410. }
  411. row = db.QueryRow(`SELECT 'ReportVersionIndex'::regclass`)
  412. if err := row.Scan(&t); err != nil {
  413. if _, err = db.Exec(`CREATE INDEX ReportVersionIndex ON Reports (ReportVersion)`); err != nil {
  414. return err
  415. }
  416. }
  417. // V3
  418. row = db.QueryRow(`SELECT attname FROM pg_attribute WHERE attrelid = (SELECT oid FROM pg_class WHERE relname = 'reports') AND attname = 'uptime'`)
  419. if err := row.Scan(&t); err != nil {
  420. // The Uptime column doesn't exist; add the new columns.
  421. _, err = db.Exec(`ALTER TABLE Reports
  422. ADD COLUMN Uptime INTEGER NOT NULL DEFAULT 0,
  423. ADD COLUMN NATType VARCHAR(32) NOT NULL DEFAULT '',
  424. ADD COLUMN AlwaysLocalNets BOOLEAN NOT NULL DEFAULT FALSE,
  425. ADD COLUMN CacheIgnoredFiles BOOLEAN NOT NULL DEFAULT FALSE,
  426. ADD COLUMN OverwriteRemoteDeviceNames BOOLEAN NOT NULL DEFAULT FALSE,
  427. ADD COLUMN ProgressEmitterEnabled BOOLEAN NOT NULL DEFAULT FALSE,
  428. ADD COLUMN CustomDefaultFolderPath BOOLEAN NOT NULL DEFAULT FALSE,
  429. ADD COLUMN WeakHashSelection VARCHAR(32) NOT NULL DEFAULT '',
  430. ADD COLUMN CustomTrafficClass BOOLEAN NOT NULL DEFAULT FALSE,
  431. ADD COLUMN CustomTempIndexMinBlocks BOOLEAN NOT NULL DEFAULT FALSE,
  432. ADD COLUMN TemporariesDisabled BOOLEAN NOT NULL DEFAULT FALSE,
  433. ADD COLUMN TemporariesCustom BOOLEAN NOT NULL DEFAULT FALSE,
  434. ADD COLUMN LimitBandwidthInLan BOOLEAN NOT NULL DEFAULT FALSE,
  435. ADD COLUMN CustomReleaseURL BOOLEAN NOT NULL DEFAULT FALSE,
  436. ADD COLUMN RestartOnWakeup BOOLEAN NOT NULL DEFAULT FALSE,
  437. ADD COLUMN CustomStunServers BOOLEAN NOT NULL DEFAULT FALSE,
  438. ADD COLUMN FolderScanProgressDisabled INTEGER NOT NULL DEFAULT 0,
  439. ADD COLUMN FolderConflictsDisabled INTEGER NOT NULL DEFAULT 0,
  440. ADD COLUMN FolderConflictsUnlimited INTEGER NOT NULL DEFAULT 0,
  441. ADD COLUMN FolderConflictsOther INTEGER NOT NULL DEFAULT 0,
  442. ADD COLUMN FolderDisableSparseFiles INTEGER NOT NULL DEFAULT 0,
  443. ADD COLUMN FolderDisableTempIndexes INTEGER NOT NULL DEFAULT 0,
  444. ADD COLUMN FolderAlwaysWeakHash INTEGER NOT NULL DEFAULT 0,
  445. ADD COLUMN FolderCustomWeakHashThreshold INTEGER NOT NULL DEFAULT 0,
  446. ADD COLUMN FolderPullOrder JSONB NOT NULL DEFAULT '{}',
  447. ADD COLUMN FolderFilesystemType JSONB NOT NULL DEFAULT '{}',
  448. ADD COLUMN GUIEnabled INTEGER NOT NULL DEFAULT 0,
  449. ADD COLUMN GUIUseTLS INTEGER NOT NULL DEFAULT 0,
  450. ADD COLUMN GUIUseAuth INTEGER NOT NULL DEFAULT 0,
  451. ADD COLUMN GUIUseAPIKey INTEGER NOT NULL DEFAULT 0,
  452. ADD COLUMN GUIInsecureAdminAccess INTEGER NOT NULL DEFAULT 0,
  453. ADD COLUMN GUIDebugging INTEGER NOT NULL DEFAULT 0,
  454. ADD COLUMN GUIInsecureSkipHostCheck INTEGER NOT NULL DEFAULT 0,
  455. ADD COLUMN GUIInsecureAllowFrameLoading INTEGER NOT NULL DEFAULT 0,
  456. ADD COLUMN GUIListenLocal INTEGER NOT NULL DEFAULT 0,
  457. ADD COLUMN GUIListenUnspecified INTEGER NOT NULL DEFAULT 0,
  458. ADD COLUMN GUITheme JSONB NOT NULL DEFAULT '{}',
  459. ADD COLUMN BlocksTotal INTEGER NOT NULL DEFAULT 0,
  460. ADD COLUMN BlocksRenamed INTEGER NOT NULL DEFAULT 0,
  461. ADD COLUMN BlocksReused INTEGER NOT NULL DEFAULT 0,
  462. ADD COLUMN BlocksPulled INTEGER NOT NULL DEFAULT 0,
  463. ADD COLUMN BlocksCopyOrigin INTEGER NOT NULL DEFAULT 0,
  464. ADD COLUMN BlocksCopyOriginShifted INTEGER NOT NULL DEFAULT 0,
  465. ADD COLUMN BlocksCopyElsewhere INTEGER NOT NULL DEFAULT 0,
  466. ADD COLUMN Transport JSONB NOT NULL DEFAULT '{}',
  467. ADD COLUMN IgnoreLines INTEGER NOT NULL DEFAULT 0,
  468. ADD COLUMN IgnoreInverts INTEGER NOT NULL DEFAULT 0,
  469. ADD COLUMN IgnoreFolded INTEGER NOT NULL DEFAULT 0,
  470. ADD COLUMN IgnoreDeletable INTEGER NOT NULL DEFAULT 0,
  471. ADD COLUMN IgnoreRooted INTEGER NOT NULL DEFAULT 0,
  472. ADD COLUMN IgnoreIncludes INTEGER NOT NULL DEFAULT 0,
  473. ADD COLUMN IgnoreEscapedIncludes INTEGER NOT NULL DEFAULT 0,
  474. ADD COLUMN IgnoreDoubleStars INTEGER NOT NULL DEFAULT 0,
  475. ADD COLUMN IgnoreStars INTEGER NOT NULL DEFAULT 0
  476. `)
  477. if err != nil {
  478. return err
  479. }
  480. }
  481. return nil
  482. }
  483. func insertReport(db *sql.DB, r report) error {
  484. r.Received = time.Now().UTC()
  485. fields := r.FieldPointers()
  486. params := make([]string, len(fields))
  487. for i := range params {
  488. params[i] = fmt.Sprintf("$%d", i+1)
  489. }
  490. query := "INSERT INTO Reports (" + strings.Join(r.FieldNames(), ", ") + ") VALUES (" + strings.Join(params, ", ") + ")"
  491. _, err := db.Exec(query, fields...)
  492. return err
  493. }
  494. type withDBFunc func(*sql.DB, http.ResponseWriter, *http.Request)
  495. func withDB(db *sql.DB, f withDBFunc) http.HandlerFunc {
  496. return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  497. f(db, w, r)
  498. })
  499. }
  500. func main() {
  501. log.SetFlags(log.Ltime | log.Ldate)
  502. log.SetOutput(os.Stdout)
  503. // Template
  504. fd, err := os.Open("static/index.html")
  505. if err != nil {
  506. log.Fatalln("template:", err)
  507. }
  508. bs, err := ioutil.ReadAll(fd)
  509. if err != nil {
  510. log.Fatalln("template:", err)
  511. }
  512. fd.Close()
  513. tpl = template.Must(template.New("index.html").Funcs(funcs).Parse(string(bs)))
  514. // DB
  515. db, err := sql.Open("postgres", dbConn)
  516. if err != nil {
  517. log.Fatalln("database:", err)
  518. }
  519. err = setupDB(db)
  520. if err != nil {
  521. log.Fatalln("database:", err)
  522. }
  523. // TLS
  524. cert, err := tls.LoadX509KeyPair(certFile, keyFile)
  525. if err != nil {
  526. log.Fatalln("tls:", err)
  527. }
  528. cfg := &tls.Config{
  529. Certificates: []tls.Certificate{cert},
  530. SessionTicketsDisabled: true,
  531. }
  532. // HTTPS
  533. listener, err := tls.Listen("tcp", listenAddr, cfg)
  534. if err != nil {
  535. log.Fatalln("https:", err)
  536. }
  537. srv := http.Server{
  538. ReadTimeout: 5 * time.Second,
  539. WriteTimeout: 5 * time.Second,
  540. }
  541. http.HandleFunc("/", withDB(db, rootHandler))
  542. http.HandleFunc("/newdata", withDB(db, newDataHandler))
  543. http.HandleFunc("/summary.json", withDB(db, summaryHandler))
  544. http.HandleFunc("/movement.json", withDB(db, movementHandler))
  545. http.HandleFunc("/performance.json", withDB(db, performanceHandler))
  546. http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static"))))
  547. err = srv.Serve(listener)
  548. if err != nil {
  549. log.Fatalln("https:", err)
  550. }
  551. }
  552. var (
  553. cacheData []byte
  554. cacheTime time.Time
  555. cacheMut sync.Mutex
  556. )
  557. const maxCacheTime = 5 * 60 * time.Second
  558. func rootHandler(db *sql.DB, w http.ResponseWriter, r *http.Request) {
  559. if r.URL.Path == "/" || r.URL.Path == "/index.html" {
  560. cacheMut.Lock()
  561. defer cacheMut.Unlock()
  562. if time.Since(cacheTime) > maxCacheTime {
  563. rep := getReport(db)
  564. buf := new(bytes.Buffer)
  565. err := tpl.Execute(buf, rep)
  566. if err != nil {
  567. log.Println(err)
  568. http.Error(w, "Template Error", http.StatusInternalServerError)
  569. return
  570. }
  571. cacheData = buf.Bytes()
  572. cacheTime = time.Now()
  573. }
  574. w.Header().Set("Content-Type", "text/html; charset=utf-8")
  575. w.Write(cacheData)
  576. } else {
  577. http.Error(w, "Not found", 404)
  578. return
  579. }
  580. }
  581. func newDataHandler(db *sql.DB, w http.ResponseWriter, r *http.Request) {
  582. defer r.Body.Close()
  583. var rep report
  584. rep.Date = time.Now().UTC().Format("20060102")
  585. lr := &io.LimitedReader{R: r.Body, N: 40 * 1024}
  586. if err := json.NewDecoder(lr).Decode(&rep); err != nil {
  587. log.Println("json decode:", err)
  588. http.Error(w, "JSON Decode Error", http.StatusInternalServerError)
  589. return
  590. }
  591. if err := rep.Validate(); err != nil {
  592. log.Println("validate:", err)
  593. log.Printf("%#v", rep)
  594. http.Error(w, "Validation Error", http.StatusInternalServerError)
  595. return
  596. }
  597. if err := insertReport(db, rep); err != nil {
  598. log.Println("insert:", err)
  599. log.Printf("%#v", rep)
  600. http.Error(w, "Database Error", http.StatusInternalServerError)
  601. return
  602. }
  603. }
  604. func summaryHandler(db *sql.DB, w http.ResponseWriter, r *http.Request) {
  605. s, err := getSummary(db)
  606. if err != nil {
  607. log.Println("summaryHandler:", err)
  608. http.Error(w, "Database Error", http.StatusInternalServerError)
  609. return
  610. }
  611. bs, err := s.MarshalJSON()
  612. if err != nil {
  613. log.Println("summaryHandler:", err)
  614. http.Error(w, "JSON Encode Error", http.StatusInternalServerError)
  615. return
  616. }
  617. w.Header().Set("Content-Type", "application/json")
  618. w.Write(bs)
  619. }
  620. func movementHandler(db *sql.DB, w http.ResponseWriter, r *http.Request) {
  621. s, err := getMovement(db)
  622. if err != nil {
  623. log.Println("movementHandler:", err)
  624. http.Error(w, "Database Error", http.StatusInternalServerError)
  625. return
  626. }
  627. bs, err := json.Marshal(s)
  628. if err != nil {
  629. log.Println("movementHandler:", err)
  630. http.Error(w, "JSON Encode Error", http.StatusInternalServerError)
  631. return
  632. }
  633. w.Header().Set("Content-Type", "application/json")
  634. w.Write(bs)
  635. }
  636. func performanceHandler(db *sql.DB, w http.ResponseWriter, r *http.Request) {
  637. s, err := getPerformance(db)
  638. if err != nil {
  639. log.Println("performanceHandler:", err)
  640. http.Error(w, "Database Error", http.StatusInternalServerError)
  641. return
  642. }
  643. bs, err := json.Marshal(s)
  644. if err != nil {
  645. log.Println("performanceHandler:", err)
  646. http.Error(w, "JSON Encode Error", http.StatusInternalServerError)
  647. return
  648. }
  649. w.Header().Set("Content-Type", "application/json")
  650. w.Write(bs)
  651. }
  652. type category struct {
  653. Values [4]float64
  654. Key string
  655. Descr string
  656. Unit string
  657. Binary bool
  658. }
  659. type feature struct {
  660. Key string
  661. Pct float64
  662. }
  663. func getReport(db *sql.DB) map[string]interface{} {
  664. nodes := 0
  665. var versions []string
  666. var platforms []string
  667. var numFolders []int
  668. var numDevices []int
  669. var totFiles []int
  670. var maxFiles []int
  671. var totMiB []int
  672. var maxMiB []int
  673. var memoryUsage []int
  674. var sha256Perf []float64
  675. var memorySize []int
  676. var compilers []string
  677. var builders []string
  678. v2Reports := 0
  679. features := map[string]float64{
  680. "Rate limiting": 0,
  681. "Upgrades allowed (automatic)": 0,
  682. "Upgrades allowed (manual)": 0,
  683. "Folders, automatic normalization": 0,
  684. "Folders, ignore deletes": 0,
  685. "Folders, ignore permissions": 0,
  686. "Folders, master mode": 0,
  687. "Folders, simple versioning": 0,
  688. "Folders, external versioning": 0,
  689. "Folders, staggered versioning": 0,
  690. "Folders, trashcan versioning": 0,
  691. "Devices, compress always": 0,
  692. "Devices, compress metadata": 0,
  693. "Devices, compress nothing": 0,
  694. "Devices, custom certificate": 0,
  695. "Devices, dynamic addresses": 0,
  696. "Devices, static addresses": 0,
  697. "Devices, introducer": 0,
  698. "Relaying, enabled": 0,
  699. "Relaying, default relays": 0,
  700. "Relaying, other relays": 0,
  701. "Discovery, global enabled": 0,
  702. "Discovery, local enabled": 0,
  703. "Discovery, default servers (using DNS)": 0,
  704. "Discovery, default servers (using IP)": 0,
  705. "Discovery, other servers": 0,
  706. }
  707. var numCPU []int
  708. var rep report
  709. rows, err := db.Query(`SELECT ` + strings.Join(rep.FieldNames(), ",") + ` FROM Reports WHERE Received > now() - '1 day'::INTERVAL`)
  710. if err != nil {
  711. log.Println("sql:", err)
  712. return nil
  713. }
  714. defer rows.Close()
  715. for rows.Next() {
  716. err := rows.Scan(rep.FieldPointers()...)
  717. if err != nil {
  718. log.Println("sql:", err)
  719. return nil
  720. }
  721. nodes++
  722. versions = append(versions, transformVersion(rep.Version))
  723. platforms = append(platforms, rep.Platform)
  724. if m := compilerRe.FindStringSubmatch(rep.LongVersion); len(m) == 3 {
  725. compilers = append(compilers, m[1])
  726. builders = append(builders, m[2])
  727. }
  728. if rep.NumFolders > 0 {
  729. numFolders = append(numFolders, rep.NumFolders)
  730. }
  731. if rep.NumDevices > 0 {
  732. numDevices = append(numDevices, rep.NumDevices)
  733. }
  734. if rep.TotFiles > 0 {
  735. totFiles = append(totFiles, rep.TotFiles)
  736. }
  737. if rep.FolderMaxFiles > 0 {
  738. maxFiles = append(maxFiles, rep.FolderMaxFiles)
  739. }
  740. if rep.TotMiB > 0 {
  741. totMiB = append(totMiB, rep.TotMiB*(1<<20))
  742. }
  743. if rep.FolderMaxMiB > 0 {
  744. maxMiB = append(maxMiB, rep.FolderMaxMiB*(1<<20))
  745. }
  746. if rep.MemoryUsageMiB > 0 {
  747. memoryUsage = append(memoryUsage, rep.MemoryUsageMiB*(1<<20))
  748. }
  749. if rep.SHA256Perf > 0 {
  750. sha256Perf = append(sha256Perf, rep.SHA256Perf*(1<<20))
  751. }
  752. if rep.MemorySize > 0 {
  753. memorySize = append(memorySize, rep.MemorySize*(1<<20))
  754. }
  755. if rep.URVersion >= 2 {
  756. v2Reports++
  757. numCPU = append(numCPU, rep.NumCPU)
  758. if rep.UsesRateLimit {
  759. features["Rate limiting"]++
  760. }
  761. if rep.UpgradeAllowedAuto {
  762. features["Upgrades allowed (automatic)"]++
  763. }
  764. if rep.UpgradeAllowedManual {
  765. features["Upgrades allowed (manual)"]++
  766. }
  767. if rep.FolderUses.AutoNormalize > 0 {
  768. features["Folders, automatic normalization"]++
  769. }
  770. if rep.FolderUses.IgnoreDelete > 0 {
  771. features["Folders, ignore deletes"]++
  772. }
  773. if rep.FolderUses.IgnorePerms > 0 {
  774. features["Folders, ignore permissions"]++
  775. }
  776. if rep.FolderUses.ReadOnly > 0 {
  777. features["Folders, master mode"]++
  778. }
  779. if rep.FolderUses.SimpleVersioning > 0 {
  780. features["Folders, simple versioning"]++
  781. }
  782. if rep.FolderUses.ExternalVersioning > 0 {
  783. features["Folders, external versioning"]++
  784. }
  785. if rep.FolderUses.StaggeredVersioning > 0 {
  786. features["Folders, staggered versioning"]++
  787. }
  788. if rep.FolderUses.TrashcanVersioning > 0 {
  789. features["Folders, trashcan versioning"]++
  790. }
  791. if rep.DeviceUses.CompressAlways > 0 {
  792. features["Devices, compress always"]++
  793. }
  794. if rep.DeviceUses.CompressMetadata > 0 {
  795. features["Devices, compress metadata"]++
  796. }
  797. if rep.DeviceUses.CompressNever > 0 {
  798. features["Devices, compress nothing"]++
  799. }
  800. if rep.DeviceUses.CustomCertName > 0 {
  801. features["Devices, custom certificate"]++
  802. }
  803. if rep.DeviceUses.DynamicAddr > 0 {
  804. features["Devices, dynamic addresses"]++
  805. }
  806. if rep.DeviceUses.StaticAddr > 0 {
  807. features["Devices, static addresses"]++
  808. }
  809. if rep.DeviceUses.Introducer > 0 {
  810. features["Devices, introducer"]++
  811. }
  812. if rep.Relays.Enabled {
  813. features["Relaying, enabled"]++
  814. }
  815. if rep.Relays.DefaultServers > 0 {
  816. features["Relaying, default relays"]++
  817. }
  818. if rep.Relays.OtherServers > 0 {
  819. features["Relaying, other relays"]++
  820. }
  821. if rep.Announce.GlobalEnabled {
  822. features["Discovery, global enabled"]++
  823. }
  824. if rep.Announce.LocalEnabled {
  825. features["Discovery, local enabled"]++
  826. }
  827. if rep.Announce.DefaultServersDNS > 0 {
  828. features["Discovery, default servers (using DNS)"]++
  829. }
  830. if rep.Announce.DefaultServersIP > 0 {
  831. features["Discovery, default servers (using IP)"]++
  832. }
  833. if rep.Announce.DefaultServersIP > 0 {
  834. features["Discovery, other servers"]++
  835. }
  836. }
  837. }
  838. var categories []category
  839. categories = append(categories, category{
  840. Values: statsForInts(totFiles),
  841. Descr: "Files Managed per Device",
  842. })
  843. categories = append(categories, category{
  844. Values: statsForInts(maxFiles),
  845. Descr: "Files in Largest Folder",
  846. })
  847. categories = append(categories, category{
  848. Values: statsForInts(totMiB),
  849. Descr: "Data Managed per Device",
  850. Unit: "B",
  851. Binary: true,
  852. })
  853. categories = append(categories, category{
  854. Values: statsForInts(maxMiB),
  855. Descr: "Data in Largest Folder",
  856. Unit: "B",
  857. Binary: true,
  858. })
  859. categories = append(categories, category{
  860. Values: statsForInts(numDevices),
  861. Descr: "Number of Devices in Cluster",
  862. })
  863. categories = append(categories, category{
  864. Values: statsForInts(numFolders),
  865. Descr: "Number of Folders Configured",
  866. })
  867. categories = append(categories, category{
  868. Values: statsForInts(memoryUsage),
  869. Descr: "Memory Usage",
  870. Unit: "B",
  871. Binary: true,
  872. })
  873. categories = append(categories, category{
  874. Values: statsForInts(memorySize),
  875. Descr: "System Memory",
  876. Unit: "B",
  877. Binary: true,
  878. })
  879. categories = append(categories, category{
  880. Values: statsForFloats(sha256Perf),
  881. Descr: "SHA-256 Hashing Performance",
  882. Unit: "B/s",
  883. Binary: true,
  884. })
  885. categories = append(categories, category{
  886. Values: statsForInts(numCPU),
  887. Descr: "Number of CPU cores",
  888. })
  889. var featureList []feature
  890. var featureNames []string
  891. for key := range features {
  892. featureNames = append(featureNames, key)
  893. }
  894. sort.Strings(featureNames)
  895. if v2Reports > 0 {
  896. for _, key := range featureNames {
  897. featureList = append(featureList, feature{
  898. Key: key,
  899. Pct: (100 * features[key]) / float64(v2Reports),
  900. })
  901. }
  902. sort.Sort(sort.Reverse(sortableFeatureList(featureList)))
  903. }
  904. r := make(map[string]interface{})
  905. r["nodes"] = nodes
  906. r["v2nodes"] = v2Reports
  907. r["categories"] = categories
  908. r["versions"] = group(byVersion, analyticsFor(versions, 2000), 5)
  909. r["platforms"] = group(byPlatform, analyticsFor(platforms, 2000), 5)
  910. r["compilers"] = group(byCompiler, analyticsFor(compilers, 2000), 3)
  911. r["builders"] = analyticsFor(builders, 12)
  912. r["features"] = featureList
  913. return r
  914. }
  915. func ensureDir(dir string, mode int) {
  916. fi, err := os.Stat(dir)
  917. if os.IsNotExist(err) {
  918. os.MkdirAll(dir, 0700)
  919. } else if mode >= 0 && err == nil && int(fi.Mode()&0777) != mode {
  920. os.Chmod(dir, os.FileMode(mode))
  921. }
  922. }
  923. var plusRe = regexp.MustCompile(`\+.*$`)
  924. // transformVersion returns a version number formatted correctly, with all
  925. // development versions aggregated into one.
  926. func transformVersion(v string) string {
  927. if v == "unknown-dev" {
  928. return v
  929. }
  930. if !strings.HasPrefix(v, "v") {
  931. v = "v" + v
  932. }
  933. v = plusRe.ReplaceAllString(v, " (+dev)")
  934. return v
  935. }
  936. type summary struct {
  937. versions map[string]int // version string to count index
  938. max map[string]int // version string to max users per day
  939. rows map[string][]int // date to list of counts
  940. }
  941. func newSummary() summary {
  942. return summary{
  943. versions: make(map[string]int),
  944. max: make(map[string]int),
  945. rows: make(map[string][]int),
  946. }
  947. }
  948. func (s *summary) setCount(date, version string, count int) {
  949. idx, ok := s.versions[version]
  950. if !ok {
  951. idx = len(s.versions)
  952. s.versions[version] = idx
  953. }
  954. if s.max[version] < count {
  955. s.max[version] = count
  956. }
  957. row := s.rows[date]
  958. if len(row) <= idx {
  959. old := row
  960. row = make([]int, idx+1)
  961. copy(row, old)
  962. s.rows[date] = row
  963. }
  964. row[idx] = count
  965. }
  966. func (s *summary) MarshalJSON() ([]byte, error) {
  967. var versions []string
  968. for v := range s.versions {
  969. versions = append(versions, v)
  970. }
  971. sort.Strings(versions)
  972. var filtered []string
  973. for _, v := range versions {
  974. if s.max[v] > 50 {
  975. filtered = append(filtered, v)
  976. }
  977. }
  978. versions = filtered
  979. headerRow := []interface{}{"Day"}
  980. for _, v := range versions {
  981. headerRow = append(headerRow, v)
  982. }
  983. var table [][]interface{}
  984. table = append(table, headerRow)
  985. var dates []string
  986. for k := range s.rows {
  987. dates = append(dates, k)
  988. }
  989. sort.Strings(dates)
  990. for _, date := range dates {
  991. row := []interface{}{date}
  992. for _, ver := range versions {
  993. idx := s.versions[ver]
  994. if len(s.rows[date]) > idx && s.rows[date][idx] > 0 {
  995. row = append(row, s.rows[date][idx])
  996. } else {
  997. row = append(row, nil)
  998. }
  999. }
  1000. table = append(table, row)
  1001. }
  1002. return json.Marshal(table)
  1003. }
  1004. func getSummary(db *sql.DB) (summary, error) {
  1005. s := newSummary()
  1006. rows, err := db.Query(`SELECT Day, Version, Count FROM VersionSummary WHERE Day > now() - '2 year'::INTERVAL;`)
  1007. if err != nil {
  1008. return summary{}, err
  1009. }
  1010. defer rows.Close()
  1011. for rows.Next() {
  1012. var day time.Time
  1013. var ver string
  1014. var num int
  1015. err := rows.Scan(&day, &ver, &num)
  1016. if err != nil {
  1017. return summary{}, err
  1018. }
  1019. if ver == "v0.0" {
  1020. // ?
  1021. continue
  1022. }
  1023. // SUPER UGLY HACK to avoid having to do sorting properly
  1024. if len(ver) == 4 { // v0.x
  1025. ver = ver[:3] + "0" + ver[3:] // now v0.0x
  1026. }
  1027. s.setCount(day.Format("2006-01-02"), ver, num)
  1028. }
  1029. return s, nil
  1030. }
  1031. func getMovement(db *sql.DB) ([][]interface{}, error) {
  1032. rows, err := db.Query(`SELECT Day, Added, Removed, Bounced FROM UserMovement WHERE Day > now() - '2 year'::INTERVAL ORDER BY Day`)
  1033. if err != nil {
  1034. return nil, err
  1035. }
  1036. defer rows.Close()
  1037. res := [][]interface{}{
  1038. {"Day", "Joined", "Left", "Bounced"},
  1039. }
  1040. for rows.Next() {
  1041. var day time.Time
  1042. var added, removed, bounced int
  1043. err := rows.Scan(&day, &added, &removed, &bounced)
  1044. if err != nil {
  1045. return nil, err
  1046. }
  1047. row := []interface{}{day.Format("2006-01-02"), added, -removed, bounced}
  1048. if removed == 0 {
  1049. row[2] = nil
  1050. }
  1051. if bounced == 0 {
  1052. row[3] = nil
  1053. }
  1054. res = append(res, row)
  1055. }
  1056. return res, nil
  1057. }
  1058. func getPerformance(db *sql.DB) ([][]interface{}, error) {
  1059. rows, err := db.Query(`SELECT Day, TotFiles, TotMiB, SHA256Perf, MemorySize, MemoryUsageMiB FROM Performance WHERE Day > '2014-06-20'::TIMESTAMP ORDER BY Day`)
  1060. if err != nil {
  1061. return nil, err
  1062. }
  1063. defer rows.Close()
  1064. res := [][]interface{}{
  1065. {"Day", "TotFiles", "TotMiB", "SHA256Perf", "MemorySize", "MemoryUsageMiB"},
  1066. }
  1067. for rows.Next() {
  1068. var day time.Time
  1069. var sha256Perf float64
  1070. var totFiles, totMiB, memorySize, memoryUsage int
  1071. err := rows.Scan(&day, &totFiles, &totMiB, &sha256Perf, &memorySize, &memoryUsage)
  1072. if err != nil {
  1073. return nil, err
  1074. }
  1075. row := []interface{}{day.Format("2006-01-02"), totFiles, totMiB, float64(int(sha256Perf*10)) / 10, memorySize, memoryUsage}
  1076. res = append(res, row)
  1077. }
  1078. return res, nil
  1079. }
  1080. type sortableFeatureList []feature
  1081. func (l sortableFeatureList) Len() int {
  1082. return len(l)
  1083. }
  1084. func (l sortableFeatureList) Swap(a, b int) {
  1085. l[a], l[b] = l[b], l[a]
  1086. }
  1087. func (l sortableFeatureList) Less(a, b int) bool {
  1088. if l[a].Pct != l[b].Pct {
  1089. return l[a].Pct < l[b].Pct
  1090. }
  1091. return l[a].Key > l[b].Key
  1092. }