Experimenting with speeding up the installer.
Added support for foreign keys to the MySQL adapter. activity_stream_matches now has a foreign key to help enforce referential integrity. Added the AddForeignKey method to the database adapters. Shortened a couple of API bits to ever slow slightly reduce the lengths of the strings. Fixed a phrase group I missed for logged out users in init.js Fixed a bug where deleting a topic would break the alert list when there is an alert event relating to it. You will need to run the updater / patcher for this commit.
This commit is contained in:
parent
634b03936c
commit
839df17de3
@ -14,7 +14,7 @@ func NewPrimaryKeySpitter() *PrimaryKeySpitter {
|
||||
func (spit *PrimaryKeySpitter) Hook(name string, args ...interface{}) error {
|
||||
if name == "CreateTableStart" {
|
||||
var found string
|
||||
var table = args[0].(*qgen.DB_Install_Table)
|
||||
var table = args[0].(*qgen.DBInstallTable)
|
||||
for _, key := range table.Keys {
|
||||
if key.Type == "primary" {
|
||||
expl := strings.Split(key.Columns, ",")
|
||||
|
@ -45,8 +45,8 @@ func createTables(adapter qgen.Adapter) error {
|
||||
tblColumn{"temp_group", "int", 0, false, false, "0"}, // For temporary groups, set this to zero when a temporary group isn't in effect
|
||||
},
|
||||
[]tblKey{
|
||||
tblKey{"uid", "primary"},
|
||||
tblKey{"name", "unique"},
|
||||
tblKey{"uid", "primary","",false},
|
||||
tblKey{"name", "unique","",false},
|
||||
},
|
||||
)
|
||||
|
||||
@ -64,7 +64,7 @@ func createTables(adapter qgen.Adapter) error {
|
||||
tblColumn{"tag", "varchar", 50, false, false, "''"},
|
||||
},
|
||||
[]tblKey{
|
||||
tblKey{"gid", "primary"},
|
||||
tblKey{"gid", "primary","",false},
|
||||
},
|
||||
)
|
||||
|
||||
@ -83,7 +83,7 @@ func createTables(adapter qgen.Adapter) error {
|
||||
tblColumn{"createdAt", "createdAt", 0, false, false, ""},
|
||||
},
|
||||
[]tblKey{
|
||||
tblKey{"uid", "primary"},
|
||||
tblKey{"uid", "primary","",false},
|
||||
},
|
||||
)
|
||||
|
||||
@ -128,7 +128,7 @@ func createTables(adapter qgen.Adapter) error {
|
||||
tblColumn{"temporary", "boolean", 0, false, false, ""}, // special case for permanent bans to do the necessary bookkeeping, might be removed in the future
|
||||
},
|
||||
[]tblKey{
|
||||
tblKey{"uid", "primary"},
|
||||
tblKey{"uid", "primary","",false},
|
||||
},
|
||||
)
|
||||
|
||||
@ -138,7 +138,7 @@ func createTables(adapter qgen.Adapter) error {
|
||||
tblColumn{"uid", "int", 0, false, false, ""}, // TODO: Make this a foreign key
|
||||
},
|
||||
[]tblKey{
|
||||
tblKey{"uid", "primary"},
|
||||
tblKey{"uid", "primary","",false},
|
||||
},
|
||||
)
|
||||
|
||||
@ -191,7 +191,7 @@ func createTables(adapter qgen.Adapter) error {
|
||||
tblColumn{"lastReplyerID", "int", 0, false, false, "0"},
|
||||
},
|
||||
[]tblKey{
|
||||
tblKey{"fid", "primary"},
|
||||
tblKey{"fid", "primary","",false},
|
||||
},
|
||||
)
|
||||
|
||||
@ -204,7 +204,7 @@ func createTables(adapter qgen.Adapter) error {
|
||||
},
|
||||
[]tblKey{
|
||||
// TODO: Test to see that the compound primary key works
|
||||
tblKey{"fid,gid", "primary"},
|
||||
tblKey{"fid,gid", "primary","",false},
|
||||
},
|
||||
)
|
||||
|
||||
@ -240,8 +240,8 @@ func createTables(adapter qgen.Adapter) error {
|
||||
tblColumn{"data", "varchar", 200, false, false, "''"},
|
||||
},
|
||||
[]tblKey{
|
||||
tblKey{"tid", "primary"},
|
||||
tblKey{"content", "fulltext"},
|
||||
tblKey{"tid", "primary","",false},
|
||||
tblKey{"content", "fulltext","",false},
|
||||
},
|
||||
)
|
||||
|
||||
@ -264,8 +264,8 @@ func createTables(adapter qgen.Adapter) error {
|
||||
tblColumn{"poll", "int", 0, false, false, "0"},
|
||||
},
|
||||
[]tblKey{
|
||||
tblKey{"rid", "primary"},
|
||||
tblKey{"content", "fulltext"},
|
||||
tblKey{"rid", "primary","",false},
|
||||
tblKey{"content", "fulltext","",false},
|
||||
},
|
||||
)
|
||||
|
||||
@ -281,7 +281,7 @@ func createTables(adapter qgen.Adapter) error {
|
||||
tblColumn{"extra", "varchar", 200, false, false, ""},
|
||||
},
|
||||
[]tblKey{
|
||||
tblKey{"attachID", "primary"},
|
||||
tblKey{"attachID", "primary","",false},
|
||||
},
|
||||
)
|
||||
|
||||
@ -295,7 +295,7 @@ func createTables(adapter qgen.Adapter) error {
|
||||
// TODO: Add a createdBy column?
|
||||
},
|
||||
[]tblKey{
|
||||
tblKey{"reviseID", "primary"},
|
||||
tblKey{"reviseID", "primary","",false},
|
||||
},
|
||||
)
|
||||
|
||||
@ -309,7 +309,7 @@ func createTables(adapter qgen.Adapter) error {
|
||||
tblColumn{"votes", "int", 0, false, false, "0"},
|
||||
},
|
||||
[]tblKey{
|
||||
tblKey{"pollID", "primary"},
|
||||
tblKey{"pollID", "primary","",false},
|
||||
},
|
||||
)
|
||||
|
||||
@ -344,7 +344,7 @@ func createTables(adapter qgen.Adapter) error {
|
||||
tblColumn{"ipaddress", "varchar", 200, false, false, "0.0.0.0.0"},
|
||||
},
|
||||
[]tblKey{
|
||||
tblKey{"rid", "primary"},
|
||||
tblKey{"rid", "primary","",false},
|
||||
},
|
||||
)
|
||||
|
||||
@ -363,7 +363,10 @@ func createTables(adapter qgen.Adapter) error {
|
||||
[]tblColumn{
|
||||
tblColumn{"watcher", "int", 0, false, false, ""}, // TODO: Make this a foreign key
|
||||
tblColumn{"asid", "int", 0, false, false, ""}, // TODO: Make this a foreign key
|
||||
}, nil,
|
||||
},
|
||||
[]tblKey{
|
||||
tblKey{"asid,asid","foreign","activity_stream",true},
|
||||
},
|
||||
)
|
||||
|
||||
qgen.Install.CreateTable("activity_stream", "", "",
|
||||
@ -376,7 +379,7 @@ func createTables(adapter qgen.Adapter) error {
|
||||
tblColumn{"elementID", "int", 0, false, false, ""}, /* the ID of the element being acted upon */
|
||||
},
|
||||
[]tblKey{
|
||||
tblKey{"asid", "primary"},
|
||||
tblKey{"asid", "primary","",false},
|
||||
},
|
||||
)
|
||||
|
||||
@ -398,7 +401,7 @@ func createTables(adapter qgen.Adapter) error {
|
||||
tblColumn{"constraints", "varchar", 200, false, false, "''"},
|
||||
},
|
||||
[]tblKey{
|
||||
tblKey{"name", "unique"},
|
||||
tblKey{"name", "unique","",false},
|
||||
},
|
||||
)
|
||||
|
||||
@ -409,7 +412,7 @@ func createTables(adapter qgen.Adapter) error {
|
||||
tblColumn{"replacement", "varchar", 200, false, false, ""},
|
||||
},
|
||||
[]tblKey{
|
||||
tblKey{"wfid", "primary"},
|
||||
tblKey{"wfid", "primary","",false},
|
||||
},
|
||||
)
|
||||
|
||||
@ -420,7 +423,7 @@ func createTables(adapter qgen.Adapter) error {
|
||||
tblColumn{"installed", "boolean", 0, false, false, "0"},
|
||||
},
|
||||
[]tblKey{
|
||||
tblKey{"uname", "unique"},
|
||||
tblKey{"uname", "unique","",false},
|
||||
},
|
||||
)
|
||||
|
||||
@ -431,7 +434,7 @@ func createTables(adapter qgen.Adapter) error {
|
||||
//tblColumn{"profileUserVars", "text", 0, false, false, "''"},
|
||||
},
|
||||
[]tblKey{
|
||||
tblKey{"uname", "unique"},
|
||||
tblKey{"uname", "unique","",false},
|
||||
},
|
||||
)
|
||||
|
||||
@ -446,7 +449,7 @@ func createTables(adapter qgen.Adapter) error {
|
||||
tblColumn{"data", "text", 0, false, false, "''"},
|
||||
},
|
||||
[]tblKey{
|
||||
tblKey{"wid", "primary"},
|
||||
tblKey{"wid", "primary","",false},
|
||||
},
|
||||
)
|
||||
|
||||
@ -455,7 +458,7 @@ func createTables(adapter qgen.Adapter) error {
|
||||
tblColumn{"mid", "int", 0, false, true, ""},
|
||||
},
|
||||
[]tblKey{
|
||||
tblKey{"mid", "primary"},
|
||||
tblKey{"mid", "primary","",false},
|
||||
},
|
||||
)
|
||||
|
||||
@ -479,7 +482,7 @@ func createTables(adapter qgen.Adapter) error {
|
||||
tblColumn{"adminOnly", "boolean", 0, false, false, "0"},
|
||||
},
|
||||
[]tblKey{
|
||||
tblKey{"miid", "primary"},
|
||||
tblKey{"miid", "primary","",false},
|
||||
},
|
||||
)
|
||||
|
||||
@ -495,7 +498,7 @@ func createTables(adapter qgen.Adapter) error {
|
||||
tblColumn{"menuID", "int", 0, false, false, "-1"}, // simple sidebar menu
|
||||
},
|
||||
[]tblKey{
|
||||
tblKey{"pid", "primary"},
|
||||
tblKey{"pid", "primary","",false},
|
||||
},
|
||||
)
|
||||
|
||||
@ -510,7 +513,7 @@ func createTables(adapter qgen.Adapter) error {
|
||||
tblColumn{"doneAt", "createdAt", 0, false, false, ""},
|
||||
},
|
||||
[]tblKey{
|
||||
tblKey{"rlid", "primary"},
|
||||
tblKey{"rlid", "primary","",false},
|
||||
},
|
||||
)
|
||||
|
||||
@ -523,7 +526,7 @@ func createTables(adapter qgen.Adapter) error {
|
||||
tblColumn{"doneAt", "createdAt", 0, false, false, ""},
|
||||
},
|
||||
[]tblKey{
|
||||
tblKey{"lid", "primary"},
|
||||
tblKey{"lid", "primary","",false},
|
||||
},
|
||||
)
|
||||
|
||||
|
@ -180,6 +180,8 @@ type TopicStmts struct {
|
||||
createLike *sql.Stmt
|
||||
addLikesToTopic *sql.Stmt
|
||||
delete *sql.Stmt
|
||||
deleteActivity *sql.Stmt
|
||||
deleteActivitySubs *sql.Stmt
|
||||
edit *sql.Stmt
|
||||
setPoll *sql.Stmt
|
||||
createAction *sql.Stmt
|
||||
@ -204,6 +206,8 @@ func init() {
|
||||
createLike: acc.Insert("likes").Columns("weight, targetItem, targetType, sentBy, createdAt").Fields("?,?,?,?,UTC_TIMESTAMP()").Prepare(),
|
||||
addLikesToTopic: acc.Update("topics").Set("likeCount = likeCount + ?").Where("tid = ?").Prepare(),
|
||||
delete: acc.Delete("topics").Where("tid = ?").Prepare(),
|
||||
deleteActivity: acc.Delete("activity_stream").Where("elementID = ? AND elementType = 'topic'").Prepare(),
|
||||
deleteActivitySubs: acc.Delete("activity_subscriptions").Where("targetID = ? AND targetType = 'topic'").Prepare(),
|
||||
edit: acc.Update("topics").Set("title = ?, content = ?, parsed_content = ?").Where("tid = ?").Prepare(), // TODO: Only run the content update bits on non-polls, does this matter?
|
||||
setPoll: acc.Update("topics").Set("content = '', parsed_content = '', poll = ?").Where("tid = ? AND poll = 0").Prepare(),
|
||||
createAction: acc.Insert("replies").Columns("tid, actionType, ipaddress, createdBy, createdAt, lastUpdated, content, parsed_content").Fields("?,?,?,?,UTC_TIMESTAMP(),UTC_TIMESTAMP(),'',''").Prepare(),
|
||||
@ -325,6 +329,14 @@ func (topic *Topic) Delete() error {
|
||||
|
||||
_, err = topicStmts.delete.Exec(topic.ID)
|
||||
topic.cacheRemove()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = topicStmts.deleteActivitySubs.Exec(topic.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = topicStmts.deleteActivity.Exec(topic.ID)
|
||||
return err
|
||||
}
|
||||
|
||||
|
131
install/mysql.go
131
install/mysql.go
@ -10,10 +10,12 @@ import (
|
||||
"bytes"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"os"
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/Azareal/Gosora/query_gen"
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
@ -93,53 +95,140 @@ func (ins *MysqlInstaller) InitDatabase() (err error) {
|
||||
fmt.Println("The database was successfully created")
|
||||
}
|
||||
|
||||
fmt.Println("Switching to database ", ins.dbName)
|
||||
/*fmt.Println("Switching to database ", ins.dbName)
|
||||
_, err = db.Exec("USE " + ins.dbName)
|
||||
if err != nil {
|
||||
return err
|
||||
}*/
|
||||
db.Close()
|
||||
|
||||
db, err = sql.Open("mysql", ins.dbUsername+_dbPassword+"@tcp("+ins.dbHost+":"+ins.dbPort+")/" + ins.dbName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Make sure that the connection is alive..
|
||||
err = db.Ping()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println("Successfully connected to the database")
|
||||
|
||||
// Ready the query builder
|
||||
ins.db = db
|
||||
qgen.Builder.SetConn(db)
|
||||
return qgen.Builder.SetAdapter("mysql")
|
||||
}
|
||||
|
||||
func(ins *MysqlInstaller) createTable(f os.FileInfo) error {
|
||||
table := strings.TrimPrefix(f.Name(), "query_")
|
||||
ext := filepath.Ext(table)
|
||||
if ext != ".sql" {
|
||||
return nil
|
||||
}
|
||||
table = strings.TrimSuffix(table, ext)
|
||||
|
||||
// ? - This is mainly here for tests, although it might allow the installer to overwrite a production database, so we might want to proceed with caution
|
||||
q := "DROP TABLE IF EXISTS `" + table + "`;"
|
||||
_, err := ins.db.Exec(q)
|
||||
if err != nil {
|
||||
fmt.Println("Failed query:", q)
|
||||
fmt.Println("e:",err)
|
||||
return err
|
||||
}
|
||||
|
||||
data, err := ioutil.ReadFile("./schema/mysql/" + f.Name())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data = bytes.TrimSpace(data)
|
||||
|
||||
_, err = ins.db.Exec(string(data))
|
||||
if err != nil {
|
||||
fmt.Println("Failed query:", string(data))
|
||||
fmt.Println("e:",err)
|
||||
return err
|
||||
}
|
||||
fmt.Printf("Created table '%s'\n", table)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ins *MysqlInstaller) TableDefs() (err error) {
|
||||
fmt.Println("Creating the tables")
|
||||
files, _ := ioutil.ReadDir("./schema/mysql/")
|
||||
for _, f := range files {
|
||||
files, err := ioutil.ReadDir("./schema/mysql/")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// TODO: Can we reduce the amount of boilerplate here?
|
||||
after := []string{"activity_stream_matches"}
|
||||
c1 := make(chan os.FileInfo)
|
||||
c2 := make(chan os.FileInfo)
|
||||
e := make(chan error)
|
||||
var wg sync.WaitGroup
|
||||
r := func(c chan os.FileInfo) {
|
||||
wg.Add(1)
|
||||
for f := range c {
|
||||
err := ins.createTable(f)
|
||||
if err != nil {
|
||||
e <- err
|
||||
}
|
||||
}
|
||||
wg.Done()
|
||||
}
|
||||
go r(c1)
|
||||
go r(c2)
|
||||
|
||||
var a []os.FileInfo
|
||||
Outer:
|
||||
for i, f := range files {
|
||||
if !strings.HasPrefix(f.Name(), "query_") {
|
||||
continue
|
||||
}
|
||||
|
||||
var table, ext string
|
||||
table = strings.TrimPrefix(f.Name(), "query_")
|
||||
ext = filepath.Ext(table)
|
||||
table := strings.TrimPrefix(f.Name(), "query_")
|
||||
ext := filepath.Ext(table)
|
||||
if ext != ".sql" {
|
||||
continue
|
||||
}
|
||||
table = strings.TrimSuffix(table, ext)
|
||||
|
||||
// ? - This is mainly here for tests, although it might allow the installer to overwrite a production database, so we might want to proceed with caution
|
||||
_, err = ins.db.Exec("DROP TABLE IF EXISTS `" + table + "`;")
|
||||
if err != nil {
|
||||
fmt.Println("Failed query:", "DROP TABLE IF EXISTS `"+table+"`;")
|
||||
return err
|
||||
for _, tbl := range after {
|
||||
if tbl == table {
|
||||
a = append(a, f)
|
||||
continue Outer
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("Creating table '%s'\n", table)
|
||||
data, err := ioutil.ReadFile("./schema/mysql/" + f.Name())
|
||||
if err != nil {
|
||||
return err
|
||||
if i%2 == 0 {
|
||||
c1 <- f
|
||||
} else {
|
||||
c2 <- f
|
||||
}
|
||||
data = bytes.TrimSpace(data)
|
||||
}
|
||||
close(c1)
|
||||
close(c2)
|
||||
wg.Wait()
|
||||
close(e)
|
||||
|
||||
var first error
|
||||
for err := range e {
|
||||
if first == nil {
|
||||
first = err
|
||||
}
|
||||
}
|
||||
if first != nil {
|
||||
return first
|
||||
}
|
||||
|
||||
_, err = ins.db.Exec(string(data))
|
||||
for _, f := range a {
|
||||
if !strings.HasPrefix(f.Name(), "query_") {
|
||||
continue
|
||||
}
|
||||
err := ins.createTable(f)
|
||||
if err != nil {
|
||||
fmt.Println("Failed query:", string(data))
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -32,6 +32,7 @@ func init() {
|
||||
addPatch(17, patch17)
|
||||
addPatch(18, patch18)
|
||||
addPatch(19, patch19)
|
||||
addPatch(20, patch20)
|
||||
}
|
||||
|
||||
func patch0(scanner *bufio.Scanner) (err error) {
|
||||
@ -49,7 +50,7 @@ func patch0(scanner *bufio.Scanner) (err error) {
|
||||
tblColumn{"mid", "int", 0, false, true, ""},
|
||||
},
|
||||
[]tblKey{
|
||||
tblKey{"mid", "primary"},
|
||||
tblKey{"mid", "primary","",false},
|
||||
},
|
||||
))
|
||||
if err != nil {
|
||||
@ -76,7 +77,7 @@ func patch0(scanner *bufio.Scanner) (err error) {
|
||||
tblColumn{"adminOnly", "boolean", 0, false, false, "0"},
|
||||
},
|
||||
[]tblKey{
|
||||
tblKey{"miid", "primary"},
|
||||
tblKey{"miid", "primary","",false},
|
||||
},
|
||||
))
|
||||
if err != nil {
|
||||
@ -183,7 +184,7 @@ func patch3(scanner *bufio.Scanner) error {
|
||||
tblColumn{"doneAt", "createdAt", 0, false, false, ""},
|
||||
},
|
||||
[]tblKey{
|
||||
tblKey{"rlid", "primary"},
|
||||
tblKey{"rlid", "primary","",false},
|
||||
},
|
||||
))
|
||||
}
|
||||
@ -246,7 +247,7 @@ func patch4(scanner *bufio.Scanner) error {
|
||||
tblColumn{"menuID", "int", 0, false, false, "-1"},
|
||||
},
|
||||
[]tblKey{
|
||||
tblKey{"pid", "primary"},
|
||||
tblKey{"pid", "primary","",false},
|
||||
},
|
||||
))
|
||||
if err != nil {
|
||||
@ -289,7 +290,7 @@ func patch5(scanner *bufio.Scanner) error {
|
||||
tblColumn{"createdAt", "createdAt", 0, false, false, ""},
|
||||
},
|
||||
[]tblKey{
|
||||
tblKey{"uid", "primary"},
|
||||
tblKey{"uid", "primary","",false},
|
||||
},
|
||||
))
|
||||
if err != nil {
|
||||
@ -309,7 +310,7 @@ func patch7(scanner *bufio.Scanner) error {
|
||||
tblColumn{"uid", "int", 0, false, false, ""}, // TODO: Make this a foreign key
|
||||
},
|
||||
[]tblKey{
|
||||
tblKey{"uid", "primary"},
|
||||
tblKey{"uid", "primary","",false},
|
||||
},
|
||||
))
|
||||
}
|
||||
@ -391,7 +392,7 @@ func patch9(scanner *bufio.Scanner) error {
|
||||
tblColumn{"doneAt", "createdAt", 0, false, false, ""},
|
||||
},
|
||||
[]tblKey{
|
||||
tblKey{"lid", "primary"},
|
||||
tblKey{"lid", "primary","",false},
|
||||
},
|
||||
))
|
||||
}
|
||||
@ -514,7 +515,7 @@ func patch12(scanner *bufio.Scanner) error {
|
||||
}
|
||||
|
||||
func patch13(scanner *bufio.Scanner) error {
|
||||
err := execStmt(qgen.Builder.AddColumn("widgets", tblColumn{"wid", "int", 0, false, true, ""}, &tblKey{"wid", "primary"}))
|
||||
err := execStmt(qgen.Builder.AddColumn("widgets", tblColumn{"wid", "int", 0, false, true, ""}, &tblKey{"wid", "primary","",false}))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -523,15 +524,15 @@ func patch13(scanner *bufio.Scanner) error {
|
||||
}
|
||||
|
||||
func patch14(scanner *bufio.Scanner) error {
|
||||
err := execStmt(qgen.Builder.AddKey("topics", "title", tblKey{"title", "fulltext"}))
|
||||
err := execStmt(qgen.Builder.AddKey("topics", "title", tblKey{"title", "fulltext","",false}))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = execStmt(qgen.Builder.AddKey("topics", "content", tblKey{"content", "fulltext"}))
|
||||
err = execStmt(qgen.Builder.AddKey("topics", "content", tblKey{"content", "fulltext","",false}))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = execStmt(qgen.Builder.AddKey("replies", "content", tblKey{"content", "fulltext"}))
|
||||
err = execStmt(qgen.Builder.AddKey("replies", "content", tblKey{"content", "fulltext","",false}))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -602,4 +603,27 @@ func patch19(scanner *bufio.Scanner) error {
|
||||
tblColumn{"createdAt", "datetime", 0, false, false, ""},
|
||||
}, nil,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
func patch20(scanner *bufio.Scanner) error {
|
||||
err := acc().Select("activity_stream_matches").Cols("asid").Each(func(rows *sql.Rows) error {
|
||||
var asid int
|
||||
err := rows.Scan(&asid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = acc().Select("activity_stream").Cols("asid").Where("asid = ?").QueryRow(asid).Scan(&asid)
|
||||
if err != sql.ErrNoRows {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = acc().Delete("activity_stream_matches").Where("asid = ?").Run(asid)
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return execStmt(qgen.Builder.AddForeignKey("activity_stream_matches", "asid","activity_stream","asid",true))
|
||||
}
|
||||
|
@ -138,7 +138,7 @@ function loadAlerts(menuAlerts) {
|
||||
$.ajax({
|
||||
type: 'get',
|
||||
dataType: 'json',
|
||||
url:'/api/?action=get&module=alerts',
|
||||
url:'/api/?module=alerts',
|
||||
success: (data) => {
|
||||
if("errmsg" in data) {
|
||||
setAlertError(menuAlerts,data.errmsg)
|
||||
@ -147,8 +147,8 @@ function loadAlerts(menuAlerts) {
|
||||
alertList = [];
|
||||
alertMapping = {};
|
||||
for(var i in data.msgs) addAlert(data.msgs[i]);
|
||||
console.log("data.msgCount:",data.msgCount)
|
||||
alertCount = data.msgCount;
|
||||
console.log("data.count:",data.count)
|
||||
alertCount = data.count;
|
||||
updateAlertList(menuAlerts)
|
||||
},
|
||||
error: (magic,theStatus,error) => {
|
||||
|
@ -178,7 +178,7 @@ function initPhrases(loggedIn, panel = false) {
|
||||
console.log("in initPhrases")
|
||||
console.log("tmlInits:",tmplInits)
|
||||
let e = "";
|
||||
if(loggedIn && !panel) e = ",topic_list,topic";
|
||||
if(loggedIn && !panel) e = ",status,topic_list,topic";
|
||||
else if(panel) e = ",analytics,panel"; // TODO: Request phrases for just one section of the control panel?
|
||||
else e = ",status,topic_list";
|
||||
fetchPhrases("alerts,paginator"+e) // TODO: Break this up?
|
||||
|
@ -120,6 +120,10 @@ func (build *builder) AddKey(table string, column string, key DBTableKey) (stmt
|
||||
return build.prepare(build.adapter.AddKey("", table, column, key))
|
||||
}
|
||||
|
||||
func (build *builder) AddForeignKey(table string, column string, ftable string, fcolumn string, cascade bool) (stmt *sql.Stmt, err error) {
|
||||
return build.prepare(build.adapter.AddForeignKey("", table, column, ftable, fcolumn, cascade))
|
||||
}
|
||||
|
||||
func (build *builder) SimpleInsert(table string, columns string, fields string) (stmt *sql.Stmt, err error) {
|
||||
return build.prepare(build.adapter.SimpleInsert("", table, columns, fields))
|
||||
}
|
||||
|
@ -3,17 +3,17 @@ package qgen
|
||||
var Install *installer
|
||||
|
||||
func init() {
|
||||
Install = &installer{instructions: []DB_Install_Instruction{}}
|
||||
Install = &installer{instructions: []DBInstallInstruction{}}
|
||||
}
|
||||
|
||||
type DB_Install_Instruction struct {
|
||||
type DBInstallInstruction struct {
|
||||
Table string
|
||||
Contents string
|
||||
Type string
|
||||
}
|
||||
|
||||
// TODO: Add methods to this to construct it OO-like
|
||||
type DB_Install_Table struct {
|
||||
type DBInstallTable struct {
|
||||
Name string
|
||||
Charset string
|
||||
Collation string
|
||||
@ -25,8 +25,8 @@ type DB_Install_Table struct {
|
||||
// TODO: Re-implement the query generation, query builder and installer adapters as layers on-top of a query text adapter
|
||||
type installer struct {
|
||||
adapter Adapter
|
||||
instructions []DB_Install_Instruction
|
||||
tables []*DB_Install_Table // TODO: Use this in Record() in the next commit to allow us to auto-migrate settings rather than manually patching them in on upgrade
|
||||
instructions []DBInstallInstruction
|
||||
tables []*DBInstallTable // TODO: Use this in Record() in the next commit to allow us to auto-migrate settings rather than manually patching them in on upgrade
|
||||
plugins []QueryPlugin
|
||||
}
|
||||
|
||||
@ -41,7 +41,7 @@ func (install *installer) SetAdapter(name string) error {
|
||||
|
||||
func (install *installer) SetAdapterInstance(adapter Adapter) {
|
||||
install.adapter = adapter
|
||||
install.instructions = []DB_Install_Instruction{}
|
||||
install.instructions = []DBInstallInstruction{}
|
||||
}
|
||||
|
||||
func (install *installer) AddPlugins(plugins ...QueryPlugin) {
|
||||
@ -49,7 +49,7 @@ func (install *installer) AddPlugins(plugins ...QueryPlugin) {
|
||||
}
|
||||
|
||||
func (install *installer) CreateTable(table string, charset string, collation string, columns []DBTableColumn, keys []DBTableKey) error {
|
||||
tableStruct := &DB_Install_Table{table, charset, collation, columns, keys}
|
||||
tableStruct := &DBInstallTable{table, charset, collation, columns, keys}
|
||||
err := install.RunHook("CreateTableStart", tableStruct)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -62,7 +62,7 @@ func (install *installer) CreateTable(table string, charset string, collation st
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
install.instructions = append(install.instructions, DB_Install_Instruction{table, res, "create-table"})
|
||||
install.instructions = append(install.instructions, DBInstallInstruction{table, res, "create-table"})
|
||||
install.tables = append(install.tables, tableStruct)
|
||||
return nil
|
||||
}
|
||||
@ -81,7 +81,7 @@ func (install *installer) AddIndex(table string, iname string, colname string) e
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
install.instructions = append(install.instructions, DB_Install_Instruction{table, res, "index"})
|
||||
install.instructions = append(install.instructions, DBInstallInstruction{table, res, "index"})
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -99,7 +99,7 @@ func (install *installer) SimpleInsert(table string, columns string, fields stri
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
install.instructions = append(install.instructions, DB_Install_Instruction{table, res, "insert"})
|
||||
install.instructions = append(install.instructions, DBInstallInstruction{table, res, "insert"})
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -53,6 +53,7 @@ func (adapter *MssqlAdapter) DropTable(name string, table string) (string, error
|
||||
return querystr, nil
|
||||
}
|
||||
|
||||
// TODO: Add support for foreign keys?
|
||||
// TODO: Convert any remaining stringy types to nvarchar
|
||||
// We may need to change the CreateTable API to better suit Mssql and the other database drivers which are coming up
|
||||
func (adapter *MssqlAdapter) CreateTable(name string, table string, charset string, collation string, columns []DBTableColumn, keys []DBTableKey) (string, error) {
|
||||
@ -174,6 +175,25 @@ func (adapter *MssqlAdapter) AddKey(name string, table string, column string, ke
|
||||
return "", errors.New("not implemented")
|
||||
}
|
||||
|
||||
// TODO: Implement this
|
||||
// TODO: Test to make sure everything works here
|
||||
func (adapter *MssqlAdapter) AddForeignKey(name string, table string, column string, ftable string, fcolumn string, cascade bool) (out string, e error) {
|
||||
var c = func(str string, val bool) {
|
||||
if e != nil || !val {
|
||||
return
|
||||
}
|
||||
e = errors.New("You need a "+str+" for this table")
|
||||
}
|
||||
c("name",table=="")
|
||||
c("column",column=="")
|
||||
c("ftable",ftable=="")
|
||||
c("fcolumn",fcolumn=="")
|
||||
if e != nil {
|
||||
return "", e
|
||||
}
|
||||
return "", errors.New("not implemented")
|
||||
}
|
||||
|
||||
func (adapter *MssqlAdapter) SimpleInsert(name string, table string, columns string, fields string) (string, error) {
|
||||
if table == "" {
|
||||
return "", errors.New("You need a name for this table")
|
||||
|
@ -112,11 +112,20 @@ func (adapter *MysqlAdapter) CreateTable(name string, table string, charset stri
|
||||
if key.Type != "unique" {
|
||||
querystr += " key"
|
||||
}
|
||||
querystr += "("
|
||||
for _, column := range strings.Split(key.Columns, ",") {
|
||||
querystr += "`" + column + "`,"
|
||||
if key.Type == "foreign" {
|
||||
cols := strings.Split(key.Columns, ",")
|
||||
querystr += "(`" + cols[0] + "`) REFERENCES `" + key.FTable + "`(`" + cols[1] + "`)"
|
||||
if key.Cascade {
|
||||
querystr += " ON DELETE CASCADE"
|
||||
}
|
||||
querystr += ","
|
||||
} else {
|
||||
querystr += "("
|
||||
for _, column := range strings.Split(key.Columns, ",") {
|
||||
querystr += "`" + column + "`,"
|
||||
}
|
||||
querystr = querystr[0:len(querystr)-1] + "),"
|
||||
}
|
||||
querystr = querystr[0:len(querystr)-1] + "),"
|
||||
}
|
||||
}
|
||||
|
||||
@ -173,12 +182,12 @@ func (adapter *MysqlAdapter) parseColumn(column DBTableColumn) (col DBTableColum
|
||||
|
||||
// TODO: Support AFTER column
|
||||
// TODO: Test to make sure everything works here
|
||||
func (adapter *MysqlAdapter) AddColumn(name string, table string, column DBTableColumn, key *DBTableKey) (string, error) {
|
||||
func (a *MysqlAdapter) AddColumn(name string, table string, column DBTableColumn, key *DBTableKey) (string, error) {
|
||||
if table == "" {
|
||||
return "", errors.New("You need a name for this table")
|
||||
}
|
||||
|
||||
column, size, end := adapter.parseColumn(column)
|
||||
column, size, end := a.parseColumn(column)
|
||||
querystr := "ALTER TABLE `" + table + "` ADD COLUMN " + "`" + column.Name + "` " + column.Type + size + end
|
||||
|
||||
if key != nil {
|
||||
@ -191,12 +200,12 @@ func (adapter *MysqlAdapter) AddColumn(name string, table string, column DBTable
|
||||
}
|
||||
|
||||
// TODO: Shunt the table name logic and associated stmt list up to the a higher layer to reduce the amount of unnecessary overhead in the builder / accumulator
|
||||
adapter.pushStatement(name, "add-column", querystr)
|
||||
a.pushStatement(name, "add-column", querystr)
|
||||
return querystr, nil
|
||||
}
|
||||
|
||||
// TODO: Test to make sure everything works here
|
||||
func (adapter *MysqlAdapter) AddIndex(name string, table string, iname string, colname string) (string, error) {
|
||||
func (a *MysqlAdapter) AddIndex(name string, table string, iname string, colname string) (string, error) {
|
||||
if table == "" {
|
||||
return "", errors.New("You need a name for this table")
|
||||
}
|
||||
@ -209,23 +218,50 @@ func (adapter *MysqlAdapter) AddIndex(name string, table string, iname string, c
|
||||
|
||||
querystr := "ALTER TABLE `" + table + "` ADD INDEX " + "`" + iname + "` (`" + colname + "`);"
|
||||
// TODO: Shunt the table name logic and associated stmt list up to the a higher layer to reduce the amount of unnecessary overhead in the builder / accumulator
|
||||
adapter.pushStatement(name, "add-index", querystr)
|
||||
a.pushStatement(name, "add-index", querystr)
|
||||
return querystr, nil
|
||||
}
|
||||
|
||||
// TODO: Test to make sure everything works here
|
||||
// Only supports FULLTEXT right now
|
||||
func (adapter *MysqlAdapter) AddKey(name string, table string, column string, key DBTableKey) (string, error) {
|
||||
func (a *MysqlAdapter) AddKey(name string, table string, column string, key DBTableKey) (string, error) {
|
||||
if table == "" {
|
||||
return "", errors.New("You need a name for this table")
|
||||
}
|
||||
if key.Type != "fulltext" {
|
||||
var querystr string
|
||||
if key.Type == "fulltext" {
|
||||
querystr = "ALTER TABLE `" + table + "` ADD FULLTEXT(`" + column + "`)"
|
||||
} else {
|
||||
return "", errors.New("Only fulltext is supported by AddKey right now")
|
||||
}
|
||||
querystr := "ALTER TABLE `" + table + "` ADD FULLTEXT(`" + column + "`)"
|
||||
|
||||
// TODO: Shunt the table name logic and associated stmt list up to the a higher layer to reduce the amount of unnecessary overhead in the builder / accumulator
|
||||
adapter.pushStatement(name, "add-key", querystr)
|
||||
a.pushStatement(name, "add-key", querystr)
|
||||
return querystr, nil
|
||||
}
|
||||
|
||||
func (a *MysqlAdapter) AddForeignKey(name string, table string, column string, ftable string, fcolumn string, cascade bool) (out string, e error) {
|
||||
var c = func(str string, val bool) {
|
||||
if e != nil || !val {
|
||||
return
|
||||
}
|
||||
e = errors.New("You need a "+str+" for this table")
|
||||
}
|
||||
c("name",table=="")
|
||||
c("column",column=="")
|
||||
c("ftable",ftable=="")
|
||||
c("fcolumn",fcolumn=="")
|
||||
if e != nil {
|
||||
return "", e
|
||||
}
|
||||
|
||||
querystr := "ALTER TABLE `"+table+"` ADD CONSTRAINT `fk_"+column+"` FOREIGN KEY(`"+column+"`) REFERENCES `"+ftable+"`(`"+fcolumn+"`)"
|
||||
if cascade {
|
||||
querystr += " ON DELETE CASCADE"
|
||||
}
|
||||
|
||||
// TODO: Shunt the table name logic and associated stmt list up to the a higher layer to reduce the amount of unnecessary overhead in the builder / accumulator
|
||||
a.pushStatement(name, "add-foreign-key", querystr)
|
||||
return querystr, nil
|
||||
}
|
||||
|
||||
|
@ -147,6 +147,25 @@ func (adapter *PgsqlAdapter) AddKey(name string, table string, column string, ke
|
||||
return "", errors.New("not implemented")
|
||||
}
|
||||
|
||||
// TODO: Implement this
|
||||
// TODO: Test to make sure everything works here
|
||||
func (adapter *PgsqlAdapter) AddForeignKey(name string, table string, column string, ftable string, fcolumn string, cascade bool) (out string, e error) {
|
||||
var c = func(str string, val bool) {
|
||||
if e != nil || !val {
|
||||
return
|
||||
}
|
||||
e = errors.New("You need a "+str+" for this table")
|
||||
}
|
||||
c("name",table=="")
|
||||
c("column",column=="")
|
||||
c("ftable",ftable=="")
|
||||
c("fcolumn",fcolumn=="")
|
||||
if e != nil {
|
||||
return "", e
|
||||
}
|
||||
return "", errors.New("not implemented")
|
||||
}
|
||||
|
||||
// TODO: Test this
|
||||
// ! We need to get the last ID out of this somehow, maybe add returning to every query? Might require some sort of wrapper over the sql statements
|
||||
func (adapter *PgsqlAdapter) SimpleInsert(name string, table string, columns string, fields string) (string, error) {
|
||||
|
@ -23,6 +23,10 @@ type DBTableColumn struct {
|
||||
type DBTableKey struct {
|
||||
Columns string
|
||||
Type string
|
||||
|
||||
// Foreign keys only
|
||||
FTable string
|
||||
Cascade bool
|
||||
}
|
||||
|
||||
type DBSelect struct {
|
||||
@ -111,6 +115,7 @@ type Adapter interface {
|
||||
AddColumn(name string, table string, column DBTableColumn, key *DBTableKey) (string, error)
|
||||
AddIndex(name string, table string, iname string, colname string) (string, error)
|
||||
AddKey(name string, table string, column string, key DBTableKey) (string, error)
|
||||
AddForeignKey(name string, table string, column string, ftable string, fcolumn string, cascade bool) (out string, e error)
|
||||
SimpleInsert(name string, table string, columns string, fields string) (string, error)
|
||||
SimpleUpdate(up *updatePrebuilder) (string, error)
|
||||
SimpleUpdateSelect(up *updatePrebuilder) (string, error) // ! Experimental
|
||||
|
16
routes.go
16
routes.go
@ -10,6 +10,7 @@ import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"errors"
|
||||
"log"
|
||||
"net/http"
|
||||
@ -27,7 +28,7 @@ var successJSONBytes = []byte(`{"success":"1"}`)
|
||||
|
||||
// TODO: Refactor this
|
||||
// TODO: Use the phrase system
|
||||
var phraseLoginAlerts = []byte(`{"msgs":[{"msg":"Login to see your alerts","path":"/accounts/login"}],"msgCount":0}`)
|
||||
var phraseLoginAlerts = []byte(`{"msgs":[{"msg":"Login to see your alerts","path":"/accounts/login"}],"count":0}`)
|
||||
|
||||
// TODO: Refactor this endpoint
|
||||
// TODO: Move this into the routes package
|
||||
@ -40,6 +41,9 @@ func routeAPI(w http.ResponseWriter, r *http.Request, user c.User) c.RouteError
|
||||
}
|
||||
|
||||
action := r.FormValue("action")
|
||||
if action == "" {
|
||||
action = "get"
|
||||
}
|
||||
if action != "get" && action != "set" {
|
||||
return c.PreErrorJS("Invalid Action", w, r)
|
||||
}
|
||||
@ -86,8 +90,8 @@ func routeAPI(w http.ResponseWriter, r *http.Request, user c.User) c.RouteError
|
||||
}
|
||||
|
||||
var msglist string
|
||||
var msgCount int
|
||||
err = stmts.getActivityCountByWatcher.QueryRow(user.ID).Scan(&msgCount)
|
||||
var count int
|
||||
err = stmts.getActivityCountByWatcher.QueryRow(user.ID).Scan(&count)
|
||||
if err == ErrNoRows {
|
||||
return c.PreErrorJS("Couldn't find the parent topic", w, r)
|
||||
} else if err != nil {
|
||||
@ -141,7 +145,7 @@ func routeAPI(w http.ResponseWriter, r *http.Request, user c.User) c.RouteError
|
||||
if len(msglist) != 0 {
|
||||
msglist = msglist[0 : len(msglist)-1]
|
||||
}
|
||||
_, _ = w.Write([]byte(`{"msgs":[` + msglist + `],"msgCount":` + strconv.Itoa(msgCount) + `}`))
|
||||
_, _ = io.WriteString(w, `{"msgs":[` + msglist + `],"count":` + strconv.Itoa(count) + `}`)
|
||||
default:
|
||||
return c.PreErrorJS("Invalid Module", w, r)
|
||||
}
|
||||
@ -296,9 +300,9 @@ func routeJSAntispam(w http.ResponseWriter, r *http.Request, user c.User) c.Rout
|
||||
jsToken := hex.EncodeToString(h.Sum(nil))
|
||||
|
||||
var innerCode = "`document.getElementByld('golden-watch').value = '" + jsToken + "';`"
|
||||
w.Write([]byte(`let hihi = ` + innerCode + `;
|
||||
io.WriteString(w, `let hihi = ` + innerCode + `;
|
||||
hihi = hihi.replace('ld','Id');
|
||||
eval(hihi);`))
|
||||
eval(hihi);`)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -452,8 +452,7 @@ func CreateTopic(w http.ResponseWriter, r *http.Request, user c.User, header *c.
|
||||
}
|
||||
}
|
||||
|
||||
ctpage := c.CreateTopicPage{header, forumList, fid}
|
||||
return renderTemplate("create_topic", w, r, header, ctpage)
|
||||
return renderTemplate("create_topic", w, r, header, c.CreateTopicPage{header, forumList, fid})
|
||||
}
|
||||
|
||||
func CreateTopicSubmit(w http.ResponseWriter, r *http.Request, user c.User) c.RouteError {
|
||||
@ -461,7 +460,6 @@ func CreateTopicSubmit(w http.ResponseWriter, r *http.Request, user c.User) c.Ro
|
||||
if err != nil {
|
||||
return c.LocalError("The provided ForumID is not a valid number.", w, r, user)
|
||||
}
|
||||
|
||||
// TODO: Add hooks to make use of headerLite
|
||||
lite, ferr := c.SimpleForumUserCheck(w, r, &user, fid)
|
||||
if ferr != nil {
|
||||
|
@ -1,4 +1,5 @@
|
||||
CREATE TABLE [activity_stream_matches] (
|
||||
[watcher] int not null,
|
||||
[asid] int not null
|
||||
[asid] int not null,
|
||||
foreign key([asid],[asid])
|
||||
);
|
@ -1,4 +1,5 @@
|
||||
CREATE TABLE `activity_stream_matches` (
|
||||
`watcher` int not null,
|
||||
`asid` int not null
|
||||
`asid` int not null,
|
||||
foreign key(`asid`) REFERENCES `activity_stream`(`asid`) ON DELETE CASCADE
|
||||
);
|
@ -1,4 +1,5 @@
|
||||
CREATE TABLE "activity_stream_matches" (
|
||||
`watcher` int not null,
|
||||
`asid` int not null
|
||||
`asid` int not null,
|
||||
foreign key(`asid`,`asid`)
|
||||
);
|
@ -7,7 +7,7 @@
|
||||
{{range .Header.PreScriptsAsync}}
|
||||
<script async type="text/javascript" src="/static/{{.}}"></script>{{end}}
|
||||
<meta property="x-loggedin" content="{{.CurrentUser.Loggedin}}" />
|
||||
<script type="text/javascript" src="/static/init.js?i=5"></script>
|
||||
<script type="text/javascript" src="/static/init.js?i=6"></script>
|
||||
{{range .Header.ScriptsAsync}}
|
||||
<script async type="text/javascript" src="/static/{{.}}"></script>{{end}}
|
||||
<script type="text/javascript" src="/static/jquery-3.1.1.min.js"></script>
|
||||
|
20
tickloop.go
20
tickloop.go
@ -5,8 +5,10 @@ import (
|
||||
"log"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
"database/sql"
|
||||
|
||||
c "github.com/Azareal/Gosora/common"
|
||||
"github.com/Azareal/Gosora/query_gen"
|
||||
)
|
||||
|
||||
// TODO: Name the tasks so we can figure out which one it was when something goes wrong? Or maybe toss it up WithStack down there?
|
||||
@ -55,6 +57,7 @@ func tickLoop(thumbChan chan bool) {
|
||||
secondTicker := time.NewTicker(time.Second)
|
||||
fifteenMinuteTicker := time.NewTicker(15 * time.Minute)
|
||||
hourTicker := time.NewTicker(time.Hour)
|
||||
dailyTicker := time.NewTicker(time.Hour * 24)
|
||||
for {
|
||||
select {
|
||||
case <-halfSecondTicker.C:
|
||||
@ -122,6 +125,23 @@ func tickLoop(thumbChan chan bool) {
|
||||
|
||||
runTasks(c.ScheduledHourTasks)
|
||||
runHook("after_hour_tick")
|
||||
// TODO: Handle the instance going down a lot better
|
||||
case <-dailyTicker.C:
|
||||
// TODO: Find a more efficient way of doing this
|
||||
err := qgen.NewAcc().Select("activity_stream").Cols("asid").EachInt(func(asid int) error {
|
||||
count, err := qgen.NewAcc().Count("activity_stream_matches").Where("asid = ?").Total()
|
||||
if err != sql.ErrNoRows {
|
||||
return err
|
||||
}
|
||||
if count > 0 {
|
||||
return nil
|
||||
}
|
||||
_, err = qgen.NewAcc().Delete("activity_stream").Where("asid = ?").Run(asid)
|
||||
return err
|
||||
})
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
c.LogError(err)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Handle the daily clean-up.
|
||||
|
Loading…
Reference in New Issue
Block a user