mirror of
https://github.com/qdm12/ddns-updater.git
synced 2026-04-05 08:54:09 -04:00
HTTP server refactor (#164)
* Rework current server * Update call is blocking * Run first update without blocking
This commit is contained in:
@@ -150,9 +150,12 @@ func _main(ctx context.Context, timeNow func() time.Time) int {
|
||||
runner := update.NewRunner(db, updater, ipGetter, p.cooldown, logger, timeNow)
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
forceUpdate := make(chan struct{})
|
||||
go runner.Run(ctx, p.period, forceUpdate)
|
||||
forceUpdate <- struct{}{}
|
||||
|
||||
go runner.Run(ctx, p.period)
|
||||
|
||||
// note: errors are logged within the goroutine,
|
||||
// no need to collect the resulting errors.
|
||||
go runner.ForceUpdate(ctx)
|
||||
|
||||
const healthServerAddr = "127.0.0.1:9999"
|
||||
isHealthy := health.MakeIsHealthy(db, net.LookupIP, logger)
|
||||
@@ -164,7 +167,7 @@ func _main(ctx context.Context, timeNow func() time.Time) int {
|
||||
|
||||
address := fmt.Sprintf("0.0.0.0:%d", p.listeningPort)
|
||||
uiDir := p.dir + "/ui"
|
||||
server := server.New(address, p.rootURL, uiDir, db, logger.WithPrefix("http server: "), forceUpdate)
|
||||
server := server.New(ctx, address, p.rootURL, uiDir, db, logger.WithPrefix("http server: "), runner)
|
||||
wg.Add(1)
|
||||
go server.Run(ctx, wg)
|
||||
notify(1, fmt.Sprintf("Launched with %d records to watch", len(records)))
|
||||
|
||||
1
go.mod
1
go.mod
@@ -3,6 +3,7 @@ module github.com/qdm12/ddns-updater
|
||||
go 1.15
|
||||
|
||||
require (
|
||||
github.com/go-chi/chi v1.5.1
|
||||
github.com/kyokomi/emoji v2.2.4+incompatible
|
||||
github.com/ovh/go-ovh v1.1.0
|
||||
github.com/qdm12/golibs v0.0.0-20210110211000-0a3a4541ae09
|
||||
|
||||
2
go.sum
2
go.sum
@@ -14,6 +14,8 @@ github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s=
|
||||
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
|
||||
github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb h1:D4uzjWwKYQ5XnAvUbuvHW93esHg7F8N/OYeBBcJoTr0=
|
||||
github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
|
||||
github.com/go-chi/chi v1.5.1 h1:kfTK3Cxd/dkMu/rKs5ZceWYp+t5CtiE7vmaTv3LjC6w=
|
||||
github.com/go-chi/chi v1.5.1/go.mod h1:REp24E+25iKvxgeTfHmdUoL5x15kBiDBlnIl5bCwe2k=
|
||||
github.com/go-openapi/analysis v0.0.0-20180825180245-b006789cd277/go.mod h1:k70tL6pCuVxPJOHXQ+wIac1FUrvNkHolPie/cLEU6hI=
|
||||
github.com/go-openapi/analysis v0.17.0 h1:8JV+dzJJiK46XqGLqqLav8ZfEiJECp8jlOFhpiCdZ+0=
|
||||
github.com/go-openapi/analysis v0.17.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik=
|
||||
|
||||
35
internal/server/error.go
Normal file
35
internal/server/error.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type errJSONWrapper struct {
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
func httpError(w http.ResponseWriter, status int, errString string) {
|
||||
w.WriteHeader(status)
|
||||
if errString == "" {
|
||||
errString = http.StatusText(status)
|
||||
}
|
||||
body := errJSONWrapper{Error: errString}
|
||||
_ = json.NewEncoder(w).Encode(body)
|
||||
}
|
||||
|
||||
type errorsJSONWrapper struct {
|
||||
Errors []string `json:"errors"`
|
||||
}
|
||||
|
||||
func httpErrors(w http.ResponseWriter, status int, errors []error) {
|
||||
w.WriteHeader(status)
|
||||
|
||||
errs := make([]string, len(errors))
|
||||
for i := range errors {
|
||||
errs[i] = errors[i].Error()
|
||||
}
|
||||
|
||||
body := errorsJSONWrapper{Errors: errs}
|
||||
_ = json.NewEncoder(w).Encode(body)
|
||||
}
|
||||
24
internal/server/fileserver.go
Normal file
24
internal/server/fileserver.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
)
|
||||
|
||||
func fileServer(router chi.Router, path string, root http.FileSystem) {
|
||||
if path != "/" && path[len(path)-1] != '/' {
|
||||
router.Get(path,
|
||||
http.RedirectHandler(path+"/", http.StatusMovedPermanently).ServeHTTP)
|
||||
path += "/"
|
||||
}
|
||||
path += "*"
|
||||
|
||||
router.Get(path, func(w http.ResponseWriter, r *http.Request) {
|
||||
rctx := chi.RouteContext(r.Context())
|
||||
pathPrefix := strings.TrimSuffix(rctx.RoutePattern(), "/*")
|
||||
fs := http.StripPrefix(pathPrefix, http.FileServer(root))
|
||||
fs.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
@@ -1,63 +1,50 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"context"
|
||||
"net/http"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/go-chi/chi/middleware"
|
||||
"github.com/qdm12/ddns-updater/internal/data"
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/qdm12/golibs/logging"
|
||||
"github.com/qdm12/ddns-updater/internal/update"
|
||||
)
|
||||
|
||||
func newHandler(rootURL, uiDir string, db data.Database,
|
||||
logger logging.Logger, forceUpdate chan<- struct{}) http.Handler {
|
||||
return &handler{
|
||||
rootURL: rootURL,
|
||||
uiDir: uiDir,
|
||||
db: db,
|
||||
logger: logger, // TODO log middleware
|
||||
// TODO build information
|
||||
timeNow: time.Now,
|
||||
forceUpdate: forceUpdate,
|
||||
}
|
||||
}
|
||||
|
||||
type handler struct {
|
||||
// Configuration
|
||||
rootURL string
|
||||
uiDir string
|
||||
// Channels
|
||||
forceUpdate chan<- struct{}
|
||||
// Objects and mock functions
|
||||
db data.Database
|
||||
logger logging.Logger
|
||||
type handlers struct {
|
||||
ctx context.Context
|
||||
// Objects
|
||||
db data.Database
|
||||
runner update.Runner
|
||||
indexTemplate *template.Template
|
||||
// Mockable functions
|
||||
timeNow func() time.Time
|
||||
}
|
||||
|
||||
func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
h.logger.Info("HTTP %s %s", r.Method, r.RequestURI)
|
||||
func newHandler(ctx context.Context, rootURL, uiDir string,
|
||||
db data.Database, runner update.Runner) http.Handler {
|
||||
indexTemplate := template.Must(template.ParseFiles(uiDir + "/index.html"))
|
||||
|
||||
r.RequestURI = strings.TrimPrefix(r.RequestURI, h.rootURL)
|
||||
switch {
|
||||
case r.Method == http.MethodGet && r.RequestURI == h.rootURL+"/":
|
||||
t := template.Must(template.ParseFiles(h.uiDir + "/index.html"))
|
||||
var htmlData models.HTMLData
|
||||
for _, record := range h.db.SelectAll() {
|
||||
row := record.HTML(h.timeNow())
|
||||
htmlData.Rows = append(htmlData.Rows, row)
|
||||
}
|
||||
if err := t.ExecuteTemplate(w, "index.html", htmlData); err != nil {
|
||||
h.logger.Warn(err)
|
||||
fmt.Fprint(w, "An error occurred creating this webpage")
|
||||
}
|
||||
case r.Method == http.MethodGet && r.RequestURI == h.rootURL+"/update":
|
||||
h.logger.Info("Update started manually")
|
||||
h.forceUpdate <- struct{}{}
|
||||
http.Redirect(w, r, h.rootURL, 301)
|
||||
default:
|
||||
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
||||
handlers := &handlers{
|
||||
ctx: ctx,
|
||||
db: db,
|
||||
indexTemplate: indexTemplate,
|
||||
// TODO build information
|
||||
timeNow: time.Now,
|
||||
runner: runner,
|
||||
}
|
||||
|
||||
router := chi.NewRouter()
|
||||
|
||||
router.Use(middleware.Logger, middleware.CleanPath)
|
||||
|
||||
router.Get(rootURL+"/", handlers.index)
|
||||
|
||||
router.Get(rootURL+"/update", handlers.update)
|
||||
|
||||
// UI file server for other paths
|
||||
fileServer(router, rootURL+"/", http.Dir(uiDir))
|
||||
|
||||
return router
|
||||
}
|
||||
|
||||
18
internal/server/index.go
Normal file
18
internal/server/index.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
)
|
||||
|
||||
func (h *handlers) index(w http.ResponseWriter, r *http.Request) {
|
||||
var htmlData models.HTMLData
|
||||
for _, record := range h.db.SelectAll() {
|
||||
row := record.HTML(h.timeNow())
|
||||
htmlData.Rows = append(htmlData.Rows, row)
|
||||
}
|
||||
if err := h.indexTemplate.ExecuteTemplate(w, "index.html", htmlData); err != nil {
|
||||
httpError(w, http.StatusInternalServerError, "failed generating webpage: "+err.Error())
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/data"
|
||||
"github.com/qdm12/ddns-updater/internal/update"
|
||||
"github.com/qdm12/golibs/logging"
|
||||
)
|
||||
|
||||
@@ -20,9 +21,9 @@ type server struct {
|
||||
handler http.Handler
|
||||
}
|
||||
|
||||
func New(address, rootURL, uiDir string, db data.Database, logger logging.Logger,
|
||||
forceUpdate chan<- struct{}) Server {
|
||||
handler := newHandler(rootURL, uiDir, db, logger, forceUpdate)
|
||||
func New(ctx context.Context, address, rootURL, uiDir string, db data.Database, logger logging.Logger,
|
||||
runner update.Runner) Server {
|
||||
handler := newHandler(ctx, rootURL, uiDir, db, runner)
|
||||
return &server{
|
||||
address: address,
|
||||
logger: logger,
|
||||
|
||||
18
internal/server/update.go
Normal file
18
internal/server/update.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func (h *handlers) update(w http.ResponseWriter, r *http.Request) {
|
||||
start := h.timeNow()
|
||||
errors := h.runner.ForceUpdate(h.ctx)
|
||||
duration := h.timeNow().Sub(start)
|
||||
if len(errors) > 0 {
|
||||
httpErrors(w, http.StatusInternalServerError, errors)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusAccepted)
|
||||
message := "All records updated successfully in " + duration.String()
|
||||
_, _ = w.Write([]byte(message))
|
||||
}
|
||||
@@ -13,12 +13,15 @@ import (
|
||||
)
|
||||
|
||||
type Runner interface {
|
||||
Run(ctx context.Context, period time.Duration, force <-chan struct{})
|
||||
Run(ctx context.Context, period time.Duration)
|
||||
ForceUpdate(ctx context.Context) []error
|
||||
}
|
||||
|
||||
type runner struct {
|
||||
db data.Database
|
||||
updater Updater
|
||||
force chan struct{}
|
||||
forceResult chan []error
|
||||
cooldown time.Duration
|
||||
netLookupIP func(hostname string) ([]net.IP, error)
|
||||
ipGetter IPGetter
|
||||
@@ -31,6 +34,8 @@ func NewRunner(db data.Database, updater Updater, ipGetter IPGetter,
|
||||
return &runner{
|
||||
db: db,
|
||||
updater: updater,
|
||||
force: make(chan struct{}),
|
||||
forceResult: make(chan []error),
|
||||
cooldown: cooldown,
|
||||
netLookupIP: net.LookupIP,
|
||||
ipGetter: ipGetter,
|
||||
@@ -201,7 +206,7 @@ func setInitialUpToDateStatus(db data.Database, id int, updateIP net.IP, now tim
|
||||
return db.Update(id, record)
|
||||
}
|
||||
|
||||
func (r *runner) updateNecessary(ctx context.Context) {
|
||||
func (r *runner) updateNecessary(ctx context.Context) (errors []error) {
|
||||
records := r.db.SelectAll()
|
||||
doIP, doIPv4, doIPv6 := doIPVersion(records)
|
||||
ip, ipv4, ipv6, errors := r.getNewIPs(ctx, doIP, doIPv4, doIPv6)
|
||||
@@ -219,6 +224,7 @@ func (r *runner) updateNecessary(ctx context.Context) {
|
||||
}
|
||||
updateIP := getIPMatchingVersion(ip, ipv4, ipv6, record.Settings.IPVersion())
|
||||
if err := setInitialUpToDateStatus(r.db, id, updateIP, now); err != nil {
|
||||
errors = append(errors, err)
|
||||
r.logger.Error(err)
|
||||
}
|
||||
}
|
||||
@@ -227,22 +233,36 @@ func (r *runner) updateNecessary(ctx context.Context) {
|
||||
updateIP := getIPMatchingVersion(ip, ipv4, ipv6, record.Settings.IPVersion())
|
||||
r.logger.Info("Updating record %s to use %s", record.Settings, updateIP)
|
||||
if err := r.updater.Update(ctx, id, updateIP, r.timeNow()); err != nil {
|
||||
errors = append(errors, err)
|
||||
r.logger.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
func (r *runner) Run(ctx context.Context, period time.Duration, force <-chan struct{}) {
|
||||
func (r *runner) Run(ctx context.Context, period time.Duration) {
|
||||
ticker := time.NewTicker(period)
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
r.updateNecessary(ctx)
|
||||
case <-force:
|
||||
r.updateNecessary(ctx)
|
||||
case <-r.force:
|
||||
r.forceResult <- r.updateNecessary(ctx)
|
||||
case <-ctx.Done():
|
||||
ticker.Stop()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *runner) ForceUpdate(ctx context.Context) (errs []error) {
|
||||
r.force <- struct{}{}
|
||||
|
||||
select {
|
||||
case errs = <-r.forceResult:
|
||||
case <-ctx.Done():
|
||||
errs = []error{ctx.Err()}
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user