// 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 . 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 } func commonHeaders(w http.ResponseWriter) { // 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("Allow", "POST, GET, OPTIONS") } func (store *visitsStore) handleOptions(w http.ResponseWriter, r *http.Request) { commonHeaders(w) w.WriteHeader(http.StatusOK) } func (store *visitsStore) handlePost(w http.ResponseWriter, r *http.Request) { commonHeaders(w) w.Header().Set("content-type", "application/json") 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(store.visit(s)))) } func (store *visitsStore) handleGet(w http.ResponseWriter, r *http.Request) { commonHeaders(w) w.Header().Set("content-type", "application/json") store.Lock() jsonBytes, err := json.Marshal(store.m) store.Unlock() if err != nil { w.WriteHeader(http.StatusInternalServerError) w.Write([]byte("cannot serialize")) return } w.WriteHeader(http.StatusOK) w.Write(jsonBytes) } 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{}} http.HandleFunc("GET /", store.handleGet) http.HandleFunc("POST /", store.handlePost) http.HandleFunc("OPTIONS /", store.handleOptions) log.Println("Starting web server at ", *addr, "...") http.ListenAndServe(*addr, nil) }