Browse Source

Add SQLite db for data storage

master
NGnius 4 years ago
parent
commit
d2a6526154
8 changed files with 414 additions and 38 deletions
  1. +4
    -1
      .gitignore
  2. +4
    -0
      config.go
  3. +2
    -0
      go.mod
  4. +13
    -7
      handlers.go
  5. +6
    -6
      json_structs.go
  6. +27
    -24
      main.go
  7. +135
    -0
      sql_service.go
  8. +223
    -0
      sql_structs.go

+ 4
- 1
.gitignore View File

@@ -16,5 +16,8 @@
go.sum

# build binary
./leadercraft-s
leadercraft-s

# sqlite default db
test.sqlite


+ 4
- 0
config.go View File

@@ -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() {


+ 2
- 0
go.mod View File

@@ -1,3 +1,5 @@
module git.exmods.org/NGnius/leadercraft-s

go 1.13

require github.com/mattn/go-sqlite3 v2.0.3+incompatible

+ 13
- 7
handlers.go View File

@@ -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)


+ 6
- 6
json_structs.go View File

@@ -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))
}



+ 27
- 24
main.go View File

@@ -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)
}
}

+ 135
- 0
sql_service.go View File

@@ -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)
}
}
}

+ 223
- 0
sql_structs.go View File

@@ -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}
}