betsy-button

Family health button
git clone https://git.woozle.org/neale/betsy-button.git

Neale Pickett  ·  2025-06-29

betsyd.go

 1package main
 2
 3import (
 4	"flag"
 5	"fmt"
 6	"log"
 7	"net/http"
 8	"os"
 9	"path/filepath"
10	"time"
11)
12
13var CheckinWindow time.Duration
14var StateDirectory string
15var WebDirectory string
16var ListenAddress string
17
18func logfile(req *http.Request) string {
19	id := req.PathValue("id")
20	if id == "" {
21		return ""
22	}
23	if len(id) > 40 {
24		return ""
25	}
26	name := filepath.Clean("/" + id + ".log")
27	return filepath.Join(StateDirectory, name)
28}
29
30func checkin(w http.ResponseWriter, req *http.Request) {
31        now := time.Now()
32        stamp := now.Format(time.RFC3339)
33
34	name := logfile(req)
35	if name == "" {
36		http.Error(w, "Invalid ID", http.StatusBadRequest)
37		return
38	}
39
40	f, err := os.OpenFile(name, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
41	if err != nil {
42                http.Error(w, err.Error(), http.StatusInternalServerError)
43                return   
44	}
45	defer f.Close()
46
47	fmt.Fprintln(f, stamp)
48	fmt.Fprintln(w, stamp)
49}
50
51func status(w http.ResponseWriter, req *http.Request) {
52        now := time.Now()
53
54	name := logfile(req)
55	if name == "" {
56		http.Error(w, "Invalid ID", http.StatusBadRequest)
57		return
58	}
59
60        info, err := os.Stat(name)
61        if err != nil {
62                http.Error(w, err.Error(), http.StatusNotFound)
63                return
64        }
65
66        checkin := info.ModTime()
67        if checkin.Add(CheckinWindow).Before(now) {
68                w.WriteHeader(http.StatusNotFound)
69                fmt.Fprintln(w, "Checkin window expired", checkin)
70                return
71        }
72
73        fmt.Fprintln(w, "Checkin OK", checkin)
74}
75
76func main() {
77	flag.DurationVar(&CheckinWindow, "checkin", 24 * time.Hour, "Checkin interval")
78	flag.StringVar(&WebDirectory, "web", "web", "Path to static web content")
79	flag.StringVar(&StateDirectory, "state", "state", "Path to stored state")
80	flag.StringVar(&ListenAddress, "listen", ":8080", "Listen address")
81	flag.Parse()
82
83        http.HandleFunc("GET /state/{id}", status)
84        http.HandleFunc("POST /state/{id}", checkin)
85	http.Handle("GET /log/", http.StripPrefix("/log", http.FileServer(http.Dir(StateDirectory))))
86	http.Handle("/", http.FileServer(http.Dir(WebDirectory)))
87	log.Println("Listening on", ListenAddress)
88        log.Fatal(http.ListenAndServe(ListenAddress, nil))
89}