Steven Polley
ecfa12d3ed
All checks were successful
continuous-integration/drone/push Build is passing
186 lines
5.3 KiB
Go
186 lines
5.3 KiB
Go
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"encoding/json"
|
|
"flag"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"os/signal"
|
|
"strings"
|
|
"text/template"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
statsURL = "https://covid19stats.alberta.ca"
|
|
cacheTimeout = time.Hour * 8
|
|
)
|
|
|
|
var (
|
|
cache *Cache
|
|
t *template.Template // HTML Templates used for web mode. Initialized as a null pointer has near zero overhead when cli mode is run
|
|
)
|
|
|
|
// ABGovCovidData stores the Javascript object which is ripped from the statsURL. It's parsed as JSON encoded data.
|
|
type ABGovCovidData [][]string
|
|
|
|
// They screwed this up so much, the first index in the column, not the row. WTF alberta
|
|
// [0] = case number alberta
|
|
// [1] = date
|
|
// [2] = geo zone
|
|
// [3] = gender
|
|
// [4] = age range
|
|
// [5] = case status
|
|
// [6] = confirmed/suspected
|
|
|
|
// Cache keeps a local copy of the data in memory. The cache will only update if a web request comes in, not on a fixed timer.
|
|
// This is to ensure reduced load on Alberta's servers and to ensure a quick response when possible, and err on the side of caution when not possible.
|
|
type Cache struct {
|
|
Data struct {
|
|
ActiveCasesEdmonton int
|
|
TotalCasesEdmonton int
|
|
ActiveCasesAlberta int
|
|
TotalCasesAlberta int
|
|
}
|
|
UpdatedDate time.Time
|
|
}
|
|
|
|
func main() {
|
|
webMode := flag.Bool("web", false, "listen on port 8080 as web server")
|
|
flag.Parse()
|
|
if *webMode {
|
|
// Setup a web server
|
|
stop := make(chan os.Signal, 1)
|
|
signal.Notify(stop, os.Interrupt)
|
|
// Initial load of data
|
|
go func() {
|
|
for {
|
|
var err error
|
|
cache, err = getUpdatedData()
|
|
if err != nil {
|
|
log.Printf("cache is empty and failed to download data from ab website: %v", err)
|
|
fmt.Println("sleeping for 2 hours")
|
|
time.Sleep(time.Hour * 2)
|
|
continue
|
|
}
|
|
break
|
|
}
|
|
}()
|
|
// Web server routing
|
|
go func() {
|
|
http.Handle("/public/", http.StripPrefix("/public/", http.FileServer(http.Dir("public"))))
|
|
http.HandleFunc("/", homePageHandler)
|
|
log.Fatal(http.ListenAndServe(":8080", nil))
|
|
}()
|
|
log.Println("started covid19-edmonton server on 8080")
|
|
|
|
// Parse all template files at startup
|
|
var err error
|
|
t, err = template.ParseGlob("./templates/*")
|
|
if err != nil {
|
|
log.Fatalf("couldn't parse HTML templates in templates directory: %v", err)
|
|
}
|
|
|
|
<-stop
|
|
log.Println("shutting covid19-edmonton server down")
|
|
} else {
|
|
var err error
|
|
cache, err = getUpdatedData()
|
|
if err != nil {
|
|
log.Fatalf("failed to get data from alberta government website: %v", err)
|
|
}
|
|
// CLI mode, print output to stdout
|
|
fmt.Println("Edmonton Active: ", cache.Data.ActiveCasesEdmonton)
|
|
fmt.Println("Edmonton Total: ", cache.Data.TotalCasesEdmonton)
|
|
fmt.Println("Alberta Active: ", cache.Data.ActiveCasesAlberta)
|
|
fmt.Println("Alberta Total: ", cache.Data.TotalCasesAlberta)
|
|
|
|
log.Print("\n\nPress 'Enter' to continue...")
|
|
bufio.NewReader(os.Stdin).ReadBytes('\n')
|
|
}
|
|
}
|
|
|
|
// getUpdatedData reaches out to Alberta's website to get the most recent data and returns a pointer to a cache and an error
|
|
func getUpdatedData() (*Cache, error) {
|
|
log.Printf("getting up to date data from AB website")
|
|
// Download the latest stats
|
|
resp, err := http.Get(statsURL)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to download stats page '%s': %v", statsURL, err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
bodyB, err := ioutil.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read stats page response body: %v", err)
|
|
}
|
|
|
|
// Parse the HTML for the data
|
|
|
|
// Find the beginning, trim off everything before
|
|
split := strings.Split(string(bodyB), "\"data\":[[\"1\",")
|
|
body := fmt.Sprintf("%s%s", "[[\"1\",", split[1])
|
|
|
|
// Find the end, trim off everything after
|
|
split = strings.Split(body, "]]")
|
|
body = fmt.Sprintf("%s%s", split[0], "]]")
|
|
|
|
// Parse the json string into a struct
|
|
data := ABGovCovidData{}
|
|
err = json.Unmarshal([]byte(body), &data)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to parse data as json: %v", err)
|
|
}
|
|
|
|
cache := &Cache{UpdatedDate: time.Now().Add(-time.Hour * 6)}
|
|
|
|
// count the cases
|
|
for i := range data[2] {
|
|
if data[2][i] == "Edmonton Zone" {
|
|
cache.Data.TotalCasesEdmonton++
|
|
if data[5][i] == "Active" {
|
|
cache.Data.ActiveCasesEdmonton++
|
|
}
|
|
}
|
|
if data[5][i] == "Active" {
|
|
cache.Data.ActiveCasesAlberta++
|
|
}
|
|
cache.Data.TotalCasesAlberta++
|
|
}
|
|
|
|
return cache, nil
|
|
}
|
|
|
|
// Web Mode only: HTTP GET /
|
|
func homePageHandler(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method == "GET" {
|
|
if cache == nil {
|
|
w.WriteHeader(http.StatusFailedDependency)
|
|
log.Printf("unable to serve page due to no cached data, possibly due to application still starting up, or AB government site down, or changed formatting - may need to review how page is parsed")
|
|
return
|
|
}
|
|
if time.Now().Add(-time.Hour * 6).After(cache.UpdatedDate.Add(cacheTimeout)) {
|
|
w.WriteHeader(http.StatusOK)
|
|
tempCache, err := getUpdatedData() // Hold tempCache in case there's an error, we don't want to nullify our pointer to a working cache that has aged. We will proceed with aged data.
|
|
if err != nil {
|
|
log.Printf("failed to update cache: %v", err)
|
|
} else {
|
|
cache = tempCache // we retrieved it successfully, so overwrite our old data
|
|
}
|
|
}
|
|
|
|
err := t.ExecuteTemplate(w, "index.html", cache)
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
log.Fatalf("failed to execute index.html template: %v", err)
|
|
}
|
|
|
|
} else {
|
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
|
}
|
|
}
|