feat: customize launcher version check and CI/CD release workflow for G-Launcher
This commit is contained in:
1 parent
a5205d1bed
commit
88a1f1c40a
12 files changed
+435
-61
No files matched your search
@@ -1,57 +1,179 @@
|
||||
package gitService
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"crypto/subtle"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"g-launcher/pkg/constant"
|
||||
"g-launcher/pkg/models"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"github.com/minio/selfupdate"
|
||||
"net/url"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"g-launcher/pkg/constant"
|
||||
"g-launcher/pkg/models"
|
||||
|
||||
"github.com/minio/selfupdate"
|
||||
)
|
||||
|
||||
type GitService struct{}
|
||||
|
||||
|
||||
func (g *GitService) GetLatestLauncherVersion() (bool, string, string) {
|
||||
resp, err := http.Get(constant.LauncherGitUrl)
|
||||
if err != nil {
|
||||
return false, "", err.Error()
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
|
||||
var releases []models.ReleaseType
|
||||
err = json.Unmarshal(body, &releases)
|
||||
if err != nil {
|
||||
return false, "", err.Error()
|
||||
}
|
||||
|
||||
if len(releases) == 0 {
|
||||
return false, "", "no releases found"
|
||||
}
|
||||
|
||||
return true, releases[0].TagName, ""
|
||||
type launcherManifest struct {
|
||||
Version string `json:"version"`
|
||||
SHA256 string `json:"sha256"`
|
||||
}
|
||||
|
||||
var launcherVersionPattern = regexp.MustCompile(`^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$`)
|
||||
|
||||
func (g *GitService) GetLatestLauncherVersion() (bool, string, string) {
|
||||
// First, attempt to check via latest.json manifest
|
||||
if constant.LauncherManifestURL != "" {
|
||||
manifest, err := fetchLauncherManifest(constant.LauncherManifestURL)
|
||||
if err == nil && manifest.Version != "" {
|
||||
return true, manifest.Version, ""
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to Gitea/Forgejo/GitHub Releases API
|
||||
if constant.LauncherGitUrl != "" {
|
||||
resp, err := http.Get(constant.LauncherGitUrl)
|
||||
if err == nil {
|
||||
defer resp.Body.Close()
|
||||
body, readErr := io.ReadAll(resp.Body)
|
||||
if readErr == nil {
|
||||
var releases []models.ReleaseType
|
||||
if json.Unmarshal(body, &releases) == nil && len(releases) > 0 {
|
||||
return true, releases[0].TagName, ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false, "", "no launcher updates found"
|
||||
}
|
||||
|
||||
func (g *GitService) UpdateLauncherProgress(version string) (bool, string) {
|
||||
asset, ok := g.getReleaseAsset(version, constant.LauncherGitUrl, constant.LauncherFile)
|
||||
if !ok {
|
||||
return false, "no release found"
|
||||
if !constant.LauncherUpdatesEnabled {
|
||||
return false, "launcher updates are disabled in development builds"
|
||||
}
|
||||
|
||||
resp, err := http.Get(asset.BrowserDownloadURL)
|
||||
if err != nil {
|
||||
return false, err.Error()
|
||||
// Mode 1: Manifest-based update with SHA-256 integrity verification
|
||||
if constant.LauncherManifestURL != "" && constant.LauncherDownloadURL != "" {
|
||||
manifest, err := fetchLauncherManifest(constant.LauncherManifestURL)
|
||||
if err == nil && manifest.Version == version && manifest.SHA256 != "" {
|
||||
return g.applyManifestUpdate(manifest)
|
||||
}
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
err = selfupdate.Apply(resp.Body, selfupdate.Options{})
|
||||
if err != nil {
|
||||
return false, err.Error()
|
||||
// Mode 2: Release API fallback
|
||||
if constant.LauncherGitUrl != "" {
|
||||
asset, ok := g.getReleaseAsset(version, constant.LauncherGitUrl, constant.LauncherFile)
|
||||
if ok && asset.BrowserDownloadURL != "" {
|
||||
resp, err := http.Get(asset.BrowserDownloadURL)
|
||||
if err != nil {
|
||||
return false, err.Error()
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
err = selfupdate.Apply(resp.Body, selfupdate.Options{})
|
||||
if err != nil {
|
||||
return false, err.Error()
|
||||
}
|
||||
return true, ""
|
||||
}
|
||||
}
|
||||
|
||||
return false, "could not locate valid launcher update package"
|
||||
}
|
||||
|
||||
func (g *GitService) applyManifestUpdate(manifest launcherManifest) (bool, string) {
|
||||
client := &http.Client{Timeout: 10 * time.Minute}
|
||||
response, err := client.Get(constant.LauncherDownloadURL)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("download launcher update: %w", err).Error()
|
||||
}
|
||||
defer response.Body.Close()
|
||||
if response.StatusCode < http.StatusOK || response.StatusCode >= http.StatusMultipleChoices {
|
||||
return false, fmt.Sprintf("download launcher update: server returned %s", response.Status)
|
||||
}
|
||||
|
||||
temporaryFile, err := os.CreateTemp("", "g-launcher-update-*.exe")
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("create temporary launcher update: %w", err).Error()
|
||||
}
|
||||
temporaryPath := temporaryFile.Name()
|
||||
defer os.Remove(temporaryPath)
|
||||
|
||||
hash := sha256.New()
|
||||
if _, err := io.Copy(io.MultiWriter(temporaryFile, hash), response.Body); err != nil {
|
||||
temporaryFile.Close()
|
||||
return false, fmt.Errorf("save launcher update: %w", err).Error()
|
||||
}
|
||||
if err := temporaryFile.Close(); err != nil {
|
||||
return false, fmt.Errorf("close launcher update: %w", err).Error()
|
||||
}
|
||||
|
||||
expectedHash, err := hex.DecodeString(manifest.SHA256)
|
||||
if err != nil || subtle.ConstantTimeCompare(hash.Sum(nil), expectedHash) != 1 {
|
||||
return false, "launcher update checksum mismatch"
|
||||
}
|
||||
|
||||
updateFile, err := os.Open(temporaryPath)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("open launcher update: %w", err).Error()
|
||||
}
|
||||
defer updateFile.Close()
|
||||
if err := selfupdate.Apply(updateFile, selfupdate.Options{}); err != nil {
|
||||
return false, fmt.Errorf("apply launcher update: %w", err).Error()
|
||||
}
|
||||
|
||||
return true, ""
|
||||
}
|
||||
}
|
||||
|
||||
func fetchLauncherManifest(manifestURL string) (launcherManifest, error) {
|
||||
parsedURL, err := url.Parse(manifestURL)
|
||||
if err != nil || (parsedURL.Scheme != "https" && parsedURL.Scheme != "http") {
|
||||
return launcherManifest{}, fmt.Errorf("invalid launcher manifest URL")
|
||||
}
|
||||
query := parsedURL.Query()
|
||||
query.Set("_", fmt.Sprintf("%d", time.Now().UnixNano()))
|
||||
parsedURL.RawQuery = query.Encode()
|
||||
|
||||
request, err := http.NewRequest(http.MethodGet, parsedURL.String(), nil)
|
||||
if err != nil {
|
||||
return launcherManifest{}, fmt.Errorf("create launcher manifest request: %w", err)
|
||||
}
|
||||
request.Header.Set("Cache-Control", "no-cache")
|
||||
request.Header.Set("User-Agent", "G-Launcher")
|
||||
|
||||
client := &http.Client{Timeout: 20 * time.Second}
|
||||
response, err := client.Do(request)
|
||||
if err != nil {
|
||||
return launcherManifest{}, fmt.Errorf("check launcher updates: %w", err)
|
||||
}
|
||||
defer response.Body.Close()
|
||||
if response.StatusCode < http.StatusOK || response.StatusCode >= http.StatusMultipleChoices {
|
||||
return launcherManifest{}, fmt.Errorf("check launcher updates: server returned %s", response.Status)
|
||||
}
|
||||
|
||||
var manifest launcherManifest
|
||||
if err := json.NewDecoder(io.LimitReader(response.Body, 1<<20)).Decode(&manifest); err != nil {
|
||||
return launcherManifest{}, fmt.Errorf("read launcher update manifest: %w", err)
|
||||
}
|
||||
if !launcherVersionPattern.MatchString(manifest.Version) {
|
||||
return launcherManifest{}, fmt.Errorf("launcher update manifest has invalid version %q", manifest.Version)
|
||||
}
|
||||
manifest.SHA256 = strings.TrimSpace(manifest.SHA256)
|
||||
if len(manifest.SHA256) != sha256.Size*2 {
|
||||
return launcherManifest{}, fmt.Errorf("launcher update manifest has invalid SHA-256")
|
||||
}
|
||||
if _, err := hex.DecodeString(manifest.SHA256); err != nil {
|
||||
return launcherManifest{}, fmt.Errorf("launcher update manifest has invalid SHA-256: %w", err)
|
||||
}
|
||||
manifest.SHA256 = strings.ToLower(manifest.SHA256)
|
||||
return manifest, nil
|
||||
}
|
||||
Reference in new issue
Block a user