birdtown-visit-counter/birdtown-visit-counter.go

95 lines
2.3 KiB
Go

package main
import (
"encoding/json"
"flag"
"log"
"net/http"
"os"
"strconv"
"sync"
)
type visitsStore struct {
m map[string]int
*sync.RWMutex
}
func (s *visitsStore) visit(ending string) int {
s.Lock()
defer s.Unlock()
oldVisits, has := s.m[ending]
var newVisits int
if has {
newVisits = oldVisits + 1
} else {
newVisits = 1
}
s.m[ending] = newVisits
return newVisits
}
type visitsHandler struct {
store *visitsStore
}
func (h *visitsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// CORS: this is expected to be called from a different domain,
// hoops require jumping
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Max-Age", "86400")
w.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization")
w.Header().Set("content-type", "application/json")
if r.Method == http.MethodPost {
var s string
if err := json.NewDecoder(r.Body).Decode(&s); err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("cannot parse data"))
return
}
w.WriteHeader(http.StatusOK)
w.Write(([]byte)(strconv.Itoa(h.store.visit(s))))
return
}
if r.Method == http.MethodGet {
jsonBytes, err := json.Marshal(h.store.m)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("cannot serialize"))
return
}
w.WriteHeader(http.StatusOK)
w.Write(jsonBytes)
return
}
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusOK)
return
}
w.Header().Set("Allow", "POST, GET, OPTIONS")
w.WriteHeader(http.StatusMethodNotAllowed)
}
func main() {
addr := flag.String("addr", ":11568", "Listen address")
initialFile := flag.String("file", "visits.json", "File with initial data in JSON format")
flag.Parse()
initialVisits := make(map[string]int)
initialVisitsJson, err := os.ReadFile(*initialFile)
if err == nil {
json.Unmarshal(initialVisitsJson, &initialVisits)
} else {
log.Println("initial file", *initialFile, "could not be read, defaulting to empty data")
}
store := visitsStore{m: initialVisits, RWMutex: &sync.RWMutex{}}
visitsH := &visitsHandler{store: &store}
http.Handle("/visits", visitsH)
log.Println("Starting web server...")
http.ListenAndServe(*addr, nil)
}