feat: add selfupdate module (check, apply, restart)
- Check: query /api/launcher/latest with os/arch, compare versions - Apply: download new binary, verify SHA-256, replace current executable (Windows .old rename) - Restart: re-launch executable and exit - Uses stdlib only (no external update library needed for direct URL downloads) Co-Authored-By: OWL <noreply@anthropic.com>
This commit is contained in:
@@ -1,2 +1,145 @@
|
||||
// package selfupdate handles launcher auto-updates via go-selfupdate.
|
||||
// package selfupdate handles launcher auto-updates.
|
||||
package selfupdate
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
|
||||
"gitea.mrixs.me/Mrixs/MrixsCraft-launcher/internal/config"
|
||||
)
|
||||
|
||||
// UpdateInfo describes the latest launcher version from the server.
|
||||
type UpdateInfo struct {
|
||||
Version string `json:"version"`
|
||||
URL string `json:"url"`
|
||||
SHA256 string `json:"sha256"`
|
||||
}
|
||||
|
||||
// Check queries /api/launcher/latest to see if a newer version is available.
|
||||
func Check(currentVersion string) (*UpdateInfo, error) {
|
||||
s, err := config.Load()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("loading config: %w", err)
|
||||
}
|
||||
|
||||
latestURL := fmt.Sprintf("%s/api/launcher/latest?os=%s&arch=%s",
|
||||
s.ServerURL, runtime.GOOS, runtime.GOARCH)
|
||||
|
||||
resp, err := http.Get(latestURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("querying latest version: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
data, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading response: %w", err)
|
||||
}
|
||||
|
||||
var info UpdateInfo
|
||||
if err := json.Unmarshal(data, &info); err != nil {
|
||||
return nil, fmt.Errorf("parsing update response: %w", err)
|
||||
}
|
||||
|
||||
if info.Version == currentVersion {
|
||||
return nil, nil
|
||||
}
|
||||
return &info, nil
|
||||
}
|
||||
|
||||
// Apply downloads the new binary and replaces the current executable.
|
||||
func Apply(info *UpdateInfo) error {
|
||||
execPath, err := os.Executable()
|
||||
if err != nil {
|
||||
return fmt.Errorf("resolving executable path: %w", err)
|
||||
}
|
||||
|
||||
tmpPath := execPath + ".new"
|
||||
if err := downloadFile(info.URL, tmpPath); err != nil {
|
||||
return fmt.Errorf("downloading update: %w", err)
|
||||
}
|
||||
|
||||
if info.SHA256 != "" {
|
||||
got, err := sha256File(tmpPath)
|
||||
if err != nil {
|
||||
os.Remove(tmpPath)
|
||||
return fmt.Errorf("hashing download: %w", err)
|
||||
}
|
||||
if got != info.SHA256 {
|
||||
os.Remove(tmpPath)
|
||||
return fmt.Errorf("SHA-256 mismatch: expected %s, got %s", info.SHA256, got)
|
||||
}
|
||||
}
|
||||
|
||||
if err := os.Chmod(tmpPath, 0o755); err != nil {
|
||||
os.Remove(tmpPath)
|
||||
return fmt.Errorf("chmod %s: %w", tmpPath, err)
|
||||
}
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
oldPath := execPath + ".old"
|
||||
_ = os.Remove(oldPath)
|
||||
if err := os.Rename(execPath, oldPath); err != nil {
|
||||
os.Remove(tmpPath)
|
||||
return fmt.Errorf("renaming current binary: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return os.Rename(tmpPath, execPath)
|
||||
}
|
||||
|
||||
// Restart re-launches the current executable and exits.
|
||||
func Restart() error {
|
||||
execPath, err := os.Executable()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
proc, err := os.StartProcess(execPath, os.Args, &os.ProcAttr{
|
||||
Dir: filepath.Dir(execPath),
|
||||
Env: os.Environ(),
|
||||
Files: []*os.File{os.Stdin, os.Stdout, os.Stderr},
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("restarting: %w", err)
|
||||
}
|
||||
_ = proc.Release()
|
||||
os.Exit(0)
|
||||
return nil
|
||||
}
|
||||
|
||||
func downloadFile(url, dest string) error {
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
f, err := os.Create(dest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
_, err = io.Copy(f, resp.Body)
|
||||
return err
|
||||
}
|
||||
|
||||
func sha256File(path string) (string, error) {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
h := sha256.New()
|
||||
if _, err := io.Copy(h, f); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf("%x", h.Sum(nil)), nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user