track favicon stats

experiment with tracking average route performance
temporary error route stub
optimise dumprequest
add DisableAnalytics config setting
fix double hyphens in slugs being mistaken for sql injection
more querygen tests

You wil need to run the updater / patcher for this commit.
This commit is contained in:
Azareal 2020-02-26 20:34:38 +10:00
parent 04fdf4c318
commit ea1037bd63
19 changed files with 841 additions and 705 deletions

View File

@ -662,6 +662,7 @@ func createTables(adapter qgen.Adapter) (err error) {
createTable("viewchunks", "", "", createTable("viewchunks", "", "",
[]tC{ []tC{
tC{"count", "int", 0, false, false, "0"}, tC{"count", "int", 0, false, false, "0"},
tC{"avg", "int", 0, false, false, "0"},
tC{"createdAt", "datetime", 0, false, false, ""}, tC{"createdAt", "datetime", 0, false, false, ""},
tC{"route", "varchar", 200, false, false, ""}, // todo: set a default empty here tC{"route", "varchar", 200, false, false, ""}, // todo: set a default empty here
}, nil, }, nil,

View File

@ -50,7 +50,7 @@ func (co *DefaultAgentViewCounter) insertChunk(count int64, agent int) error {
func (co *DefaultAgentViewCounter) Bump(agent int) { func (co *DefaultAgentViewCounter) Bump(agent int) {
// TODO: Test this check // TODO: Test this check
c.DebugDetail("co.buckets[", agent, "]: ", co.buckets[agent]) c.DebugDetail("buckets[", agent, "]: ", co.buckets[agent])
if len(co.buckets) <= agent || agent < 0 { if len(co.buckets) <= agent || agent < 0 {
return return
} }

View File

@ -157,7 +157,7 @@ func (co *DefaultLangViewCounter) Bump(langCode string) (validCode bool) {
} }
// TODO: Test this check // TODO: Test this check
c.DebugDetail("co.buckets[", id, "]: ", co.buckets[id]) c.DebugDetail("buckets[", id, "]: ", co.buckets[id])
if len(co.buckets) <= id || id < 0 { if len(co.buckets) <= id || id < 0 {
return validCode return validCode
} }

View File

@ -85,7 +85,7 @@ func (ref *DefaultReferrerTracker) insertChunk(referrer string, count int64) err
if count == 0 { if count == 0 {
return nil return nil
} }
c.DebugDetailf("Inserting a vchunk with a count of %d for referrer %s", count, referrer) c.DebugDetailf("Inserting a vchunk with a count of %d for ref %s", count, referrer)
_, err := ref.insert.Exec(count, referrer) _, err := ref.insert.Exec(count, referrer)
return err return err
} }

View File

@ -2,6 +2,8 @@ package counters
import ( import (
"database/sql" "database/sql"
"sync"
"time"
c "github.com/Azareal/Gosora/common" c "github.com/Azareal/Gosora/common"
qgen "github.com/Azareal/Gosora/query_gen" qgen "github.com/Azareal/Gosora/query_gen"
@ -10,56 +12,68 @@ import (
var RouteViewCounter *DefaultRouteViewCounter var RouteViewCounter *DefaultRouteViewCounter
type RVBucket struct {
counter int
avg int
sync.Mutex
}
// TODO: Make this lockless? // TODO: Make this lockless?
type DefaultRouteViewCounter struct { type DefaultRouteViewCounter struct {
buckets []*RWMutexCounterBucket //[RouteID]count buckets []*RVBucket //[RouteID]count
insert *sql.Stmt insert *sql.Stmt
insert5 *sql.Stmt insert5 *sql.Stmt
} }
func NewDefaultRouteViewCounter(acc *qgen.Accumulator) (*DefaultRouteViewCounter, error) { func NewDefaultRouteViewCounter(acc *qgen.Accumulator) (*DefaultRouteViewCounter, error) {
routeBuckets := make([]*RWMutexCounterBucket, len(routeMapEnum)) routeBuckets := make([]*RVBucket, len(routeMapEnum))
for bucketID, _ := range routeBuckets { for bucketID, _ := range routeBuckets {
routeBuckets[bucketID] = &RWMutexCounterBucket{counter: 0} routeBuckets[bucketID] = &RVBucket{counter: 0, avg: 0}
} }
fields := "?,UTC_TIMESTAMP(),?" fields := "?,?,UTC_TIMESTAMP(),?"
co := &DefaultRouteViewCounter{ co := &DefaultRouteViewCounter{
buckets: routeBuckets, buckets: routeBuckets,
insert: acc.Insert("viewchunks").Columns("count,createdAt,route").Fields(fields).Prepare(), insert: acc.Insert("viewchunks").Columns("count,avg,createdAt,route").Fields(fields).Prepare(),
insert5: acc.BulkInsert("viewchunks").Columns("count,createdAt,route").Fields(fields,fields,fields,fields,fields).Prepare(), insert5: acc.BulkInsert("viewchunks").Columns("count,avg,createdAt,route").Fields(fields, fields, fields, fields, fields).Prepare(),
}
if !c.Config.DisableAnalytics {
c.AddScheduledFifteenMinuteTask(co.Tick) // There could be a lot of routes, so we don't want to be running this every second
//c.AddScheduledSecondTask(co.Tick)
c.AddShutdownTask(co.Tick)
} }
c.AddScheduledFifteenMinuteTask(co.Tick) // There could be a lot of routes, so we don't want to be running this every second
//c.AddScheduledSecondTask(co.Tick)
c.AddShutdownTask(co.Tick)
return co, acc.FirstError() return co, acc.FirstError()
} }
type RVCount struct { type RVCount struct {
RouteID int RouteID int
Count int Count int
Avg int
} }
func (co *DefaultRouteViewCounter) Tick() error { func (co *DefaultRouteViewCounter) Tick() (err error) {
var tb []RVCount var tb []RVCount
for routeID, b := range co.buckets { for routeID, b := range co.buckets {
var count int var count, avg int
b.RLock() b.Lock()
count = b.counter count = b.counter
b.counter = 0 b.counter = 0
b.RUnlock() avg = b.avg
b.avg = 0
b.Unlock()
if count == 0 { if count == 0 {
continue continue
} }
tb = append(tb, RVCount{routeID,count}) tb = append(tb, RVCount{routeID, count, avg})
} }
// TODO: Expand on this? // TODO: Expand on this?
var i int var i int
if len(tb) >= 5 { if len(tb) >= 5 {
for ; len(tb) > (i+5); i += 5 { for ; len(tb) > (i + 5); i += 5 {
err := co.insert5Chunk(tb[i:i+5]) err := co.insert5Chunk(tb[i : i+5])
if err != nil { if err != nil {
c.DebugLogf("tb: %+v\n", tb) c.DebugLogf("tb: %+v\n", tb)
c.DebugLog("i: ", i) c.DebugLog("i: ", i)
@ -69,7 +83,8 @@ func (co *DefaultRouteViewCounter) Tick() error {
} }
for ; len(tb) > i; i++ { for ; len(tb) > i; i++ {
err := co.insertChunk(tb[i].Count, tb[i].RouteID) it := tb[i]
err = co.insertChunk(it.Count, it.Avg, it.RouteID)
if err != nil { if err != nil {
c.DebugLogf("tb: %+v\n", tb) c.DebugLogf("tb: %+v\n", tb)
c.DebugLog("i: ", i) c.DebugLog("i: ", i)
@ -80,32 +95,40 @@ func (co *DefaultRouteViewCounter) Tick() error {
return nil return nil
} }
func (co *DefaultRouteViewCounter) insertChunk(count, route int) error { func (co *DefaultRouteViewCounter) insertChunk(count, avg, route int) error {
routeName := reverseRouteMapEnum[route] routeName := reverseRouteMapEnum[route]
c.DebugLogf("Inserting a vchunk with a count of %d for route %s (%d)", count, routeName, route) c.DebugLogf("Inserting a vchunk with a count of %d, avg of %d for route %s (%d)", count, avg, routeName, route)
_, err := co.insert.Exec(count, routeName) _, err := co.insert.Exec(count, avg, routeName)
return err return err
} }
func (co *DefaultRouteViewCounter) insert5Chunk(rvs []RVCount) error { func (co *DefaultRouteViewCounter) insert5Chunk(rvs []RVCount) error {
args := make([]interface{}, len(rvs) * 2) args := make([]interface{}, len(rvs)*3)
i := 0 i := 0
for _, rv := range rvs { for _, rv := range rvs {
routeName := reverseRouteMapEnum[rv.RouteID] routeName := reverseRouteMapEnum[rv.RouteID]
c.DebugLogf("Queueing a vchunk with a count of %d for routes %s (%d)", rv.Count, routeName, rv.RouteID) if rv.Avg == 0 {
c.DebugLogf("Queueing a vchunk with a count of %d for routes %s (%d)", rv.Count, routeName, rv.RouteID)
} else {
c.DebugLogf("Queueing a vchunk with count %d, avg %d for routes %s (%d)", rv.Count, rv.Avg, routeName, rv.RouteID)
}
args[i] = rv.Count args[i] = rv.Count
args[i+1] = routeName args[i+1] = rv.Avg
i += 2 args[i+2] = routeName
i += 3
} }
c.DebugLogf("args: %+v\n", args) c.DebugDetailf("args: %+v\n", args)
_, err := co.insert5.Exec(args...) _, err := co.insert5.Exec(args...)
return err return err
} }
func (co *DefaultRouteViewCounter) Bump(route int) { func (co *DefaultRouteViewCounter) Bump(route int) {
if c.Config.DisableAnalytics {
return
}
// TODO: Test this check // TODO: Test this check
b := co.buckets[route] b := co.buckets[route]
c.DebugDetail("co.buckets[", route, "]: ", b) c.DebugDetail("buckets[", route, "]: ", b)
if len(co.buckets) <= route || route < 0 { if len(co.buckets) <= route || route < 0 {
return return
} }
@ -114,3 +137,27 @@ func (co *DefaultRouteViewCounter) Bump(route int) {
b.counter++ b.counter++
b.Unlock() b.Unlock()
} }
// TODO: Eliminate the lock?
func (co *DefaultRouteViewCounter) Bump2(route int, t time.Time) {
if c.Config.DisableAnalytics {
return
}
// TODO: Test this check
b := co.buckets[route]
c.DebugDetail("buckets[", route, "]: ", b)
if len(co.buckets) <= route || route < 0 {
return
}
micro := int(time.Since(t).Microseconds())
b.Lock()
b.counter++
if micro != b.avg {
if b.avg == 0 {
b.avg = micro
} else {
b.avg = (micro + b.avg) / 2
}
}
b.Unlock()
}

View File

@ -5,7 +5,7 @@ import (
"sync/atomic" "sync/atomic"
c "github.com/Azareal/Gosora/common" c "github.com/Azareal/Gosora/common"
"github.com/Azareal/Gosora/query_gen" qgen "github.com/Azareal/Gosora/query_gen"
"github.com/pkg/errors" "github.com/pkg/errors"
) )
@ -22,7 +22,7 @@ func NewTopicCounter() (*DefaultTopicCounter, error) {
acc := qgen.NewAcc() acc := qgen.NewAcc()
co := &DefaultTopicCounter{ co := &DefaultTopicCounter{
currentBucket: 0, currentBucket: 0,
insert: acc.Insert("topicchunks").Columns("count, createdAt").Fields("?,UTC_TIMESTAMP()").Prepare(), insert: acc.Insert("topicchunks").Columns("count,createdAt").Fields("?,UTC_TIMESTAMP()").Prepare(),
} }
c.AddScheduledFifteenMinuteTask(co.Tick) c.AddScheduledFifteenMinuteTask(co.Tick)
//c.AddScheduledSecondTask(co.Tick) //c.AddScheduledSecondTask(co.Tick)
@ -44,7 +44,7 @@ func (co *DefaultTopicCounter) Tick() (err error) {
atomic.AddInt64(&co.buckets[oldBucket], -previousViewChunk) atomic.AddInt64(&co.buckets[oldBucket], -previousViewChunk)
err = co.insertChunk(previousViewChunk) err = co.insertChunk(previousViewChunk)
if err != nil { if err != nil {
return errors.Wrap(errors.WithStack(err),"topics counter") return errors.Wrap(errors.WithStack(err), "topics counter")
} }
return nil return nil
} }

View File

@ -97,7 +97,7 @@ func (s *SQLReplyStore) Exists(id int) bool {
} }
// TODO: Write a test for this // TODO: Write a test for this
func (s *SQLReplyStore) Create(t *Topic, content, ip string, uid int) (rid int, err error) { func (s *SQLReplyStore) Create(t *Topic, content, ip string, uid int) (id int, err error) {
if Config.DisablePostIP { if Config.DisablePostIP {
ip = "" ip = ""
} }
@ -110,8 +110,8 @@ func (s *SQLReplyStore) Create(t *Topic, content, ip string, uid int) (rid int,
if err != nil { if err != nil {
return 0, err return 0, err
} }
rid = int(lastID) id = int(lastID)
return rid, t.AddReply(rid, uid) return id, t.AddReply(id, uid)
} }
// TODO: Write a test for this // TODO: Write a test for this

View File

@ -113,6 +113,7 @@ type config struct {
EnableCDNPush bool EnableCDNPush bool
DisableNoavatarRange bool DisableNoavatarRange bool
DisableDefaultNoavatar bool DisableDefaultNoavatar bool
DisableAnalytics bool
RefNoTrack bool RefNoTrack bool
RefNoRef bool RefNoRef bool

View File

@ -116,6 +116,8 @@ DisableNoavatarRange - This switch lets you disable the noavatar algorithm which
DisableDefaultNoavatar - This switch lets you disable the default noavatar algorithm which may intercept noavatars for increased efficiency. Default: false DisableDefaultNoavatar - This switch lets you disable the default noavatar algorithm which may intercept noavatars for increased efficiency. Default: false
DisableAnalytics - This switch lets you disable the analytics subsystem. Default: false
RefNoTrack - This switch disables tracking the referrers of users who click from another site to your site and the referrers of any requests to resources from other sites as-well. RefNoTrack - This switch disables tracking the referrers of users who click from another site to your site and the referrers of any requests to resources from other sites as-well.
RefNoRef - This switch makes it so that if a user clicks on a link, then the incoming site won't know which site they're coming from. RefNoRef - This switch makes it so that if a user clicks on a link, then the incoming site won't know which site they're coming from.

File diff suppressed because it is too large Load Diff

59
main.go
View File

@ -32,6 +32,7 @@ import (
qgen "github.com/Azareal/Gosora/query_gen" qgen "github.com/Azareal/Gosora/query_gen"
"github.com/Azareal/Gosora/routes" "github.com/Azareal/Gosora/routes"
"github.com/fsnotify/fsnotify" "github.com/fsnotify/fsnotify"
//"github.com/lucas-clemente/quic-go/http3" //"github.com/lucas-clemente/quic-go/http3"
"github.com/pkg/errors" "github.com/pkg/errors"
) )
@ -272,21 +273,37 @@ func storeInit() (err error) {
} }
log.Print("Initialising the view counters") log.Print("Initialising the view counters")
co.GlobalViewCounter, err = co.NewGlobalViewCounter(acc) if !c.Config.DisableAnalytics {
if err != nil { co.GlobalViewCounter, err = co.NewGlobalViewCounter(acc)
return errors.WithStack(err) if err != nil {
} return errors.WithStack(err)
co.AgentViewCounter, err = co.NewDefaultAgentViewCounter(acc) }
if err != nil { co.AgentViewCounter, err = co.NewDefaultAgentViewCounter(acc)
return errors.WithStack(err) if err != nil {
} return errors.WithStack(err)
co.OSViewCounter, err = co.NewDefaultOSViewCounter(acc) }
if err != nil { co.OSViewCounter, err = co.NewDefaultOSViewCounter(acc)
return errors.WithStack(err) if err != nil {
} return errors.WithStack(err)
co.LangViewCounter, err = co.NewDefaultLangViewCounter(acc) }
if err != nil { co.LangViewCounter, err = co.NewDefaultLangViewCounter(acc)
return errors.WithStack(err) if err != nil {
return errors.WithStack(err)
}
if !c.Config.RefNoTrack {
co.ReferrerTracker, err = co.NewDefaultReferrerTracker()
if err != nil {
return errors.WithStack(err)
}
}
co.MemoryCounter, err = co.NewMemoryCounter(acc)
if err != nil {
return errors.WithStack(err)
}
co.PerfCounter, err = co.NewDefaultPerfCounter(acc)
if err != nil {
return errors.WithStack(err)
}
} }
co.RouteViewCounter, err = co.NewDefaultRouteViewCounter(acc) co.RouteViewCounter, err = co.NewDefaultRouteViewCounter(acc)
if err != nil { if err != nil {
@ -308,18 +325,6 @@ func storeInit() (err error) {
if err != nil { if err != nil {
return errors.WithStack(err) return errors.WithStack(err)
} }
co.ReferrerTracker, err = co.NewDefaultReferrerTracker()
if err != nil {
return errors.WithStack(err)
}
co.MemoryCounter, err = co.NewMemoryCounter(acc)
if err != nil {
return errors.WithStack(err)
}
co.PerfCounter, err = co.NewDefaultPerfCounter(acc)
if err != nil {
return errors.WithStack(err)
}
log.Print("Initialising the meta store") log.Print("Initialising the meta store")
c.Meta, err = meta.NewDefaultMetaStore(acc) c.Meta, err = meta.NewDefaultMetaStore(acc)

View File

@ -50,6 +50,7 @@ func init() {
addPatch(30, patch30) addPatch(30, patch30)
addPatch(31, patch31) addPatch(31, patch31)
addPatch(32, patch32) addPatch(32, patch32)
addPatch(33, patch33)
} }
func patch0(scanner *bufio.Scanner) (err error) { func patch0(scanner *bufio.Scanner) (err error) {
@ -428,22 +429,22 @@ func patch10(scanner *bufio.Scanner) error {
err = acc().Select("topics").Cols("tid").EachInt(func(tid int) error { err = acc().Select("topics").Cols("tid").EachInt(func(tid int) error {
stid := itoa(tid) stid := itoa(tid)
count, err := acc().Count("attachments").Where("originTable = 'topics' and originID = " + stid).Total() count, err := acc().Count("attachments").Where("originTable = 'topics' and originID=" + stid).Total()
if err != nil { if err != nil {
return err return err
} }
hasReply := false hasReply := false
err = acc().Select("replies").Cols("rid").Where("tid = " + stid).Orderby("rid DESC").Limit("1").EachInt(func(rid int) error { err = acc().Select("replies").Cols("rid").Where("tid=" + stid).Orderby("rid DESC").Limit("1").EachInt(func(rid int) error {
hasReply = true hasReply = true
_, err := acc().Update("topics").Set("lastReplyID = ?, attachCount = ?").Where("tid = "+stid).Exec(rid, count) _, err := acc().Update("topics").Set("lastReplyID=?, attachCount=?").Where("tid="+stid).Exec(rid, count)
return err return err
}) })
if err != nil { if err != nil {
return err return err
} }
if !hasReply { if !hasReply {
_, err = acc().Update("topics").Set("attachCount = ?").Where("tid = " + stid).Exec(count) _, err = acc().Update("topics").Set("attachCount=?").Where("tid=" + stid).Exec(count)
} }
return err return err
}) })
@ -894,14 +895,17 @@ func patch31(scanner *bufio.Scanner) error {
return nil return nil
} }
func patch32(scanner *bufio.Scanner) error { func patch32(scanner *bufio.Scanner) error {
return execStmt(qgen.Builder.CreateTable("perfchunks", "", "", return execStmt(qgen.Builder.CreateTable("perfchunks", "", "",
[]tC{ []tC{
tC{"low", "int", 0, false, false, "0"}, tC{"low", "int", 0, false, false, "0"},
tC{"high", "int", 0, false, false, "0"}, tC{"high", "int", 0, false, false, "0"},
tC{"avg", "int", 0, false, false, "0"}, tC{"avg", "int", 0, false, false, "0"},
tC{"createdAt", "datetime", 0, false, false, ""}, tC{"createdAt", "datetime", 0, false, false, ""},
}, nil, }, nil,
)) ))
}
func patch33(scanner *bufio.Scanner) error {
return execStmt(qgen.Builder.AddColumn("viewchunks", tC{"avg", "int", 0, false, false, "0"}, nil))
} }

View File

@ -31,6 +31,8 @@ func TestProcessWhere(t *testing.T) {
expectTokens(t, whs, MT{TokenColumn, "uid"}, MT{TokenOp, "="}, MT{TokenNumber, "0"}) expectTokens(t, whs, MT{TokenColumn, "uid"}, MT{TokenOp, "="}, MT{TokenNumber, "0"})
whs = processWhere("uid = '1'") whs = processWhere("uid = '1'")
expectTokens(t, whs, MT{TokenColumn, "uid"}, MT{TokenOp, "="}, MT{TokenString, "1"}) expectTokens(t, whs, MT{TokenColumn, "uid"}, MT{TokenOp, "="}, MT{TokenString, "1"})
whs = processWhere("uid = 't'")
expectTokens(t, whs, MT{TokenColumn, "uid"}, MT{TokenOp, "="}, MT{TokenString, "t"})
whs = processWhere("uid = ''") whs = processWhere("uid = ''")
expectTokens(t, whs, MT{TokenColumn, "uid"}, MT{TokenOp, "="}, MT{TokenString, ""}) expectTokens(t, whs, MT{TokenColumn, "uid"}, MT{TokenOp, "="}, MT{TokenString, ""})
whs = processWhere("uid = '") whs = processWhere("uid = '")
@ -42,8 +44,12 @@ func TestProcessWhere(t *testing.T) {
expectTokens(t, whs, MT{TokenColumn, "uid"}, MT{TokenOp, "="}, MT{TokenNumber, "1"}) expectTokens(t, whs, MT{TokenColumn, "uid"}, MT{TokenOp, "="}, MT{TokenNumber, "1"})
whs = processWhere("uid=0") whs = processWhere("uid=0")
expectTokens(t, whs, MT{TokenColumn, "uid"}, MT{TokenOp, "="}, MT{TokenNumber, "0"}) expectTokens(t, whs, MT{TokenColumn, "uid"}, MT{TokenOp, "="}, MT{TokenNumber, "0"})
whs = processWhere("uid=20")
expectTokens(t, whs, MT{TokenColumn, "uid"}, MT{TokenOp, "="}, MT{TokenNumber, "20"})
whs = processWhere("uid='1'") whs = processWhere("uid='1'")
expectTokens(t, whs, MT{TokenColumn, "uid"}, MT{TokenOp, "="}, MT{TokenString, "1"}) expectTokens(t, whs, MT{TokenColumn, "uid"}, MT{TokenOp, "="}, MT{TokenString, "1"})
whs = processWhere("uid='t'")
expectTokens(t, whs, MT{TokenColumn, "uid"}, MT{TokenOp, "="}, MT{TokenString, "t"})
whs = processWhere("uid") whs = processWhere("uid")
expectTokens(t, whs, MT{TokenColumn, "uid"}) expectTokens(t, whs, MT{TokenColumn, "uid"})

View File

@ -47,23 +47,24 @@ func main() {
allRouteNames = append(allRouteNames, RouteName{name, strings.Replace(name, "common.", "c.", -1)}) allRouteNames = append(allRouteNames, RouteName{name, strings.Replace(name, "common.", "c.", -1)})
allRouteMap[name] = len(allRouteNames) - 1 allRouteMap[name] = len(allRouteNames) - 1
} }
mapIt("routes.Error")
countToIndents := func(indent int) (indentor string) { countToIndents := func(indent int) (indentor string) {
for i := 0; i < indent; i++ { for i := 0; i < indent; i++ {
indentor += "\t" indentor += "\t"
} }
return indentor return indentor
} }
runBefore := func(runnables []Runnable, indent int) (out string) { runBefore := func(runnables []Runnable, indentCount int) (out string) {
indentor := countToIndents(indent) indent := countToIndents(indentCount)
if len(runnables) > 0 { if len(runnables) > 0 {
for _, runnable := range runnables { for _, runnable := range runnables {
if runnable.Literal { if runnable.Literal {
out += "\n\t" + indentor + runnable.Contents out += "\n\t" + indent + runnable.Contents
} else { } else {
out += "\n" + indentor + "err = c." + runnable.Contents + "(w,req,user)\n" + out += "\n" + indent + "err = c." + runnable.Contents + "(w,req,user)\n" +
indentor + "if err != nil {\n" + indent + "if err != nil {\n" +
indentor + "\treturn err\n" + indent + "\treturn err\n" +
indentor + "}\n" + indentor indent + "}\n" + indent
} }
} }
} }
@ -74,13 +75,13 @@ func main() {
mapIt(route.Name) mapIt(route.Name)
end := len(route.Path) - 1 end := len(route.Path) - 1
out += "\n\t\tcase \"" + route.Path[0:end] + "\":" out += "\n\t\tcase \"" + route.Path[0:end] + "\":"
//out += "\n\t\t\tid = " + strconv.Itoa(allRouteMap[route.Name])
out += runBefore(route.RunBefore, 4) out += runBefore(route.RunBefore, 4)
out += "\n\t\t\tco.RouteViewCounter.Bump(" + strconv.Itoa(allRouteMap[route.Name]) + ")"
if !route.Action && !route.NoHead { if !route.Action && !route.NoHead {
out += "\n\t\t\thead, err := c.UserCheck(w,req,&user)" out += "\n\t\t\th, err := c.UserCheck(w,req,&user)"
out += "\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}" out += "\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}"
vcpy := route.Vars vcpy := route.Vars
route.Vars = []string{"head"} route.Vars = []string{"h"}
route.Vars = append(route.Vars, vcpy...) route.Vars = append(route.Vars, vcpy...)
} }
out += "\n\t\t\terr = " + strings.Replace(route.Name, "common.", "c.", -1) + "(w,req,user" out += "\n\t\t\terr = " + strings.Replace(route.Name, "common.", "c.", -1) + "(w,req,user"
@ -88,6 +89,11 @@ func main() {
out += "," + item out += "," + item
} }
out += `)` out += `)`
if !route.Action && !route.NoHead {
out += "\n\t\t\tco.RouteViewCounter.Bump2(" + strconv.Itoa(allRouteMap[route.Name]) + ", h.StartedAt)"
} else {
out += "\n\t\t\tco.RouteViewCounter.Bump(" + strconv.Itoa(allRouteMap[route.Name]) + ")"
}
} }
for _, group := range r.routeGroups { for _, group := range r.routeGroups {
@ -105,6 +111,7 @@ func main() {
mapIt(route.Name) mapIt(route.Name)
out += "\n\t\t\t\tcase \"" + route.Path + "\":" out += "\n\t\t\t\tcase \"" + route.Path + "\":"
//out += "\n\t\t\t\t\tid = " + strconv.Itoa(allRouteMap[route.Name])
if len(route.RunBefore) > 0 { if len(route.RunBefore) > 0 {
skipRunnable: skipRunnable:
for _, runnable := range route.RunBefore { for _, runnable := range route.RunBefore {
@ -136,12 +143,11 @@ func main() {
} }
} }
} }
out += "\n\t\t\t\t\tco.RouteViewCounter.Bump(" + strconv.Itoa(allRouteMap[route.Name]) + ")"
if !route.Action && !route.NoHead && !group.NoHead { if !route.Action && !route.NoHead && !group.NoHead {
out += "\n\t\t\t\thead, err := c.UserCheck(w,req,&user)" out += "\n\t\t\t\th, err := c.UserCheck(w,req,&user)"
out += "\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}" out += "\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}"
vcpy := route.Vars vcpy := route.Vars
route.Vars = []string{"head"} route.Vars = []string{"h"}
route.Vars = append(route.Vars, vcpy...) route.Vars = append(route.Vars, vcpy...)
} }
out += "\n\t\t\t\t\terr = " + strings.Replace(route.Name, "common.", "c.", -1) + "(w,req,user" out += "\n\t\t\t\t\terr = " + strings.Replace(route.Name, "common.", "c.", -1) + "(w,req,user"
@ -149,18 +155,23 @@ func main() {
out += "," + item out += "," + item
} }
out += ")" out += ")"
if !route.Action && !route.NoHead && !group.NoHead {
out += "\n\t\t\t\t\tco.RouteViewCounter.Bump2(" + strconv.Itoa(allRouteMap[route.Name]) + ", h.StartedAt)"
} else {
out += "\n\t\t\t\t\tco.RouteViewCounter.Bump(" + strconv.Itoa(allRouteMap[route.Name]) + ")"
}
} }
if defaultRoute.Name != "" { if defaultRoute.Name != "" {
mapIt(defaultRoute.Name) mapIt(defaultRoute.Name)
out += "\n\t\t\t\tdefault:" out += "\n\t\t\t\tdefault:"
//out += "\n\t\t\t\t\tid = " + strconv.Itoa(allRouteMap[defaultRoute.Name])
out += runBefore(defaultRoute.RunBefore, 4) out += runBefore(defaultRoute.RunBefore, 4)
out += "\n\t\t\t\t\tco.RouteViewCounter.Bump(" + strconv.Itoa(allRouteMap[defaultRoute.Name]) + ")"
if !defaultRoute.Action && !defaultRoute.NoHead && !group.NoHead { if !defaultRoute.Action && !defaultRoute.NoHead && !group.NoHead {
out += "\n\t\t\t\t\thead, err := c.UserCheck(w,req,&user)" out += "\n\t\t\t\t\th, err := c.UserCheck(w,req,&user)"
out += "\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}" out += "\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}"
vcpy := defaultRoute.Vars vcpy := defaultRoute.Vars
defaultRoute.Vars = []string{"head"} defaultRoute.Vars = []string{"h"}
defaultRoute.Vars = append(defaultRoute.Vars, vcpy...) defaultRoute.Vars = append(defaultRoute.Vars, vcpy...)
} }
out += "\n\t\t\t\t\terr = " + strings.Replace(defaultRoute.Name, "common.", "c.", -1) + "(w,req,user" out += "\n\t\t\t\t\terr = " + strings.Replace(defaultRoute.Name, "common.", "c.", -1) + "(w,req,user"
@ -168,6 +179,11 @@ func main() {
out += ", " + item out += ", " + item
} }
out += ")" out += ")"
if !defaultRoute.Action && !defaultRoute.NoHead && !group.NoHead {
out += "\n\t\t\t\t\tco.RouteViewCounter.Bump2(" + strconv.Itoa(allRouteMap[defaultRoute.Name]) + ", h.StartedAt)"
} else {
out += "\n\t\t\t\t\tco.RouteViewCounter.Bump(" + strconv.Itoa(allRouteMap[defaultRoute.Name]) + ")"
}
} }
out += ` out += `
}` }`
@ -180,6 +196,7 @@ func main() {
mapIt("routes.RobotsTxt") mapIt("routes.RobotsTxt")
mapIt("routes.SitemapXml") mapIt("routes.SitemapXml")
mapIt("routes.OpenSearchXml") mapIt("routes.OpenSearchXml")
mapIt("routes.Favicon")
mapIt("routes.BadRoute") mapIt("routes.BadRoute")
mapIt("routes.HTTPSRedirect") mapIt("routes.HTTPSRedirect")
tmplVars.AllRouteNames = allRouteNames tmplVars.AllRouteNames = allRouteNames
@ -462,7 +479,7 @@ func (r *GenRouter) DumpRequest(req *http.Request, prepend string) {
var heads string var heads string
for key, value := range req.Header { for key, value := range req.Header {
for _, vvalue := range value { for _, vvalue := range value {
heads += "Header '" + c.SanitiseSingleLine(key) + "': " + c.SanitiseSingleLine(vvalue) + "!\n" heads += "Header '" + c.SanitiseSingleLine(key) + "': " + c.SanitiseSingleLine(vvalue) + "\n"
} }
} }
@ -472,7 +489,7 @@ func (r *GenRouter) DumpRequest(req *http.Request, prepend string) {
"Host: " + c.SanitiseSingleLine(req.Host) + "\n" + "Host: " + c.SanitiseSingleLine(req.Host) + "\n" +
"URL.Path: " + c.SanitiseSingleLine(req.URL.Path) + "\n" + "URL.Path: " + c.SanitiseSingleLine(req.URL.Path) + "\n" +
"URL.RawQuery: " + c.SanitiseSingleLine(req.URL.RawQuery) + "\n" + "URL.RawQuery: " + c.SanitiseSingleLine(req.URL.RawQuery) + "\n" +
"Referer(): " + c.SanitiseSingleLine(req.Referer()) + "\n" + "Referer: " + c.SanitiseSingleLine(req.Referer()) + "\n" +
"RemoteAddr: " + req.RemoteAddr + "\n") "RemoteAddr: " + req.RemoteAddr + "\n")
} }
@ -562,9 +579,9 @@ func (r *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
break break
} }
} }
lowerPath := strings.ToLower(req.URL.Path) lp := strings.ToLower(req.URL.Path)
// TODO: Flag any requests which has a dot with anything but a number after that // TODO: Flag any requests which has a dot with anything but a number after that
if strings.Contains(req.URL.Path,"..") || strings.Contains(req.URL.Path,"--") || strings.Contains(lowerPath,".php") || strings.Contains(lowerPath,".asp") || strings.Contains(lowerPath,".cgi") || strings.Contains(lowerPath,".py") || strings.Contains(lowerPath,".sql") || strings.Contains(lowerPath,".action") { if strings.Contains(lp,"..")/* || strings.Contains(lp,"--")*/ || strings.Contains(lp,".php") || strings.Contains(lp,".asp") || strings.Contains(lp,".cgi") || strings.Contains(lp,".py") || strings.Contains(lp,".sql") || strings.Contains(lp,".action") {
r.SuspiciousRequest(req,"Bad snippet in path") r.SuspiciousRequest(req,"Bad snippet in path")
} }
@ -572,6 +589,7 @@ func (r *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
if req.URL.Path == "/" { if req.URL.Path == "/" {
req.URL.Path = c.Config.DefaultPath req.URL.Path = c.Config.DefaultPath
} }
//log.Print("URL.Path: ", req.URL.Path)
var prefix, extraData string var prefix, extraData string
prefix = req.URL.Path[0:strings.IndexByte(req.URL.Path[1:],'/') + 1] prefix = req.URL.Path[0:strings.IndexByte(req.URL.Path[1:],'/') + 1]
@ -603,10 +621,14 @@ func (r *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
r.DumpRequest(req,"before routes.StaticFile") r.DumpRequest(req,"before routes.StaticFile")
} }
// Increment the request counter // Increment the request counter
co.GlobalViewCounter.Bump() if !c.Config.DisableAnalytics {
co.GlobalViewCounter.Bump()
}
if prefix == "/s" { //old prefix: /static if prefix == "/s" { //old prefix: /static
co.RouteViewCounter.Bump({{index .AllRouteMap "routes.StaticFile"}}) if !c.Config.DisableAnalytics {
co.RouteViewCounter.Bump({{index .AllRouteMap "routes.StaticFile"}})
}
req.URL.Path += extraData req.URL.Path += extraData
routes.StaticFile(w, req) routes.StaticFile(w, req)
return return
@ -626,8 +648,10 @@ func (r *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// Track the user agents. Unfortunately, everyone pretends to be Mozilla, so this'll be a little less efficient than I would like. // Track the user agents. Unfortunately, everyone pretends to be Mozilla, so this'll be a little less efficient than I would like.
// TODO: Add a setting to disable this? // TODO: Add a setting to disable this?
// TODO: Use a more efficient detector instead of smashing every possible combination in // TODO: Use a more efficient detector instead of smashing every possible combination in
ua := strings.TrimSpace(strings.Replace(strings.TrimPrefix(req.UserAgent(),"Mozilla/5.0 ")," Safari/537.36","",-1)) // Noise, no one's going to be running this and it would require some sort of agent ranking system to determine which identifier should be prioritised over another
var agent string var agent string
if !c.Config.DisableAnalytics {
ua := strings.TrimSpace(strings.Replace(strings.TrimPrefix(req.UserAgent(),"Mozilla/5.0 ")," Safari/537.36","",-1)) // Noise, no one's going to be running this and it would require some sort of agent ranking system to determine which identifier should be prioritised over another
if ua == "" { if ua == "" {
co.AgentViewCounter.Bump({{.AllAgentMap.blank}}) co.AgentViewCounter.Bump({{.AllAgentMap.blank}})
if c.Dev.DebugMode { if c.Dev.DebugMode {
@ -736,8 +760,7 @@ func (r *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// TODO: Do we want to track missing language headers too? Maybe as it's own type, e.g. "noheader"? // TODO: Do we want to track missing language headers too? Maybe as it's own type, e.g. "noheader"?
lang := req.Header.Get("Accept-Language") lang := req.Header.Get("Accept-Language")
if lang != "" { if lang != "" {
lang = strings.TrimSpace(lang) lLang := strings.Split(strings.TrimSpace(lang),"-")
lLang := strings.Split(lang,"-")
tLang := strings.Split(strings.Split(lLang[0],";")[0],",") tLang := strings.Split(strings.Split(lLang[0],";")[0],",")
c.DebugDetail("tLang:", tLang) c.DebugDetail("tLang:", tLang)
var llLang string var llLang string
@ -764,12 +787,15 @@ func (r *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
ref = strings.TrimPrefix(strings.TrimPrefix(ref,"http://"),"https://") ref = strings.TrimPrefix(strings.TrimPrefix(ref,"http://"),"https://")
ref = strings.Split(ref,"/")[0] ref = strings.Split(ref,"/")[0]
portless := strings.Split(ref,":")[0] portless := strings.Split(ref,":")[0]
// TODO: Handle c.Site.Host in uppercase too?
if portless != "localhost" && portless != "127.0.0.1" && portless != c.Site.Host { if portless != "localhost" && portless != "127.0.0.1" && portless != c.Site.Host {
co.ReferrerTracker.Bump(ref) co.ReferrerTracker.Bump(ref)
} }
} }
} }
}
// Deal with the session stuff, etc. // Deal with the session stuff, etc.
user, ok := c.PreRoute(w, req) user, ok := c.PreRoute(w, req)
if !ok { if !ok {
@ -804,12 +830,15 @@ func (r *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
if ferr != nil { if ferr != nil {
r.handleError(ferr,w,req,user) r.handleError(ferr,w,req,user)
} }
/*if !c.Config.DisableAnalytics {
co.RouteViewCounter.Bump(id)
}*/
hTbl.VhookNoRet("router_end", w, req, user, prefix, extraData) hTbl.VhookNoRet("router_end", w, req, user, prefix, extraData)
//c.StoppedServer("Profile end") //c.StoppedServer("Profile end")
} }
func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c.User, prefix, extraData string) c.RouteError { func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c.User, prefix, extraData string) /*(id int, orerr */c.RouteError/*)*/ {
var err c.RouteError var err c.RouteError
switch(prefix) {` + out + ` switch(prefix) {` + out + `
/*case "/sitemaps": // TODO: Count these views /*case "/sitemaps": // TODO: Count these views
@ -817,6 +846,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
err = sitemapSwitch(w,req)*/ err = sitemapSwitch(w,req)*/
case "/uploads": case "/uploads":
if extraData == "" { if extraData == "" {
co.RouteViewCounter.Bump({{index .AllRouteMap "routes.UploadedFile"}})
return c.NotFound(w,req,nil) return c.NotFound(w,req,nil)
} }
gzw, ok := w.(c.GzipResponseWriter) gzw, ok := w.(c.GzipResponseWriter)
@ -826,10 +856,10 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
h.Del("Content-Type") h.Del("Content-Type")
h.Del("Content-Encoding") h.Del("Content-Encoding")
} }
co.RouteViewCounter.Bump({{index .AllRouteMap "routes.UploadedFile"}})
req.URL.Path += extraData req.URL.Path += extraData
// TODO: Find a way to propagate errors up from this? // TODO: Find a way to propagate errors up from this?
r.UploadHandler(w,req) // TODO: Count these views r.UploadHandler(w,req) // TODO: Count these views
co.RouteViewCounter.Bump({{index .AllRouteMap "routes.UploadedFile"}})
return nil return nil
case "": case "":
// Stop the favicons, robots.txt file, etc. resolving to the topics list // Stop the favicons, robots.txt file, etc. resolving to the topics list
@ -848,6 +878,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
} }
req.URL.Path = "/s/favicon.ico" req.URL.Path = "/s/favicon.ico"
routes.StaticFile(w,req) routes.StaticFile(w,req)
co.RouteViewCounter.Bump({{index .AllRouteMap "routes.Favicon"}})
return nil return nil
case "opensearch.xml": case "opensearch.xml":
co.RouteViewCounter.Bump({{index .AllRouteMap "routes.OpenSearchXml"}}) co.RouteViewCounter.Bump({{index .AllRouteMap "routes.OpenSearchXml"}})
@ -856,17 +887,19 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
co.RouteViewCounter.Bump({{index .AllRouteMap "routes.SitemapXml"}}) co.RouteViewCounter.Bump({{index .AllRouteMap "routes.SitemapXml"}})
return routes.SitemapXml(w,req)*/ return routes.SitemapXml(w,req)*/
} }
co.RouteViewCounter.Bump({{index .AllRouteMap "routes.Error"}})
return c.NotFound(w,req,nil) return c.NotFound(w,req,nil)
default: default:
// A fallback for dynamic routes, e.g. ones declared by plugins // A fallback for dynamic routes, e.g. ones declared by plugins
r.RLock() r.RLock()
handle, ok := r.extraRoutes[req.URL.Path] h, ok := r.extraRoutes[req.URL.Path]
r.RUnlock() r.RUnlock()
if ok { if ok {
co.RouteViewCounter.Bump({{index .AllRouteMap "routes.DynamicRoute"}}) // TODO: Be more specific about *which* dynamic route it is
req.URL.Path += extraData req.URL.Path += extraData
return handle(w,req,user) // TODO: Be more specific about *which* dynamic route it is
co.RouteViewCounter.Bump({{index .AllRouteMap "routes.DynamicRoute"}})
return h(w,req,user)
} }
lp := strings.ToLower(req.URL.Path) lp := strings.ToLower(req.URL.Path)
@ -904,5 +937,8 @@ func writeFile(name, content string) {
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
f.Close() err = f.Close()
if err != nil {
log.Fatal(err)
}
} }

View File

@ -63,8 +63,9 @@ func (r *RouteImpl) NoGzip() *RouteImpl {
return r.LitBeforeMultiline(`gzw, ok := w.(c.GzipResponseWriter) return r.LitBeforeMultiline(`gzw, ok := w.(c.GzipResponseWriter)
if ok { if ok {
w = gzw.ResponseWriter w = gzw.ResponseWriter
w.Header().Del("Content-Type") h := w.Header()
w.Header().Del("Content-Encoding") h.Del("Content-Type")
h.Del("Content-Encoding")
}`) }`)
} }
@ -77,15 +78,15 @@ func blankRoute() *RouteImpl {
return &RouteImpl{"", "", false, false, []string{}, []Runnable{}, nil} return &RouteImpl{"", "", false, false, []string{}, []Runnable{}, nil}
} }
func route(fname string, path string, action bool, special bool, args ...string) *RouteImpl { func route(fname, path string, action, special bool, args ...string) *RouteImpl {
return &RouteImpl{fname, path, action, special, args, []Runnable{}, nil} return &RouteImpl{fname, path, action, special, args, []Runnable{}, nil}
} }
func View(fname string, path string, args ...string) *RouteImpl { func View(fname, path string, args ...string) *RouteImpl {
return route(fname, path, false, false, args...) return route(fname, path, false, false, args...)
} }
func MView(fname string, path string, args ...string) *RouteImpl { func MView(fname, path string, args ...string) *RouteImpl {
route := route(fname, path, false, false, args...) route := route(fname, path, false, false, args...)
if !route.hasBefore("SuperModOnly", "AdminOnly") { if !route.hasBefore("SuperModOnly", "AdminOnly") {
route.Before("MemberOnly") route.Before("MemberOnly")
@ -93,7 +94,7 @@ func MView(fname string, path string, args ...string) *RouteImpl {
return route return route
} }
func MemberView(fname string, path string, args ...string) *RouteImpl { func MemberView(fname, path string, args ...string) *RouteImpl {
route := route(fname, path, false, false, args...) route := route(fname, path, false, false, args...)
if !route.hasBefore("SuperModOnly", "AdminOnly") { if !route.hasBefore("SuperModOnly", "AdminOnly") {
route.Before("MemberOnly") route.Before("MemberOnly")
@ -101,7 +102,7 @@ func MemberView(fname string, path string, args ...string) *RouteImpl {
return route return route
} }
func ModView(fname string, path string, args ...string) *RouteImpl { func ModView(fname, path string, args ...string) *RouteImpl {
route := route(fname, path, false, false, args...) route := route(fname, path, false, false, args...)
if !route.hasBefore("AdminOnly") { if !route.hasBefore("AdminOnly") {
route.Before("SuperModOnly") route.Before("SuperModOnly")
@ -109,7 +110,7 @@ func ModView(fname string, path string, args ...string) *RouteImpl {
return route return route
} }
func Action(fname string, path string, args ...string) *RouteImpl { func Action(fname, path string, args ...string) *RouteImpl {
route := route(fname, path, true, false, args...) route := route(fname, path, true, false, args...)
route.Before("NoSessionMismatch") route.Before("NoSessionMismatch")
if !route.hasBefore("SuperModOnly", "AdminOnly") { if !route.hasBefore("SuperModOnly", "AdminOnly") {
@ -118,11 +119,11 @@ func Action(fname string, path string, args ...string) *RouteImpl {
return route return route
} }
func AnonAction(fname string, path string, args ...string) *RouteImpl { func AnonAction(fname, path string, args ...string) *RouteImpl {
return route(fname, path, true, false, args...).Before("ParseForm") return route(fname, path, true, false, args...).Before("ParseForm")
} }
func Special(fname string, path string, args ...string) *RouteImpl { func Special(fname, path string, args ...string) *RouteImpl {
return route(fname, path, false, true, args...).LitBefore("req.URL.Path += extraData") return route(fname, path, false, true, args...).LitBefore("req.URL.Path += extraData")
} }
@ -131,7 +132,7 @@ type uploadAction struct {
Route *RouteImpl Route *RouteImpl
} }
func UploadAction(fname string, path string, args ...string) *uploadAction { func UploadAction(fname, path string, args ...string) *uploadAction {
route := route(fname, path, true, false, args...) route := route(fname, path, true, false, args...)
if !route.hasBefore("SuperModOnly", "AdminOnly") { if !route.hasBefore("SuperModOnly", "AdminOnly") {
route.Before("MemberOnly") route.Before("MemberOnly")
@ -154,6 +155,6 @@ type RouteSet struct {
Items []*RouteImpl Items []*RouteImpl
} }
func Set(name string, path string, routes ...*RouteImpl) RouteSet { func Set(name, path string, routes ...*RouteImpl) RouteSet {
return RouteSet{name, path, routes} return RouteSet{name, path, routes}
} }

View File

@ -7,6 +7,12 @@ func UploadedFile() {
} }
func BadRoute() { func BadRoute() {
} }
func Favicon() {
}
// TODO: Temporary stub for handling route group errors
func Error() {
}
// Real implementation is in router_gen/main.go, this is just a stub to map the analytics onto // Real implementation is in router_gen/main.go, this is just a stub to map the analytics onto
func HTTPSRedirect() { func HTTPSRedirect() {

View File

@ -1,5 +1,6 @@
CREATE TABLE [viewchunks] ( CREATE TABLE [viewchunks] (
[count] int DEFAULT 0 not null, [count] int DEFAULT 0 not null,
[avg] int DEFAULT 0 not null,
[createdAt] datetime not null, [createdAt] datetime not null,
[route] nvarchar (200) not null [route] nvarchar (200) not null
); );

View File

@ -1,5 +1,6 @@
CREATE TABLE `viewchunks` ( CREATE TABLE `viewchunks` (
`count` int DEFAULT 0 not null, `count` int DEFAULT 0 not null,
`avg` int DEFAULT 0 not null,
`createdAt` datetime not null, `createdAt` datetime not null,
`route` varchar(200) not null `route` varchar(200) not null
); );

View File

@ -1,5 +1,6 @@
CREATE TABLE "viewchunks" ( CREATE TABLE "viewchunks" (
`count` int DEFAULT 0 not null, `count` int DEFAULT 0 not null,
`avg` int DEFAULT 0 not null,
`createdAt` timestamp not null, `createdAt` timestamp not null,
`route` varchar (200) not null `route` varchar (200) not null
); );