Bläddra i källkod

Add automatic update checks.

- A check for new versions on the GitHub releases pages happens
  once every 24 hours. When a new version is available, a notice
  is displayed on the admin UI.
Kailash Nadh 5 år sedan
förälder
incheckning
d8a60d1295
7 ändrade filer med 107 tillägg och 11 borttagningar
  1. 1 1
      Makefile
  2. 7 5
      cmd/admin.go
  3. 0 1
      cmd/handlers.go
  4. 8 1
      cmd/main.go
  5. 78 0
      cmd/updates.go
  6. 5 1
      frontend/src/App.vue
  7. 8 2
      frontend/src/assets/style.scss

+ 1 - 1
Makefile

@@ -20,7 +20,7 @@ deps:
 # Build steps.
 .PHONY: build
 build:
-	go build -o ${BIN} -ldflags="-s -w -X 'main.buildString=${BUILDSTR}'" cmd/*.go
+	go build -o ${BIN} -ldflags="-s -w -X 'main.buildString=${BUILDSTR}' -X 'main.versionString=${VERSION}'" cmd/*.go
 
 .PHONY: build-frontend
 build-frontend:

+ 7 - 5
cmd/admin.go

@@ -13,11 +13,12 @@ import (
 )
 
 type configScript struct {
-	RootURL       string   `json:"rootURL"`
-	FromEmail     string   `json:"fromEmail"`
-	Messengers    []string `json:"messengers"`
-	MediaProvider string   `json:"mediaProvider"`
-	NeedsRestart  bool     `json:"needsRestart"`
+	RootURL       string     `json:"rootURL"`
+	FromEmail     string     `json:"fromEmail"`
+	Messengers    []string   `json:"messengers"`
+	MediaProvider string     `json:"mediaProvider"`
+	NeedsRestart  bool       `json:"needsRestart"`
+	Update        *AppUpdate `json:"update"`
 }
 
 // handleGetConfigScript returns general configuration as a Javascript
@@ -35,6 +36,7 @@ func handleGetConfigScript(c echo.Context) error {
 
 	app.Lock()
 	out.NeedsRestart = app.needsRestart
+	out.Update = app.update
 	app.Unlock()
 
 	var (

+ 0 - 1
cmd/handlers.go

@@ -34,7 +34,6 @@ var reUUID = regexp.MustCompile("^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[
 func registerHTTPHandlers(e *echo.Echo) {
 	// Group of private handlers with BasicAuth.
 	g := e.Group("", middleware.BasicAuth(basicAuth))
-
 	g.GET("/", handleIndexPage)
 	g.GET("/api/health", handleHealthCheck)
 	g.GET("/api/config.js", handleGetConfigScript)

+ 8 - 1
cmd/main.go

@@ -42,6 +42,9 @@ type App struct {
 	// Global variable that stores the state indicating that a restart is required
 	// after a settings update.
 	needsRestart bool
+
+	// Global state that stores data on an available remote update.
+	update *AppUpdate
 	sync.Mutex
 }
 
@@ -53,7 +56,8 @@ var (
 	db      *sqlx.DB
 	queries *Queries
 
-	buildString string
+	buildString   string
+	versionString string
 )
 
 func init() {
@@ -137,6 +141,9 @@ func main() {
 	// Start the app server.
 	srv := initHTTPServer(app)
 
+	// Star the update checker.
+	go checkUpdates(versionString, time.Hour*24, app)
+
 	// Wait for the reload signal with a callback to gracefully shut down resources.
 	// The `wait` channel is passed to awaitReload to wait for the callback to finish
 	// within N seconds, or do a force reload.

+ 78 - 0
cmd/updates.go

@@ -0,0 +1,78 @@
+package main
+
+import (
+	"encoding/json"
+	"io/ioutil"
+	"net/http"
+	"regexp"
+	"time"
+
+	"golang.org/x/mod/semver"
+)
+
+const updateCheckURL = "https://api.github.com/repos/knadh/listmonk/releases/latest"
+
+type remoteUpdateResp struct {
+	Version string `json:"tag_name"`
+	URL     string `json:"html_url"`
+}
+
+// AppUpdate contains information of a new update available to the app that
+// is sent to the frontend.
+type AppUpdate struct {
+	Version string `json:"version"`
+	URL     string `json:"url"`
+}
+
+var reSemver = regexp.MustCompile(`-(.*)`)
+
+// checkUpdates is a blocking function that checks for updates to the app
+// at the given intervals. On detecting a new update (new semver), it
+// sets the global update status that renders a prompt on the UI.
+func checkUpdates(curVersion string, interval time.Duration, app *App) {
+	// Strip -* suffix.
+	curVersion = reSemver.ReplaceAllString(curVersion, "")
+
+	time.Sleep(time.Second * 1)
+	ticker := time.NewTicker(interval)
+	for ; true; <-ticker.C {
+		resp, err := http.Get(updateCheckURL)
+		if err != nil {
+			app.log.Printf("error checking for remote update: %v", err)
+			continue
+		}
+
+		if resp.StatusCode != 200 {
+			app.log.Printf("non 200 response on remote update check: %d", resp.StatusCode)
+			continue
+		}
+
+		b, err := ioutil.ReadAll(resp.Body)
+		if err != nil {
+			app.log.Printf("error reading remote update payload: %v", err)
+			continue
+		}
+		resp.Body.Close()
+
+		var up remoteUpdateResp
+		if err := json.Unmarshal(b, &up); err != nil {
+			app.log.Printf("error unmarshalling remote update payload: %v", err)
+			continue
+		}
+
+		// There is an update. Set it on the global app state.
+		if semver.IsValid(up.Version) {
+			v := reSemver.ReplaceAllString(up.Version, "")
+			if semver.Compare(v, curVersion) > 0 {
+				app.Lock()
+				app.update = &AppUpdate{
+					Version: up.Version,
+					URL:     up.URL,
+				}
+				app.Unlock()
+
+				app.log.Printf("new update %s found", up.Version)
+			}
+		}
+	}
+}

+ 5 - 1
frontend/src/App.vue

@@ -84,7 +84,7 @@
 
       <!-- body //-->
       <div class="main">
-        <div class="global-notices" v-if="serverConfig.needsRestart">
+        <div class="global-notices" v-if="serverConfig.needsRestart || serverConfig.update">
           <div v-if="serverConfig.needsRestart" class="notification is-danger">
             Settings have changed. Pause all running campaigns and restart the app
              &mdash;
@@ -94,6 +94,10 @@
                 Restart
             </b-button>
           </div>
+          <div v-if="serverConfig.update" class="notification is-success">
+            A new update ({{ serverConfig.update.version }}) is available.
+            <a :href="serverConfig.update.url" target="_blank">View</a>
+          </div>
         </div>
 
         <router-view :key="$route.fullPath" />

+ 8 - 2
frontend/src/assets/style.scss

@@ -158,11 +158,17 @@ section {
 }
 .notification {
   padding: 10px 15px;
+  border-left: 5px solid #eee;
+
   &.is-danger {
     background: $white-ter;
     color: $black;
-    border-left: 5px solid $red;
-    font-weight: bold;
+    border-left-color: $red;
+  }
+  &.is-success {
+    background: $white-ter;
+    color: $black;
+    border-left-color: $green;
   }
 }