birdtown-visit-counter/birdtown-visit-counter.go
2024-09-17 18:12:23 +03:00

114 lines
3.2 KiB
Go

// This file is part of birdtown-visit-counter, and is © 2024 Alexander Khodyrev.
//
// Full source code is available at https://git.akho.name/akho/birdtown-visit-counter
//
// birdtown-visit-counter is free software: you can redistribute it and/or
// modify it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the License,
// or (at your option) any later version.
//
// birdtown-visit-counter is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License
// for more details.
//
// You should have received a copy of the GNU Affero General Public License along
// with birdtown-visit-counter. If not, see <https://www.gnu.org/licenses/>.
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 {
h.store.Lock()
jsonBytes, err := json.Marshal(h.store.m)
h.store.Unlock()
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("/", visitsH)
log.Println("Starting web server...")
http.ListenAndServe(*addr, nil)
}