Final pre-launch version: Update logic
This commit is contained in:
+14
-2
@@ -36,6 +36,9 @@ import (
|
||||
// appVersion is set at build time via ldflags.
|
||||
var appVersion = "dev"
|
||||
|
||||
// appUpdateURL is set at build time via ldflags.
|
||||
var appUpdateURL = ""
|
||||
|
||||
// Service is the main API surface exposed to the frontend via Wails bindings.
|
||||
type Service struct {
|
||||
store *storage.Store
|
||||
@@ -1298,8 +1301,17 @@ func (s *Service) ImportSSHConfig(configPath string, folder string) (int, error)
|
||||
|
||||
// CheckForUpdates queries GitHub Releases for a newer version.
|
||||
func (s *Service) CheckForUpdates() (map[string]interface{}, error) {
|
||||
checker := updater.NewChecker(appVersion)
|
||||
info, err := checker.Check(context.Background())
|
||||
ctx := context.Background()
|
||||
var info *updater.UpdateInfo
|
||||
var err error
|
||||
|
||||
if appUpdateURL != "" {
|
||||
checker := updater.NewURLChecker(appUpdateURL, appVersion)
|
||||
info, err = checker.Check(ctx)
|
||||
} else {
|
||||
checker := updater.NewChecker(appVersion)
|
||||
info, err = checker.Check(ctx)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -146,3 +146,27 @@ func (a *App) DomReady(ctx context.Context) {
|
||||
})
|
||||
log.Println("DOM ready – backend:ready event emitted")
|
||||
}
|
||||
|
||||
// EmitOpenAbout tells the frontend to open the About tab.
|
||||
func (a *App) EmitOpenAbout() {
|
||||
a.mu.Lock()
|
||||
ctx := a.ctx
|
||||
a.mu.Unlock()
|
||||
if ctx == nil {
|
||||
log.Println("unable to open About: application context not initialised")
|
||||
return
|
||||
}
|
||||
runtime.EventsEmit(ctx, "app:open-about", nil)
|
||||
}
|
||||
|
||||
// RequestQuit asks the runtime to quit the application.
|
||||
func (a *App) RequestQuit() {
|
||||
a.mu.Lock()
|
||||
ctx := a.ctx
|
||||
a.mu.Unlock()
|
||||
if ctx == nil {
|
||||
log.Println("unable to quit: application context not initialised")
|
||||
return
|
||||
}
|
||||
runtime.Quit(ctx)
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -56,6 +57,23 @@ type Checker struct {
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
// Manifest describes a self-hosted update manifest.
|
||||
type Manifest struct {
|
||||
LatestVersion string `json:"latestVersion"`
|
||||
Version string `json:"version"`
|
||||
ReleaseURL string `json:"releaseURL"`
|
||||
ReleaseName string `json:"releaseName"`
|
||||
ReleaseNotes string `json:"releaseNotes"`
|
||||
PublishedAt string `json:"publishedAt"`
|
||||
}
|
||||
|
||||
// URLChecker performs version checks against a self-hosted update manifest.
|
||||
type URLChecker struct {
|
||||
url string
|
||||
current string
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
// NewChecker creates an update checker for the given current version.
|
||||
// The version string should be a semver tag (e.g. "v1.2.3" or "1.2.3").
|
||||
func NewChecker(currentVersion string) *Checker {
|
||||
@@ -69,6 +87,17 @@ func NewChecker(currentVersion string) *Checker {
|
||||
}
|
||||
}
|
||||
|
||||
// NewURLChecker creates a checker for a self-hosted update manifest URL.
|
||||
func NewURLChecker(url string, currentVersion string) *URLChecker {
|
||||
return &URLChecker{
|
||||
url: url,
|
||||
current: normaliseVersion(currentVersion),
|
||||
client: &http.Client{
|
||||
Timeout: requestTimeout,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// SetRepository overrides the GitHub repository to check.
|
||||
func (c *Checker) SetRepository(owner, repo string) {
|
||||
c.owner = owner
|
||||
@@ -134,6 +163,51 @@ func (c *Checker) Check(ctx context.Context) (*UpdateInfo, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Check queries the update manifest URL and compares it to the current version.
|
||||
func (c *URLChecker) Check(ctx context.Context) (*UpdateInfo, error) {
|
||||
if c.url == "" {
|
||||
return nil, fmt.Errorf("updater: update manifest URL is empty")
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.url, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("updater: failed to create request: %w", err)
|
||||
}
|
||||
req.Header.Set("Accept", "application/json, text/plain")
|
||||
req.Header.Set("User-Agent", "NexusTerminal-UpdateChecker")
|
||||
|
||||
resp, err := c.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("updater: request failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("updater: update server returned %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("updater: failed to read response: %w", err)
|
||||
}
|
||||
|
||||
info, ok := parseManifest(body, c.current)
|
||||
if ok {
|
||||
return info, nil
|
||||
}
|
||||
|
||||
latest := normaliseVersion(strings.TrimSpace(string(body)))
|
||||
if latest == "" {
|
||||
return nil, fmt.Errorf("updater: update manifest is empty")
|
||||
}
|
||||
|
||||
return &UpdateInfo{
|
||||
Available: isNewer(latest, c.current),
|
||||
CurrentVersion: c.current,
|
||||
LatestVersion: latest,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// normaliseVersion strips a leading "v" and trims whitespace.
|
||||
func normaliseVersion(v string) string {
|
||||
v = strings.TrimSpace(v)
|
||||
@@ -141,6 +215,32 @@ func normaliseVersion(v string) string {
|
||||
return v
|
||||
}
|
||||
|
||||
func parseManifest(body []byte, current string) (*UpdateInfo, bool) {
|
||||
var manifest Manifest
|
||||
if err := json.Unmarshal(body, &manifest); err != nil {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
latest := manifest.LatestVersion
|
||||
if latest == "" {
|
||||
latest = manifest.Version
|
||||
}
|
||||
latest = normaliseVersion(latest)
|
||||
if latest == "" {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
return &UpdateInfo{
|
||||
Available: isNewer(latest, current),
|
||||
CurrentVersion: current,
|
||||
LatestVersion: latest,
|
||||
ReleaseURL: manifest.ReleaseURL,
|
||||
ReleaseName: manifest.ReleaseName,
|
||||
ReleaseNotes: manifest.ReleaseNotes,
|
||||
PublishedAt: manifest.PublishedAt,
|
||||
}, true
|
||||
}
|
||||
|
||||
// isNewer performs a simple string comparison of dot-separated version parts.
|
||||
// Returns true if latest > current (basic semver, no pre-release handling).
|
||||
func isNewer(latest, current string) bool {
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package updater
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
@@ -64,3 +67,40 @@ func TestTruncateNotes(t *testing.T) {
|
||||
t.Errorf("expected 501 runes, got %d", len([]rune(got)))
|
||||
}
|
||||
}
|
||||
|
||||
func TestURLCheckerManifestJSON(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{"latestVersion":"1.2.3","releaseURL":"https://example.com/release"}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
checker := NewURLChecker(server.URL, "1.2.0")
|
||||
info, err := checker.Check(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if !info.Available {
|
||||
t.Fatalf("expected update to be available")
|
||||
}
|
||||
if info.LatestVersion != "1.2.3" {
|
||||
t.Fatalf("unexpected latest version: %s", info.LatestVersion)
|
||||
}
|
||||
}
|
||||
|
||||
func TestURLCheckerPlainText(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
_, _ = w.Write([]byte("2.0.0"))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
checker := NewURLChecker(server.URL, "1.9.0")
|
||||
info, err := checker.Check(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if !info.Available {
|
||||
t.Fatalf("expected update to be available")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user