Compare commits
No commits in common. "e003427f9f86b0b3898cca67b39a96e391fd1b16" and "10380a7901e4dd153388b832668d4eeb64ad06a8" have entirely different histories.
e003427f9f
...
10380a7901
@ -217,10 +217,10 @@ func main() {
|
|||||||
log.Fatalf("usage: %s ADDRESS BDF-FILE\n", os.Args[0])
|
log.Fatalf("usage: %s ADDRESS BDF-FILE\n", os.Args[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
address, bdfPath := os.Args[1], os.Args[2]
|
address, bdf_path := os.Args[1], os.Args[2]
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
fi, err := os.Open(bdfPath)
|
fi, err := os.Open(bdf_path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalln(err)
|
log.Fatalln(err)
|
||||||
}
|
}
|
||||||
@ -235,5 +235,5 @@ func main() {
|
|||||||
|
|
||||||
log.Println("starting server")
|
log.Println("starting server")
|
||||||
http.HandleFunc("/", handle)
|
http.HandleFunc("/", handle)
|
||||||
log.Fatalln(http.ListenAndServe(address, nil))
|
log.Fatal(http.ListenAndServe(address, nil))
|
||||||
}
|
}
|
||||||
|
@ -1,68 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<title>{{ template "Title" . }} - sklad</title>
|
|
||||||
<meta http-equiv=Content-Type content="text/html; charset=utf-8">
|
|
||||||
<meta name=viewport content="width=device-width, initial-scale=1">
|
|
||||||
<style>
|
|
||||||
html, body { min-height: 100vh; }
|
|
||||||
body { padding: 1rem; box-sizing: border-box;
|
|
||||||
margin: 0 auto; max-width: 50rem;
|
|
||||||
border-left: 1px solid #ccc; border-right: 1px solid #ccc;
|
|
||||||
font-family: sans-serif; }
|
|
||||||
|
|
||||||
section { border: 1px outset #ccc; padding: 0 .5rem; margin: 1rem 0; }
|
|
||||||
section > p { margin: 0 0 .5rem 0; }
|
|
||||||
|
|
||||||
header, footer { display: flex; justify-content: space-between;
|
|
||||||
align-items: center; flex-wrap: wrap; padding-top: .5em; }
|
|
||||||
header { margin: 0 -.5rem; padding: .5rem .5rem 0 .5rem;
|
|
||||||
background: linear-gradient(0deg, transparent, #f8f8f8); }
|
|
||||||
body > header { margin: -1rem -1rem 0 -1rem; padding: 1rem 1rem 0 1rem;
|
|
||||||
background: linear-gradient(0deg, transparent, #eeeeee); }
|
|
||||||
|
|
||||||
header *,
|
|
||||||
footer * { display: inline-block; }
|
|
||||||
header > *,
|
|
||||||
footer > * { margin: 0 0 .5rem 0; }
|
|
||||||
header > *:not(:last-child),
|
|
||||||
footer > *:not(:last-child) { margin-right: .5rem; }
|
|
||||||
|
|
||||||
header > h2,
|
|
||||||
header > h3 { flex-grow: 1; }
|
|
||||||
|
|
||||||
/* Don't ask me why this is an improvement on mobile browsers. */
|
|
||||||
input[type=submit], input[type=text], input[type=password],
|
|
||||||
select, textarea { border: 1px inset #ccc; padding: .25rem; }
|
|
||||||
input[type=submit] { border-style: outset; }
|
|
||||||
select { border-style: solid; }
|
|
||||||
|
|
||||||
a { color: inherit; }
|
|
||||||
textarea { padding: .5rem; box-sizing: border-box; width: 100%;
|
|
||||||
font-family: inherit; resize: vertical; }
|
|
||||||
select { max-width: 15rem; }
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
|
|
||||||
<header>
|
|
||||||
<h1>sklad</h1>
|
|
||||||
|
|
||||||
{{ block "HeaderControls" . }}
|
|
||||||
<a href=/>Obaly</a>
|
|
||||||
<a href=/series>Řady</a>
|
|
||||||
|
|
||||||
<form method=get action=/search>
|
|
||||||
<input type=text name=q><input type=submit value="Hledat">
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<form method=post action=/logout>
|
|
||||||
<input type=submit value="Odhlásit">
|
|
||||||
</form>
|
|
||||||
{{ end }}
|
|
||||||
|
|
||||||
</header>
|
|
||||||
|
|
||||||
{{ template "Content" . }}
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@ -1,93 +0,0 @@
|
|||||||
{{ define "Title" }}{{ or .Id "Obaly" }}{{ end }}
|
|
||||||
{{ define "Content" }}
|
|
||||||
|
|
||||||
{{ if .Id }}
|
|
||||||
|
|
||||||
<section>
|
|
||||||
<header>
|
|
||||||
<h2>{{ .Id }}</h2>
|
|
||||||
<form method=post action="/label?id={{ .Id }}">
|
|
||||||
<input type=submit value="Vytisknout štítek">
|
|
||||||
</form>
|
|
||||||
<form method=post action="/?id={{ .Id }}&remove">
|
|
||||||
<input type=submit value="Odstranit">
|
|
||||||
</form>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<form method=post action="/?id={{ .Id }}">
|
|
||||||
<textarea name=description rows=5>
|
|
||||||
{{ .Description }}
|
|
||||||
</textarea>
|
|
||||||
<footer>
|
|
||||||
<div>
|
|
||||||
<label for=series>Řada:</label>
|
|
||||||
<select name=series id=series>
|
|
||||||
{{ range $prefix, $desc := .AllSeries }}
|
|
||||||
<option value="{{ $prefix }}"
|
|
||||||
{{ if eq $prefix $.Series }}selected{{ end }}
|
|
||||||
>{{ $prefix }} — {{ $desc }}</option>
|
|
||||||
{{ end }}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label for=parent>Nadobal:</label>
|
|
||||||
<input type=text name=parent id=parent value="{{ .Parent }}">
|
|
||||||
</div>
|
|
||||||
<input type=submit value="Uložit">
|
|
||||||
</footer>
|
|
||||||
</form>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<h2>Podobaly</h3>
|
|
||||||
|
|
||||||
{{ else }}
|
|
||||||
<section>
|
|
||||||
<header>
|
|
||||||
<h2>Nový obal</h2>
|
|
||||||
</header>
|
|
||||||
<form method=post action="/">
|
|
||||||
<textarea name=description rows=5
|
|
||||||
placeholder="Popis obalu nebo jeho obsahu"></textarea>
|
|
||||||
<footer>
|
|
||||||
<div>
|
|
||||||
<label for=series>Řada:</label>
|
|
||||||
<select name=series id=series>
|
|
||||||
{{ range $prefix, $desc := .AllSeries }}
|
|
||||||
<option value="{{ $prefix }}"
|
|
||||||
{{ if eq $prefix $.Series }}selected{{ end }}
|
|
||||||
>{{ $prefix }} — {{ $desc }}</option>
|
|
||||||
{{ end }}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label for=parent>Nadobal:</label>
|
|
||||||
<input type=text name=parent id=parent value="">
|
|
||||||
</div>
|
|
||||||
<input type=submit value="Uložit">
|
|
||||||
</footer>
|
|
||||||
</form>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<h2>Obaly nejvyšší úrovně</h2>
|
|
||||||
{{ end }}
|
|
||||||
|
|
||||||
{{ range .Children }}
|
|
||||||
<section>
|
|
||||||
<header>
|
|
||||||
<h3><a href="/container?id={{ .Id }}">{{ .Id }}</a></h3>
|
|
||||||
<form method=post action="/label?id={{ .Id }}">
|
|
||||||
<input type=submit value="Vytisknout štítek">
|
|
||||||
</form>
|
|
||||||
<form method=post action="/?id={{ .Id }}&remove">
|
|
||||||
<input type=submit value="Odstranit">
|
|
||||||
</form>
|
|
||||||
</header>
|
|
||||||
{{ if .Description }}
|
|
||||||
<p>{{ .Description }}
|
|
||||||
{{ end }}
|
|
||||||
</section>
|
|
||||||
{{ else }}
|
|
||||||
<p>Obal je prázdný.
|
|
||||||
{{ end }}
|
|
||||||
|
|
||||||
{{ end }}
|
|
157
sklad/db.go
157
sklad/db.go
@ -1,157 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
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 *os.File
|
|
||||||
|
|
||||||
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 {
|
|
||||||
// Write a timestamp.
|
|
||||||
e := json.NewEncoder(dbLog)
|
|
||||||
e.SetIndent("", " ")
|
|
||||||
if err := e.Encode(time.Now().Format(time.RFC3339)); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Back up the current database contents.
|
|
||||||
if err := e.Encode(&dbLast); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := dbLog.Sync(); 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 != "" {
|
|
||||||
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.
|
|
||||||
// This could probably be optimized but it would stop being obvious.
|
|
||||||
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
|
|
||||||
}
|
|
@ -1,17 +0,0 @@
|
|||||||
{{ define "Title" }}Přihlášení{{ end }}
|
|
||||||
{{ define "HeaderControls" }}<!-- text/template requires content -->{{ end }}
|
|
||||||
{{ define "Content" }}
|
|
||||||
|
|
||||||
<h2>Přihlášení</h2>
|
|
||||||
|
|
||||||
<form method=post>
|
|
||||||
<label for=password>Heslo:</label>
|
|
||||||
<input type=password name=password id=password
|
|
||||||
><input type=submit value="Přihlásit">
|
|
||||||
</form>
|
|
||||||
|
|
||||||
{{ if .IncorrectPassword }}
|
|
||||||
<p>Bylo zadáno nesprávné heslo.
|
|
||||||
{{ end }}
|
|
||||||
|
|
||||||
{{ end }}
|
|
233
sklad/main.go
233
sklad/main.go
@ -1,233 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"html/template"
|
|
||||||
"io"
|
|
||||||
"log"
|
|
||||||
"math/rand"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
var templates = map[string]*template.Template{}
|
|
||||||
|
|
||||||
func executeTemplate(name string, w io.Writer, data interface{}) {
|
|
||||||
if err := templates[name].Execute(w, data); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func wrap(inner func(http.ResponseWriter, *http.Request)) func(
|
|
||||||
http.ResponseWriter, *http.Request) {
|
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if err := r.ParseForm(); err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if r.Method == http.MethodGet {
|
|
||||||
w.Header().Set("Cache-Control", "no-store")
|
|
||||||
}
|
|
||||||
inner(w, r)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleLogin(w http.ResponseWriter, r *http.Request) {
|
|
||||||
redirect := r.FormValue("redirect")
|
|
||||||
if redirect == "" {
|
|
||||||
redirect = "/"
|
|
||||||
}
|
|
||||||
|
|
||||||
session := sessionGet(w, r)
|
|
||||||
if session.LoggedIn {
|
|
||||||
http.Redirect(w, r, redirect, http.StatusSeeOther)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
params := struct {
|
|
||||||
IncorrectPassword bool
|
|
||||||
}{}
|
|
||||||
|
|
||||||
switch r.Method {
|
|
||||||
case http.MethodGet:
|
|
||||||
// We're just going to render the template.
|
|
||||||
case http.MethodPost:
|
|
||||||
if r.FormValue("password") == db.Password {
|
|
||||||
session.LoggedIn = true
|
|
||||||
http.Redirect(w, r, redirect, http.StatusSeeOther)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
params.IncorrectPassword = true
|
|
||||||
default:
|
|
||||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
executeTemplate("login.tmpl", w, ¶ms)
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleLogout(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if r.Method != http.MethodPost {
|
|
||||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
session := r.Context().Value(sessionContextKey{}).(*Session)
|
|
||||||
session.LoggedIn = false
|
|
||||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleContainer(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if r.Method == http.MethodPost {
|
|
||||||
// TODO
|
|
||||||
}
|
|
||||||
if r.Method != http.MethodGet {
|
|
||||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
allSeries := map[string]string{}
|
|
||||||
for _, s := range indexSeries {
|
|
||||||
allSeries[s.Prefix] = s.Description
|
|
||||||
}
|
|
||||||
|
|
||||||
children := []*Container{}
|
|
||||||
id := ContainerId(r.FormValue("id"))
|
|
||||||
description := ""
|
|
||||||
series := ""
|
|
||||||
parent := ContainerId("")
|
|
||||||
|
|
||||||
if id == "" {
|
|
||||||
children = indexChildren[id]
|
|
||||||
} else if container, ok := indexContainer[id]; ok {
|
|
||||||
children = indexChildren[id]
|
|
||||||
description = container.Description
|
|
||||||
series = container.Series
|
|
||||||
parent = container.Parent
|
|
||||||
}
|
|
||||||
|
|
||||||
params := struct {
|
|
||||||
Id ContainerId
|
|
||||||
Description string
|
|
||||||
Children []*Container
|
|
||||||
Series string
|
|
||||||
Parent ContainerId
|
|
||||||
AllSeries map[string]string
|
|
||||||
}{
|
|
||||||
Id: id,
|
|
||||||
Description: description,
|
|
||||||
Children: children,
|
|
||||||
Series: series,
|
|
||||||
Parent: parent,
|
|
||||||
AllSeries: allSeries,
|
|
||||||
}
|
|
||||||
|
|
||||||
executeTemplate("container.tmpl", w, ¶ms)
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleSeries(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if r.Method == http.MethodPost {
|
|
||||||
// TODO
|
|
||||||
}
|
|
||||||
if r.Method != http.MethodGet {
|
|
||||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
allSeries := map[string]string{}
|
|
||||||
for _, s := range indexSeries {
|
|
||||||
allSeries[s.Prefix] = s.Description
|
|
||||||
}
|
|
||||||
|
|
||||||
prefix := r.FormValue("prefix")
|
|
||||||
description := ""
|
|
||||||
|
|
||||||
if prefix == "" {
|
|
||||||
} else if series, ok := indexSeries[prefix]; ok {
|
|
||||||
description = series.Description
|
|
||||||
}
|
|
||||||
|
|
||||||
params := struct {
|
|
||||||
Prefix string
|
|
||||||
Description string
|
|
||||||
AllSeries map[string]string
|
|
||||||
}{
|
|
||||||
Prefix: prefix,
|
|
||||||
Description: description,
|
|
||||||
AllSeries: allSeries,
|
|
||||||
}
|
|
||||||
|
|
||||||
executeTemplate("series.tmpl", w, ¶ms)
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleSearch(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if r.Method != http.MethodGet {
|
|
||||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
query := r.FormValue("q")
|
|
||||||
_ = query
|
|
||||||
|
|
||||||
// TODO: Query the database for exact matches and fulltext.
|
|
||||||
|
|
||||||
params := struct{}{}
|
|
||||||
|
|
||||||
executeTemplate("search.tmpl", w, ¶ms)
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleLabel(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if r.Method != http.MethodPost {
|
|
||||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
id := r.FormValue("id")
|
|
||||||
_ = id
|
|
||||||
|
|
||||||
// TODO: See if such a container exists, print a label on the printer.
|
|
||||||
|
|
||||||
params := struct{}{}
|
|
||||||
|
|
||||||
executeTemplate("label.tmpl", w, ¶ms)
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
// Randomize the RNG for session string generation.
|
|
||||||
rand.Seed(time.Now().UnixNano())
|
|
||||||
|
|
||||||
if len(os.Args) != 3 {
|
|
||||||
log.Fatalf("Usage: %s ADDRESS DATABASE-FILE\n", os.Args[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
var address string
|
|
||||||
address, dbPath = os.Args[1], os.Args[2]
|
|
||||||
|
|
||||||
// Load database.
|
|
||||||
if err := loadDatabase(); err != nil {
|
|
||||||
log.Fatalln(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load HTML templates from the current working directory.
|
|
||||||
m, err := filepath.Glob("*.tmpl")
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalln(err)
|
|
||||||
}
|
|
||||||
for _, name := range m {
|
|
||||||
templates[name] = template.Must(template.ParseFiles("base.tmpl", name))
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Eventually we will need to load a font file for label printing.
|
|
||||||
// - The path might be part of configuration, or implicit by filename.
|
|
||||||
|
|
||||||
http.HandleFunc("/login", wrap(handleLogin))
|
|
||||||
http.HandleFunc("/logout", sessionWrap(wrap(handleLogout)))
|
|
||||||
|
|
||||||
http.HandleFunc("/", sessionWrap(wrap(handleContainer)))
|
|
||||||
http.HandleFunc("/series", sessionWrap(wrap(handleSeries)))
|
|
||||||
http.HandleFunc("/search", sessionWrap(wrap(handleSearch)))
|
|
||||||
http.HandleFunc("/label", sessionWrap(wrap(handleLabel)))
|
|
||||||
|
|
||||||
log.Fatalln(http.ListenAndServe(address, nil))
|
|
||||||
}
|
|
@ -1,43 +0,0 @@
|
|||||||
{{ define "Title" }}{{ or .Prefix "Řady" }}{{ end }}
|
|
||||||
{{ define "Content" }}
|
|
||||||
|
|
||||||
{{ if .Prefix }}
|
|
||||||
<h2>{{ .Prefix }}</h2>
|
|
||||||
|
|
||||||
{{ if .Description }}
|
|
||||||
<p>{{ .Description }}
|
|
||||||
{{ end }}
|
|
||||||
{{ else }}
|
|
||||||
|
|
||||||
<section>
|
|
||||||
<form method=post action="/series">
|
|
||||||
<header>
|
|
||||||
<h3>Nová řada</h3>
|
|
||||||
<input type=text name=prefix placeholder="Prefix řady">
|
|
||||||
<input type=text name=description placeholder="Popis řady"
|
|
||||||
><input type=submit value="Uložit">
|
|
||||||
</form>
|
|
||||||
</header>
|
|
||||||
</form>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{{ range $prefix, $desc := .AllSeries }}
|
|
||||||
<section>
|
|
||||||
<header>
|
|
||||||
<h3><a href="/series?prefix={{ $prefix }}">{{ $prefix }}</a></h3>
|
|
||||||
<form method=post action="/series?prefix={{ $prefix }}">
|
|
||||||
<input type=text name=description value="{{ $desc }}"
|
|
||||||
><input type=submit value="Uložit">
|
|
||||||
</form>
|
|
||||||
<form method=post action="/series?prefix={{ $prefix }}&remove">
|
|
||||||
<input type=submit value="Odstranit">
|
|
||||||
</form>
|
|
||||||
</header>
|
|
||||||
</section>
|
|
||||||
{{ else }}
|
|
||||||
<p>Nejsou žádné řady.
|
|
||||||
{{ end }}
|
|
||||||
|
|
||||||
{{ end }}
|
|
||||||
|
|
||||||
{{ end }}
|
|
@ -1,66 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/hex"
|
|
||||||
"math/rand"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
)
|
|
||||||
|
|
||||||
// session storage indexed by a random UUID
|
|
||||||
var sessions = map[string]*Session{}
|
|
||||||
|
|
||||||
type Session struct {
|
|
||||||
LoggedIn bool // may access the DB
|
|
||||||
}
|
|
||||||
|
|
||||||
type sessionContextKey struct{}
|
|
||||||
|
|
||||||
func sessionGenId() string {
|
|
||||||
u := make([]byte, 16)
|
|
||||||
if _, err := rand.Read(u); err != nil {
|
|
||||||
panic("cannot generate random bytes")
|
|
||||||
}
|
|
||||||
return hex.EncodeToString(u)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: We don't want to keep an unlimited amount of cookies in the storage.
|
|
||||||
// - The essential question is: how do we avoid DoS?
|
|
||||||
// - Which cookies are worth keeping?
|
|
||||||
// - Definitely logged-in users, only one person should know the password.
|
|
||||||
// - Evict by FIFO? LRU?
|
|
||||||
func sessionGet(w http.ResponseWriter, r *http.Request) (session *Session) {
|
|
||||||
if c, _ := r.Cookie("sessionid"); c != nil {
|
|
||||||
session, _ = sessions[c.Value]
|
|
||||||
}
|
|
||||||
if session == nil {
|
|
||||||
id := sessionGenId()
|
|
||||||
session = &Session{LoggedIn: false}
|
|
||||||
sessions[id] = session
|
|
||||||
http.SetCookie(w, &http.Cookie{Name: "sessionid", Value: id})
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func sessionWrap(inner func(http.ResponseWriter, *http.Request)) func(
|
|
||||||
http.ResponseWriter, *http.Request) {
|
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// We might also try no-cache with an ETag for the whole database,
|
|
||||||
// though I don't expect any substantial improvements of anything.
|
|
||||||
w.Header().Set("Cache-Control", "no-store")
|
|
||||||
|
|
||||||
redirect := "/login"
|
|
||||||
if r.RequestURI != "/" {
|
|
||||||
redirect += "?redirect=" + url.QueryEscape(r.RequestURI)
|
|
||||||
}
|
|
||||||
|
|
||||||
session := sessionGet(w, r)
|
|
||||||
if !session.LoggedIn {
|
|
||||||
http.Redirect(w, r, redirect, http.StatusSeeOther)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
inner(w, r.WithContext(
|
|
||||||
context.WithValue(r.Context(), sessionContextKey{}, session)))
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
x
Reference in New Issue
Block a user