Browse Source

sklad: move the database into its own file

master
Přemysl Janouch 1 year ago
parent
commit
8c3aaa8261
Signed by: Přemysl Janouch <p@janouch.name> GPG Key ID: A0420B94F92B9493
2 changed files with 154 additions and 138 deletions
  1. +149
    -0
      sklad/db.go
  2. +5
    -138
      sklad/main.go

+ 149
- 0
sklad/db.go View File

@@ -0,0 +1,149 @@
package main

import (
"encoding/json"
"errors"
"fmt"
"io"
"os"
)

type Series struct {
Prefix string // PK: prefix
Description string // what kind of containers this is for
}

type Container struct {
Series string // PK: what series does this belong to
Number uint // PK: order within the series
Parent ContainerId // the container we're in, if any, otherwise ""
Description string // description and/or contents of this container
}

type ContainerId string

func (c *Container) Id() ContainerId {
return ContainerId(fmt.Sprintf("%s%s%d", db.Prefix, c.Series, c.Number))
}

type Database struct {
Password string // password for web users
Prefix string // prefix for all container IDs
Series []*Series // all known series
Containers []*Container // all known containers
}

var (
dbPath string
db Database
dbLast Database
dbLog io.Writer

indexSeries = map[string]*Series{}
indexContainer = map[ContainerId]*Container{}
indexChildren = map[ContainerId][]*Container{}
)

// TODO: Some functions to add, remove and change things in the database.
// Indexes must be kept valid, just like any invariants.

// TODO: A function for fulltext search in series (1. Prefix, 2. Description).

// TODO: A function for fulltext search in containers (1. Id, 2. Description).

func dbCommit() error {
// Back up the current database contents.
e := json.NewEncoder(dbLog)
e.SetIndent("", " ")
if err := e.Encode(&dbLast); err != nil {
return err
}

// Atomically replace the current database file.
tempPath := dbPath + ".new"
temp, err := os.OpenFile(tempPath, os.O_WRONLY|os.O_CREATE, 0644)
if err != nil {
return err
}
defer temp.Close()

e = json.NewEncoder(temp)
e.SetIndent("", " ")
if err := e.Encode(&db); err != nil {
return err
}

if err := os.Rename(tempPath, dbPath); err != nil {
return err
}

dbLast = db
return nil
}

// loadDatabase loads the database from a simple JSON file. We do not use
// any SQL stuff or even external KV storage because there is no real need
// for our trivial use case, with our general amount of data.
func loadDatabase() error {
dbFile, err := os.Open(dbPath)
if err != nil {
return err
}
if err := json.NewDecoder(dbFile).Decode(&db); err != nil {
return err
}

// Further validate the database.
if db.Prefix == "" {
return errors.New("misconfigured prefix")
}

// Construct indexes for primary keys, validate against duplicates.
for _, pv := range db.Series {
if _, ok := indexSeries[pv.Prefix]; ok {
return fmt.Errorf("duplicate series: %s", pv.Prefix)
}
indexSeries[pv.Prefix] = pv
}
for _, pv := range db.Containers {
id := pv.Id()
if _, ok := indexContainer[id]; ok {
return fmt.Errorf("duplicate container: %s", id)
}
indexContainer[id] = pv
}

// Construct an index that goes from parent containers to their children.
for _, pv := range db.Containers {
if pv.Parent == "" {
continue
}
if _, ok := indexContainer[pv.Parent]; !ok {
return fmt.Errorf("container %s has a nonexistent parent %s",
pv.Id(), pv.Parent)
}
indexChildren[pv.Parent] = append(indexChildren[pv.Parent], pv)
}

// Validate that no container is a parent of itself on any level.
for _, pv := range db.Containers {
parents := map[ContainerId]bool{pv.Id(): true}
for pv.Parent != "" {
if parents[pv.Parent] {
return fmt.Errorf("%s contains itself", pv.Parent)
}
parents[pv.Parent] = true
pv = indexContainer[pv.Parent]
}
}

// Open database log file for appending.
if dbLog, err = os.OpenFile(dbPath+".log",
os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644); err != nil {
return err
}

// Remember the current state of the database.
dbLast = db
return nil
}

+ 5
- 138
sklad/main.go View File

@@ -1,155 +1,19 @@
package main

