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