sklad: move the database into its own file
This commit is contained in:
parent
3fe6d7d071
commit
8c3aaa8261
|
@ -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
|
||||||
|
}
|
143
sklad/main.go
143
sklad/main.go
|
@ -1,155 +1,19 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"html/template"
|
"html/template"
|
||||||
"io"
|
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"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 (
|
var (
|
||||||
templates *template.Template
|
templates *template.Template
|
||||||
// TODO: Some kind of session storage, somewhere.
|
|
||||||
|
|
||||||
dbPath string
|
// session storage: UUID -> net.SplitHostPort(http.Server.RemoteAddr)[0]
|
||||||
db Database
|
sessions = map[string]string{}
|
||||||
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.
|
|
||||||
|
|
||||||
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() {
|
func main() {
|
||||||
if len(os.Args) != 3 {
|
if len(os.Args) != 3 {
|
||||||
log.Fatalf("usage: %s ADDRESS DATABASE\n", os.Args[0])
|
log.Fatalf("usage: %s ADDRESS DATABASE\n", os.Args[0])
|
||||||
|
@ -170,6 +34,9 @@ func main() {
|
||||||
log.Fatalln(err)
|
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.
|
// TODO: Some routing, don't forget about sessions.
|
||||||
// - https://stackoverflow.com/a/33880971/76313
|
// - https://stackoverflow.com/a/33880971/76313
|
||||||
//
|
//
|
||||||
|
|
Loading…
Reference in New Issue