Personal warehouse management system
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

383 lines
9.0KB

  1. package main
  2. import (
  3. "errors"
  4. "html/template"
  5. "io"
  6. "log"
  7. "math/rand"
  8. "net/http"
  9. "net/url"
  10. "os"
  11. "path"
  12. "path/filepath"
  13. "strings"
  14. "time"
  15. "janouch.name/sklad/imgutil"
  16. "janouch.name/sklad/label"
  17. "janouch.name/sklad/ql"
  18. )
  19. var templates = map[string]*template.Template{}
  20. func executeTemplate(name string, w io.Writer, data interface{}) {
  21. if err := templates[name].Execute(w, data); err != nil {
  22. panic(err)
  23. }
  24. }
  25. func handleLogin(w http.ResponseWriter, r *http.Request) {
  26. redirect := r.FormValue("redirect")
  27. if redirect == "" {
  28. redirect = "container"
  29. }
  30. session := sessionGet(w, r)
  31. if session.LoggedIn {
  32. http.Redirect(w, r, redirect, http.StatusSeeOther)
  33. return
  34. }
  35. params := struct {
  36. IncorrectPassword bool
  37. }{}
  38. switch r.Method {
  39. case http.MethodGet:
  40. // We're just going to render the template.
  41. case http.MethodPost:
  42. if r.FormValue("password") == db.Password {
  43. session.LoggedIn = true
  44. http.Redirect(w, r, redirect, http.StatusSeeOther)
  45. return
  46. }
  47. params.IncorrectPassword = true
  48. default:
  49. w.WriteHeader(http.StatusMethodNotAllowed)
  50. return
  51. }
  52. executeTemplate("login.tmpl", w, &params)
  53. }
  54. func handleLogout(w http.ResponseWriter, r *http.Request) {
  55. if r.Method != http.MethodPost {
  56. w.WriteHeader(http.StatusMethodNotAllowed)
  57. return
  58. }
  59. session := r.Context().Value(sessionContextKey{}).(*Session)
  60. session.LoggedIn = false
  61. http.Redirect(w, r, "login", http.StatusSeeOther)
  62. }
  63. func handleContainerPost(r *http.Request) error {
  64. id := ContainerId(r.FormValue("id"))
  65. description := strings.TrimSpace(r.FormValue("description"))
  66. series := r.FormValue("series")
  67. parent := ContainerId(strings.TrimSpace(r.FormValue("parent")))
  68. _, remove := r.Form["remove"]
  69. if container, ok := indexContainer[id]; ok {
  70. if remove {
  71. return dbContainerRemove(container)
  72. } else {
  73. c := *container
  74. c.Description = description
  75. c.Series = series
  76. return dbContainerUpdate(container, c)
  77. }
  78. } else if remove {
  79. return errNoSuchContainer
  80. } else {
  81. return dbContainerCreate(&Container{
  82. Series: series,
  83. Parent: parent,
  84. Description: description,
  85. })
  86. }
  87. }
  88. func handleContainer(w http.ResponseWriter, r *http.Request) {
  89. var err error
  90. if r.Method == http.MethodPost {
  91. err = handleContainerPost(r)
  92. // FIXME: This is rather ugly. When removing, we want to keep
  93. // the context id, in addition to the id being changed.
  94. // TODO: If there were no errors, redirect the user to GET,
  95. // which is related to the previous comment.
  96. // TODO: If there were errors, use the last data as a prefill.
  97. } else if r.Method != http.MethodGet {
  98. w.WriteHeader(http.StatusMethodNotAllowed)
  99. return
  100. }
  101. allSeries := map[string]string{}
  102. for _, s := range indexSeries {
  103. allSeries[s.Prefix] = s.Description
  104. }
  105. var container *Container
  106. children := indexChildren[""]
  107. if c, ok := indexContainer[ContainerId(r.FormValue("id"))]; ok {
  108. children = c.Children()
  109. container = c
  110. }
  111. params := struct {
  112. Error error
  113. ErrorNoSuchSeries bool
  114. ErrorContainerAlreadyExists bool
  115. ErrorNoSuchContainer bool
  116. ErrorCannotChangeSeriesNotEmpty bool
  117. ErrorCannotChangeNumber bool
  118. ErrorWouldContainItself bool
  119. ErrorContainerInUse bool
  120. Container *Container
  121. Children []*Container
  122. AllSeries map[string]string
  123. }{
  124. Error: err,
  125. ErrorNoSuchSeries: err == errNoSuchSeries,
  126. ErrorContainerAlreadyExists: err == errContainerAlreadyExists,
  127. ErrorNoSuchContainer: err == errNoSuchContainer,
  128. ErrorCannotChangeSeriesNotEmpty: err == errCannotChangeSeriesNotEmpty,
  129. ErrorCannotChangeNumber: err == errCannotChangeNumber,
  130. ErrorWouldContainItself: err == errWouldContainItself,
  131. ErrorContainerInUse: err == errContainerInUse,
  132. Container: container,
  133. Children: children,
  134. AllSeries: allSeries,
  135. }
  136. executeTemplate("container.tmpl", w, &params)
  137. }
  138. func handleSeriesPost(r *http.Request) error {
  139. prefix := strings.TrimSpace(r.FormValue("prefix"))
  140. description := strings.TrimSpace(r.FormValue("description"))
  141. _, remove := r.Form["remove"]
  142. if series, ok := indexSeries[prefix]; ok {
  143. if remove {
  144. return dbSeriesRemove(series)
  145. } else {
  146. s := *series
  147. s.Description = description
  148. return dbSeriesUpdate(series, s)
  149. }
  150. } else if remove {
  151. return errNoSuchSeries
  152. } else {
  153. return dbSeriesCreate(&Series{
  154. Prefix: prefix,
  155. Description: description,
  156. })
  157. }
  158. }
  159. func handleSeries(w http.ResponseWriter, r *http.Request) {
  160. var err error
  161. if r.Method == http.MethodPost {
  162. err = handleSeriesPost(r)
  163. // XXX: This is rather ugly.
  164. r.Form = url.Values{}
  165. } else if r.Method != http.MethodGet {
  166. w.WriteHeader(http.StatusMethodNotAllowed)
  167. return
  168. }
  169. allSeries := map[string]*Series{}
  170. for _, s := range indexSeries {
  171. allSeries[s.Prefix] = s
  172. }
  173. prefix := r.FormValue("prefix")
  174. description := ""
  175. if prefix == "" {
  176. } else if series, ok := indexSeries[prefix]; ok {
  177. description = series.Description
  178. } else {
  179. err = errNoSuchSeries
  180. }
  181. params := struct {
  182. Error error
  183. ErrorInvalidPrefix bool
  184. ErrorSeriesAlreadyExists bool
  185. ErrorCannotChangePrefix bool
  186. ErrorNoSuchSeries bool
  187. ErrorSeriesInUse bool
  188. Prefix string
  189. Description string
  190. AllSeries map[string]*Series
  191. }{
  192. Error: err,
  193. ErrorInvalidPrefix: err == errInvalidPrefix,
  194. ErrorSeriesAlreadyExists: err == errSeriesAlreadyExists,
  195. ErrorCannotChangePrefix: err == errCannotChangePrefix,
  196. ErrorNoSuchSeries: err == errNoSuchSeries,
  197. ErrorSeriesInUse: err == errSeriesInUse,
  198. Prefix: prefix,
  199. Description: description,
  200. AllSeries: allSeries,
  201. }
  202. executeTemplate("series.tmpl", w, &params)
  203. }
  204. func handleSearch(w http.ResponseWriter, r *http.Request) {
  205. if r.Method != http.MethodGet {
  206. w.WriteHeader(http.StatusMethodNotAllowed)
  207. return
  208. }
  209. query := r.FormValue("q")
  210. params := struct {
  211. Query string
  212. Series []*Series
  213. Containers []*Container
  214. }{
  215. Query: query,
  216. Series: dbSearchSeries(query),
  217. Containers: dbSearchContainers(query),
  218. }
  219. executeTemplate("search.tmpl", w, &params)
  220. }
  221. func printLabel(id string) error {
  222. printer, err := ql.Open()
  223. if err != nil {
  224. return err
  225. }
  226. if printer == nil {
  227. return errors.New("no suitable printer found")
  228. }
  229. defer printer.Close()
  230. /*
  231. printer.StatusNotify = func(status *ql.Status) {
  232. log.Printf("\x1b[1mreceived status\x1b[m\n%+v\n%s",
  233. status[:], status)
  234. }
  235. */
  236. if err := printer.Initialize(); err != nil {
  237. return err
  238. }
  239. if err := printer.UpdateStatus(); err != nil {
  240. return err
  241. }
  242. mediaInfo := ql.GetMediaInfo(
  243. printer.LastStatus.MediaWidthMM(),
  244. printer.LastStatus.MediaLengthMM(),
  245. )
  246. if mediaInfo == nil {
  247. return errors.New("unknown media")
  248. }
  249. return printer.Print(&imgutil.LeftRotate{Image: label.GenLabelForHeight(
  250. labelFont, id, mediaInfo.PrintAreaPins, db.BDFScale)})
  251. }
  252. func handleLabel(w http.ResponseWriter, r *http.Request) {
  253. if r.Method != http.MethodPost {
  254. w.WriteHeader(http.StatusMethodNotAllowed)
  255. return
  256. }
  257. params := struct {
  258. Id string
  259. UnknownId bool
  260. Error error
  261. }{
  262. Id: r.FormValue("id"),
  263. }
  264. if c := indexContainer[ContainerId(params.Id)]; c == nil {
  265. params.UnknownId = true
  266. } else {
  267. params.Error = printLabel(params.Id)
  268. }
  269. executeTemplate("label.tmpl", w, &params)
  270. }
  271. func handle(w http.ResponseWriter, r *http.Request) {
  272. if err := r.ParseForm(); err != nil {
  273. http.Error(w, err.Error(), http.StatusInternalServerError)
  274. return
  275. }
  276. if r.Method == http.MethodGet {
  277. w.Header().Set("Cache-Control", "no-store")
  278. }
  279. switch _, base := path.Split(r.URL.Path); base {
  280. case "login":
  281. handleLogin(w, r)
  282. case "logout":
  283. sessionWrap(handleLogout)(w, r)
  284. case "container":
  285. sessionWrap(handleContainer)(w, r)
  286. case "series":
  287. sessionWrap(handleSeries)(w, r)
  288. case "search":
  289. sessionWrap(handleSearch)(w, r)
  290. case "label":
  291. sessionWrap(handleLabel)(w, r)
  292. case "":
  293. http.Redirect(w, r, "container", http.StatusSeeOther)
  294. default:
  295. http.NotFound(w, r)
  296. }
  297. }
  298. var funcMap = template.FuncMap{
  299. "max": func(i, j int) int {
  300. if i > j {
  301. return i
  302. }
  303. return j
  304. },
  305. "lines": func(s string) int {
  306. return strings.Count(s, "\n") + 1
  307. },
  308. }
  309. func main() {
  310. // Randomize the RNG for session string generation.
  311. rand.Seed(time.Now().UnixNano())
  312. if len(os.Args) != 3 {
  313. log.Fatalf("Usage: %s ADDRESS DATABASE-FILE\n", os.Args[0])
  314. }
  315. var address string
  316. address, dbPath = os.Args[1], os.Args[2]
  317. // Load database.
  318. if err := loadDatabase(); err != nil {
  319. log.Fatalln(err)
  320. }
  321. // Load HTML templates from the current working directory.
  322. m, err := filepath.Glob("*.tmpl")
  323. if err != nil {
  324. log.Fatalln(err)
  325. }
  326. for _, name := range m {
  327. templates[name] = template.Must(template.New("base.tmpl").
  328. Funcs(funcMap).ParseFiles("base.tmpl", name))
  329. }
  330. http.HandleFunc("/", handle)
  331. log.Fatalln(http.ListenAndServe(address, nil))
  332. }