main.go 49 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715
  1. // Copyright (C) 2018 The Syncthing Authors.
  2. //
  3. // This Source Code Form is subject to the terms of the Mozilla Public
  4. // License, v. 2.0. If a copy of the MPL was not distributed with this file,
  5. // You can obtain one at https://mozilla.org/MPL/2.0/.
  6. package main
  7. import (
  8. "bytes"
  9. "crypto/tls"
  10. "database/sql"
  11. "database/sql/driver"
  12. "encoding/json"
  13. "errors"
  14. "fmt"
  15. "html/template"
  16. "io"
  17. "io/ioutil"
  18. "log"
  19. "net"
  20. "net/http"
  21. "os"
  22. "regexp"
  23. "sort"
  24. "strings"
  25. "sync"
  26. "time"
  27. "unicode"
  28. "github.com/lib/pq"
  29. geoip2 "github.com/oschwald/geoip2-golang"
  30. )
  31. var (
  32. useHTTP = os.Getenv("UR_USE_HTTP") != ""
  33. debug = os.Getenv("UR_DEBUG") != ""
  34. keyFile = getEnvDefault("UR_KEY_FILE", "key.pem")
  35. certFile = getEnvDefault("UR_CRT_FILE", "crt.pem")
  36. dbConn = getEnvDefault("UR_DB_URL", "postgres://user:password@localhost/ur?sslmode=disable")
  37. listenAddr = getEnvDefault("UR_LISTEN", "0.0.0.0:8443")
  38. geoIPPath = getEnvDefault("UR_GEOIP", "GeoLite2-City.mmdb")
  39. tpl *template.Template
  40. compilerRe = regexp.MustCompile(`\(([A-Za-z0-9()., -]+) \w+-\w+(?:| android| default)\) ([\[email protected]]+)`)
  41. progressBarClass = []string{"", "progress-bar-success", "progress-bar-info", "progress-bar-warning", "progress-bar-danger"}
  42. featureOrder = []string{"Various", "Folder", "Device", "Connection", "GUI"}
  43. knownVersions = []string{"v2", "v3"}
  44. knownDistributions = []distributionMatch{
  45. // Maps well known builders to the official distribution method that
  46. // they represent
  47. {regexp.MustCompile("android-.*[email protected]"), "Google Play"},
  48. {regexp.MustCompile("[email protected]"), "GitHub"},
  49. {regexp.MustCompile("[email protected]"), "APT"},
  50. {regexp.MustCompile("[email protected]"), "Docker Hub"},
  51. {regexp.MustCompile("[email protected]"), "GitHub"},
  52. {regexp.MustCompile("[email protected]"), "Snapcraft"},
  53. {regexp.MustCompile("android-.*vagrant@basebox-stretch64"), "F-Droid"},
  54. {regexp.MustCompile("builduser@svetlemodry"), "Arch (3rd party)"},
  55. {regexp.MustCompile("[email protected]"), "Synology (Kastelo)"},
  56. {regexp.MustCompile("@debian"), "Debian (3rd party)"},
  57. {regexp.MustCompile("@fedora"), "Fedora (3rd party)"},
  58. {regexp.MustCompile(`\bbrew@`), "Homebrew (3rd party)"},
  59. {regexp.MustCompile("."), "Others"},
  60. }
  61. )
  62. type distributionMatch struct {
  63. matcher *regexp.Regexp
  64. distribution string
  65. }
  66. var funcs = map[string]interface{}{
  67. "commatize": commatize,
  68. "number": number,
  69. "proportion": proportion,
  70. "counter": func() *counter {
  71. return &counter{}
  72. },
  73. "progressBarClassByIndex": func(a int) string {
  74. return progressBarClass[a%len(progressBarClass)]
  75. },
  76. "slice": func(numParts, whichPart int, input []feature) []feature {
  77. var part []feature
  78. perPart := (len(input) / numParts) + len(input)%2
  79. parts := make([][]feature, 0, numParts)
  80. for len(input) >= perPart {
  81. part, input = input[:perPart], input[perPart:]
  82. parts = append(parts, part)
  83. }
  84. if len(input) > 0 {
  85. parts = append(parts, input[:])
  86. }
  87. return parts[whichPart-1]
  88. },
  89. }
  90. func getEnvDefault(key, def string) string {
  91. if val := os.Getenv(key); val != "" {
  92. return val
  93. }
  94. return def
  95. }
  96. type IntMap map[string]int
  97. func (p IntMap) Value() (driver.Value, error) {
  98. return json.Marshal(p)
  99. }
  100. func (p *IntMap) Scan(src interface{}) error {
  101. source, ok := src.([]byte)
  102. if !ok {
  103. return errors.New("Type assertion .([]byte) failed.")
  104. }
  105. var i map[string]int
  106. err := json.Unmarshal(source, &i)
  107. if err != nil {
  108. return err
  109. }
  110. *p = i
  111. return nil
  112. }
  113. type report struct {
  114. Received time.Time // Only from DB
  115. UniqueID string
  116. Version string
  117. LongVersion string
  118. Platform string
  119. NumFolders int
  120. NumDevices int
  121. TotFiles int
  122. FolderMaxFiles int
  123. TotMiB int
  124. FolderMaxMiB int
  125. MemoryUsageMiB int
  126. SHA256Perf float64
  127. MemorySize int
  128. // v2 fields
  129. URVersion int
  130. NumCPU int
  131. FolderUses struct {
  132. SendOnly int
  133. ReceiveOnly int
  134. IgnorePerms int
  135. IgnoreDelete int
  136. AutoNormalize int
  137. SimpleVersioning int
  138. ExternalVersioning int
  139. StaggeredVersioning int
  140. TrashcanVersioning int
  141. }
  142. DeviceUses struct {
  143. Introducer int
  144. CustomCertName int
  145. CompressAlways int
  146. CompressMetadata int
  147. CompressNever int
  148. DynamicAddr int
  149. StaticAddr int
  150. }
  151. Announce struct {
  152. GlobalEnabled bool
  153. LocalEnabled bool
  154. DefaultServersDNS int
  155. DefaultServersIP int
  156. OtherServers int
  157. }
  158. Relays struct {
  159. Enabled bool
  160. DefaultServers int
  161. OtherServers int
  162. }
  163. UsesRateLimit bool
  164. UpgradeAllowedManual bool
  165. UpgradeAllowedAuto bool
  166. // V2.5 fields (fields that were in v2 but never added to the database
  167. UpgradeAllowedPre bool
  168. RescanIntvs pq.Int64Array
  169. // v3 fields
  170. Uptime int
  171. NATType string
  172. AlwaysLocalNets bool
  173. CacheIgnoredFiles bool
  174. OverwriteRemoteDeviceNames bool
  175. ProgressEmitterEnabled bool
  176. CustomDefaultFolderPath bool
  177. WeakHashSelection string
  178. CustomTrafficClass bool
  179. CustomTempIndexMinBlocks bool
  180. TemporariesDisabled bool
  181. TemporariesCustom bool
  182. LimitBandwidthInLan bool
  183. CustomReleaseURL bool
  184. RestartOnWakeup bool
  185. CustomStunServers bool
  186. FolderUsesV3 struct {
  187. ScanProgressDisabled int
  188. ConflictsDisabled int
  189. ConflictsUnlimited int
  190. ConflictsOther int
  191. DisableSparseFiles int
  192. DisableTempIndexes int
  193. AlwaysWeakHash int
  194. CustomWeakHashThreshold int
  195. FsWatcherEnabled int
  196. PullOrder IntMap
  197. FilesystemType IntMap
  198. FsWatcherDelays pq.Int64Array
  199. }
  200. GUIStats struct {
  201. Enabled int
  202. UseTLS int
  203. UseAuth int
  204. InsecureAdminAccess int
  205. Debugging int
  206. InsecureSkipHostCheck int
  207. InsecureAllowFrameLoading int
  208. ListenLocal int
  209. ListenUnspecified int
  210. Theme IntMap
  211. }
  212. BlockStats struct {
  213. Total int
  214. Renamed int
  215. Reused int
  216. Pulled int
  217. CopyOrigin int
  218. CopyOriginShifted int
  219. CopyElsewhere int
  220. }
  221. TransportStats IntMap
  222. IgnoreStats struct {
  223. Lines int
  224. Inverts int
  225. Folded int
  226. Deletable int
  227. Rooted int
  228. Includes int
  229. EscapedIncludes int
  230. DoubleStars int
  231. Stars int
  232. }
  233. // V3 fields added late in the RC
  234. WeakHashEnabled bool
  235. // Generated
  236. Date string
  237. Address string
  238. }
  239. func (r *report) Validate() error {
  240. if r.UniqueID == "" || r.Version == "" || r.Platform == "" {
  241. return errors.New("missing required field")
  242. }
  243. if len(r.Date) != 8 {
  244. return errors.New("date not initialized")
  245. }
  246. // Some fields may not be null.
  247. if r.RescanIntvs == nil {
  248. r.RescanIntvs = []int64{}
  249. }
  250. if r.FolderUsesV3.FsWatcherDelays == nil {
  251. r.FolderUsesV3.FsWatcherDelays = []int64{}
  252. }
  253. return nil
  254. }
  255. func (r *report) FieldPointers() []interface{} {
  256. // All the fields of the report, in the same order as the database fields.
  257. return []interface{}{
  258. &r.Received, &r.UniqueID, &r.Version, &r.LongVersion, &r.Platform,
  259. &r.NumFolders, &r.NumDevices, &r.TotFiles, &r.FolderMaxFiles,
  260. &r.TotMiB, &r.FolderMaxMiB, &r.MemoryUsageMiB, &r.SHA256Perf,
  261. &r.MemorySize, &r.Date,
  262. // V2
  263. &r.URVersion, &r.NumCPU, &r.FolderUses.SendOnly, &r.FolderUses.IgnorePerms,
  264. &r.FolderUses.IgnoreDelete, &r.FolderUses.AutoNormalize, &r.DeviceUses.Introducer,
  265. &r.DeviceUses.CustomCertName, &r.DeviceUses.CompressAlways,
  266. &r.DeviceUses.CompressMetadata, &r.DeviceUses.CompressNever,
  267. &r.DeviceUses.DynamicAddr, &r.DeviceUses.StaticAddr,
  268. &r.Announce.GlobalEnabled, &r.Announce.LocalEnabled,
  269. &r.Announce.DefaultServersDNS, &r.Announce.DefaultServersIP,
  270. &r.Announce.OtherServers, &r.Relays.Enabled, &r.Relays.DefaultServers,
  271. &r.Relays.OtherServers, &r.UsesRateLimit, &r.UpgradeAllowedManual,
  272. &r.UpgradeAllowedAuto, &r.FolderUses.SimpleVersioning,
  273. &r.FolderUses.ExternalVersioning, &r.FolderUses.StaggeredVersioning,
  274. &r.FolderUses.TrashcanVersioning,
  275. // V2.5
  276. &r.UpgradeAllowedPre, &r.RescanIntvs,
  277. // V3
  278. &r.Uptime, &r.NATType, &r.AlwaysLocalNets, &r.CacheIgnoredFiles,
  279. &r.OverwriteRemoteDeviceNames, &r.ProgressEmitterEnabled, &r.CustomDefaultFolderPath,
  280. &r.WeakHashSelection, &r.CustomTrafficClass, &r.CustomTempIndexMinBlocks,
  281. &r.TemporariesDisabled, &r.TemporariesCustom, &r.LimitBandwidthInLan,
  282. &r.CustomReleaseURL, &r.RestartOnWakeup, &r.CustomStunServers,
  283. &r.FolderUsesV3.ScanProgressDisabled, &r.FolderUsesV3.ConflictsDisabled,
  284. &r.FolderUsesV3.ConflictsUnlimited, &r.FolderUsesV3.ConflictsOther,
  285. &r.FolderUsesV3.DisableSparseFiles, &r.FolderUsesV3.DisableTempIndexes,
  286. &r.FolderUsesV3.AlwaysWeakHash, &r.FolderUsesV3.CustomWeakHashThreshold,
  287. &r.FolderUsesV3.FsWatcherEnabled,
  288. &r.FolderUsesV3.PullOrder, &r.FolderUsesV3.FilesystemType,
  289. &r.FolderUsesV3.FsWatcherDelays,
  290. &r.GUIStats.Enabled, &r.GUIStats.UseTLS, &r.GUIStats.UseAuth,
  291. &r.GUIStats.InsecureAdminAccess,
  292. &r.GUIStats.Debugging, &r.GUIStats.InsecureSkipHostCheck,
  293. &r.GUIStats.InsecureAllowFrameLoading, &r.GUIStats.ListenLocal,
  294. &r.GUIStats.ListenUnspecified, &r.GUIStats.Theme,
  295. &r.BlockStats.Total, &r.BlockStats.Renamed,
  296. &r.BlockStats.Reused, &r.BlockStats.Pulled, &r.BlockStats.CopyOrigin,
  297. &r.BlockStats.CopyOriginShifted, &r.BlockStats.CopyElsewhere,
  298. &r.TransportStats,
  299. &r.IgnoreStats.Lines, &r.IgnoreStats.Inverts, &r.IgnoreStats.Folded,
  300. &r.IgnoreStats.Deletable, &r.IgnoreStats.Rooted, &r.IgnoreStats.Includes,
  301. &r.IgnoreStats.EscapedIncludes, &r.IgnoreStats.DoubleStars, &r.IgnoreStats.Stars,
  302. // V3 added late in the RC
  303. &r.WeakHashEnabled,
  304. &r.Address,
  305. // Receive only folders
  306. &r.FolderUses.ReceiveOnly,
  307. }
  308. }
  309. func (r *report) FieldNames() []string {
  310. // The database fields that back this struct in PostgreSQL
  311. return []string{
  312. // V1
  313. "Received",
  314. "UniqueID",
  315. "Version",
  316. "LongVersion",
  317. "Platform",
  318. "NumFolders",
  319. "NumDevices",
  320. "TotFiles",
  321. "FolderMaxFiles",
  322. "TotMiB",
  323. "FolderMaxMiB",
  324. "MemoryUsageMiB",
  325. "SHA256Perf",
  326. "MemorySize",
  327. "Date",
  328. // V2
  329. "ReportVersion",
  330. "NumCPU",
  331. "FolderRO",
  332. "FolderIgnorePerms",
  333. "FolderIgnoreDelete",
  334. "FolderAutoNormalize",
  335. "DeviceIntroducer",
  336. "DeviceCustomCertName",
  337. "DeviceCompressAlways",
  338. "DeviceCompressMetadata",
  339. "DeviceCompressNever",
  340. "DeviceDynamicAddr",
  341. "DeviceStaticAddr",
  342. "AnnounceGlobalEnabled",
  343. "AnnounceLocalEnabled",
  344. "AnnounceDefaultServersDNS",
  345. "AnnounceDefaultServersIP",
  346. "AnnounceOtherServers",
  347. "RelayEnabled",
  348. "RelayDefaultServers",
  349. "RelayOtherServers",
  350. "RateLimitEnabled",
  351. "UpgradeAllowedManual",
  352. "UpgradeAllowedAuto",
  353. // v0.12.19+
  354. "FolderSimpleVersioning",
  355. "FolderExternalVersioning",
  356. "FolderStaggeredVersioning",
  357. "FolderTrashcanVersioning",
  358. // V2.5
  359. "UpgradeAllowedPre",
  360. "RescanIntvs",
  361. // V3
  362. "Uptime",
  363. "NATType",
  364. "AlwaysLocalNets",
  365. "CacheIgnoredFiles",
  366. "OverwriteRemoteDeviceNames",
  367. "ProgressEmitterEnabled",
  368. "CustomDefaultFolderPath",
  369. "WeakHashSelection",
  370. "CustomTrafficClass",
  371. "CustomTempIndexMinBlocks",
  372. "TemporariesDisabled",
  373. "TemporariesCustom",
  374. "LimitBandwidthInLan",
  375. "CustomReleaseURL",
  376. "RestartOnWakeup",
  377. "CustomStunServers",
  378. "FolderScanProgressDisabled",
  379. "FolderConflictsDisabled",
  380. "FolderConflictsUnlimited",
  381. "FolderConflictsOther",
  382. "FolderDisableSparseFiles",
  383. "FolderDisableTempIndexes",
  384. "FolderAlwaysWeakHash",
  385. "FolderCustomWeakHashThreshold",
  386. "FolderFsWatcherEnabled",
  387. "FolderPullOrder",
  388. "FolderFilesystemType",
  389. "FolderFsWatcherDelays",
  390. "GUIEnabled",
  391. "GUIUseTLS",
  392. "GUIUseAuth",
  393. "GUIInsecureAdminAccess",
  394. "GUIDebugging",
  395. "GUIInsecureSkipHostCheck",
  396. "GUIInsecureAllowFrameLoading",
  397. "GUIListenLocal",
  398. "GUIListenUnspecified",
  399. "GUITheme",
  400. "BlocksTotal",
  401. "BlocksRenamed",
  402. "BlocksReused",
  403. "BlocksPulled",
  404. "BlocksCopyOrigin",
  405. "BlocksCopyOriginShifted",
  406. "BlocksCopyElsewhere",
  407. "Transport",
  408. "IgnoreLines",
  409. "IgnoreInverts",
  410. "IgnoreFolded",
  411. "IgnoreDeletable",
  412. "IgnoreRooted",
  413. "IgnoreIncludes",
  414. "IgnoreEscapedIncludes",
  415. "IgnoreDoubleStars",
  416. "IgnoreStars",
  417. // V3 added late in the RC
  418. "WeakHashEnabled",
  419. "Address",
  420. // Receive only folders
  421. "FolderRecvOnly",
  422. }
  423. }
  424. func setupDB(db *sql.DB) error {
  425. _, err := db.Exec(`CREATE TABLE IF NOT EXISTS Reports (
  426. Received TIMESTAMP NOT NULL,
  427. UniqueID VARCHAR(32) NOT NULL,
  428. Version VARCHAR(32) NOT NULL,
  429. LongVersion VARCHAR(256) NOT NULL,
  430. Platform VARCHAR(32) NOT NULL,
  431. NumFolders INTEGER NOT NULL,
  432. NumDevices INTEGER NOT NULL,
  433. TotFiles INTEGER NOT NULL,
  434. FolderMaxFiles INTEGER NOT NULL,
  435. TotMiB INTEGER NOT NULL,
  436. FolderMaxMiB INTEGER NOT NULL,
  437. MemoryUsageMiB INTEGER NOT NULL,
  438. SHA256Perf DOUBLE PRECISION NOT NULL,
  439. MemorySize INTEGER NOT NULL,
  440. Date VARCHAR(8) NOT NULL
  441. )`)
  442. if err != nil {
  443. return err
  444. }
  445. var t string
  446. row := db.QueryRow(`SELECT 'UniqueIDIndex'::regclass`)
  447. if err := row.Scan(&t); err != nil {
  448. if _, err = db.Exec(`CREATE UNIQUE INDEX UniqueIDIndex ON Reports (Date, UniqueID)`); err != nil {
  449. return err
  450. }
  451. }
  452. row = db.QueryRow(`SELECT 'ReceivedIndex'::regclass`)
  453. if err := row.Scan(&t); err != nil {
  454. if _, err = db.Exec(`CREATE INDEX ReceivedIndex ON Reports (Received)`); err != nil {
  455. return err
  456. }
  457. }
  458. // V2
  459. row = db.QueryRow(`SELECT attname FROM pg_attribute WHERE attrelid = (SELECT oid FROM pg_class WHERE relname = 'reports') AND attname = 'reportversion'`)
  460. if err := row.Scan(&t); err != nil {
  461. // The ReportVersion column doesn't exist; add the new columns.
  462. _, err = db.Exec(`ALTER TABLE Reports
  463. ADD COLUMN ReportVersion INTEGER NOT NULL DEFAULT 0,
  464. ADD COLUMN NumCPU INTEGER NOT NULL DEFAULT 0,
  465. ADD COLUMN FolderRO INTEGER NOT NULL DEFAULT 0,
  466. ADD COLUMN FolderIgnorePerms INTEGER NOT NULL DEFAULT 0,
  467. ADD COLUMN FolderIgnoreDelete INTEGER NOT NULL DEFAULT 0,
  468. ADD COLUMN FolderAutoNormalize INTEGER NOT NULL DEFAULT 0,
  469. ADD COLUMN DeviceIntroducer INTEGER NOT NULL DEFAULT 0,
  470. ADD COLUMN DeviceCustomCertName INTEGER NOT NULL DEFAULT 0,
  471. ADD COLUMN DeviceCompressAlways INTEGER NOT NULL DEFAULT 0,
  472. ADD COLUMN DeviceCompressMetadata INTEGER NOT NULL DEFAULT 0,
  473. ADD COLUMN DeviceCompressNever INTEGER NOT NULL DEFAULT 0,
  474. ADD COLUMN DeviceDynamicAddr INTEGER NOT NULL DEFAULT 0,
  475. ADD COLUMN DeviceStaticAddr INTEGER NOT NULL DEFAULT 0,
  476. ADD COLUMN AnnounceGlobalEnabled BOOLEAN NOT NULL DEFAULT FALSE,
  477. ADD COLUMN AnnounceLocalEnabled BOOLEAN NOT NULL DEFAULT FALSE,
  478. ADD COLUMN AnnounceDefaultServersDNS INTEGER NOT NULL DEFAULT 0,
  479. ADD COLUMN AnnounceDefaultServersIP INTEGER NOT NULL DEFAULT 0,
  480. ADD COLUMN AnnounceOtherServers INTEGER NOT NULL DEFAULT 0,
  481. ADD COLUMN RelayEnabled BOOLEAN NOT NULL DEFAULT FALSE,
  482. ADD COLUMN RelayDefaultServers INTEGER NOT NULL DEFAULT 0,
  483. ADD COLUMN RelayOtherServers INTEGER NOT NULL DEFAULT 0,
  484. ADD COLUMN RateLimitEnabled BOOLEAN NOT NULL DEFAULT FALSE,
  485. ADD COLUMN UpgradeAllowedManual BOOLEAN NOT NULL DEFAULT FALSE,
  486. ADD COLUMN UpgradeAllowedAuto BOOLEAN NOT NULL DEFAULT FALSE,
  487. ADD COLUMN FolderSimpleVersioning INTEGER NOT NULL DEFAULT 0,
  488. ADD COLUMN FolderExternalVersioning INTEGER NOT NULL DEFAULT 0,
  489. ADD COLUMN FolderStaggeredVersioning INTEGER NOT NULL DEFAULT 0,
  490. ADD COLUMN FolderTrashcanVersioning INTEGER NOT NULL DEFAULT 0
  491. `)
  492. if err != nil {
  493. return err
  494. }
  495. }
  496. row = db.QueryRow(`SELECT 'ReportVersionIndex'::regclass`)
  497. if err := row.Scan(&t); err != nil {
  498. if _, err = db.Exec(`CREATE INDEX ReportVersionIndex ON Reports (ReportVersion)`); err != nil {
  499. return err
  500. }
  501. }
  502. // V2.5
  503. row = db.QueryRow(`SELECT attname FROM pg_attribute WHERE attrelid = (SELECT oid FROM pg_class WHERE relname = 'reports') AND attname = 'upgradeallowedpre'`)
  504. if err := row.Scan(&t); err != nil {
  505. // The ReportVersion column doesn't exist; add the new columns.
  506. _, err = db.Exec(`ALTER TABLE Reports
  507. ADD COLUMN UpgradeAllowedPre BOOLEAN NOT NULL DEFAULT FALSE,
  508. ADD COLUMN RescanIntvs INT[] NOT NULL DEFAULT '{}'
  509. `)
  510. if err != nil {
  511. return err
  512. }
  513. }
  514. // V3
  515. row = db.QueryRow(`SELECT attname FROM pg_attribute WHERE attrelid = (SELECT oid FROM pg_class WHERE relname = 'reports') AND attname = 'uptime'`)
  516. if err := row.Scan(&t); err != nil {
  517. // The Uptime column doesn't exist; add the new columns.
  518. _, err = db.Exec(`ALTER TABLE Reports
  519. ADD COLUMN Uptime INTEGER NOT NULL DEFAULT 0,
  520. ADD COLUMN NATType VARCHAR(32) NOT NULL DEFAULT '',
  521. ADD COLUMN AlwaysLocalNets BOOLEAN NOT NULL DEFAULT FALSE,
  522. ADD COLUMN CacheIgnoredFiles BOOLEAN NOT NULL DEFAULT FALSE,
  523. ADD COLUMN OverwriteRemoteDeviceNames BOOLEAN NOT NULL DEFAULT FALSE,
  524. ADD COLUMN ProgressEmitterEnabled BOOLEAN NOT NULL DEFAULT FALSE,
  525. ADD COLUMN CustomDefaultFolderPath BOOLEAN NOT NULL DEFAULT FALSE,
  526. ADD COLUMN WeakHashSelection VARCHAR(32) NOT NULL DEFAULT '',
  527. ADD COLUMN CustomTrafficClass BOOLEAN NOT NULL DEFAULT FALSE,
  528. ADD COLUMN CustomTempIndexMinBlocks BOOLEAN NOT NULL DEFAULT FALSE,
  529. ADD COLUMN TemporariesDisabled BOOLEAN NOT NULL DEFAULT FALSE,
  530. ADD COLUMN TemporariesCustom BOOLEAN NOT NULL DEFAULT FALSE,
  531. ADD COLUMN LimitBandwidthInLan BOOLEAN NOT NULL DEFAULT FALSE,
  532. ADD COLUMN CustomReleaseURL BOOLEAN NOT NULL DEFAULT FALSE,
  533. ADD COLUMN RestartOnWakeup BOOLEAN NOT NULL DEFAULT FALSE,
  534. ADD COLUMN CustomStunServers BOOLEAN NOT NULL DEFAULT FALSE,
  535. ADD COLUMN FolderScanProgressDisabled INTEGER NOT NULL DEFAULT 0,
  536. ADD COLUMN FolderConflictsDisabled INTEGER NOT NULL DEFAULT 0,
  537. ADD COLUMN FolderConflictsUnlimited INTEGER NOT NULL DEFAULT 0,
  538. ADD COLUMN FolderConflictsOther INTEGER NOT NULL DEFAULT 0,
  539. ADD COLUMN FolderDisableSparseFiles INTEGER NOT NULL DEFAULT 0,
  540. ADD COLUMN FolderDisableTempIndexes INTEGER NOT NULL DEFAULT 0,
  541. ADD COLUMN FolderAlwaysWeakHash INTEGER NOT NULL DEFAULT 0,
  542. ADD COLUMN FolderCustomWeakHashThreshold INTEGER NOT NULL DEFAULT 0,
  543. ADD COLUMN FolderFsWatcherEnabled INTEGER NOT NULL DEFAULT 0,
  544. ADD COLUMN FolderPullOrder JSONB NOT NULL DEFAULT '{}',
  545. ADD COLUMN FolderFilesystemType JSONB NOT NULL DEFAULT '{}',
  546. ADD COLUMN FolderFsWatcherDelays INT[] NOT NULL DEFAULT '{}',
  547. ADD COLUMN GUIEnabled INTEGER NOT NULL DEFAULT 0,
  548. ADD COLUMN GUIUseTLS INTEGER NOT NULL DEFAULT 0,
  549. ADD COLUMN GUIUseAuth INTEGER NOT NULL DEFAULT 0,
  550. ADD COLUMN GUIInsecureAdminAccess INTEGER NOT NULL DEFAULT 0,
  551. ADD COLUMN GUIDebugging INTEGER NOT NULL DEFAULT 0,
  552. ADD COLUMN GUIInsecureSkipHostCheck INTEGER NOT NULL DEFAULT 0,
  553. ADD COLUMN GUIInsecureAllowFrameLoading INTEGER NOT NULL DEFAULT 0,
  554. ADD COLUMN GUIListenLocal INTEGER NOT NULL DEFAULT 0,
  555. ADD COLUMN GUIListenUnspecified INTEGER NOT NULL DEFAULT 0,
  556. ADD COLUMN GUITheme JSONB NOT NULL DEFAULT '{}',
  557. ADD COLUMN BlocksTotal INTEGER NOT NULL DEFAULT 0,
  558. ADD COLUMN BlocksRenamed INTEGER NOT NULL DEFAULT 0,
  559. ADD COLUMN BlocksReused INTEGER NOT NULL DEFAULT 0,
  560. ADD COLUMN BlocksPulled INTEGER NOT NULL DEFAULT 0,
  561. ADD COLUMN BlocksCopyOrigin INTEGER NOT NULL DEFAULT 0,
  562. ADD COLUMN BlocksCopyOriginShifted INTEGER NOT NULL DEFAULT 0,
  563. ADD COLUMN BlocksCopyElsewhere INTEGER NOT NULL DEFAULT 0,
  564. ADD COLUMN Transport JSONB NOT NULL DEFAULT '{}',
  565. ADD COLUMN IgnoreLines INTEGER NOT NULL DEFAULT 0,
  566. ADD COLUMN IgnoreInverts INTEGER NOT NULL DEFAULT 0,
  567. ADD COLUMN IgnoreFolded INTEGER NOT NULL DEFAULT 0,
  568. ADD COLUMN IgnoreDeletable INTEGER NOT NULL DEFAULT 0,
  569. ADD COLUMN IgnoreRooted INTEGER NOT NULL DEFAULT 0,
  570. ADD COLUMN IgnoreIncludes INTEGER NOT NULL DEFAULT 0,
  571. ADD COLUMN IgnoreEscapedIncludes INTEGER NOT NULL DEFAULT 0,
  572. ADD COLUMN IgnoreDoubleStars INTEGER NOT NULL DEFAULT 0,
  573. ADD COLUMN IgnoreStars INTEGER NOT NULL DEFAULT 0
  574. `)
  575. if err != nil {
  576. return err
  577. }
  578. }
  579. // V3 added late in the RC
  580. row = db.QueryRow(`SELECT attname FROM pg_attribute WHERE attrelid = (SELECT oid FROM pg_class WHERE relname = 'reports') AND attname = 'weakhashenabled'`)
  581. if err := row.Scan(&t); err != nil {
  582. // The WeakHashEnabled column doesn't exist; add the new columns.
  583. _, err = db.Exec(`ALTER TABLE Reports
  584. ADD COLUMN WeakHashEnabled BOOLEAN NOT NULL DEFAULT FALSE
  585. ADD COLUMN Address VARCHAR(45) NOT NULL DEFAULT ''
  586. `)
  587. if err != nil {
  588. return err
  589. }
  590. }
  591. // Receive only added ad-hoc
  592. row = db.QueryRow(`SELECT attname FROM pg_attribute WHERE attrelid = (SELECT oid FROM pg_class WHERE relname = 'reports') AND attname = 'folderrecvonly'`)
  593. if err := row.Scan(&t); err != nil {
  594. // The RecvOnly column doesn't exist; add it.
  595. _, err = db.Exec(`ALTER TABLE Reports
  596. ADD COLUMN FolderRecvOnly INTEGER NOT NULL DEFAULT 0
  597. `)
  598. if err != nil {
  599. return err
  600. }
  601. }
  602. return nil
  603. }
  604. func insertReport(db *sql.DB, r report) error {
  605. r.Received = time.Now().UTC()
  606. fields := r.FieldPointers()
  607. params := make([]string, len(fields))
  608. for i := range params {
  609. params[i] = fmt.Sprintf("$%d", i+1)
  610. }
  611. query := "INSERT INTO Reports (" + strings.Join(r.FieldNames(), ", ") + ") VALUES (" + strings.Join(params, ", ") + ")"
  612. _, err := db.Exec(query, fields...)
  613. return err
  614. }
  615. type withDBFunc func(*sql.DB, http.ResponseWriter, *http.Request)
  616. func withDB(db *sql.DB, f withDBFunc) http.HandlerFunc {
  617. return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  618. f(db, w, r)
  619. })
  620. }
  621. func main() {
  622. log.SetFlags(log.Ltime | log.Ldate | log.Lshortfile)
  623. log.SetOutput(os.Stdout)
  624. // Template
  625. fd, err := os.Open("static/index.html")
  626. if err != nil {
  627. log.Fatalln("template:", err)
  628. }
  629. bs, err := ioutil.ReadAll(fd)
  630. if err != nil {
  631. log.Fatalln("template:", err)
  632. }
  633. fd.Close()
  634. tpl = template.Must(template.New("index.html").Funcs(funcs).Parse(string(bs)))
  635. // DB
  636. db, err := sql.Open("postgres", dbConn)
  637. if err != nil {
  638. log.Fatalln("database:", err)
  639. }
  640. err = setupDB(db)
  641. if err != nil {
  642. log.Fatalln("database:", err)
  643. }
  644. // TLS & Listening
  645. var listener net.Listener
  646. if useHTTP {
  647. listener, err = net.Listen("tcp", listenAddr)
  648. } else {
  649. var cert tls.Certificate
  650. cert, err = tls.LoadX509KeyPair(certFile, keyFile)
  651. if err != nil {
  652. log.Fatalln("tls:", err)
  653. }
  654. cfg := &tls.Config{
  655. Certificates: []tls.Certificate{cert},
  656. SessionTicketsDisabled: true,
  657. }
  658. listener, err = tls.Listen("tcp", listenAddr, cfg)
  659. }
  660. if err != nil {
  661. log.Fatalln("listen:", err)
  662. }
  663. srv := http.Server{
  664. ReadTimeout: 5 * time.Second,
  665. WriteTimeout: 15 * time.Second,
  666. }
  667. http.HandleFunc("/", withDB(db, rootHandler))
  668. http.HandleFunc("/newdata", withDB(db, newDataHandler))
  669. http.HandleFunc("/summary.json", withDB(db, summaryHandler))
  670. http.HandleFunc("/movement.json", withDB(db, movementHandler))
  671. http.HandleFunc("/performance.json", withDB(db, performanceHandler))
  672. http.HandleFunc("/blockstats.json", withDB(db, blockStatsHandler))
  673. http.HandleFunc("/locations.json", withDB(db, locationsHandler))
  674. http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static"))))
  675. go cacheRefresher(db)
  676. err = srv.Serve(listener)
  677. if err != nil {
  678. log.Fatalln("https:", err)
  679. }
  680. }
  681. var (
  682. cachedIndex []byte
  683. cachedLocations []byte
  684. cacheTime time.Time
  685. cacheMut sync.Mutex
  686. )
  687. const maxCacheTime = 15 * time.Minute
  688. func cacheRefresher(db *sql.DB) {
  689. ticker := time.NewTicker(maxCacheTime - time.Minute)
  690. defer ticker.Stop()
  691. for range ticker.C {
  692. cacheMut.Lock()
  693. if err := refreshCacheLocked(db); err != nil {
  694. log.Println(err)
  695. }
  696. cacheMut.Unlock()
  697. }
  698. }
  699. func refreshCacheLocked(db *sql.DB) error {
  700. rep := getReport(db)
  701. buf := new(bytes.Buffer)
  702. err := tpl.Execute(buf, rep)
  703. if err != nil {
  704. return err
  705. }
  706. cachedIndex = buf.Bytes()
  707. cacheTime = time.Now()
  708. locs := rep["locations"].(map[location]int)
  709. wlocs := make([]weightedLocation, 0, len(locs))
  710. for loc, w := range locs {
  711. wlocs = append(wlocs, weightedLocation{loc, w})
  712. }
  713. cachedLocations, _ = json.Marshal(wlocs)
  714. return nil
  715. }
  716. func rootHandler(db *sql.DB, w http.ResponseWriter, r *http.Request) {
  717. if r.URL.Path == "/" || r.URL.Path == "/index.html" {
  718. cacheMut.Lock()
  719. defer cacheMut.Unlock()
  720. if time.Since(cacheTime) > maxCacheTime {
  721. if err := refreshCacheLocked(db); err != nil {
  722. log.Println(err)
  723. http.Error(w, "Template Error", http.StatusInternalServerError)
  724. return
  725. }
  726. }
  727. w.Header().Set("Content-Type", "text/html; charset=utf-8")
  728. w.Write(cachedIndex)
  729. } else {
  730. http.Error(w, "Not found", 404)
  731. return
  732. }
  733. }
  734. func locationsHandler(db *sql.DB, w http.ResponseWriter, r *http.Request) {
  735. cacheMut.Lock()
  736. defer cacheMut.Unlock()
  737. if time.Since(cacheTime) > maxCacheTime {
  738. if err := refreshCacheLocked(db); err != nil {
  739. log.Println(err)
  740. http.Error(w, "Template Error", http.StatusInternalServerError)
  741. return
  742. }
  743. }
  744. w.Header().Set("Content-Type", "application/json; charset=utf-8")
  745. w.Write(cachedLocations)
  746. }
  747. func newDataHandler(db *sql.DB, w http.ResponseWriter, r *http.Request) {
  748. defer r.Body.Close()
  749. addr := r.Header.Get("X-Forwarded-For")
  750. if addr != "" {
  751. addr = strings.Split(addr, ", ")[0]
  752. } else {
  753. addr = r.RemoteAddr
  754. }
  755. if host, _, err := net.SplitHostPort(addr); err == nil {
  756. addr = host
  757. }
  758. if net.ParseIP(addr) == nil {
  759. addr = ""
  760. }
  761. var rep report
  762. rep.Date = time.Now().UTC().Format("20060102")
  763. rep.Address = addr
  764. lr := &io.LimitedReader{R: r.Body, N: 40 * 1024}
  765. bs, _ := ioutil.ReadAll(lr)
  766. if err := json.Unmarshal(bs, &rep); err != nil {
  767. log.Println("decode:", err)
  768. if debug {
  769. log.Printf("%s", bs)
  770. }
  771. http.Error(w, "JSON Decode Error", http.StatusInternalServerError)
  772. return
  773. }
  774. if err := rep.Validate(); err != nil {
  775. log.Println("validate:", err)
  776. if debug {
  777. log.Printf("%#v", rep)
  778. }
  779. http.Error(w, "Validation Error", http.StatusInternalServerError)
  780. return
  781. }
  782. if err := insertReport(db, rep); err != nil {
  783. if err.Error() == `pq: duplicate key value violates unique constraint "uniqueidindex"` {
  784. // We already have a report today for the same unique ID; drop
  785. // this one without complaining.
  786. return
  787. }
  788. log.Println("insert:", err)
  789. if debug {
  790. log.Printf("%#v", rep)
  791. }
  792. http.Error(w, "Database Error", http.StatusInternalServerError)
  793. return
  794. }
  795. }
  796. func summaryHandler(db *sql.DB, w http.ResponseWriter, r *http.Request) {
  797. s, err := getSummary(db)
  798. if err != nil {
  799. log.Println("summaryHandler:", err)
  800. http.Error(w, "Database Error", http.StatusInternalServerError)
  801. return
  802. }
  803. bs, err := s.MarshalJSON()
  804. if err != nil {
  805. log.Println("summaryHandler:", err)
  806. http.Error(w, "JSON Encode Error", http.StatusInternalServerError)
  807. return
  808. }
  809. w.Header().Set("Content-Type", "application/json")
  810. w.Write(bs)
  811. }
  812. func movementHandler(db *sql.DB, w http.ResponseWriter, r *http.Request) {
  813. s, err := getMovement(db)
  814. if err != nil {
  815. log.Println("movementHandler:", err)
  816. http.Error(w, "Database Error", http.StatusInternalServerError)
  817. return
  818. }
  819. bs, err := json.Marshal(s)
  820. if err != nil {
  821. log.Println("movementHandler:", err)
  822. http.Error(w, "JSON Encode Error", http.StatusInternalServerError)
  823. return
  824. }
  825. w.Header().Set("Content-Type", "application/json")
  826. w.Write(bs)
  827. }
  828. func performanceHandler(db *sql.DB, w http.ResponseWriter, r *http.Request) {
  829. s, err := getPerformance(db)
  830. if err != nil {
  831. log.Println("performanceHandler:", err)
  832. http.Error(w, "Database Error", http.StatusInternalServerError)
  833. return
  834. }
  835. bs, err := json.Marshal(s)
  836. if err != nil {
  837. log.Println("performanceHandler:", err)
  838. http.Error(w, "JSON Encode Error", http.StatusInternalServerError)
  839. return
  840. }
  841. w.Header().Set("Content-Type", "application/json")
  842. w.Write(bs)
  843. }
  844. func blockStatsHandler(db *sql.DB, w http.ResponseWriter, r *http.Request) {
  845. s, err := getBlockStats(db)
  846. if err != nil {
  847. log.Println("blockStatsHandler:", err)
  848. http.Error(w, "Database Error", http.StatusInternalServerError)
  849. return
  850. }
  851. bs, err := json.Marshal(s)
  852. if err != nil {
  853. log.Println("blockStatsHandler:", err)
  854. http.Error(w, "JSON Encode Error", http.StatusInternalServerError)
  855. return
  856. }
  857. w.Header().Set("Content-Type", "application/json")
  858. w.Write(bs)
  859. }
  860. type category struct {
  861. Values [4]float64
  862. Key string
  863. Descr string
  864. Unit string
  865. Type NumberType
  866. }
  867. type feature struct {
  868. Key string
  869. Version string
  870. Count int
  871. Pct float64
  872. }
  873. type featureGroup struct {
  874. Key string
  875. Version string
  876. Counts map[string]int
  877. }
  878. // Used in the templates
  879. type counter struct {
  880. n int
  881. }
  882. func (c *counter) Current() int {
  883. return c.n
  884. }
  885. func (c *counter) Increment() string {
  886. c.n++
  887. return ""
  888. }
  889. func (c *counter) DrawTwoDivider() bool {
  890. return c.n != 0 && c.n%2 == 0
  891. }
  892. // add sets a key in a nested map, initializing things if needed as we go.
  893. func add(storage map[string]map[string]int, parent, child string, value int) {
  894. n, ok := storage[parent]
  895. if !ok {
  896. n = make(map[string]int)
  897. storage[parent] = n
  898. }
  899. n[child] += value
  900. }
  901. // inc makes sure that even for unused features, we initialize them in the
  902. // feature map. Furthermore, this acts as a helper that accepts booleans
  903. // to increment by one, or integers to increment by that integer.
  904. func inc(storage map[string]int, key string, i interface{}) {
  905. cv := storage[key]
  906. switch v := i.(type) {
  907. case bool:
  908. if v {
  909. cv++
  910. }
  911. case int:
  912. cv += v
  913. }
  914. storage[key] = cv
  915. }
  916. type location struct {
  917. Latitude float64 `json:"lat"`
  918. Longitude float64 `json:"lon"`
  919. }
  920. type weightedLocation struct {
  921. location
  922. Weight int `json:"weight"`
  923. }
  924. func getReport(db *sql.DB) map[string]interface{} {
  925. geoip, err := geoip2.Open(geoIPPath)
  926. if err != nil {
  927. log.Println("opening geoip db", err)
  928. geoip = nil
  929. } else {
  930. defer geoip.Close()
  931. }
  932. nodes := 0
  933. countriesTotal := 0
  934. var versions []string
  935. var platforms []string
  936. var numFolders []int
  937. var numDevices []int
  938. var totFiles []int
  939. var maxFiles []int
  940. var totMiB []int
  941. var maxMiB []int
  942. var memoryUsage []int
  943. var sha256Perf []float64
  944. var memorySize []int
  945. var uptime []int
  946. var compilers []string
  947. var builders []string
  948. var distributions []string
  949. locations := make(map[location]int)
  950. countries := make(map[string]int)
  951. reports := make(map[string]int)
  952. totals := make(map[string]int)
  953. // category -> version -> feature -> count
  954. features := make(map[string]map[string]map[string]int)
  955. // category -> version -> feature -> group -> count
  956. featureGroups := make(map[string]map[string]map[string]map[string]int)
  957. for _, category := range featureOrder {
  958. features[category] = make(map[string]map[string]int)
  959. featureGroups[category] = make(map[string]map[string]map[string]int)
  960. for _, version := range knownVersions {
  961. features[category][version] = make(map[string]int)
  962. featureGroups[category][version] = make(map[string]map[string]int)
  963. }
  964. }
  965. // Initialize some features that hide behind if conditions, and might not
  966. // be initialized.
  967. add(featureGroups["Various"]["v2"], "Upgrades", "Pre-release", 0)
  968. add(featureGroups["Various"]["v2"], "Upgrades", "Automatic", 0)
  969. add(featureGroups["Various"]["v2"], "Upgrades", "Manual", 0)
  970. add(featureGroups["Various"]["v2"], "Upgrades", "Disabled", 0)
  971. add(featureGroups["Various"]["v3"], "Temporary Retention", "Disabled", 0)
  972. add(featureGroups["Various"]["v3"], "Temporary Retention", "Custom", 0)
  973. add(featureGroups["Various"]["v3"], "Temporary Retention", "Default", 0)
  974. add(featureGroups["Connection"]["v3"], "IP version", "IPv4", 0)
  975. add(featureGroups["Connection"]["v3"], "IP version", "IPv6", 0)
  976. add(featureGroups["Connection"]["v3"], "IP version", "Unknown", 0)
  977. var numCPU []int
  978. var rep report
  979. rows, err := db.Query(`SELECT ` + strings.Join(rep.FieldNames(), ",") + ` FROM Reports WHERE Received > now() - '1 day'::INTERVAL`)
  980. if err != nil {
  981. log.Println("sql:", err)
  982. return nil
  983. }
  984. defer rows.Close()
  985. for rows.Next() {
  986. err := rows.Scan(rep.FieldPointers()...)
  987. if err != nil {
  988. log.Println("sql:", err)
  989. return nil
  990. }
  991. if geoip != nil && rep.Address != "" {
  992. if addr, err := net.ResolveTCPAddr("tcp", net.JoinHostPort(rep.Address, "0")); err == nil {
  993. city, err := geoip.City(addr.IP)
  994. if err == nil {
  995. loc := location{
  996. Latitude: city.Location.Latitude,
  997. Longitude: city.Location.Longitude,
  998. }
  999. locations[loc]++
  1000. countries[city.Country.Names["en"]]++
  1001. countriesTotal++
  1002. }
  1003. }
  1004. }
  1005. nodes++
  1006. versions = append(versions, transformVersion(rep.Version))
  1007. platforms = append(platforms, rep.Platform)
  1008. if m := compilerRe.FindStringSubmatch(rep.LongVersion); len(m) == 3 {
  1009. compilers = append(compilers, m[1])
  1010. builders = append(builders, m[2])
  1011. loop:
  1012. for _, d := range knownDistributions {
  1013. if d.matcher.MatchString(rep.LongVersion) {
  1014. distributions = append(distributions, d.distribution)
  1015. break loop
  1016. }
  1017. }
  1018. }
  1019. if rep.NumFolders > 0 {
  1020. numFolders = append(numFolders, rep.NumFolders)
  1021. }
  1022. if rep.NumDevices > 0 {
  1023. numDevices = append(numDevices, rep.NumDevices)
  1024. }
  1025. if rep.TotFiles > 0 {
  1026. totFiles = append(totFiles, rep.TotFiles)
  1027. }
  1028. if rep.FolderMaxFiles > 0 {
  1029. maxFiles = append(maxFiles, rep.FolderMaxFiles)
  1030. }
  1031. if rep.TotMiB > 0 {
  1032. totMiB = append(totMiB, rep.TotMiB*(1<<20))
  1033. }
  1034. if rep.FolderMaxMiB > 0 {
  1035. maxMiB = append(maxMiB, rep.FolderMaxMiB*(1<<20))
  1036. }
  1037. if rep.MemoryUsageMiB > 0 {
  1038. memoryUsage = append(memoryUsage, rep.MemoryUsageMiB*(1<<20))
  1039. }
  1040. if rep.SHA256Perf > 0 {
  1041. sha256Perf = append(sha256Perf, rep.SHA256Perf*(1<<20))
  1042. }
  1043. if rep.MemorySize > 0 {
  1044. memorySize = append(memorySize, rep.MemorySize*(1<<20))
  1045. }
  1046. if rep.Uptime > 0 {
  1047. uptime = append(uptime, rep.Uptime)
  1048. }
  1049. totals["Device"] += rep.NumDevices
  1050. totals["Folder"] += rep.NumFolders
  1051. if rep.URVersion >= 2 {
  1052. reports["v2"]++
  1053. numCPU = append(numCPU, rep.NumCPU)
  1054. // Various
  1055. inc(features["Various"]["v2"], "Rate limiting", rep.UsesRateLimit)
  1056. if rep.UpgradeAllowedPre {
  1057. add(featureGroups["Various"]["v2"], "Upgrades", "Pre-release", 1)
  1058. } else if rep.UpgradeAllowedAuto {
  1059. add(featureGroups["Various"]["v2"], "Upgrades", "Automatic", 1)
  1060. } else if rep.UpgradeAllowedManual {
  1061. add(featureGroups["Various"]["v2"], "Upgrades", "Manual", 1)
  1062. } else {
  1063. add(featureGroups["Various"]["v2"], "Upgrades", "Disabled", 1)
  1064. }
  1065. // Folders
  1066. inc(features["Folder"]["v2"], "Automatic normalization", rep.FolderUses.AutoNormalize)
  1067. inc(features["Folder"]["v2"], "Ignore deletes", rep.FolderUses.IgnoreDelete)
  1068. inc(features["Folder"]["v2"], "Ignore permissions", rep.FolderUses.IgnorePerms)
  1069. inc(features["Folder"]["v2"], "Mode, send only", rep.FolderUses.SendOnly)
  1070. inc(features["Folder"]["v2"], "Mode, receive only", rep.FolderUses.ReceiveOnly)
  1071. add(featureGroups["Folder"]["v2"], "Versioning", "Simple", rep.FolderUses.SimpleVersioning)
  1072. add(featureGroups["Folder"]["v2"], "Versioning", "External", rep.FolderUses.ExternalVersioning)
  1073. add(featureGroups["Folder"]["v2"], "Versioning", "Staggered", rep.FolderUses.StaggeredVersioning)
  1074. add(featureGroups["Folder"]["v2"], "Versioning", "Trashcan", rep.FolderUses.TrashcanVersioning)
  1075. add(featureGroups["Folder"]["v2"], "Versioning", "Disabled", rep.NumFolders-rep.FolderUses.SimpleVersioning-rep.FolderUses.ExternalVersioning-rep.FolderUses.StaggeredVersioning-rep.FolderUses.TrashcanVersioning)
  1076. // Device
  1077. inc(features["Device"]["v2"], "Custom certificate", rep.DeviceUses.CustomCertName)
  1078. inc(features["Device"]["v2"], "Introducer", rep.DeviceUses.Introducer)
  1079. add(featureGroups["Device"]["v2"], "Compress", "Always", rep.DeviceUses.CompressAlways)
  1080. add(featureGroups["Device"]["v2"], "Compress", "Metadata", rep.DeviceUses.CompressMetadata)
  1081. add(featureGroups["Device"]["v2"], "Compress", "Nothing", rep.DeviceUses.CompressNever)
  1082. add(featureGroups["Device"]["v2"], "Addresses", "Dynamic", rep.DeviceUses.DynamicAddr)
  1083. add(featureGroups["Device"]["v2"], "Addresses", "Static", rep.DeviceUses.StaticAddr)
  1084. // Connections
  1085. inc(features["Connection"]["v2"], "Relaying, enabled", rep.Relays.Enabled)
  1086. inc(features["Connection"]["v2"], "Discovery, global enabled", rep.Announce.GlobalEnabled)
  1087. inc(features["Connection"]["v2"], "Discovery, local enabled", rep.Announce.LocalEnabled)
  1088. add(featureGroups["Connection"]["v2"], "Discovery", "Default servers (using DNS)", rep.Announce.DefaultServersDNS)
  1089. add(featureGroups["Connection"]["v2"], "Discovery", "Default servers (using IP)", rep.Announce.DefaultServersIP)
  1090. add(featureGroups["Connection"]["v2"], "Discovery", "Other servers", rep.Announce.DefaultServersIP)
  1091. add(featureGroups["Connection"]["v2"], "Relaying", "Default relays", rep.Relays.DefaultServers)
  1092. add(featureGroups["Connection"]["v2"], "Relaying", "Other relays", rep.Relays.OtherServers)
  1093. }
  1094. if rep.URVersion >= 3 {
  1095. reports["v3"]++
  1096. inc(features["Various"]["v3"], "Custom LAN classification", rep.AlwaysLocalNets)
  1097. inc(features["Various"]["v3"], "Ignore caching", rep.CacheIgnoredFiles)
  1098. inc(features["Various"]["v3"], "Overwrite device names", rep.OverwriteRemoteDeviceNames)
  1099. inc(features["Various"]["v3"], "Download progress disabled", !rep.ProgressEmitterEnabled)
  1100. inc(features["Various"]["v3"], "Custom default path", rep.CustomDefaultFolderPath)
  1101. inc(features["Various"]["v3"], "Custom traffic class", rep.CustomTrafficClass)
  1102. inc(features["Various"]["v3"], "Custom temporary index threshold", rep.CustomTempIndexMinBlocks)
  1103. inc(features["Various"]["v3"], "Weak hash enabled", rep.WeakHashEnabled)
  1104. inc(features["Various"]["v3"], "LAN rate limiting", rep.LimitBandwidthInLan)
  1105. inc(features["Various"]["v3"], "Custom release server", rep.CustomReleaseURL)
  1106. inc(features["Various"]["v3"], "Restart after suspend", rep.RestartOnWakeup)
  1107. inc(features["Various"]["v3"], "Custom stun servers", rep.CustomStunServers)
  1108. inc(features["Various"]["v3"], "Ignore patterns", rep.IgnoreStats.Lines > 0)
  1109. if rep.NATType != "" {
  1110. natType := rep.NATType
  1111. natType = strings.Replace(natType, "unknown", "Unknown", -1)
  1112. natType = strings.Replace(natType, "Symetric", "Symmetric", -1)
  1113. add(featureGroups["Various"]["v3"], "NAT Type", natType, 1)
  1114. }
  1115. if rep.TemporariesDisabled {
  1116. add(featureGroups["Various"]["v3"], "Temporary Retention", "Disabled", 1)
  1117. } else if rep.TemporariesCustom {
  1118. add(featureGroups["Various"]["v3"], "Temporary Retention", "Custom", 1)
  1119. } else {
  1120. add(featureGroups["Various"]["v3"], "Temporary Retention", "Default", 1)
  1121. }
  1122. inc(features["Folder"]["v3"], "Scan progress disabled", rep.FolderUsesV3.ScanProgressDisabled)
  1123. inc(features["Folder"]["v3"], "Disable sharing of partial files", rep.FolderUsesV3.DisableTempIndexes)
  1124. inc(features["Folder"]["v3"], "Disable sparse files", rep.FolderUsesV3.DisableSparseFiles)
  1125. inc(features["Folder"]["v3"], "Weak hash, always", rep.FolderUsesV3.AlwaysWeakHash)
  1126. inc(features["Folder"]["v3"], "Weak hash, custom threshold", rep.FolderUsesV3.CustomWeakHashThreshold)
  1127. inc(features["Folder"]["v3"], "Filesystem watcher", rep.FolderUsesV3.FsWatcherEnabled)
  1128. add(featureGroups["Folder"]["v3"], "Conflicts", "Disabled", rep.FolderUsesV3.ConflictsDisabled)
  1129. add(featureGroups["Folder"]["v3"], "Conflicts", "Unlimited", rep.FolderUsesV3.ConflictsUnlimited)
  1130. add(featureGroups["Folder"]["v3"], "Conflicts", "Limited", rep.FolderUsesV3.ConflictsOther)
  1131. for key, value := range rep.FolderUsesV3.PullOrder {
  1132. add(featureGroups["Folder"]["v3"], "Pull Order", prettyCase(key), value)
  1133. }
  1134. totals["GUI"] += rep.GUIStats.Enabled
  1135. inc(features["GUI"]["v3"], "Auth Enabled", rep.GUIStats.UseAuth)
  1136. inc(features["GUI"]["v3"], "TLS Enabled", rep.GUIStats.UseTLS)
  1137. inc(features["GUI"]["v3"], "Insecure Admin Access", rep.GUIStats.InsecureAdminAccess)
  1138. inc(features["GUI"]["v3"], "Skip Host check", rep.GUIStats.InsecureSkipHostCheck)
  1139. inc(features["GUI"]["v3"], "Allow Frame loading", rep.GUIStats.InsecureAllowFrameLoading)
  1140. add(featureGroups["GUI"]["v3"], "Listen address", "Local", rep.GUIStats.ListenLocal)
  1141. add(featureGroups["GUI"]["v3"], "Listen address", "Unspecified", rep.GUIStats.ListenUnspecified)
  1142. add(featureGroups["GUI"]["v3"], "Listen address", "Other", rep.GUIStats.Enabled-rep.GUIStats.ListenLocal-rep.GUIStats.ListenUnspecified)
  1143. for theme, count := range rep.GUIStats.Theme {
  1144. add(featureGroups["GUI"]["v3"], "Theme", prettyCase(theme), count)
  1145. }
  1146. for transport, count := range rep.TransportStats {
  1147. add(featureGroups["Connection"]["v3"], "Transport", strings.Title(transport), count)
  1148. if strings.HasSuffix(transport, "4") {
  1149. add(featureGroups["Connection"]["v3"], "IP version", "IPv4", count)
  1150. } else if strings.HasSuffix(transport, "6") {
  1151. add(featureGroups["Connection"]["v3"], "IP version", "IPv6", count)
  1152. } else {
  1153. add(featureGroups["Connection"]["v3"], "IP version", "Unknown", count)
  1154. }
  1155. }
  1156. }
  1157. }
  1158. var categories []category
  1159. categories = append(categories, category{
  1160. Values: statsForInts(totFiles),
  1161. Descr: "Files Managed per Device",
  1162. })
  1163. categories = append(categories, category{
  1164. Values: statsForInts(maxFiles),
  1165. Descr: "Files in Largest Folder",
  1166. })
  1167. categories = append(categories, category{
  1168. Values: statsForInts(totMiB),
  1169. Descr: "Data Managed per Device",
  1170. Unit: "B",
  1171. Type: NumberBinary,
  1172. })
  1173. categories = append(categories, category{
  1174. Values: statsForInts(maxMiB),
  1175. Descr: "Data in Largest Folder",
  1176. Unit: "B",
  1177. Type: NumberBinary,
  1178. })
  1179. categories = append(categories, category{
  1180. Values: statsForInts(numDevices),
  1181. Descr: "Number of Devices in Cluster",
  1182. })
  1183. categories = append(categories, category{
  1184. Values: statsForInts(numFolders),
  1185. Descr: "Number of Folders Configured",
  1186. })
  1187. categories = append(categories, category{
  1188. Values: statsForInts(memoryUsage),
  1189. Descr: "Memory Usage",
  1190. Unit: "B",
  1191. Type: NumberBinary,
  1192. })
  1193. categories = append(categories, category{
  1194. Values: statsForInts(memorySize),
  1195. Descr: "System Memory",
  1196. Unit: "B",
  1197. Type: NumberBinary,
  1198. })
  1199. categories = append(categories, category{
  1200. Values: statsForFloats(sha256Perf),
  1201. Descr: "SHA-256 Hashing Performance",
  1202. Unit: "B/s",
  1203. Type: NumberBinary,
  1204. })
  1205. categories = append(categories, category{
  1206. Values: statsForInts(numCPU),
  1207. Descr: "Number of CPU cores",
  1208. })
  1209. categories = append(categories, category{
  1210. Values: statsForInts(uptime),
  1211. Descr: "Uptime (v3)",
  1212. Type: NumberDuration,
  1213. })
  1214. reportFeatures := make(map[string][]feature)
  1215. for featureType, versions := range features {
  1216. var featureList []feature
  1217. for version, featureMap := range versions {
  1218. // We count totals of the given feature type, for example number of
  1219. // folders or devices, if that doesn't exist, we work out percentage
  1220. // against the total of the version reports. Things like "Various"
  1221. // never have counts.
  1222. total, ok := totals[featureType]
  1223. if !ok {
  1224. total = reports[version]
  1225. }
  1226. for key, count := range featureMap {
  1227. featureList = append(featureList, feature{
  1228. Key: key,
  1229. Version: version,
  1230. Count: count,
  1231. Pct: (100 * float64(count)) / float64(total),
  1232. })
  1233. }
  1234. }
  1235. sort.Sort(sort.Reverse(sortableFeatureList(featureList)))
  1236. reportFeatures[featureType] = featureList
  1237. }
  1238. reportFeatureGroups := make(map[string][]featureGroup)
  1239. for featureType, versions := range featureGroups {
  1240. var featureList []featureGroup
  1241. for version, featureMap := range versions {
  1242. for key, counts := range featureMap {
  1243. featureList = append(featureList, featureGroup{
  1244. Key: key,
  1245. Version: version,
  1246. Counts: counts,
  1247. })
  1248. }
  1249. }
  1250. reportFeatureGroups[featureType] = featureList
  1251. }
  1252. var countryList []feature
  1253. for country, count := range countries {
  1254. countryList = append(countryList, feature{
  1255. Key: country,
  1256. Count: count,
  1257. Pct: (100 * float64(count)) / float64(countriesTotal),
  1258. })
  1259. sort.Sort(sort.Reverse(sortableFeatureList(countryList)))
  1260. }
  1261. r := make(map[string]interface{})
  1262. r["features"] = reportFeatures
  1263. r["featureGroups"] = reportFeatureGroups
  1264. r["nodes"] = nodes
  1265. r["versionNodes"] = reports
  1266. r["categories"] = categories
  1267. r["versions"] = group(byVersion, analyticsFor(versions, 2000), 10)
  1268. r["versionPenetrations"] = penetrationLevels(analyticsFor(versions, 2000), []float64{50, 75, 90, 95})
  1269. r["platforms"] = group(byPlatform, analyticsFor(platforms, 2000), 10)
  1270. r["compilers"] = group(byCompiler, analyticsFor(compilers, 2000), 5)
  1271. r["builders"] = analyticsFor(builders, 12)
  1272. r["distributions"] = analyticsFor(distributions, len(knownDistributions))
  1273. r["featureOrder"] = featureOrder
  1274. r["locations"] = locations
  1275. r["contries"] = countryList
  1276. return r
  1277. }
  1278. var (
  1279. plusRe = regexp.MustCompile(`\+.*$`)
  1280. plusStr = "(+dev)"
  1281. )
  1282. // transformVersion returns a version number formatted correctly, with all
  1283. // development versions aggregated into one.
  1284. func transformVersion(v string) string {
  1285. if v == "unknown-dev" {
  1286. return v
  1287. }
  1288. if !strings.HasPrefix(v, "v") {
  1289. v = "v" + v
  1290. }
  1291. v = plusRe.ReplaceAllString(v, " "+plusStr)
  1292. return v
  1293. }
  1294. type summary struct {
  1295. versions map[string]int // version string to count index
  1296. max map[string]int // version string to max users per day
  1297. rows map[string][]int // date to list of counts
  1298. }
  1299. func newSummary() summary {
  1300. return summary{
  1301. versions: make(map[string]int),
  1302. max: make(map[string]int),
  1303. rows: make(map[string][]int),
  1304. }
  1305. }
  1306. func (s *summary) setCount(date, version string, count int) {
  1307. idx, ok := s.versions[version]
  1308. if !ok {
  1309. idx = len(s.versions)
  1310. s.versions[version] = idx
  1311. }
  1312. if s.max[version] < count {
  1313. s.max[version] = count
  1314. }
  1315. row := s.rows[date]
  1316. if len(row) <= idx {
  1317. old := row
  1318. row = make([]int, idx+1)
  1319. copy(row, old)
  1320. s.rows[date] = row
  1321. }
  1322. row[idx] = count
  1323. }
  1324. func (s *summary) MarshalJSON() ([]byte, error) {
  1325. var versions []string
  1326. for v := range s.versions {
  1327. versions = append(versions, v)
  1328. }
  1329. sort.Strings(versions)
  1330. var filtered []string
  1331. for _, v := range versions {
  1332. if s.max[v] > 50 {
  1333. filtered = append(filtered, v)
  1334. }
  1335. }
  1336. versions = filtered
  1337. headerRow := []interface{}{"Day"}
  1338. for _, v := range versions {
  1339. headerRow = append(headerRow, v)
  1340. }
  1341. var table [][]interface{}
  1342. table = append(table, headerRow)
  1343. var dates []string
  1344. for k := range s.rows {
  1345. dates = append(dates, k)
  1346. }
  1347. sort.Strings(dates)
  1348. for _, date := range dates {
  1349. row := []interface{}{date}
  1350. for _, ver := range versions {
  1351. idx := s.versions[ver]
  1352. if len(s.rows[date]) > idx && s.rows[date][idx] > 0 {
  1353. row = append(row, s.rows[date][idx])
  1354. } else {
  1355. row = append(row, nil)
  1356. }
  1357. }
  1358. table = append(table, row)
  1359. }
  1360. return json.Marshal(table)
  1361. }
  1362. func getSummary(db *sql.DB) (summary, error) {
  1363. s := newSummary()
  1364. rows, err := db.Query(`SELECT Day, Version, Count FROM VersionSummary WHERE Day > now() - '2 year'::INTERVAL;`)
  1365. if err != nil {
  1366. return summary{}, err
  1367. }
  1368. defer rows.Close()
  1369. for rows.Next() {
  1370. var day time.Time
  1371. var ver string
  1372. var num int
  1373. err := rows.Scan(&day, &ver, &num)
  1374. if err != nil {
  1375. return summary{}, err
  1376. }
  1377. if ver == "v0.0" {
  1378. // ?
  1379. continue
  1380. }
  1381. // SUPER UGLY HACK to avoid having to do sorting properly
  1382. if len(ver) == 4 && strings.HasPrefix(ver, "v0.") { // v0.x
  1383. ver = ver[:3] + "0" + ver[3:] // now v0.0x
  1384. }
  1385. s.setCount(day.Format("2006-01-02"), ver, num)
  1386. }
  1387. return s, nil
  1388. }
  1389. func getMovement(db *sql.DB) ([][]interface{}, error) {
  1390. rows, err := db.Query(`SELECT Day, Added, Removed, Bounced FROM UserMovement WHERE Day > now() - '2 year'::INTERVAL ORDER BY Day`)
  1391. if err != nil {
  1392. return nil, err
  1393. }
  1394. defer rows.Close()
  1395. res := [][]interface{}{
  1396. {"Day", "Joined", "Left", "Bounced"},
  1397. }
  1398. for rows.Next() {
  1399. var day time.Time
  1400. var added, removed, bounced int
  1401. err := rows.Scan(&day, &added, &removed, &bounced)
  1402. if err != nil {
  1403. return nil, err
  1404. }
  1405. row := []interface{}{day.Format("2006-01-02"), added, -removed, bounced}
  1406. if removed == 0 {
  1407. row[2] = nil
  1408. }
  1409. if bounced == 0 {
  1410. row[3] = nil
  1411. }
  1412. res = append(res, row)
  1413. }
  1414. return res, nil
  1415. }
  1416. func getPerformance(db *sql.DB) ([][]interface{}, error) {
  1417. rows, err := db.Query(`SELECT Day, TotFiles, TotMiB, SHA256Perf, MemorySize, MemoryUsageMiB FROM Performance WHERE Day > '2014-06-20'::TIMESTAMP ORDER BY Day`)
  1418. if err != nil {
  1419. return nil, err
  1420. }
  1421. defer rows.Close()
  1422. res := [][]interface{}{
  1423. {"Day", "TotFiles", "TotMiB", "SHA256Perf", "MemorySize", "MemoryUsageMiB"},
  1424. }
  1425. for rows.Next() {
  1426. var day time.Time
  1427. var sha256Perf float64
  1428. var totFiles, totMiB, memorySize, memoryUsage int
  1429. err := rows.Scan(&day, &totFiles, &totMiB, &sha256Perf, &memorySize, &memoryUsage)
  1430. if err != nil {
  1431. return nil, err
  1432. }
  1433. row := []interface{}{day.Format("2006-01-02"), totFiles, totMiB, float64(int(sha256Perf*10)) / 10, memorySize, memoryUsage}
  1434. res = append(res, row)
  1435. }
  1436. return res, nil
  1437. }
  1438. func getBlockStats(db *sql.DB) ([][]interface{}, error) {
  1439. rows, err := db.Query(`SELECT Day, Reports, Pulled, Renamed, Reused, CopyOrigin, CopyOriginShifted, CopyElsewhere FROM BlockStats WHERE Day > '2017-10-23'::TIMESTAMP ORDER BY Day`)
  1440. if err != nil {
  1441. return nil, err
  1442. }
  1443. defer rows.Close()
  1444. res := [][]interface{}{
  1445. {"Day", "Number of Reports", "Transferred (GiB)", "Saved by renaming files (GiB)", "Saved by resuming transfer (GiB)", "Saved by reusing data from old file (GiB)", "Saved by reusing shifted data from old file (GiB)", "Saved by reusing data from other files (GiB)"},
  1446. }
  1447. blocksToGb := float64(8 * 1024)
  1448. for rows.Next() {
  1449. var day time.Time
  1450. var reports, pulled, renamed, reused, copyOrigin, copyOriginShifted, copyElsewhere float64
  1451. err := rows.Scan(&day, &reports, &pulled, &renamed, &reused, &copyOrigin, &copyOriginShifted, &copyElsewhere)
  1452. if err != nil {
  1453. return nil, err
  1454. }
  1455. row := []interface{}{
  1456. day.Format("2006-01-02"),
  1457. reports,
  1458. pulled / blocksToGb,
  1459. renamed / blocksToGb,
  1460. reused / blocksToGb,
  1461. copyOrigin / blocksToGb,
  1462. copyOriginShifted / blocksToGb,
  1463. copyElsewhere / blocksToGb,
  1464. }
  1465. res = append(res, row)
  1466. }
  1467. return res, nil
  1468. }
  1469. type sortableFeatureList []feature
  1470. func (l sortableFeatureList) Len() int {
  1471. return len(l)
  1472. }
  1473. func (l sortableFeatureList) Swap(a, b int) {
  1474. l[a], l[b] = l[b], l[a]
  1475. }
  1476. func (l sortableFeatureList) Less(a, b int) bool {
  1477. if l[a].Pct != l[b].Pct {
  1478. return l[a].Pct < l[b].Pct
  1479. }
  1480. return l[a].Key > l[b].Key
  1481. }
  1482. func prettyCase(input string) string {
  1483. output := ""
  1484. for i, runeValue := range input {
  1485. if i == 0 {
  1486. runeValue = unicode.ToUpper(runeValue)
  1487. } else if unicode.IsUpper(runeValue) {
  1488. output += " "
  1489. }
  1490. output += string(runeValue)
  1491. }
  1492. return output
  1493. }