112 lines
3.3 KiB
Go
112 lines
3.3 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
|
|
}
|
|
|
|
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)
|
|
}
|