diff options
author | Přemysl Janouch <p@janouch.name> | 2019-04-14 22:30:40 +0200 |
---|---|---|
committer | Přemysl Janouch <p@janouch.name> | 2019-04-14 22:30:40 +0200 |
commit | 1331f3b5642f521236fcb1ec21ee43d5b76c0b91 (patch) | |
tree | 1213c2f2014cf0eff5a4a881b02cbebf91a604b8 /sklad | |
parent | 7d9410c6b3a724e3670941f7ec2d00e7966d0b1a (diff) | |
download | sklad-1331f3b5642f521236fcb1ec21ee43d5b76c0b91.tar.gz sklad-1331f3b5642f521236fcb1ec21ee43d5b76c0b91.tar.xz sklad-1331f3b5642f521236fcb1ec21ee43d5b76c0b91.zip |
Move commands under cmd/
Diffstat (limited to 'sklad')
-rw-r--r-- | sklad/base.tmpl | 68 | ||||
-rw-r--r-- | sklad/container.tmpl | 99 | ||||
-rw-r--r-- | sklad/db.go | 225 | ||||
-rw-r--r-- | sklad/label.tmpl | 13 | ||||
-rw-r--r-- | sklad/login.tmpl | 17 | ||||
-rw-r--r-- | sklad/main.go | 271 | ||||
-rw-r--r-- | sklad/search.tmpl | 38 | ||||
-rw-r--r-- | sklad/series.tmpl | 43 | ||||
-rw-r--r-- | sklad/session.go | 66 |
9 files changed, 0 insertions, 840 deletions
diff --git a/sklad/base.tmpl b/sklad/base.tmpl deleted file mode 100644 index d92a818..0000000 --- a/sklad/base.tmpl +++ /dev/null @@ -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 autofocus><input type=submit value="Hledat"> - </form> - - <form method=post action=/logout> - <input type=submit value="Odhlásit"> - </form> -{{ end }} - -</header> - -{{ template "Content" . }} -</body> -</html> diff --git a/sklad/container.tmpl b/sklad/container.tmpl deleted file mode 100644 index 4bacae8..0000000 --- a/sklad/container.tmpl +++ /dev/null @@ -1,99 +0,0 @@ -{{ define "Title" }}{{/* -*/}}{{ if .Container }}{{ .Container.Id }}{{ else }}Obaly{{ end }}{{ end }} -{{ define "Content" }} - -{{ if .Container }} - -<section> -<header> - <h2>{{ .Container.Id }}</h2> - <form method=post action="/label?id={{ .Container.Id }}"> - <input type=submit value="Vytisknout štítek"> - </form> - <form method=post action="/?id={{ .Container.Id }}&remove"> - <input type=submit value="Odstranit"> - </form> -</header> - -<form method=post action="/?id={{ .Container.Id }}"> -<textarea name=description rows=5> -{{ .Container.Description }} -</textarea> -<footer> - <div> - <label for=series>Řada:</label> - <select name=series id=series> -{{ range $prefix, $desc := .AllSeries }} - <option value="{{ $prefix }}" - {{ if eq $prefix $.Container.Series }}selected{{ end }} - >{{ $prefix }} — {{ $desc }}</option> -{{ end }} - </select> - </div> - <div> - <label for=parent>Nadobal:</label> - <input type=text name=parent id=parent value="{{ .Container.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 }}" - >{{ $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="/?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 }} -{{ if .Children }} -<p> -{{ range .Children }} -<a href="/?id={{ .Id }}">{{ .Id }}</a> -{{ end }} -{{ end }} -</section> -{{ else }} -<p>Obal je prázdný. -{{ end }} - -{{ end }} diff --git a/sklad/db.go b/sklad/db.go deleted file mode 100644 index def18a5..0000000 --- a/sklad/db.go +++ /dev/null @@ -1,225 +0,0 @@ -package main - -import ( - "encoding/json" - "errors" - "fmt" - "os" - "strings" - "time" - - "janouch.name/sklad/bdf" -) - -type Series struct { - Prefix string // PK: prefix - Description string // what kind of containers this is for -} - -type ContainerId string - -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 -} - -func (c *Container) Id() ContainerId { - return ContainerId(fmt.Sprintf("%s%s%d", db.Prefix, c.Series, c.Number)) -} - -func (c *Container) Children() []*Container { - // TODO: Sort this by Id, or maybe even return a map[string]*Container, - // text/template would sort that automatically. - return indexChildren[c.Id()] -} - -func (c *Container) Path() (result []ContainerId) { - for c != nil && c.Parent != "" { - c = indexContainer[c.Parent] - result = append(result, c.Id()) - } - return -} - -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 - - BDFPath string // path to bitmap font file - BDFScale int // integer scaling for the bitmap font -} - -var ( - dbPath string - db Database - dbLast Database - dbLog *os.File - - indexSeries = map[string]*Series{} - indexContainer = map[ContainerId]*Container{} - indexChildren = map[ContainerId][]*Container{} - - labelFont *bdf.Font -) - -// TODO: Some functions to add, remove and change things in the database. -// Indexes must be kept valid, just like any invariants. - -func dbSearchSeries(query string) (result []*Series) { - query = strings.ToLower(query) - added := map[string]bool{} - for _, s := range db.Series { - if query == strings.ToLower(s.Prefix) { - result = append(result, s) - added[s.Prefix] = true - } - } - for _, s := range db.Series { - if strings.Contains( - strings.ToLower(s.Description), query) && !added[s.Prefix] { - result = append(result, s) - } - } - return -} - -func dbSearchContainers(query string) (result []*Container) { - query = strings.ToLower(query) - added := map[ContainerId]bool{} - for id, c := range indexContainer { - if query == strings.ToLower(string(id)) { - result = append(result, c) - added[id] = true - } - } - for id, c := range indexContainer { - if strings.Contains( - strings.ToLower(c.Description), query) && !added[id] { - result = append(result, c) - } - } - return -} - -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] - } - } - - // Prepare label printing. - if db.BDFScale <= 0 { - db.BDFScale = 1 - } - - if f, err := os.Open(db.BDFPath); err != nil { - return fmt.Errorf("cannot load label font: %s", err) - } else { - defer f.Close() - if labelFont, err = bdf.NewFromBDF(f); err != nil { - return fmt.Errorf("cannot load label font: %s", err) - } - } - - // 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 -} diff --git a/sklad/label.tmpl b/sklad/label.tmpl deleted file mode 100644 index da17c58..0000000 --- a/sklad/label.tmpl +++ /dev/null @@ -1,13 +0,0 @@ -{{ define "Title" }}Tisk štítku{{ end }} -{{ define "Content" }} -<h2>Tisk štítku pro {{ .Id }}</h2> - -{{ if .UnknownId }} -<p>Neznámý obal. -{{ else if .Error }} -<p>Tisk selhal: {{ .Error }} -{{ else }} -<p>Tisk proběhl úspěšně. -{{ end }} - -{{ end }} diff --git a/sklad/login.tmpl b/sklad/login.tmpl deleted file mode 100644 index c34ab53..0000000 --- a/sklad/login.tmpl +++ /dev/null @@ -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 autofocus -><input type=submit value="Přihlásit"> -</form> - -{{ if .IncorrectPassword }} -<p>Bylo zadáno nesprávné heslo. -{{ end }} - -{{ end }} diff --git a/sklad/main.go b/sklad/main.go deleted file mode 100644 index 32dd68b..0000000 --- a/sklad/main.go +++ /dev/null @@ -1,271 +0,0 @@ -package main - -import ( - "errors" - "html/template" - "io" - "log" - "math/rand" - "net/http" - "os" - "path/filepath" - "time" - - "janouch.name/sklad/imgutil" - "janouch.name/sklad/label" - "janouch.name/sklad/ql" -) - -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 - } - - var container *Container - children := []*Container{} - - if id := ContainerId(r.FormValue("id")); id == "" { - children = indexChildren[""] - } else if c, ok := indexContainer[id]; ok { - children = c.Children() - container = c - } - - params := struct { - Container *Container - Children []*Container - AllSeries map[string]string - }{ - Container: container, - Children: children, - 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") - params := struct { - Query string - Series []*Series - Containers []*Container - }{ - Query: query, - Series: dbSearchSeries(query), - Containers: dbSearchContainers(query), - } - - executeTemplate("search.tmpl", w, ¶ms) -} - -func printLabel(id string) error { - printer, err := ql.Open() - if err != nil { - return err - } - if printer == nil { - return errors.New("no suitable printer found") - } - defer printer.Close() - - /* - printer.StatusNotify = func(status *ql.Status) { - log.Printf("\x1b[1mreceived status\x1b[m\n%+v\n%s", - status[:], status) - } - */ - - if err := printer.Initialize(); err != nil { - return err - } - if err := printer.UpdateStatus(); err != nil { - return err - } - - mediaInfo := ql.GetMediaInfo( - printer.LastStatus.MediaWidthMM(), - printer.LastStatus.MediaLengthMM(), - ) - if mediaInfo == nil { - return errors.New("unknown media") - } - - return printer.Print(&imgutil.LeftRotate{Image: label.GenLabelForHeight( - labelFont, id, mediaInfo.PrintAreaPins, db.BDFScale)}) -} - -func handleLabel(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodPost { - w.WriteHeader(http.StatusMethodNotAllowed) - return - } - - params := struct { - Id string - UnknownId bool - Error error - }{ - Id: r.FormValue("id"), - } - - if c := indexContainer[ContainerId(params.Id)]; c == nil { - params.UnknownId = true - } else { - params.Error = printLabel(params.Id) - } - - 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)) - } - - 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)) -} diff --git a/sklad/search.tmpl b/sklad/search.tmpl deleted file mode 100644 index cf704cf..0000000 --- a/sklad/search.tmpl +++ /dev/null @@ -1,38 +0,0 @@ -{{ define "Title" }}„{{ .Query }}“ — Vyhledávání{{ end }} -{{ define "Content" }} - -<h2>Vyhledávání: „{{ .Query }}“<h2> - -<h3>Řady</h3> - -{{ range .Series }} -<section> -<header> - <h3><a href="/series?prefix={{ .Prefix }}">{{ .Prefix }}</a></h3> - <p>{{ .Description }} -</header> -</section> -{{ else }} -<p>Neodpovídají žádné řady. -{{ end }} - -<h3>Obaly</h3> - -{{ range .Containers }} -<section> -<header> - <h3><a href="/?id={{ .Id }}">{{ .Id }}</a> -{{ range .Path }} - <small>« <a href="/?id={{ . }}">{{ . }}</a></small> -{{ end }} - </h3> -</header> -{{ if .Description }} -<p>{{ .Description }} -{{ end }} -</section> -{{ else }} -<p>Neodpovídají žádné obaly. -{{ end }} - -{{ end }} diff --git a/sklad/series.tmpl b/sklad/series.tmpl deleted file mode 100644 index 4956e3a..0000000 --- a/sklad/series.tmpl +++ /dev/null @@ -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 }} diff --git a/sklad/session.go b/sklad/session.go deleted file mode 100644 index 02fe0b0..0000000 --- a/sklad/session.go +++ /dev/null @@ -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 != "/" && r.Method == http.MethodGet { - 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))) - } -} |