389 lines
9.1 KiB
Go
389 lines
9.1 KiB
Go
package main
|
|
|
|
import (
|
|
"errors"
|
|
"html/template"
|
|
"io"
|
|
"log"
|
|
"math/rand"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"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 handleLogin(w http.ResponseWriter, r *http.Request) {
|
|
redirect := r.FormValue("redirect")
|
|
if redirect == "" {
|
|
redirect = "container"
|
|
}
|
|
|
|
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, "login", http.StatusSeeOther)
|
|
}
|
|
|
|
func handleContainerPost(r *http.Request) error {
|
|
id := ContainerId(r.FormValue("id"))
|
|
description := strings.TrimSpace(r.FormValue("description"))
|
|
series := r.FormValue("series")
|
|
parent := ContainerId(strings.TrimSpace(r.FormValue("parent")))
|
|
_, remove := r.Form["remove"]
|
|
|
|
if container, ok := indexContainer[id]; ok {
|
|
if remove {
|
|
return dbContainerRemove(container)
|
|
} else {
|
|
c := *container
|
|
c.Description = description
|
|
c.Series = series
|
|
return dbContainerUpdate(container, c)
|
|
}
|
|
} else if remove {
|
|
return errNoSuchContainer
|
|
} else {
|
|
return dbContainerCreate(&Container{
|
|
Series: series,
|
|
Parent: parent,
|
|
Description: description,
|
|
})
|
|
}
|
|
}
|
|
|
|
func handleContainer(w http.ResponseWriter, r *http.Request) {
|
|
var err error
|
|
if r.Method == http.MethodPost {
|
|
err = handleContainerPost(r)
|
|
// FIXME: This is rather ugly. When removing, we want to keep
|
|
// the context id, in addition to the id being changed.
|
|
// TODO: If there were no errors, redirect the user to GET,
|
|
// which is related to the previous comment.
|
|
// TODO: If there were errors, use the last data as a prefill.
|
|
} else 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 := indexChildren[""]
|
|
|
|
if c, ok := indexContainer[ContainerId(r.FormValue("id"))]; ok {
|
|
children = c.Children()
|
|
container = c
|
|
}
|
|
|
|
params := struct {
|
|
Error error
|
|
ErrorNoSuchSeries bool
|
|
ErrorContainerAlreadyExists bool
|
|
ErrorNoSuchContainer bool
|
|
ErrorCannotChangeSeriesNotEmpty bool
|
|
ErrorCannotChangeNumber bool
|
|
ErrorWouldContainItself bool
|
|
ErrorContainerInUse bool
|
|
Container *Container
|
|
Children []*Container
|
|
AllSeries map[string]string
|
|
}{
|
|
Error: err,
|
|
ErrorNoSuchSeries: err == errNoSuchSeries,
|
|
ErrorContainerAlreadyExists: err == errContainerAlreadyExists,
|
|
ErrorNoSuchContainer: err == errNoSuchContainer,
|
|
ErrorCannotChangeSeriesNotEmpty: err == errCannotChangeSeriesNotEmpty,
|
|
ErrorCannotChangeNumber: err == errCannotChangeNumber,
|
|
ErrorWouldContainItself: err == errWouldContainItself,
|
|
ErrorContainerInUse: err == errContainerInUse,
|
|
Container: container,
|
|
Children: children,
|
|
AllSeries: allSeries,
|
|
}
|
|
|
|
executeTemplate("container.tmpl", w, ¶ms)
|
|
}
|
|
|
|
func handleSeriesPost(r *http.Request) error {
|
|
prefix := strings.TrimSpace(r.FormValue("prefix"))
|
|
description := strings.TrimSpace(r.FormValue("description"))
|
|
_, remove := r.Form["remove"]
|
|
|
|
if series, ok := indexSeries[prefix]; ok {
|
|
if remove {
|
|
return dbSeriesRemove(series)
|
|
} else {
|
|
s := *series
|
|
s.Description = description
|
|
return dbSeriesUpdate(series, s)
|
|
}
|
|
} else if remove {
|
|
return errNoSuchSeries
|
|
} else {
|
|
return dbSeriesCreate(&Series{
|
|
Prefix: prefix,
|
|
Description: description,
|
|
})
|
|
}
|
|
}
|
|
|
|
func handleSeries(w http.ResponseWriter, r *http.Request) {
|
|
var err error
|
|
if r.Method == http.MethodPost {
|
|
err = handleSeriesPost(r)
|
|
// XXX: This is rather ugly.
|
|
r.Form = url.Values{}
|
|
} else if r.Method != http.MethodGet {
|
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
allSeries := map[string]*Series{}
|
|
for _, s := range indexSeries {
|
|
allSeries[s.Prefix] = s
|
|
}
|
|
|
|
prefix := r.FormValue("prefix")
|
|
description := ""
|
|
|
|
if prefix == "" {
|
|
} else if series, ok := indexSeries[prefix]; ok {
|
|
description = series.Description
|
|
} else {
|
|
err = errNoSuchSeries
|
|
}
|
|
|
|
params := struct {
|
|
Error error
|
|
ErrorInvalidPrefix bool
|
|
ErrorSeriesAlreadyExists bool
|
|
ErrorCannotChangePrefix bool
|
|
ErrorNoSuchSeries bool
|
|
ErrorSeriesInUse bool
|
|
Prefix string
|
|
Description string
|
|
AllSeries map[string]*Series
|
|
}{
|
|
Error: err,
|
|
ErrorInvalidPrefix: err == errInvalidPrefix,
|
|
ErrorSeriesAlreadyExists: err == errSeriesAlreadyExists,
|
|
ErrorCannotChangePrefix: err == errCannotChangePrefix,
|
|
ErrorNoSuchSeries: err == errNoSuchSeries,
|
|
ErrorSeriesInUse: err == errSeriesInUse,
|
|
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)
|
|
}
|
|
|
|
var mutex sync.Mutex
|
|
|
|
func handle(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")
|
|
}
|
|
|
|
mutex.Lock()
|
|
defer mutex.Unlock()
|
|
|
|
switch _, base := path.Split(r.URL.Path); base {
|
|
case "login":
|
|
handleLogin(w, r)
|
|
case "logout":
|
|
sessionWrap(handleLogout)(w, r)
|
|
|
|
case "container":
|
|
sessionWrap(handleContainer)(w, r)
|
|
case "series":
|
|
sessionWrap(handleSeries)(w, r)
|
|
case "search":
|
|
sessionWrap(handleSearch)(w, r)
|
|
case "label":
|
|
sessionWrap(handleLabel)(w, r)
|
|
|
|
case "":
|
|
http.Redirect(w, r, "container", http.StatusSeeOther)
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
}
|
|
|
|
var funcMap = template.FuncMap{
|
|
"max": func(i, j int) int {
|
|
if i > j {
|
|
return i
|
|
}
|
|
return j
|
|
},
|
|
"lines": func(s string) int {
|
|
return strings.Count(s, "\n") + 1
|
|
},
|
|
}
|
|
|
|
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.New("base.tmpl").
|
|
Funcs(funcMap).ParseFiles("base.tmpl", name))
|
|
}
|
|
|
|
http.HandleFunc("/", handle)
|
|
log.Fatalln(http.ListenAndServe(address, nil))
|
|
}
|