From d2a6526154d04dec6fc22a11127d175c968b75e4 Mon Sep 17 00:00:00 2001 From: NGnius Date: Thu, 13 Feb 2020 13:11:44 -0500 Subject: [PATCH] Add SQLite db for data storage --- .gitignore | 5 +- config.go | 4 + go.mod | 2 + handlers.go | 20 +++-- json_structs.go | 12 +-- main.go | 51 +++++------ sql_service.go | 135 +++++++++++++++++++++++++++++ sql_structs.go | 223 ++++++++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 414 insertions(+), 38 deletions(-) create mode 100644 sql_service.go create mode 100644 sql_structs.go diff --git a/.gitignore b/.gitignore index 17f51e2..acb9124 100644 --- a/.gitignore +++ b/.gitignore @@ -16,5 +16,8 @@ go.sum # build binary -./leadercraft-s +leadercraft-s + +# sqlite default db +test.sqlite diff --git a/config.go b/config.go index a952b45..8a86492 100644 --- a/config.go +++ b/config.go @@ -21,6 +21,10 @@ func initArgs() { flag.StringVar(&port, "port", defaultPort, "Port to listen on") flag.StringVar(&root, "root", defaultRoot, "Root working directory") flag.BoolVar(&printVersionAndExit, "version", false, "Print version and exit") + flag.StringVar(&sqlConnection, "conn", sqlConnectionDefault, "Database connection string") + flag.StringVar(&sqlServer, "sql", sqlServerDefault, "SQL Database type") + flag.BoolVar(&buildTables, "build-db", true /*false*/, "Build database tables on startup") + flag.BoolVar(&populateTables, "populate-db", true /*false*/, "Populate database with test data") } func parseArgs() { diff --git a/go.mod b/go.mod index c46576d..fb43eeb 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,5 @@ module git.exmods.org/NGnius/leadercraft-s go 1.13 + +require github.com/mattn/go-sqlite3 v2.0.3+incompatible diff --git a/handlers.go b/handlers.go index 2a49fa9..941bcac 100644 --- a/handlers.go +++ b/handlers.go @@ -24,26 +24,32 @@ func boardHandler(w http.ResponseWriter, r *http.Request) { } board := args.Get("board") if !checkArgExists(args, "count", w) || !checkArgInt(args, "count", w, 0) { + w.WriteHeader(400) return } count, _ := strconv.Atoi(args.Get("count")) if !checkArgExists(args, "start", w) || !checkArgInt(args, "start", w, 0) { + w.WriteHeader(400) return } start, _ := strconv.Atoi(args.Get("start")) // execute query result := NewResult("", r.URL.String()) - b, ok := boards[board] - if !ok { + b, err := boardByName(board) + //b, ok := boards[board] + if err != nil { + fmt.Println(err) w.WriteHeader(404) return } - if len(b.Entries) < start+count { - count = len(b.Entries) - start + bEntries, loadErr := b.SomeEntries(int64(start), int64(start+count)) + if loadErr != nil { + fmt.Println(loadErr) + w.WriteHeader(404) + return } - // result.Items = b.Entries[start : start+count] - for _, entry := range b.Entries[start : start+count] { - item := entry + for _, entry := range bEntries { + item, _ := entry.JsonObject() result.Items = append(result.Items, &item) } result.Query = fmt.Sprintf("load board[%s] from %d to %d", board, start, count+start) diff --git a/json_structs.go b/json_structs.go index c8e8d52..9036b40 100644 --- a/json_structs.go +++ b/json_structs.go @@ -8,14 +8,14 @@ import ( ) // Board a leaderboard -type Board struct { +type BoardJSON struct { ID int64 - Entries []Entry + Entries []EntryJSON Name string } // Entry an entry in a leaderboard -type Entry struct { +type EntryJSON struct { ID int64 Rank int64 Score int64 @@ -24,14 +24,14 @@ type Entry struct { } // Player a player -type Player struct { +type PlayerJSON struct { ID int64 Name string - Entries []Entry + Entries []EntryJSON } // URL get the player's URL -func (p *Player) URL() string { +func (p *PlayerJSON) URL() string { return "/player/" + strconv.Itoa(int(p.ID)) } diff --git a/main.go b/main.go index f472b95..4bb045a 100644 --- a/main.go +++ b/main.go @@ -5,6 +5,8 @@ package main // leadercraft-server import ( "fmt" "net/http" + "os" + "os/signal" ) const ( @@ -15,11 +17,11 @@ const ( ) var ( - server *http.Server - handler http.Handler - port string - root string - boards map[string]Board + server *http.Server + handler http.Handler + port string + root string + isClosing bool ) func init() { @@ -27,32 +29,33 @@ func init() { serverMux := http.NewServeMux() serverMux.HandleFunc("/load", boardHandler) handler = serverMux - boards = make(map[string]Board) - // test data - boards["test1"] = Board{ - ID: 1, - Entries: []Entry{ - Entry{ID: 1, Rank: 1, Score: 1000, PlayerName: "NGnius", PlayerURL: "/player/1"}, - Entry{ID: 2, Rank: 2, Score: 900, PlayerName: "Also NGnius", PlayerURL: "/player/2"}, - Entry{ID: 4, Rank: 3, Score: 400, PlayerName: ".xX||eDgY TeeNaGeR||Xx.", PlayerURL: "/player/3"}, - Entry{ID: 3, Rank: 4, Score: 350, PlayerName: "New username who dis?", PlayerURL: "/player/4"}, - Entry{ID: 7, Rank: 5, Score: 350, PlayerName: "Extremely Ridiculously Long Name Please don't break things", PlayerURL: "/player/5"}, - Entry{ID: 5, Rank: 6, Score: 250, PlayerName: "P|P3 |o|z", PlayerURL: "/player/3333"}, - Entry{ID: 6, Rank: 7, Score: 200, PlayerName: "Simon", PlayerURL: "/player/456"}, - Entry{ID: 24, Rank: 8, Score: 175, PlayerName: "Zettagram.com", PlayerURL: "/player/32"}, - Entry{ID: 42, Rank: 9, Score: 150, PlayerName: "The Doctor", PlayerURL: "/player/9283"}, - Entry{ID: 43, Rank: 10, Score: 140, PlayerName: "Marvin the Paranoid Android", PlayerURL: "/player/6"}, - Entry{ID: 8, Rank: 11, Score: 100, PlayerName: "IDK HOW", PlayerURL: "/player/666"}, - }, - } } func main() { parseArgs() + sqlInitErr := sqlInit() + if sqlInitErr != nil { + fmt.Printf("Failed to initialise SQL connection: %s\n", sqlInitErr) + os.Exit(1) + } + // handle interrupt (terminate) signal + signalChan := make(chan os.Signal, 1) + signal.Notify(signalChan, os.Interrupt) + go func() { + s := <-signalChan + fmt.Println("Received terminate signal " + s.String()) + isClosing = true + sqlClose() + server.Close() + }() server = &http.Server{ Addr: ":" + port, Handler: handler, } fmt.Println("Starting on " + server.Addr) - fmt.Println(server.ListenAndServe()) + //fmt.Println(server.ListenAndServe()) + err := server.ListenAndServe() + if err != nil && !isClosing { + fmt.Println(err) + } } diff --git a/sql_service.go b/sql_service.go new file mode 100644 index 0000000..9427217 --- /dev/null +++ b/sql_service.go @@ -0,0 +1,135 @@ +// NGnius 2020-02-11 + +package main + +import ( + "database/sql" + "fmt" + + _ "github.com/mattn/go-sqlite3" +) + +const ( + sqlServerDefault = "sqlite3" + sqlConnectionDefault = "test.sqlite" +) + +var ( + // command line arguments + sqlServer string + sqlConnection string + buildTables bool + populateTables bool + // internal variables + db *sql.DB +) + +func sqlInit() error { + var dbOpenErr error + db, dbOpenErr = sql.Open(sqlServer, sqlConnection) + if dbOpenErr != nil { + return dbOpenErr + } + if buildTables { + fmt.Println("Building tables in database...") + sqlBuildTables() + } + if populateTables { + fmt.Println("Populating tables in database...") + sqlPopulateTables() + } + return nil +} + +func sqlClose() error { + if db != nil { + err := db.Close() + if err != nil { + return err + } + db = nil + } + return nil +} + +func boardByName(name string) (*Board, error) { + b := &Board{} + return b, db.QueryRow("SELECT * FROM Boards WHERE name=? LIMIT 1", name).Scan(b.Intake()...) +} + +func playerByName(name string) (*Player, error) { + p := &Player{} + return p, db.QueryRow("SELECT * FROM Players WHERE name=? LIMTI 1", name).Scan(p.Intake()...) +} + +// internal operations +func sqlBuildTables() { + transaction, txErr := db.Begin() + if txErr != nil { + return + } + // test table + //transaction.Exec("CREATE TABLE IF NOT EXISTS Test (Sometext VARCHAR, Somenumber);") + //transaction.Exec("INSERT INTO Test (Sometext, Somenumber) VALUES (?,?);", "Hello sqlite", 123) + // build real tables + transaction.Exec("CREATE TABLE IF NOT EXISTS Players (id INTEGER PRIMARY KEY, name TEXT NOT NULL);") + transaction.Exec("CREATE TABLE IF NOT EXISTS Boards (id INTEGER PRIMARY KEY, name TEXT NOT NULL, description TEXT);") + transaction.Exec("CREATE TABLE IF NOT EXISTS Entries (id INTEGER PRIMARY KEY, rank INTEGER, score INTEGER NOT NULL, player INTEGER NOT NULL, board INTEGER NOT NULL, FOREIGN KEY(player) REFERENCES Players(id), FOREIGN KEY(board) REFERENCES Boards(id));") + transaction.Commit() +} + +func sqlPopulateTables() { + boards := []*Board{ + &Board{ID: 1, Name: "main-test", Description: "Overall best (test data)"}, + &Board{ID: 2, Name: "coolest-test", Description: "Coolest score (test data)"}, + &Board{ID: 3, Name: "fastest-test", Description: "Fastest time (test data)"}, + } + for _, b := range boards { + err := b.Commit() + if err != nil { + fmt.Printf("Error creating board %d: %s\n", b.ID, err) + } + } + players := []*Player{ + &Player{ID: 1, Name: "NGnius (test)"}, + &Player{ID: 2, Name: "Also NGnius (test)"}, + &Player{ID: 3, Name: ".xX||eDgY TeeNaGeR||Xx. (test)"}, + &Player{ID: 4, Name: "New username who dis? (test)"}, + &Player{ID: 5, Name: "Extremely Ridiculously Long Name to break things (test)"}, + &Player{ID: 6, Name: "P|P3 |o|z (test)"}, + &Player{ID: 7, Name: "Waldo (test)"}, + &Player{ID: 8, Name: "Zettagram.com (test)"}, + &Player{ID: 9, Name: "The Doctor (test)"}, + &Player{ID: 10, Name: "Marvin the Paranoid Android (test)"}, + &Player{ID: 11, Name: "IDK HOW (test)"}, + &Player{ID: 12, Name: "If you can read this your API may be wrong (test)"}, + &Player{ID: 13, Name: "Unlucky 7 (test)"}, + } + for _, p := range players { + err := p.Commit() + if err != nil { + fmt.Printf("Error creating player %d: %s\n", p.ID, err) + } + } + entries := []*Entry{ + &Entry{ID: 1, Rank: 1, Score: 1000, Player: players[0].ID, Board: boards[0].ID}, + &Entry{ID: 2, Rank: 2, Score: 900, Player: players[1].ID, Board: boards[0].ID}, + &Entry{ID: 3, Rank: 3, Score: 400, Player: players[2].ID, Board: boards[0].ID}, + &Entry{ID: 4, Rank: 4, Score: 350, Player: players[3].ID, Board: boards[0].ID}, + &Entry{ID: 5, Rank: 5, Score: 350, Player: players[4].ID, Board: boards[0].ID}, + &Entry{ID: 6, Rank: 6, Score: 250, Player: players[5].ID, Board: boards[0].ID}, + &Entry{ID: 7, Rank: 7, Score: 200, Player: players[6].ID, Board: boards[0].ID}, + &Entry{ID: 8, Rank: 8, Score: 175, Player: players[7].ID, Board: boards[0].ID}, + &Entry{ID: 9, Rank: 9, Score: 150, Player: players[8].ID, Board: boards[0].ID}, + &Entry{ID: 10, Rank: 10, Score: 140, Player: players[9].ID, Board: boards[0].ID}, + &Entry{ID: 11, Rank: 11, Score: 10, Player: players[10].ID, Board: boards[0].ID}, + &Entry{ID: 12, Rank: 12, Score: 60, Player: players[11].ID, Board: boards[0].ID}, + &Entry{ID: 13, Rank: 13, Score: 13, Player: players[12].ID, Board: boards[0].ID}, + } + for _, e := range entries { + err := e.Commit() + if err != nil { + fmt.Printf("Error creating entry %d: %s", e.ID, err) + } + } +} diff --git a/sql_structs.go b/sql_structs.go new file mode 100644 index 0000000..f45c585 --- /dev/null +++ b/sql_structs.go @@ -0,0 +1,223 @@ +// NGnius 2020-02-12 + +package main + +import ( + "encoding/json" + "strconv" +) + +type Jsonable interface { + Json() ([]byte, error) + JsonPretty() ([]byte, error) + Unjson([]byte) error + JsonObject() (interface{}, error) +} + +type Rower interface { + Intake() []interface{} + Output() []interface{} +} + +type Board struct { + ID int64 + Name string + Description string +} + +func LoadBoard(id int64) *Board { + b := &Board{ID: id} + b.Load() + return b +} + +func (b *Board) Load() error { + return db.QueryRow("SELECT * FROM Boards WHERE id=?", b.ID).Scan(b.Intake()...) +} + +func (b *Board) Commit() error { + tx, _ := db.Begin() + statement, _ := tx.Prepare("INSERT OR REPLACE INTO Boards(id, name, description) VALUES (?, ?, ?);") + _, err := statement.Exec(b.Output()...) + if err != nil { + tx.Rollback() + return err + } + return tx.Commit() +} + +func (b *Board) Entries() ([]*Entry, error) { + var entries []*Entry + rows, err := db.Query("SELECT * FROM Entries WHERE board=?", b.ID) + if err != nil { + return entries, err + } + count := 0 + for rows.Next() { + entries = append(entries, &Entry{}) + rows.Scan(entries[count].Intake()...) + count++ + } + return entries, nil +} + +func (b *Board) SomeEntries(start, end int64) ([]*Entry, error) { + var entries []*Entry + rows, err := db.Query("SELECT * FROM Entries WHERE board=? and rank >= ? and rank <= ? ORDER BY rank ASC;", b.ID, start, end) + if err != nil { + return entries, err + } + count := 0 + for rows.Next() { + entries = append(entries, &Entry{}) + rows.Scan(entries[count].Intake()...) + count++ + } + return entries, nil +} + +func (b *Board) Intake() []interface{} { + return []interface{}{&b.ID, &b.Name, &b.Description} +} + +func (b *Board) Output() []interface{} { + return []interface{}{b.ID, b.Name, b.Description} +} + +type Player struct { + ID int64 + Name string +} + +func LoadPlayer(id int64) *Player { + p := &Player{ID: id} + p.Load() + return p +} + +func (p *Player) Load() error { + return db.QueryRow("SELECT * FROM Players WHERE id=? LIMIT 1;", p.ID).Scan(p.Intake()...) +} + +func (p *Player) Commit() error { + tx, _ := db.Begin() + statement, _ := tx.Prepare("INSERT OR REPLACE INTO Players(id, name) VALUES (?, ?);") + _, err := statement.Exec(p.Output()...) + if err != nil { + tx.Rollback() + return err + } + return tx.Commit() +} + +func (p *Player) Entries() ([]*Entry, error) { + var entries []*Entry + rows, err := db.Query("SELECT * FROM Entries WHERE player=?", p.ID) + if err != nil { + return entries, err + } + count := 0 + for rows.Next() { + entries = append(entries, &Entry{}) + rows.Scan(entries[count].Intake()...) + count++ + } + return entries, nil +} + +func (p *Player) SomeEntries(limit int64) ([]*Entry, error) { + var entries []*Entry + rows, err := db.Query("SELECT * FROM Entries WHERE player=? ORDER BY rank ASC LIMIT ?;", p.ID, limit) + if err != nil { + return entries, err + } + count := 0 + for rows.Next() { + entries = append(entries, &Entry{}) + rows.Scan(entries[count].Intake()...) + count++ + } + return entries, nil +} + +func (p *Player) Url() string { + return "/player/" + strconv.Itoa(int(p.ID)) +} + +// implementation of Rower +func (p *Player) Intake() []interface{} { + return []interface{}{&p.ID, &p.Name} +} + +func (p *Player) Output() []interface{} { + return []interface{}{p.ID, p.Name} +} + +type Entry struct { + ID int64 + Rank int64 + Score int64 + Player int64 + Board int64 +} + +func LoadEntry(id int64) *Entry { + e := &Entry{ID: id} + e.Load() + return e +} + +func (e *Entry) Load() error { + return db.QueryRow("SELECT * FROM Entries WHERE id=? LIMIT 1;", e.ID).Scan(e.Intake()...) +} + +func (e *Entry) Commit() error { + tx, _ := db.Begin() + statement, _ := tx.Prepare("INSERT OR REPLACE INTO Entries(id, rank, score, player, board) VALUES (?, ?, ?, ?, ?);") + _, err := statement.Exec(e.Output()...) + if err != nil { + tx.Rollback() + return err + } + return tx.Commit() +} + +// implementation of Jsonable +func (e *Entry) Json() ([]byte, error) { + var data []byte + jsonObj, err := e.JsonObject() + if err != nil { + return data, err + } + return json.Marshal(jsonObj) +} + +func (e *Entry) JsonPretty() ([]byte, error) { + var data []byte + jsonObj, err := e.JsonObject() + if err != nil { + return data, err + } + return json.MarshalIndent(jsonObj, "", " ") +} + +func (e *Entry) JsonObject() (interface{}, error) { + jsonObj := EntryJSON{ID: e.ID, Rank: e.Rank, Score: e.Score} + ePlayer := &Player{ID: e.Player} + err := ePlayer.Load() + if err != nil { + return jsonObj, err + } + jsonObj.PlayerName = ePlayer.Name + jsonObj.PlayerURL = ePlayer.Url() + return jsonObj, nil +} + +// implementation of Rower +func (e *Entry) Intake() []interface{} { + return []interface{}{&e.ID, &e.Rank, &e.Score, &e.Player, &e.Board} +} + +func (e *Entry) Output() []interface{} { + return []interface{}{e.ID, e.Rank, e.Score, e.Player, e.Board} +}