aboutsummaryrefslogtreecommitdiff
path: root/main.go
diff options
context:
space:
mode:
Diffstat (limited to 'main.go')
-rw-r--r--main.go284
1 files changed, 284 insertions, 0 deletions
diff --git a/main.go b/main.go
new file mode 100644
index 0000000..f30496f
--- /dev/null
+++ b/main.go
@@ -0,0 +1,284 @@
1package main
2
3import (
4 "context"
5 "database/sql"
6 "embed"
7 "encoding/json"
8 "fmt"
9 "io"
10 "io/fs"
11 "log"
12 "net/http"
13 "os"
14 "strings"
15
16 _ "embed"
17
18 _ "github.com/mattn/go-sqlite3"
19 "github.com/pkg/errors"
20)
21
22type LocationLog struct {
23 Timestamp float64 `json:"timestamp"`
24 Latitude float64 `json:"latitude"`
25 Longitude float64 `json:"longitude"`
26 Accuracy float64 `json:"accuracy"`
27 Heading float64 `json:"heading"`
28}
29
30type ShapePoint struct {
31 Latitude float64 `json:"latitude"`
32 Longitude float64 `json:"longitude"`
33}
34
35const (
36 ShapeKindUnburned string = "unburned"
37 ShapeKindBurned string = "burned"
38)
39
40type Shape struct {
41 Kind string `json:"kind"`
42 Points []ShapePoint `json:"points"`
43}
44
45var db *sql.DB
46
47func dbLoadLocationLogs(ctx context.Context) ([]LocationLog, error) {
48 rows, err := db.QueryContext(ctx, "SELECT * FROM location")
49 if err != nil {
50 return nil, errors.Wrapf(err, "fetching all locations")
51 }
52
53 locations := []LocationLog{}
54 for rows.Next() {
55 loc := LocationLog{}
56 if err := rows.Scan(&loc.Timestamp, &loc.Latitude, &loc.Longitude, &loc.Accuracy, &loc.Heading); err != nil {
57 return nil, errors.Wrapf(err, "failed to scan location row")
58 }
59 locations = append(locations, loc)
60 }
61
62 return locations, nil
63}
64
65func dbAppendLocationLog(ctx context.Context, loc LocationLog) error {
66 if _, err := db.ExecContext(ctx, "INSERT INTO location(timestamp, latitude, longitude, accuracy, heading) VALUES (?, ?, ?, ?, ?)", loc.Timestamp, loc.Latitude, loc.Longitude, loc.Accuracy, loc.Heading); err != nil {
67 return errors.Wrapf(err, "failed to insert location log")
68 }
69 return nil
70}
71
72func dbSetShapes(ctx context.Context, shapes []Shape) error {
73 tx, err := db.Begin()
74 if err != nil {
75 return errors.Wrapf(err, "starting database transaction")
76 }
77 defer tx.Rollback()
78
79 if _, err := tx.ExecContext(ctx, "DELETE FROM shape"); err != nil {
80 return errors.Wrapf(err, "failed to truncate shapes table")
81 }
82 if _, err := tx.ExecContext(ctx, "DELETE FROM shape_point"); err != nil {
83 return errors.Wrapf(err, "faile to truncate shape points tables")
84 }
85
86 for shapeId, shape := range shapes {
87 if _, err := tx.ExecContext(ctx, "INSERT INTO shape(id, kind) VALUES (?, ?)", shapeId, shape.Kind); err != nil {
88 return errors.Wrapf(err, "failed to insert shape")
89 }
90
91 for _, point := range shape.Points {
92 if _, err := tx.ExecContext(ctx, "INSERT INTO shape_point(shape, latitude, longitude) VALUES (?, ?, ?)", shapeId, point.Latitude, point.Longitude); err != nil {
93 return errors.Wrapf(err, "failed to insert shape point")
94 }
95 }
96 }
97
98 return tx.Commit()
99}
100
101func dbGetShapes(ctx context.Context) ([]Shape, error) {
102 shapes := []Shape{}
103 srows, err := db.QueryContext(ctx, "SELECT id, kind FROM shape")
104 if err != nil {
105 return nil, errors.Wrapf(err, "failed to fetch shapes")
106 }
107
108 for srows.Next() {
109 var id int
110 var kind string
111 if err := srows.Scan(&id, &kind); err != nil {
112 return nil, errors.Wrapf(err, "failed to scan shape")
113 }
114
115 points := []ShapePoint{}
116 prows, err := db.QueryContext(ctx, "SELECT latitude, longitude FROM shape_point WHERE shape = ?", id)
117 if err != nil {
118 return nil, errors.Wrapf(err, "failed to fetch shape points")
119 }
120
121 for prows.Next() {
122 var lat float64
123 var lon float64
124 if err := prows.Scan(&lat, &lon); err != nil {
125 return nil, errors.Wrapf(err, "failed to scan shape point")
126 }
127
128 points = append(points, ShapePoint{
129 Latitude: lat,
130 Longitude: lon,
131 })
132 }
133
134 shapes = append(shapes, Shape{
135 Kind: kind,
136 Points: points,
137 })
138 }
139
140 return shapes, nil
141}
142
143func handlePostApiLog(w http.ResponseWriter, r *http.Request) {
144 body, err := io.ReadAll(r.Body)
145 if err != nil {
146 log.Printf("failed to read request body: %v\n", err)
147 http.Error(w, "failed to read request body", http.StatusBadRequest)
148 return
149 }
150
151 l := LocationLog{}
152 if err := json.Unmarshal(body, &l); err != nil {
153 log.Printf("failed to parse request body: %v\n", err)
154 http.Error(w, "failed to parse request body", http.StatusBadRequest)
155 return
156 }
157
158 log.Printf("storing %v\n", l)
159 if err := dbAppendLocationLog(r.Context(), l); err != nil {
160 log.Printf("%v", err)
161 http.Error(w, "internal error", http.StatusInternalServerError)
162 return
163 }
164}
165
166func handleGetApiLog(w http.ResponseWriter, r *http.Request) {
167 locations, err := dbLoadLocationLogs(r.Context())
168 if err != nil {
169 log.Printf("internal error: %v", err)
170 http.Error(w, "internal error", http.StatusInternalServerError)
171 return
172 }
173
174 marshaled, err := json.Marshal(locations)
175 if err != nil {
176 log.Printf("failed to marshal locations: %v\n", err)
177 http.Error(w, "internal error", http.StatusInternalServerError)
178 return
179 }
180
181 w.Write(marshaled)
182}
183
184func handleGetApiShapes(w http.ResponseWriter, r *http.Request) {
185 shapes, err := dbGetShapes(r.Context())
186 if err != nil {
187 http.Error(w, fmt.Sprintf("failed to fetch shapes: %v", err), http.StatusInternalServerError)
188 return
189 }
190
191 marshaled, err := json.Marshal(shapes)
192 if err != nil {
193 http.Error(w, fmt.Sprintf("failed to marshal shapes: %v", err), http.StatusInternalServerError)
194 return
195 }
196
197 w.Write(marshaled)
198}
199
200func handlePostApiShapes(w http.ResponseWriter, r *http.Request) {
201 body, err := io.ReadAll(r.Body)
202 if err != nil {
203 log.Printf("failed to read request body: %v\n", err)
204 http.Error(w, "failed to read request body", http.StatusBadRequest)
205 return
206 }
207
208 shapes := []Shape{}
209 if err := json.Unmarshal(body, &shapes); err != nil {
210 http.Error(w, fmt.Sprintf("failed to unmarshal request body: %v", err), http.StatusBadRequest)
211 return
212 }
213
214 if err := dbSetShapes(r.Context(), shapes); err != nil {
215 http.Error(w, fmt.Sprintf("failed to store shapes: %v", err), http.StatusInternalServerError)
216 return
217 }
218}
219
220func requireBasicAuth(h http.HandlerFunc) http.HandlerFunc {
221 return func(w http.ResponseWriter, r *http.Request) {
222 username, password, ok := r.BasicAuth()
223 required_username := os.Getenv("HTTP_USERNAME")
224 required_password := os.Getenv("HTTP_PASSWORD")
225 if !ok || username != required_username || password != required_password {
226 w.Header().Add("WWW-Authenticate", "Basic realm=\"User Visible Realm\"")
227 http.Error(w, "invalid authentication", http.StatusUnauthorized)
228 return
229 }
230 h(w, r)
231 }
232}
233
234//go:embed schema.sql
235var schema string
236
237//go:embed content/*
238var contentFs embed.FS
239
240func main() {
241 var err error
242
243 db, err = sql.Open("sqlite3", "file:database.db")
244 if err != nil {
245 log.Fatalf("failed to create sqlite database: %v\n", err)
246 }
247 defer db.Close()
248
249 if _, err := db.Exec(schema); err != nil {
250 log.Fatalf("failed to execute db schema: %v\n", err)
251 }
252
253 fs, err := fs.Sub(contentFs, "content")
254 if err != nil {
255 log.Fatal(err)
256 }
257
258 http.HandleFunc("GET /api/location", handleGetApiLog)
259 http.HandleFunc("POST /api/location", requireBasicAuth(handlePostApiLog))
260 http.HandleFunc("GET /api/shapes", handleGetApiShapes)
261 http.HandleFunc("POST /api/shapes", requireBasicAuth(handlePostApiShapes))
262 http.Handle("GET /static/", http.FileServerFS(fs))
263 http.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) {
264 http.ServeFileFS(w, r, fs, "index.html")
265 })
266 http.HandleFunc("GET /logger", requireBasicAuth(func(w http.ResponseWriter, r *http.Request) {
267 http.ServeFileFS(w, r, fs, "logger.html")
268 }))
269 http.HandleFunc("GET /shapes", requireBasicAuth(func(w http.ResponseWriter, r *http.Request) {
270 http.ServeFileFS(w, r, fs, "shapes.html")
271 }))
272
273 fsHandler := http.FileServerFS(fs)
274 http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
275 if r.URL.Path != "/" && !strings.Contains(r.URL.Path, ".") {
276 r.URL.Path += ".html"
277 }
278 fsHandler.ServeHTTP(w, r)
279 })
280
281 if err := http.ListenAndServe("0.0.0.0:8000", nil); err != nil {
282 log.Fatalf("http.ListenAndServe failed: %v", err)
283 }
284}