import (
"encoding/json"
"errors"
"fmt"
"html/template"
"io"
"log"
"net/http"
"os"
)

type Series struct {
Prefix string // PK: prefix
Description string // what kind of containers this is for
}

type Container struct {
Series string // PK: what series does this belong to
Number uint // PK: order within the series
Parent ContainerId // the container we're in, if any, otherwise ""
Description string // description and/or contents of this container
}

type ContainerId string

func (c *Container) Id() ContainerId {
return ContainerId(fmt.Sprintf("%s%s%d", db.Prefix, c.Series, c.Number))
}

type Database struct {
Password string // password for web users
Prefix string // prefix for all container IDs
Series []*Series // all known series
Containers []*Container // all known containers
}

var (
templates *template.Template
// TODO: Some kind of session storage, somewhere.

dbPath string
db Database
dbLast Database
dbLog io.Writer

indexSeries = map[string]*Series{}
indexContainer = map[ContainerId]*Container{}
indexChildren = map[ContainerId][]*Container{}
// session storage: UUID -> net.SplitHostPort(http.Server.RemoteAddr)[0]
sessions = map[string]string{}
)

// TODO: Some functions to add, remove and change things in the database.
// Indexes must be kept valid, just like any invariants.

func dbCommit() error {
// Back up the current database contents.
e := json.NewEncoder(dbLog)
e.SetIndent("", " ")
if err := e.Encode(&dbLast); err != nil {
return err
}

// Atomically replace the current database file.
tempPath := dbPath + ".new"
temp, err := os.OpenFile(tempPath, os.O_WRONLY|os.O_CREATE, 0644)
if err != nil {
return err
}
defer temp.Close()

e = json.NewEncoder(temp)
e.SetIndent("", " ")
if err := e.Encode(&db); err != nil {
return err
}

if err := os.Rename(tempPath, dbPath); err != nil {
return err
}

dbLast = db
return nil
}

// loadDatabase loads the database from a simple JSON file. We do not use
// any SQL stuff or even external KV storage because there is no real need
// for our trivial use case, with our general amount of data.
func loadDatabase() error {
dbFile, err := os.Open(dbPath)
if err != nil {
return err
}
if err := json.NewDecoder(dbFile).Decode(&db); err != nil {
return err
}

// Further validate the database.
if db.Prefix == "" {
return errors.New("misconfigured prefix")
}

// Construct indexes for primary keys, validate against duplicates.
for _, pv := range db.Series {
if _, ok := indexSeries[pv.Prefix]; ok {
return fmt.Errorf("duplicate series: %s", pv.Prefix)
}
indexSeries[pv.Prefix] = pv
}
for _, pv := range db.Containers {
id := pv.Id()
if _, ok := indexContainer[id]; ok {
return fmt.Errorf("duplicate container: %s", id)
}
indexContainer[id] = pv
}

// Construct an index that goes from parent containers to their children.
for _, pv := range db.Containers {
if pv.Parent == "" {
continue
}
if _, ok := indexContainer[pv.Parent]; !ok {
return fmt.Errorf("container %s has a nonexistent parent %s",
pv.Id(), pv.Parent)
}
indexChildren[pv.Parent] = append(indexChildren[pv.Parent], pv)
}

// Validate that no container is a parent of itself on any level.
for _, pv := range db.Containers {
parents := map[ContainerId]bool{pv.Id(): true}
for pv.Parent != "" {
if parents[pv.Parent] {
return fmt.Errorf("%s contains itself", pv.Parent)
}
parents[pv.Parent] = true
pv = indexContainer[pv.Parent]
}
}

// Open database log file for appending.
if dbLog, err = os.OpenFile(dbPath+".log",
os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644); err != nil {
return err
}

// Remember the current state of the database.
dbLast = db
return nil
}

func main() {
if len(os.Args) != 3 {
log.Fatalf("usage: %s ADDRESS DATABASE\n", os.Args[0])
@@ -170,6 +34,9 @@ func main() {
log.Fatalln(err)
}

// TODO: Eventually we will need to load a font file for label printing.
// - The path might be part of configuration, or implicit by filename.

// TODO: Some routing, don't forget about sessions.
// - https://stackoverflow.com/a/33880971/76313
//

Loading…
Cancel
Save