hivedav

A curlable free/busy scheduler with CalDAV integration
git clone https://git.in0rdr.ch/hivedav.git
Log | Files | Refs | Pull requests | README | LICENSE

commit c3b17e82eb4a515a2fc1a9bd71b14431417ed9a9
parent ff984cad70ab3182121107216a7e57a0c6d18b20
Author: Andreas Gruhler <andreas.gruhler@adfinis.com>
Date:   Mon, 18 Sep 2023 22:19:57 +0200

feat(49/52): add cron and healthz

Diffstat:
Mgo.mod | 1+
Mgo.sum | 2++
Mmain.go | 44++++++++++++++++++++++++++------------------
Mserver.go | 78+++++++++++++++++++++++++++++++++++++++++++++++-------------------------------
4 files changed, 76 insertions(+), 49 deletions(-)

diff --git a/go.mod b/go.mod @@ -29,5 +29,6 @@ require ( golang.org/x/text v0.9.0 // indirect gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/robfig/cron.v2 v2.0.0-20150107220207-be2e0b0deed5 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum @@ -488,6 +488,8 @@ gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/robfig/cron.v2 v2.0.0-20150107220207-be2e0b0deed5 h1:E846t8CnR+lv5nE+VuiKTDG/v1U2stad0QzddfJC7kY= +gopkg.in/robfig/cron.v2 v2.0.0-20150107220207-be2e0b0deed5/go.mod h1:hiOFpYm0ZJbusNj2ywpbrXowU3G8U6GIQzqn2mw1UIE= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/main.go b/main.go @@ -2,11 +2,12 @@ package main import ( "fmt" + "github.com/julienschmidt/httprouter" + "gopkg.in/robfig/cron.v2" + "hivedav/caldav" + "hivedav/config" "log" "net/http" - "hivedav/config" - "hivedav/caldav" - "github.com/julienschmidt/httprouter" ) var hivedavVersion = "v0.2-nightly" @@ -26,49 +27,56 @@ func main() { } log.Printf("Starting hivedav %s 🍯\n", hivedavVersion) - log.Println("Reading availability from:", conf.CaldavUri) log.Println("Reading as user:", conf.CaldavUser) + c := cron.New() + c.AddFunc("@every 30m", func() { updateDatabase(server) }) + c.Start() + go updateDatabase(server) + + router := httprouter.New() + router.GET("/", server.Index) + router.GET("/week/:week", server.Week) + router.ServeFiles("/css/*filepath", http.Dir("./css")) + router.GET("/healthz", server.Healthz) + log.Printf("Listening on %s:%d\n", conf.ListenAddress, conf.ListenPort) + log.Fatal(http.ListenAndServe(fmt.Sprintf("%s:%d", conf.ListenAddress, conf.ListenPort), router)) +} + +func updateDatabase(s *Server) { // Find current user principal - userPrincipal, err := caldav.UserPrincipal(conf.CaldavUri, conf.CaldavUser, conf.CaldavPassword) + userPrincipal, err := caldav.UserPrincipal(s.config.CaldavUri, s.config.CaldavUser, s.config.CaldavPassword) if err != nil { log.Fatal("Error reading current user principal: ", err) } log.Println("User principal:", userPrincipal) // Find home set of current user principal - homeSetUri, err := caldav.CalendarHome(conf.CaldavUri, conf.CaldavUser, conf.CaldavPassword, userPrincipal, conf.Calendar) + homeSetUri, err := caldav.CalendarHome(s.config.CaldavUri, s.config.CaldavUser, s.config.CaldavPassword, userPrincipal, s.config.Calendar) if err != nil { log.Fatal("Error reading home set URI: ", err) } log.Println("Calendar homeset:", homeSetUri) // Find calendar URI for the calendar specified in the config - calendarUri, err := caldav.CalendarFromHomeSet(conf.CaldavUri, conf.CaldavUser, conf.CaldavPassword, homeSetUri, conf.Calendar) + calendarUri, err := caldav.CalendarFromHomeSet(s.config.CaldavUri, s.config.CaldavUser, s.config.CaldavPassword, homeSetUri, s.config.Calendar) if err != nil { log.Fatal("Error reading calendar URI: ", err) } - log.Printf("Using calendar %d in homeset with URI '%s'\n", conf.Calendar, calendarUri) + log.Printf("Using calendar %d in homeset with URI '%s'\n", s.config.Calendar, calendarUri) log.Println("⏳ Fetching availability from CalDAV server, please wait..") // Get availability data for the calendar specified in the config - calData, err := caldav.GetAvailability(conf.CaldavUri, conf.CaldavUser, conf.CaldavPassword, calendarUri) + calData, err := caldav.GetAvailability(s.config.CaldavUri, s.config.CaldavUser, s.config.CaldavPassword, calendarUri) if err != nil { log.Fatal("Error reading calendar URI: ", err) } - err = server.updateAvailability(calData) + err = s.UpdateAvailability(calData) if err != nil { log.Fatalf("Error fetching availability: %v", err) } - log.Printf("Ready, listening on %s:%d\n", conf.ListenAddress, conf.ListenPort) - - router := httprouter.New() - router.GET("/", server.Index) - router.GET("/week/:week", server.Week) - router.ServeFiles("/css/*filepath", http.Dir("./css")) - - log.Fatal(http.ListenAndServe(fmt.Sprintf("%s:%d", conf.ListenAddress, conf.ListenPort), router)) + log.Println("Ready. Database was initialized with latest CalDAV availability data.") } diff --git a/server.go b/server.go @@ -1,36 +1,36 @@ package main import ( - "regexp" - "time" - "strconv" - "github.com/teambition/rrule-go" - "github.com/emersion/go-ical" - "hivedav/config" - "hivedav/caldav" + "database/sql" "fmt" + "github.com/emersion/go-ical" + "github.com/julienschmidt/httprouter" + _ "github.com/mattn/go-sqlite3" "github.com/pquerna/termchalk/prettytable" + "github.com/teambition/rrule-go" + "hivedav/caldav" + "hivedav/config" "html/template" - _ "github.com/mattn/go-sqlite3" - "database/sql" "io" "log" "net/http" - "github.com/julienschmidt/httprouter" + "regexp" + "strconv" "strings" + "time" ) type Server struct { config *config.Config - db *sql.DB + db *sql.DB } type TableData struct { TableHead []string - Rows [][]string - Week int - Year int - Version string + Rows [][]string + Week int + Year int + Version string } var drop = `DROP TABLE IF EXISTS availability; @@ -169,7 +169,7 @@ func (s *Server) Week(w http.ResponseWriter, req *http.Request, ps httprouter.Pa rows := make([][]string, 9) for i := range rows { - rows[i] = make([]string, 6) + rows[i] = make([]string, 6) } // Define the table header @@ -183,12 +183,12 @@ func (s *Server) Week(w http.ResponseWriter, req *http.Request, ps httprouter.Pa fmt.Sprintf("Fri %s", monday.Add(time.Hour*24*4).Format("02.01.")), } - tableData := TableData { + tableData := TableData{ TableHead: tableHead, - Rows: rows, - Week: week, - Year: year, - Version: hivedavVersion, + Rows: rows, + Week: week, + Year: year, + Version: hivedavVersion, } // TODO: use timeIt to go through the loops below, remove the static arrays @@ -198,15 +198,15 @@ func (s *Server) Week(w http.ResponseWriter, req *http.Request, ps httprouter.Pa // Working hours - Eight to Five //for h := 8; h < 17; h++ { - for _, h := range []int{8,9,10,11,12,13,14,15,16} { + for _, h := range []int{8, 9, 10, 11, 12, 13, 14, 15, 16} { var availability [5]string // Working days - Monday through Friday //for d := 0; d < 5; d++ { - for _, d := range []int{1,2,3,4,5} { + for _, d := range []int{1, 2, 3, 4, 5} { // add/remove 1 minute to make the sql time query in available() below behave correctly // convert to read/compare in UTC from db - start := timeIt.Add(time.Hour * time.Duration((d-1)*24 + h)).Add(1 * time.Minute).UTC().Format(sqlDateFmt) - end := timeIt.Add(time.Hour * time.Duration((d-1)*24 + h+1)).Add(-1 * time.Minute).UTC().Format(sqlDateFmt) + start := timeIt.Add(time.Hour * time.Duration((d-1)*24+h)).Add(1 * time.Minute).UTC().Format(sqlDateFmt) + end := timeIt.Add(time.Hour * time.Duration((d-1)*24+h+1)).Add(-1 * time.Minute).UTC().Format(sqlDateFmt) avi, err := s.available(start, end) if err != nil { log.Printf("Error getting availability on day '%d' hour '%d'\n", d, h) @@ -270,14 +270,14 @@ func (s *Server) Week(w http.ResponseWriter, req *http.Request, ps httprouter.Pa func (s *Server) Index(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { timeNow := time.Now() - _, currentWeek:= timeNow.ISOWeek() + _, currentWeek := timeNow.ISOWeek() ps := httprouter.Params{ httprouter.Param{"week", strconv.Itoa(currentWeek)}, } s.Week(w, req, ps) } -func (s *Server) updateAvailability(calData []caldav.CalData) error { +func (s *Server) UpdateAvailability(calData []caldav.CalData) error { // TODO: Make max for recurring events without end time configurable rrLimit := time.Date(2024, 12, 31, 0, 0, 0, 0, time.Local) //localNow := time.Now() @@ -309,14 +309,14 @@ func (s *Server) updateAvailability(calData []caldav.CalData) error { if c.Name == ical.CompTimezone { for _, child := range c.Children { //tzOffsetFrom = child.Props.Get(ical.PropTimezoneOffsetFrom).Value; - tzOffsetTo = child.Props.Get(ical.PropTimezoneOffsetTo).Value; + tzOffsetTo = child.Props.Get(ical.PropTimezoneOffsetTo).Value } } } var tzOffsetDuration time.Duration re := regexp.MustCompile(`^([+-][0-9]{2}).*`) - matches := re.FindStringSubmatch(tzOffsetTo) + matches := re.FindStringSubmatch(tzOffsetTo) if len(matches) == 0 { //_, offset := localNow.Zone() //tzOffsetDuration = time.Second * time.Duration(offset) @@ -327,7 +327,7 @@ func (s *Server) updateAvailability(calData []caldav.CalData) error { tzOffsetDurationInt, _ := strconv.Atoi(matches[1]) tzOffsetDuration = time.Hour * time.Duration(tzOffsetDurationInt) // reset to UTC - tzOffsetDuration = time.Hour * time.Duration(-1 * tzOffsetDurationInt) + tzOffsetDuration = time.Hour * time.Duration(-1*tzOffsetDurationInt) } for _, e := range cal.Events() { @@ -408,7 +408,7 @@ func (s *Server) updateAvailability(calData []caldav.CalData) error { } // parse time from local time zone and return UTC time for sqlite db -func parseTime(timeStr string, offset time.Duration) (time.Time, error){ +func parseTime(timeStr string, offset time.Duration) (time.Time, error) { t, err := time.Parse("20060102T150405Z", timeStr) if err != nil { t, err = time.Parse("20060102T150405", timeStr) @@ -422,6 +422,22 @@ func parseTime(timeStr string, offset time.Duration) (time.Time, error){ return t.Add(offset), nil } +// https://github.com/kelseyhightower/app-healthz +func (s *Server) Healthz(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { + var count = 0 + row := s.db.QueryRow("SELECT count(*) FROM availability") + + if err := row.Scan(&count); err == sql.ErrNoRows { + log.Println("Error reading availabilty table") + } + if count > 0 { + io.WriteString(w, fmt.Sprintf("{\"status\":\"wagwan, hive all good\",\"rows\":\"%d\",\"version\":\"%s\"}", count, hivedavVersion)) + } else { + w.WriteHeader(http.StatusInternalServerError) + io.WriteString(w, fmt.Sprintf("{\"status\":\"nah fam, db not initialized\",\"rows\":\"%d\",\"version\":\"%s\"}", count, hivedavVersion)) + } +} + func (s *Server) showCubicles() { }