2014-06-11 23:40:54 +00:00
package main
import (
2015-03-04 12:02:53 +00:00
"bytes"
2014-10-13 09:18:13 +00:00
"crypto/tls"
2015-03-04 10:31:46 +00:00
"database/sql"
2014-06-11 23:40:54 +00:00
"encoding/json"
"fmt"
2014-06-28 07:46:03 +00:00
"html/template"
2014-06-11 23:40:54 +00:00
"io"
2014-06-28 07:46:03 +00:00
"io/ioutil"
2014-06-11 23:40:54 +00:00
"log"
"net/http"
"os"
2014-06-28 07:46:03 +00:00
"regexp"
2015-05-21 06:52:19 +00:00
"sort"
2014-06-28 07:46:03 +00:00
"strings"
2014-06-11 23:40:54 +00:00
"sync"
"time"
2015-03-04 10:31:46 +00:00
_ "github.com/lib/pq"
2014-06-11 23:40:54 +00:00
)
var (
2015-06-15 08:49:48 +00:00
keyFile = getEnvDefault ( "UR_KEY_FILE" , "key.pem" )
certFile = getEnvDefault ( "UR_CRT_FILE" , "crt.pem" )
dbConn = getEnvDefault ( "UR_DB_URL" , "postgres://user:password@localhost/ur?sslmode=disable" )
listenAddr = getEnvDefault ( "UR_LISTEN" , "0.0.0.0:8443" )
tpl * template . Template
compilerRe = regexp . MustCompile ( ` \(([A-Za-z0-9()., -]+) [\w-]+ \w+\) ([\w@-]+) ` )
aggregateVersions = [ ] string { "v0.7" , "v0.8" , "v0.9" , "v0.10" }
2014-06-11 23:40:54 +00:00
)
2014-06-28 07:46:03 +00:00
var funcs = map [ string ] interface { } {
"commatize" : commatize ,
2014-06-28 09:24:25 +00:00
"number" : number ,
2014-06-28 07:46:03 +00:00
}
2015-03-04 10:31:46 +00:00
func getEnvDefault ( key , def string ) string {
if val := os . Getenv ( key ) ; val != "" {
return val
}
return def
}
type report struct {
Received time . Time // Only from DB
UniqueID string
Version string
LongVersion string
Platform string
NumFolders int
NumDevices int
TotFiles int
FolderMaxFiles int
TotMiB int
FolderMaxMiB int
MemoryUsageMiB int
SHA256Perf float64
MemorySize int
2015-09-10 12:03:34 +00:00
// v2 fields
URVersion int
NumCPU int
FolderUses struct {
ReadOnly int
IgnorePerms int
IgnoreDelete int
AutoNormalize int
}
DeviceUses struct {
Introducer int
CustomCertName int
CompressAlways int
CompressMetadata int
CompressNever int
DynamicAddr int
StaticAddr int
}
Announce struct {
GlobalEnabled bool
LocalEnabled bool
DefaultServersDNS int
DefaultServersIP int
OtherServers int
}
Relays struct {
Enabled bool
DefaultServers int
OtherServers int
}
UsesRateLimit bool
UpgradeAllowedManual bool
UpgradeAllowedAuto bool
// Generated
2015-03-04 10:31:46 +00:00
Date string
}
func ( r * report ) Validate ( ) error {
if r . UniqueID == "" || r . Version == "" || r . Platform == "" {
return fmt . Errorf ( "missing required field" )
}
if len ( r . Date ) != 8 {
return fmt . Errorf ( "date not initialized" )
}
return nil
}
2015-09-10 12:03:34 +00:00
func ( r * report ) FieldsInDBOrder ( ) [ ] interface { } {
return [ ] interface { } { r . UniqueID , r . Version , r . LongVersion , r . Platform , r . NumFolders ,
r . NumDevices , r . TotFiles , r . FolderMaxFiles , r . TotMiB , r . FolderMaxMiB ,
r . MemoryUsageMiB , r . SHA256Perf , r . MemorySize , r . Date ,
r . URVersion , r . NumCPU ,
r . FolderUses . ReadOnly , r . FolderUses . IgnorePerms , r . FolderUses . IgnoreDelete , r . FolderUses . AutoNormalize ,
r . DeviceUses . Introducer , r . DeviceUses . CustomCertName , r . DeviceUses . CompressAlways , r . DeviceUses . CompressMetadata , r . DeviceUses . CompressNever ,
r . DeviceUses . DynamicAddr , r . DeviceUses . StaticAddr ,
r . Announce . GlobalEnabled , r . Announce . LocalEnabled , r . Announce . DefaultServersDNS , r . Announce . DefaultServersIP , r . Announce . OtherServers ,
r . Relays . Enabled , r . Relays . DefaultServers , r . Relays . OtherServers ,
r . UsesRateLimit , r . UpgradeAllowedManual , r . UpgradeAllowedAuto }
}
2015-03-04 10:31:46 +00:00
func setupDB ( db * sql . DB ) error {
_ , err := db . Exec ( ` CREATE TABLE IF NOT EXISTS Reports (
Received TIMESTAMP NOT NULL ,
UniqueID VARCHAR ( 32 ) NOT NULL ,
Version VARCHAR ( 32 ) NOT NULL ,
LongVersion VARCHAR ( 256 ) NOT NULL ,
Platform VARCHAR ( 32 ) NOT NULL ,
NumFolders INTEGER NOT NULL ,
NumDevices INTEGER NOT NULL ,
TotFiles INTEGER NOT NULL ,
FolderMaxFiles INTEGER NOT NULL ,
TotMiB INTEGER NOT NULL ,
FolderMaxMiB INTEGER NOT NULL ,
MemoryUsageMiB INTEGER NOT NULL ,
SHA256Perf DOUBLE PRECISION NOT NULL ,
MemorySize INTEGER NOT NULL ,
Date VARCHAR ( 8 ) NOT NULL
) ` )
if err != nil {
return err
}
2015-09-10 12:03:34 +00:00
var t string
2015-03-04 10:31:46 +00:00
row := db . QueryRow ( ` SELECT 'UniqueIDIndex'::regclass ` )
2015-09-10 12:03:34 +00:00
if err := row . Scan ( & t ) ; err != nil {
log . Println ( err )
if _ , err = db . Exec ( ` CREATE UNIQUE INDEX UniqueIDIndex ON Reports (Date, UniqueID) ` ) ; err != nil {
return err
}
2015-03-04 10:31:46 +00:00
}
row = db . QueryRow ( ` SELECT 'ReceivedIndex'::regclass ` )
2015-09-10 12:03:34 +00:00
if err := row . Scan ( & t ) ; err != nil {
if _ , err = db . Exec ( ` CREATE INDEX ReceivedIndex ON Reports (Received) ` ) ; err != nil {
return err
}
2015-03-04 10:31:46 +00:00
}
2015-09-10 12:03:34 +00:00
row = db . QueryRow ( ` SELECT attname FROM pg_attribute WHERE attrelid = (SELECT oid FROM pg_class WHERE relname = 'reports') AND attname = 'reportversion' ` )
if err := row . Scan ( & t ) ; err != nil {
// The ReportVersion column doesn't exist; add the new columns.
_ , err = db . Exec ( ` ALTER TABLE Reports
ADD COLUMN ReportVersion INTEGER NOT NULL DEFAULT 0 ,
ADD COLUMN NumCPU INTEGER NOT NULL DEFAULT 0 ,
ADD COLUMN FolderRO INTEGER NOT NULL DEFAULT 0 ,
ADD COLUMN FolderIgnorePerms INTEGER NOT NULL DEFAULT 0 ,
ADD COLUMN FolderIgnoreDelete INTEGER NOT NULL DEFAULT 0 ,
ADD COLUMN FolderAutoNormalize INTEGER NOT NULL DEFAULT 0 ,
ADD COLUMN DeviceIntroducer INTEGER NOT NULL DEFAULT 0 ,
ADD COLUMN DeviceCustomCertName INTEGER NOT NULL DEFAULT 0 ,
ADD COLUMN DeviceCompressAlways INTEGER NOT NULL DEFAULT 0 ,
ADD COLUMN DeviceCompressMetadata INTEGER NOT NULL DEFAULT 0 ,
ADD COLUMN DeviceCompressNever INTEGER NOT NULL DEFAULT 0 ,
ADD COLUMN DeviceDynamicAddr INTEGER NOT NULL DEFAULT 0 ,
ADD COLUMN DeviceStaticAddr INTEGER NOT NULL DEFAULT 0 ,
ADD COLUMN AnnounceGlobalEnabled BOOLEAN NOT NULL DEFAULT FALSE ,
ADD COLUMN AnnounceLocalEnabled BOOLEAN NOT NULL DEFAULT FALSE ,
ADD COLUMN AnnounceDefaultServersDNS INTEGER NOT NULL DEFAULT 0 ,
ADD COLUMN AnnounceDefaultServersIP INTEGER NOT NULL DEFAULT 0 ,
ADD COLUMN AnnounceOtherServers INTEGER NOT NULL DEFAULT 0 ,
ADD COLUMN RelayEnabled BOOLEAN NOT NULL DEFAULT FALSE ,
ADD COLUMN RelayDefaultServers INTEGER NOT NULL DEFAULT 0 ,
ADD COLUMN RelayOtherServers INTEGER NOT NULL DEFAULT 0 ,
ADD COLUMN RateLimitEnabled BOOLEAN NOT NULL DEFAULT FALSE ,
ADD COLUMN UpgradeAllowedManual BOOLEAN NOT NULL DEFAULT FALSE ,
ADD COLUMN UpgradeAllowedAuto BOOLEAN NOT NULL DEFAULT FALSE
` )
if err != nil {
return err
}
}
row = db . QueryRow ( ` SELECT 'ReportVersionIndex'::regclass ` )
if err := row . Scan ( & t ) ; err != nil {
if _ , err = db . Exec ( ` CREATE INDEX ReportVersionIndex ON Reports (ReportVersion) ` ) ; err != nil {
return err
}
}
return nil
2015-03-04 10:31:46 +00:00
}
func insertReport ( db * sql . DB , r report ) error {
2015-09-10 12:03:34 +00:00
fields := r . FieldsInDBOrder ( )
params := make ( [ ] string , len ( fields ) )
for i := range params {
params [ i ] = fmt . Sprintf ( "$%d" , i + 1 )
}
query := "INSERT INTO Reports VALUES (TIMEZONE('UTC', NOW()), " + strings . Join ( params , ", " ) + ")"
_ , err := db . Exec ( query , fields ... )
2015-03-04 10:31:46 +00:00
return err
}
type withDBFunc func ( * sql . DB , http . ResponseWriter , * http . Request )
func withDB ( db * sql . DB , f withDBFunc ) http . HandlerFunc {
return http . HandlerFunc ( func ( w http . ResponseWriter , r * http . Request ) {
f ( db , w , r )
} )
}
2014-06-11 23:40:54 +00:00
func main ( ) {
2015-03-04 10:31:46 +00:00
log . SetFlags ( log . Ltime | log . Ldate )
2014-12-07 14:48:48 +00:00
log . SetOutput ( os . Stdout )
2015-03-04 10:31:46 +00:00
// Template
2014-06-11 23:40:54 +00:00
2014-06-28 07:46:03 +00:00
fd , err := os . Open ( "static/index.html" )
if err != nil {
2015-03-04 10:31:46 +00:00
log . Fatalln ( "template:" , err )
2014-06-28 07:46:03 +00:00
}
bs , err := ioutil . ReadAll ( fd )
if err != nil {
2015-03-04 10:31:46 +00:00
log . Fatalln ( "template:" , err )
2014-06-28 07:46:03 +00:00
}
fd . Close ( )
tpl = template . Must ( template . New ( "index.html" ) . Funcs ( funcs ) . Parse ( string ( bs ) ) )
2015-03-04 10:31:46 +00:00
// DB
2014-06-11 23:40:54 +00:00
2015-03-04 10:31:46 +00:00
db , err := sql . Open ( "postgres" , dbConn )
2014-10-13 09:18:13 +00:00
if err != nil {
2015-03-04 10:31:46 +00:00
log . Fatalln ( "database:" , err )
}
err = setupDB ( db )
if err != nil {
log . Fatalln ( "database:" , err )
}
// TLS
cert , err := tls . LoadX509KeyPair ( certFile , keyFile )
if err != nil {
log . Fatalln ( "tls:" , err )
2014-10-13 09:18:13 +00:00
}
cfg := & tls . Config {
Certificates : [ ] tls . Certificate { cert } ,
SessionTicketsDisabled : true ,
}
2015-03-04 10:31:46 +00:00
// HTTPS
listener , err := tls . Listen ( "tcp" , listenAddr , cfg )
2014-10-13 09:18:13 +00:00
if err != nil {
2015-03-04 10:31:46 +00:00
log . Fatalln ( "https:" , err )
2014-10-13 09:18:13 +00:00
}
srv := http . Server {
ReadTimeout : 5 * time . Second ,
WriteTimeout : 5 * time . Second ,
}
2015-03-04 10:31:46 +00:00
http . HandleFunc ( "/" , withDB ( db , rootHandler ) )
http . HandleFunc ( "/newdata" , withDB ( db , newDataHandler ) )
2015-05-21 06:52:19 +00:00
http . HandleFunc ( "/summary.json" , withDB ( db , summaryHandler ) )
2015-07-15 11:23:13 +00:00
http . HandleFunc ( "/movement.json" , withDB ( db , movementHandler ) )
2015-03-04 10:31:46 +00:00
http . Handle ( "/static/" , http . StripPrefix ( "/static/" , http . FileServer ( http . Dir ( "static" ) ) ) )
2014-10-13 09:18:13 +00:00
err = srv . Serve ( listener )
2014-06-11 23:40:54 +00:00
if err != nil {
2015-03-04 10:31:46 +00:00
log . Fatalln ( "https:" , err )
2014-06-11 23:40:54 +00:00
}
}
2015-03-04 12:02:53 +00:00
var (
cacheData [ ] byte
cacheTime time . Time
cacheMut sync . Mutex
)
const maxCacheTime = 5 * 60 * time . Second
2015-03-04 10:31:46 +00:00
func rootHandler ( db * sql . DB , w http . ResponseWriter , r * http . Request ) {
2014-06-28 07:46:03 +00:00
if r . URL . Path == "/" || r . URL . Path == "/index.html" {
2015-03-04 12:02:53 +00:00
cacheMut . Lock ( )
defer cacheMut . Unlock ( )
if time . Since ( cacheTime ) > maxCacheTime {
rep := getReport ( db )
buf := new ( bytes . Buffer )
err := tpl . Execute ( buf , rep )
if err != nil {
log . Println ( err )
http . Error ( w , "Template Error" , http . StatusInternalServerError )
return
}
cacheData = buf . Bytes ( )
cacheTime = time . Now ( )
}
2014-06-28 07:46:03 +00:00
w . Header ( ) . Set ( "Content-Type" , "text/html; charset=utf-8" )
2015-03-04 12:02:53 +00:00
w . Write ( cacheData )
2014-06-28 07:46:03 +00:00
} else {
http . Error ( w , "Not found" , 404 )
2015-03-04 12:02:53 +00:00
return
2014-06-28 07:46:03 +00:00
}
}
2015-03-04 10:31:46 +00:00
func newDataHandler ( db * sql . DB , w http . ResponseWriter , r * http . Request ) {
defer r . Body . Close ( )
2014-06-28 07:46:03 +00:00
2015-03-04 10:31:46 +00:00
var rep report
rep . Date = time . Now ( ) . UTC ( ) . Format ( "20060102" )
2014-06-11 23:40:54 +00:00
lr := & io . LimitedReader { R : r . Body , N : 10240 }
2015-03-04 10:31:46 +00:00
if err := json . NewDecoder ( lr ) . Decode ( & rep ) ; err != nil {
log . Println ( "json decode:" , err )
http . Error ( w , "JSON Decode Error" , http . StatusInternalServerError )
2014-06-11 23:40:54 +00:00
return
}
2015-03-04 10:31:46 +00:00
if err := rep . Validate ( ) ; err != nil {
log . Println ( "validate:" , err )
log . Printf ( "%#v" , rep )
http . Error ( w , "Validation Error" , http . StatusInternalServerError )
2014-06-11 23:40:54 +00:00
return
}
2014-06-16 09:14:01 +00:00
2015-03-04 10:31:46 +00:00
if err := insertReport ( db , rep ) ; err != nil {
log . Println ( "insert:" , err )
log . Printf ( "%#v" , rep )
http . Error ( w , "Database Error" , http . StatusInternalServerError )
return
2014-06-16 09:14:01 +00:00
}
}
2015-05-21 06:52:19 +00:00
func summaryHandler ( db * sql . DB , w http . ResponseWriter , r * http . Request ) {
s , err := getSummary ( db )
if err != nil {
log . Println ( "summaryHandler:" , err )
http . Error ( w , "Database Error" , http . StatusInternalServerError )
return
}
bs , err := s . MarshalJSON ( )
if err != nil {
log . Println ( "summaryHandler:" , err )
http . Error ( w , "JSON Encode Error" , http . StatusInternalServerError )
return
}
w . Header ( ) . Set ( "Content-Type" , "application/json" )
w . Write ( bs )
}
2015-07-15 11:23:13 +00:00
func movementHandler ( db * sql . DB , w http . ResponseWriter , r * http . Request ) {
s , err := getMovement ( db )
if err != nil {
log . Println ( "movementHandler:" , err )
http . Error ( w , "Database Error" , http . StatusInternalServerError )
return
}
bs , err := json . Marshal ( s )
if err != nil {
log . Println ( "movementHandler:" , err )
http . Error ( w , "JSON Encode Error" , http . StatusInternalServerError )
return
}
w . Header ( ) . Set ( "Content-Type" , "application/json" )
w . Write ( bs )
}
2014-06-28 09:24:25 +00:00
type category struct {
Values [ 4 ] float64
Key string
Descr string
Unit string
Binary bool
}
2015-03-04 12:02:53 +00:00
func getReport ( db * sql . DB ) map [ string ] interface { } {
2014-06-28 07:46:03 +00:00
nodes := 0
var versions [ ] string
var platforms [ ] string
var oses [ ] string
2015-03-04 10:31:46 +00:00
var numFolders [ ] int
var numDevices [ ] int
2014-06-28 07:46:03 +00:00
var totFiles [ ] int
var maxFiles [ ] int
var totMiB [ ] int
var maxMiB [ ] int
var memoryUsage [ ] int
var sha256Perf [ ] float64
var memorySize [ ] int
2015-05-21 06:52:19 +00:00
var compilers [ ] string
var builders [ ] string
2014-06-11 23:40:54 +00:00
2015-03-04 10:31:46 +00:00
rows , err := db . Query ( ` SELECT * FROM Reports WHERE Received > now() - '1 day'::INTERVAL ` )
if err != nil {
log . Println ( "sql:" , err )
return nil
}
defer rows . Close ( )
for rows . Next ( ) {
2014-06-11 23:40:54 +00:00
2014-06-28 07:46:03 +00:00
var rep report
2015-03-04 10:31:46 +00:00
err := rows . Scan ( & rep . Received , & rep . UniqueID , & rep . Version ,
& rep . LongVersion , & rep . Platform , & rep . NumFolders , & rep . NumDevices ,
& rep . TotFiles , & rep . FolderMaxFiles , & rep . TotMiB , & rep . FolderMaxMiB ,
& rep . MemoryUsageMiB , & rep . SHA256Perf , & rep . MemorySize , & rep . Date )
2014-06-28 07:46:03 +00:00
if err != nil {
2015-03-04 10:31:46 +00:00
log . Println ( "sql:" , err )
return nil
2014-06-28 07:46:03 +00:00
}
2014-06-11 23:40:54 +00:00
2014-06-28 07:46:03 +00:00
nodes ++
versions = append ( versions , transformVersion ( rep . Version ) )
platforms = append ( platforms , rep . Platform )
ps := strings . Split ( rep . Platform , "-" )
oses = append ( oses , ps [ 0 ] )
2015-05-21 06:52:19 +00:00
if m := compilerRe . FindStringSubmatch ( rep . LongVersion ) ; len ( m ) == 3 {
compilers = append ( compilers , m [ 1 ] )
builders = append ( builders , m [ 2 ] )
}
2015-03-04 10:31:46 +00:00
if rep . NumFolders > 0 {
numFolders = append ( numFolders , rep . NumFolders )
2014-06-28 07:46:03 +00:00
}
2015-03-04 10:31:46 +00:00
if rep . NumDevices > 0 {
numDevices = append ( numDevices , rep . NumDevices )
2014-06-28 07:46:03 +00:00
}
if rep . TotFiles > 0 {
totFiles = append ( totFiles , rep . TotFiles )
}
2015-03-04 10:31:46 +00:00
if rep . FolderMaxFiles > 0 {
maxFiles = append ( maxFiles , rep . FolderMaxFiles )
2014-06-28 07:46:03 +00:00
}
if rep . TotMiB > 0 {
2014-06-28 09:24:25 +00:00
totMiB = append ( totMiB , rep . TotMiB * ( 1 << 20 ) )
2014-06-28 07:46:03 +00:00
}
2015-03-04 10:31:46 +00:00
if rep . FolderMaxMiB > 0 {
maxMiB = append ( maxMiB , rep . FolderMaxMiB * ( 1 << 20 ) )
2014-06-11 23:40:54 +00:00
}
2014-06-28 07:46:03 +00:00
if rep . MemoryUsageMiB > 0 {
2014-06-28 09:24:25 +00:00
memoryUsage = append ( memoryUsage , rep . MemoryUsageMiB * ( 1 << 20 ) )
2014-06-28 07:46:03 +00:00
}
if rep . SHA256Perf > 0 {
2014-06-28 09:24:25 +00:00
sha256Perf = append ( sha256Perf , rep . SHA256Perf * ( 1 << 20 ) )
2014-06-28 07:46:03 +00:00
}
if rep . MemorySize > 0 {
2014-06-28 09:24:25 +00:00
memorySize = append ( memorySize , rep . MemorySize * ( 1 << 20 ) )
2014-06-28 07:46:03 +00:00
}
}
2014-06-28 09:24:25 +00:00
var categories [ ] category
categories = append ( categories , category {
Values : statsForInts ( totFiles ) ,
2015-02-15 11:00:15 +00:00
Descr : "Files Managed per Device" ,
2014-06-28 09:24:25 +00:00
} )
categories = append ( categories , category {
Values : statsForInts ( maxFiles ) ,
2015-02-15 11:00:15 +00:00
Descr : "Files in Largest Folder" ,
2014-06-28 09:24:25 +00:00
} )
categories = append ( categories , category {
Values : statsForInts ( totMiB ) ,
2015-02-15 11:00:15 +00:00
Descr : "Data Managed per Device" ,
2014-06-28 09:24:25 +00:00
Unit : "B" ,
Binary : true ,
} )
categories = append ( categories , category {
Values : statsForInts ( maxMiB ) ,
2015-02-15 11:00:15 +00:00
Descr : "Data in Largest Folder" ,
2014-06-28 09:24:25 +00:00
Unit : "B" ,
Binary : true ,
} )
categories = append ( categories , category {
2015-03-04 10:31:46 +00:00
Values : statsForInts ( numDevices ) ,
2015-02-15 11:00:15 +00:00
Descr : "Number of Devices in Cluster" ,
2014-06-28 09:24:25 +00:00
} )
categories = append ( categories , category {
2015-03-04 10:31:46 +00:00
Values : statsForInts ( numFolders ) ,
2015-02-15 11:00:15 +00:00
Descr : "Number of Folders Configured" ,
2014-06-28 09:24:25 +00:00
} )
categories = append ( categories , category {
Values : statsForInts ( memoryUsage ) ,
Descr : "Memory Usage" ,
Unit : "B" ,
Binary : true ,
} )
categories = append ( categories , category {
Values : statsForInts ( memorySize ) ,
Descr : "System Memory" ,
Unit : "B" ,
Binary : true ,
} )
categories = append ( categories , category {
Values : statsForFloats ( sha256Perf ) ,
Descr : "SHA-256 Hashing Performance" ,
Unit : "B/s" ,
Binary : true ,
} )
2014-06-28 07:46:03 +00:00
r := make ( map [ string ] interface { } )
r [ "nodes" ] = nodes
r [ "categories" ] = categories
2014-12-09 15:52:02 +00:00
r [ "versions" ] = analyticsFor ( versions , 10 )
r [ "platforms" ] = analyticsFor ( platforms , 0 )
r [ "os" ] = analyticsFor ( oses , 0 )
2015-05-21 06:52:19 +00:00
r [ "compilers" ] = analyticsFor ( compilers , 12 )
r [ "builders" ] = analyticsFor ( builders , 12 )
2014-06-28 07:46:03 +00:00
return r
}
2014-06-11 23:40:54 +00:00
func ensureDir ( dir string , mode int ) {
fi , err := os . Stat ( dir )
if os . IsNotExist ( err ) {
os . MkdirAll ( dir , 0700 )
} else if mode >= 0 && err == nil && int ( fi . Mode ( ) & 0777 ) != mode {
os . Chmod ( dir , os . FileMode ( mode ) )
}
}
2014-06-28 07:46:03 +00:00
2014-08-07 12:55:13 +00:00
var vRe = regexp . MustCompile ( ` ^(v\d+\.\d+\.\d+(?:-[a-z]\w+)?)[+\.-] ` )
2014-06-28 07:46:03 +00:00
// transformVersion returns a version number formatted correctly, with all
// development versions aggregated into one.
func transformVersion ( v string ) string {
2014-08-07 12:55:13 +00:00
if v == "unknown-dev" {
return v
}
2014-06-28 07:46:03 +00:00
if ! strings . HasPrefix ( v , "v" ) {
v = "v" + v
}
if m := vRe . FindStringSubmatch ( v ) ; len ( m ) > 0 {
return m [ 1 ] + " (+dev)"
}
2014-12-09 15:56:47 +00:00
// Truncate old versions to just the generation part
2015-06-15 08:49:48 +00:00
for _ , agg := range aggregateVersions {
if strings . HasPrefix ( v , agg ) {
return agg + ".x"
}
2014-12-09 15:56:47 +00:00
}
2014-06-28 07:46:03 +00:00
return v
}
2015-05-21 06:52:19 +00:00
type summary struct {
versions map [ string ] int // version string to count index
rows map [ string ] [ ] int // date to list of counts
}
func newSummary ( ) summary {
return summary {
versions : make ( map [ string ] int ) ,
rows : make ( map [ string ] [ ] int ) ,
}
}
func ( s * summary ) setCount ( date , version string , count int ) {
idx , ok := s . versions [ version ]
if ! ok {
idx = len ( s . versions )
s . versions [ version ] = idx
}
row := s . rows [ date ]
if len ( row ) <= idx {
old := row
row = make ( [ ] int , idx + 1 )
copy ( row , old )
s . rows [ date ] = row
}
row [ idx ] = count
}
func ( s * summary ) MarshalJSON ( ) ( [ ] byte , error ) {
var versions [ ] string
for v := range s . versions {
versions = append ( versions , v )
}
sort . Strings ( versions )
headerRow := [ ] interface { } { "Day" }
for _ , v := range versions {
headerRow = append ( headerRow , v )
}
var table [ ] [ ] interface { }
table = append ( table , headerRow )
var dates [ ] string
for k := range s . rows {
dates = append ( dates , k )
}
sort . Strings ( dates )
for _ , date := range dates {
row := [ ] interface { } { date }
for _ , ver := range versions {
idx := s . versions [ ver ]
2015-06-15 08:46:48 +00:00
if len ( s . rows [ date ] ) > idx && s . rows [ date ] [ idx ] > 0 {
2015-05-21 06:52:19 +00:00
row = append ( row , s . rows [ date ] [ idx ] )
} else {
2015-06-15 08:46:48 +00:00
row = append ( row , nil )
2015-05-21 06:52:19 +00:00
}
}
table = append ( table , row )
}
return json . Marshal ( table )
}
func getSummary ( db * sql . DB ) ( summary , error ) {
s := newSummary ( )
2015-05-29 07:51:56 +00:00
rows , err := db . Query ( ` SELECT Day, Version, Count FROM VersionSummary WHERE Day > now() - '1 year'::INTERVAL; ` )
2015-05-21 06:52:19 +00:00
if err != nil {
return summary { } , err
}
defer rows . Close ( )
for rows . Next ( ) {
var day time . Time
var ver string
var num int
err := rows . Scan ( & day , & ver , & num )
if err != nil {
return summary { } , err
}
if ver == "v0.0" {
// ?
continue
}
// SUPER UGLY HACK to avoid having to do sorting properly
if len ( ver ) == 4 { // v0.x
ver = ver [ : 3 ] + "0" + ver [ 3 : ] // now v0.0x
}
s . setCount ( day . Format ( "2006-01-02" ) , ver , num )
}
return s , nil
}
2015-07-15 11:23:13 +00:00
func getMovement ( db * sql . DB ) ( [ ] [ ] interface { } , error ) {
2015-07-15 11:45:33 +00:00
rows , err := db . Query ( ` SELECT Day, Added, Removed, Bounced FROM UserMovement WHERE Day > now() - '1 year'::INTERVAL ORDER BY Day ` )
2015-07-15 11:23:13 +00:00
if err != nil {
return nil , err
}
defer rows . Close ( )
res := [ ] [ ] interface { } {
2015-07-15 11:45:33 +00:00
{ "Day" , "Joined" , "Left" , "Bounced" } ,
2015-07-15 11:23:13 +00:00
}
for rows . Next ( ) {
var day time . Time
2015-07-15 11:45:33 +00:00
var added , removed , bounced int
err := rows . Scan ( & day , & added , & removed , & bounced )
2015-07-15 11:23:13 +00:00
if err != nil {
return nil , err
}
2015-07-15 11:45:33 +00:00
row := [ ] interface { } { day . Format ( "2006-01-02" ) , added , - removed , bounced }
2015-07-15 11:23:13 +00:00
if removed == 0 {
row [ 2 ] = nil
}
2015-07-15 11:45:33 +00:00
if bounced == 0 {
row [ 3 ] = nil
}
2015-07-15 11:23:13 +00:00
res = append ( res , row )
}
return res , nil
}