734 lines
24 KiB
Go
734 lines
24 KiB
Go
package updater
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"errors"
|
||
"fmt"
|
||
"io"
|
||
"os"
|
||
"os/exec"
|
||
"path/filepath"
|
||
"runtime"
|
||
"sort"
|
||
"strings"
|
||
"time"
|
||
|
||
"scout/pkg/client"
|
||
"scout/pkg/scoutpkg"
|
||
)
|
||
|
||
const ExitRestartNow = 42
|
||
|
||
type Config struct {
|
||
Client *client.Client
|
||
Adapter PlatformAdapter
|
||
PackageName string
|
||
CurrentVersion string
|
||
CurrentPath string
|
||
RootDir string
|
||
Platform string
|
||
Architecture string
|
||
Channel string
|
||
Entrypoint string
|
||
HealthTimeout time.Duration
|
||
KeepReleases int
|
||
}
|
||
|
||
type ReleasePointer struct {
|
||
Version string `json:"version"`
|
||
Path string `json:"path"`
|
||
Entrypoint string `json:"entrypoint"`
|
||
Platform string `json:"platform"`
|
||
Architecture string `json:"architecture"`
|
||
PackageType string `json:"package_type"`
|
||
SHA256 string `json:"sha256"`
|
||
PreparedAt time.Time `json:"prepared_at"`
|
||
ActivatedAt time.Time `json:"activated_at,omitempty"`
|
||
HealthyAt time.Time `json:"healthy_at,omitempty"`
|
||
}
|
||
|
||
type State struct {
|
||
Stage string `json:"stage"`
|
||
CurrentVersion string `json:"current_version,omitempty"`
|
||
PreviousVersion string `json:"previous_version,omitempty"`
|
||
Previous *ReleasePointer `json:"previous,omitempty"`
|
||
PendingVersion string `json:"pending_version,omitempty"`
|
||
ActivatedAt time.Time `json:"activated_at,omitempty"`
|
||
Healthy bool `json:"healthy"`
|
||
LastError string `json:"last_error,omitempty"`
|
||
UpdatedAt time.Time `json:"updated_at"`
|
||
}
|
||
|
||
type Status struct {
|
||
Current *ReleasePointer `json:"current,omitempty"`
|
||
Pending *ReleasePointer `json:"pending,omitempty"`
|
||
State State `json:"state"`
|
||
}
|
||
|
||
type Manager struct {
|
||
config Config
|
||
}
|
||
|
||
func New(config Config) (*Manager, error) {
|
||
if config.Client == nil {
|
||
return nil, errors.New("\u5ba2\u6237\u7aef\u4e0d\u80fd\u4e3a\u7a7a")
|
||
}
|
||
if strings.TrimSpace(config.PackageName) == "" {
|
||
return nil, errors.New("\u5305\u540d\u4e0d\u80fd\u4e3a\u7a7a")
|
||
}
|
||
if strings.TrimSpace(config.RootDir) == "" {
|
||
return nil, errors.New("\u66f4\u65b0\u6839\u76ee\u5f55\u4e0d\u80fd\u4e3a\u7a7a")
|
||
}
|
||
platform, architecture := config.Platform, config.Architecture
|
||
if platform == "" && architecture == "" {
|
||
platform, architecture = scoutpkg.CurrentTarget()
|
||
} else {
|
||
var err error
|
||
platform, err = scoutpkg.NormalizePlatform(platform)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
architecture, err = scoutpkg.NormalizeArchitecture(architecture)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
}
|
||
config.Platform = platform
|
||
config.Architecture = architecture
|
||
if config.Channel == "" {
|
||
config.Channel = "stable"
|
||
}
|
||
if config.HealthTimeout <= 0 {
|
||
config.HealthTimeout = 2 * time.Minute
|
||
}
|
||
if config.KeepReleases <= 0 {
|
||
config.KeepReleases = 2
|
||
}
|
||
root, err := filepath.Abs(config.RootDir)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("\u89e3\u6790\u66f4\u65b0\u6839\u76ee\u5f55\u5931\u8d25: %w", err)
|
||
}
|
||
config.RootDir = root
|
||
if config.Adapter == nil {
|
||
config.Adapter, err = NewPlatformAdapter(config.Platform)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
}
|
||
for _, name := range []string{"releases", "downloads"} {
|
||
if err := os.MkdirAll(filepath.Join(root, name), 0o755); err != nil {
|
||
return nil, fmt.Errorf("\u521b\u5efa\u66f4\u65b0\u76ee\u5f55\u5931\u8d25: %w", err)
|
||
}
|
||
}
|
||
manager := &Manager{config: config}
|
||
if _, err := manager.readPointer("current.json"); os.IsNotExist(err) && config.CurrentVersion != "" && config.CurrentPath != "" && config.Entrypoint != "" {
|
||
if _, err := manager.RegisterCurrent(config.CurrentVersion, config.CurrentPath, config.Entrypoint); err != nil {
|
||
return nil, err
|
||
}
|
||
}
|
||
return manager, nil
|
||
}
|
||
|
||
func (manager *Manager) RegisterCurrent(version, releasePath, entrypoint string) (ReleasePointer, error) {
|
||
if strings.TrimSpace(version) == "" {
|
||
return ReleasePointer{}, errors.New("\u5f53\u524d\u7248\u672c\u4e0d\u80fd\u4e3a\u7a7a")
|
||
}
|
||
releasePath, err := filepath.Abs(releasePath)
|
||
if err != nil {
|
||
return ReleasePointer{}, fmt.Errorf("\u89e3\u6790\u5f53\u524d\u7a0b\u5e8f\u76ee\u5f55\u5931\u8d25: %w", err)
|
||
}
|
||
executable, err := safeEntrypoint(releasePath, entrypoint)
|
||
if err != nil {
|
||
return ReleasePointer{}, err
|
||
}
|
||
if _, err := os.Stat(executable); err != nil {
|
||
return ReleasePointer{}, fmt.Errorf("\u5f53\u524d\u7a0b\u5e8f\u5165\u53e3\u4e0d\u5b58\u5728: %w", err)
|
||
}
|
||
if err := manager.config.Adapter.Validate(ReleasePointer{
|
||
Version: version,
|
||
Path: releasePath,
|
||
Entrypoint: entrypoint,
|
||
Platform: manager.config.Platform,
|
||
Architecture: manager.config.Architecture,
|
||
PackageType: "full",
|
||
}); err != nil {
|
||
return ReleasePointer{}, err
|
||
}
|
||
now := time.Now().UTC()
|
||
pointer := ReleasePointer{
|
||
Version: version,
|
||
Path: releasePath,
|
||
Entrypoint: entrypoint,
|
||
Platform: manager.config.Platform,
|
||
Architecture: manager.config.Architecture,
|
||
PackageType: "full",
|
||
PreparedAt: now,
|
||
ActivatedAt: now,
|
||
HealthyAt: now,
|
||
}
|
||
if err := manager.writeJSON("current.json", pointer); err != nil {
|
||
return ReleasePointer{}, err
|
||
}
|
||
state := State{Stage: "healthy", CurrentVersion: version, Healthy: true, UpdatedAt: now}
|
||
if err := manager.writeJSON("state.json", state); err != nil {
|
||
return ReleasePointer{}, err
|
||
}
|
||
return pointer, nil
|
||
}
|
||
|
||
func (manager *Manager) Check(ctx context.Context) (client.Package, bool, error) {
|
||
currentVersion := manager.currentVersion()
|
||
return manager.config.Client.CheckUpdateFor(ctx, manager.config.PackageName, currentVersion, manager.queryOptions(currentVersion))
|
||
}
|
||
|
||
func (manager *Manager) Watch(ctx context.Context) (<-chan client.Update, error) {
|
||
currentVersion := manager.currentVersion()
|
||
return manager.config.Client.WatchUpdatesFor(ctx, client.WatchRequest{
|
||
Packages: []string{manager.config.PackageName},
|
||
Versions: map[string]string{manager.config.PackageName: currentVersion},
|
||
Platform: manager.config.Platform,
|
||
Architecture: manager.config.Architecture,
|
||
Channel: manager.config.Channel,
|
||
})
|
||
}
|
||
|
||
func (manager *Manager) Prepare(ctx context.Context, version string) (ReleasePointer, error) {
|
||
if pending, err := manager.readPointer("pending.json"); err == nil {
|
||
return ReleasePointer{}, fmt.Errorf("\u5df2\u6709\u5f85\u6fc0\u6d3b\u7248\u672c %s", pending.Version)
|
||
}
|
||
currentVersion := manager.currentVersion()
|
||
options := manager.queryOptions(currentVersion)
|
||
item, err := manager.resolvePackage(ctx, version, options)
|
||
if err != nil {
|
||
return ReleasePointer{}, err
|
||
}
|
||
if currentVersion != "" && client.CompareVersions(item.Version, currentVersion) <= 0 {
|
||
return ReleasePointer{}, errors.New("\u76ee\u6807\u7248\u672c\u5fc5\u987b\u9ad8\u4e8e\u5f53\u524d\u7248\u672c")
|
||
}
|
||
releaseDir, err := manager.safeReleaseDir(item.Version)
|
||
if err != nil {
|
||
return ReleasePointer{}, err
|
||
}
|
||
if _, err := os.Stat(releaseDir); err == nil {
|
||
return ReleasePointer{}, errors.New("\u76ee\u6807\u7248\u672c\u76ee\u5f55\u5df2\u7ecf\u5b58\u5728")
|
||
} else if !os.IsNotExist(err) {
|
||
return ReleasePointer{}, fmt.Errorf("\u68c0\u67e5\u7248\u672c\u76ee\u5f55\u5931\u8d25: %w", err)
|
||
}
|
||
stagingDir, err := os.MkdirTemp(filepath.Dir(releaseDir), ".staging-"+item.Version+"-")
|
||
if err != nil {
|
||
return ReleasePointer{}, fmt.Errorf("\u521b\u5efa\u7248\u672c\u4e34\u65f6\u76ee\u5f55\u5931\u8d25: %w", err)
|
||
}
|
||
stagingReady := false
|
||
defer func() {
|
||
if !stagingReady {
|
||
_ = os.RemoveAll(stagingDir)
|
||
}
|
||
}()
|
||
|
||
fileName := item.FileName
|
||
if fileName == "" && len(item.Targets) > 0 {
|
||
fileName = item.Targets[0].FileName
|
||
}
|
||
if fileName == "" {
|
||
fileName = fmt.Sprintf("%s-%s.pkg", item.Name, item.Version)
|
||
}
|
||
downloadPath := filepath.Join(manager.config.RootDir, "downloads", item.Version+"-"+filepath.Base(fileName))
|
||
downloaded, err := manager.config.Client.DownloadFor(ctx, item.Name, item.Version, downloadPath, options)
|
||
if err != nil {
|
||
return ReleasePointer{}, err
|
||
}
|
||
|
||
entrypoint := manager.config.Entrypoint
|
||
if strings.EqualFold(filepath.Ext(fileName), ".scout") {
|
||
entrypoint, err = manager.extractScout(downloadPath, stagingDir, entrypoint)
|
||
if err != nil {
|
||
return ReleasePointer{}, err
|
||
}
|
||
} else {
|
||
destination := filepath.Join(stagingDir, filepath.Base(fileName))
|
||
if err := moveDownloadedFile(downloadPath, destination); err != nil {
|
||
return ReleasePointer{}, err
|
||
}
|
||
if entrypoint == "" {
|
||
entrypoint = filepath.Base(fileName)
|
||
}
|
||
}
|
||
if entrypoint == "" {
|
||
return ReleasePointer{}, errors.New("\u66f4\u65b0\u5305\u672a\u6307\u5b9a\u7a0b\u5e8f\u5165\u53e3")
|
||
}
|
||
executable, err := safeEntrypoint(stagingDir, entrypoint)
|
||
if err != nil {
|
||
return ReleasePointer{}, err
|
||
}
|
||
if _, err := os.Stat(executable); err != nil {
|
||
return ReleasePointer{}, fmt.Errorf("\u65b0\u7248\u672c\u7a0b\u5e8f\u5165\u53e3\u4e0d\u5b58\u5728: %w", err)
|
||
}
|
||
if err := manager.config.Adapter.Validate(ReleasePointer{
|
||
Version: item.Version,
|
||
Path: stagingDir,
|
||
Entrypoint: entrypoint,
|
||
Platform: manager.config.Platform,
|
||
Architecture: manager.config.Architecture,
|
||
PackageType: item.PackageType,
|
||
SHA256: item.SHA256,
|
||
}); err != nil {
|
||
return ReleasePointer{}, err
|
||
}
|
||
if runtime.GOOS != "windows" && manager.config.Platform != "android" {
|
||
if info, err := os.Stat(executable); err == nil && info.Mode().Perm()&0o111 == 0 {
|
||
_ = os.Chmod(executable, info.Mode().Perm()|0o755)
|
||
}
|
||
}
|
||
|
||
now := time.Now().UTC()
|
||
pointer := ReleasePointer{
|
||
Version: item.Version,
|
||
Path: releaseDir,
|
||
Entrypoint: entrypoint,
|
||
Platform: item.Platform,
|
||
Architecture: item.Architecture,
|
||
PackageType: item.PackageType,
|
||
SHA256: downloaded.SHA256,
|
||
PreparedAt: now,
|
||
}
|
||
if err := manager.writeReleaseMetadataAt(stagingDir, pointer); err != nil {
|
||
return ReleasePointer{}, err
|
||
}
|
||
if err := os.Rename(stagingDir, releaseDir); err != nil {
|
||
return ReleasePointer{}, fmt.Errorf("\u6fc0\u6d3b\u7248\u672c\u76ee\u5f55\u5931\u8d25: %w", err)
|
||
}
|
||
stagingReady = true
|
||
if err := manager.writeJSON("pending.json", pointer); err != nil {
|
||
return ReleasePointer{}, err
|
||
}
|
||
state := State{
|
||
Stage: "prepared",
|
||
CurrentVersion: currentVersion,
|
||
PendingVersion: pointer.Version,
|
||
Healthy: false,
|
||
UpdatedAt: now,
|
||
}
|
||
if err := manager.writeJSON("state.json", state); err != nil {
|
||
return ReleasePointer{}, err
|
||
}
|
||
return pointer, nil
|
||
}
|
||
|
||
func (manager *Manager) Activate() (ReleasePointer, error) {
|
||
pending, err := manager.readPointer("pending.json")
|
||
if err != nil {
|
||
return ReleasePointer{}, errors.New("\u6ca1\u6709\u5f85\u6fc0\u6d3b\u7248\u672c")
|
||
}
|
||
var previous *ReleasePointer
|
||
if current, err := manager.readPointer("current.json"); err == nil {
|
||
previous = ¤t
|
||
}
|
||
now := time.Now().UTC()
|
||
pending.ActivatedAt = now
|
||
pending.HealthyAt = time.Time{}
|
||
if err := manager.writeJSON("current.json", pending); err != nil {
|
||
return ReleasePointer{}, err
|
||
}
|
||
state := State{
|
||
Stage: "activated",
|
||
CurrentVersion: pending.Version,
|
||
PendingVersion: pending.Version,
|
||
ActivatedAt: now,
|
||
Healthy: false,
|
||
UpdatedAt: now,
|
||
}
|
||
if previous != nil {
|
||
state.Previous = previous
|
||
state.PreviousVersion = previous.Version
|
||
}
|
||
if err := manager.writeJSON("state.json", state); err != nil {
|
||
return ReleasePointer{}, err
|
||
}
|
||
return pending, nil
|
||
}
|
||
|
||
// Install 将待激活版本交给平台适配器处理。
|
||
// 桌面平台通常只需要准备文件,Android 等平台由宿主应用注入系统安装实现。
|
||
func (manager *Manager) Install(ctx context.Context) error {
|
||
pending, err := manager.readPointer("pending.json")
|
||
if err != nil {
|
||
if os.IsNotExist(err) {
|
||
return errors.New("没有待安装版本,请先准备更新")
|
||
}
|
||
return err
|
||
}
|
||
return manager.config.Adapter.Install(ctx, pending)
|
||
}
|
||
|
||
func (manager *Manager) ConfirmHealthy(version string) error {
|
||
current, err := manager.readPointer("current.json")
|
||
if err != nil {
|
||
return errors.New("\u5f53\u524d\u7248\u672c\u6307\u9488\u4e0d\u5b58\u5728")
|
||
}
|
||
if version != "" && current.Version != version {
|
||
return errors.New("\u5065\u5eb7\u786e\u8ba4\u7248\u672c\u4e0e\u5f53\u524d\u7248\u672c\u4e0d\u4e00\u81f4")
|
||
}
|
||
state, err := manager.readState()
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if state.Stage != "activated" || state.CurrentVersion != current.Version {
|
||
return errors.New("\u5f53\u524d\u72b6\u6001\u4e0d\u5141\u8bb8\u5065\u5eb7\u786e\u8ba4")
|
||
}
|
||
now := time.Now().UTC()
|
||
current.HealthyAt = now
|
||
if err := manager.writeJSON("current.json", current); err != nil {
|
||
return err
|
||
}
|
||
state.Stage = "healthy"
|
||
state.PendingVersion = ""
|
||
state.Healthy = true
|
||
state.UpdatedAt = now
|
||
if err := manager.writeJSON("state.json", state); err != nil {
|
||
return err
|
||
}
|
||
_ = os.Remove(manager.filePath("pending.json"))
|
||
return manager.Cleanup()
|
||
}
|
||
|
||
func (manager *Manager) Rollback(reason error) (ReleasePointer, error) {
|
||
state, err := manager.readState()
|
||
if err != nil {
|
||
return ReleasePointer{}, err
|
||
}
|
||
if state.Previous == nil {
|
||
return ReleasePointer{}, errors.New("\u6ca1\u6709\u53ef\u56de\u6eda\u7684\u65e7\u7248\u672c\u6307\u9488")
|
||
}
|
||
previous := *state.Previous
|
||
if _, err := safeEntrypoint(previous.Path, previous.Entrypoint); err != nil {
|
||
return ReleasePointer{}, err
|
||
}
|
||
if err := manager.writeJSON("current.json", previous); err != nil {
|
||
return ReleasePointer{}, err
|
||
}
|
||
state.Stage = "rolled_back"
|
||
state.CurrentVersion = previous.Version
|
||
state.PendingVersion = ""
|
||
state.Healthy = true
|
||
if reason != nil {
|
||
state.LastError = reason.Error()
|
||
}
|
||
state.UpdatedAt = time.Now().UTC()
|
||
if err := manager.writeJSON("state.json", state); err != nil {
|
||
return ReleasePointer{}, err
|
||
}
|
||
_ = os.Remove(manager.filePath("pending.json"))
|
||
return previous, nil
|
||
}
|
||
|
||
func (manager *Manager) RecoverStaleActivation(now time.Time) (bool, error) {
|
||
state, err := manager.readState()
|
||
if err != nil {
|
||
if os.IsNotExist(err) {
|
||
return false, nil
|
||
}
|
||
return false, err
|
||
}
|
||
if state.Stage != "activated" || state.Healthy {
|
||
return false, nil
|
||
}
|
||
if now.Before(state.ActivatedAt.Add(manager.config.HealthTimeout)) {
|
||
return false, nil
|
||
}
|
||
if state.Previous == nil {
|
||
return false, errors.New("\u5065\u5eb7\u786e\u8ba4\u8d85\u65f6\uff0c\u4f46\u6ca1\u6709\u53ef\u56de\u6eda\u7684\u65e7\u7248\u672c")
|
||
}
|
||
_, rollbackErr := manager.Rollback(errors.New("\u65b0\u7248\u672c\u5065\u5eb7\u786e\u8ba4\u8d85\u65f6\uff0c\u5df2\u81ea\u52a8\u56de\u6eda"))
|
||
return rollbackErr == nil, rollbackErr
|
||
}
|
||
|
||
func (manager *Manager) Status() (Status, error) {
|
||
result := Status{}
|
||
if current, err := manager.readPointer("current.json"); err == nil {
|
||
result.Current = ¤t
|
||
} else if !os.IsNotExist(err) {
|
||
return Status{}, err
|
||
}
|
||
if pending, err := manager.readPointer("pending.json"); err == nil {
|
||
result.Pending = &pending
|
||
} else if !os.IsNotExist(err) {
|
||
return Status{}, err
|
||
}
|
||
state, err := manager.readState()
|
||
if err != nil && !os.IsNotExist(err) {
|
||
return Status{}, err
|
||
}
|
||
result.State = state
|
||
return result, nil
|
||
}
|
||
|
||
func (manager *Manager) ResolveCurrent() (string, ReleasePointer, error) {
|
||
current, err := manager.readPointer("current.json")
|
||
if err != nil {
|
||
return "", ReleasePointer{}, errors.New("\u5f53\u524d\u7248\u672c\u6307\u9488\u4e0d\u5b58\u5728")
|
||
}
|
||
executable, err := safeEntrypoint(current.Path, current.Entrypoint)
|
||
if err != nil {
|
||
return "", ReleasePointer{}, err
|
||
}
|
||
if _, err := os.Stat(executable); err != nil {
|
||
return "", ReleasePointer{}, fmt.Errorf("\u5f53\u524d\u7a0b\u5e8f\u5165\u53e3\u4e0d\u5b58\u5728: %w", err)
|
||
}
|
||
return executable, current, nil
|
||
}
|
||
|
||
func (manager *Manager) Launch(ctx context.Context, args ...string) (*exec.Cmd, error) {
|
||
_, current, err := manager.ResolveCurrent()
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return manager.config.Adapter.Launch(ctx, LaunchRequest{
|
||
Release: current,
|
||
Args: args,
|
||
Env: append(os.Environ(),
|
||
"SCOUT_PACKAGE="+manager.config.PackageName,
|
||
"SCOUT_VERSION="+current.Version,
|
||
"SCOUT_UPDATE_ROOT="+manager.config.RootDir,
|
||
"SCOUT_PLATFORM="+manager.config.Platform,
|
||
"SCOUT_ARCHITECTURE="+manager.config.Architecture,
|
||
),
|
||
})
|
||
}
|
||
|
||
func (manager *Manager) Cleanup() error {
|
||
releasesDir := filepath.Join(manager.config.RootDir, "releases")
|
||
entries, err := os.ReadDir(releasesDir)
|
||
if err != nil {
|
||
return fmt.Errorf("\u8bfb\u53d6\u7248\u672c\u76ee\u5f55\u5931\u8d25: %w", err)
|
||
}
|
||
status, err := manager.Status()
|
||
if err != nil {
|
||
return err
|
||
}
|
||
protected := make(map[string]bool)
|
||
if status.Current != nil && isManagedRelease(releasesDir, status.Current.Path) {
|
||
protected[status.Current.Version] = true
|
||
}
|
||
if status.Pending != nil && isManagedRelease(releasesDir, status.Pending.Path) {
|
||
protected[status.Pending.Version] = true
|
||
}
|
||
candidates := make([]os.DirEntry, 0, len(entries))
|
||
for _, entry := range entries {
|
||
if entry.IsDir() && !protected[entry.Name()] {
|
||
candidates = append(candidates, entry)
|
||
}
|
||
}
|
||
sort.Slice(candidates, func(left, right int) bool {
|
||
leftInfo, _ := candidates[left].Info()
|
||
rightInfo, _ := candidates[right].Info()
|
||
return leftInfo.ModTime().After(rightInfo.ModTime())
|
||
})
|
||
keepCandidates := manager.config.KeepReleases - len(protected)
|
||
if keepCandidates < 0 {
|
||
keepCandidates = 0
|
||
}
|
||
for index, entry := range candidates {
|
||
if index < keepCandidates {
|
||
continue
|
||
}
|
||
target, err := manager.safeReleaseDir(entry.Name())
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if err := os.RemoveAll(target); err != nil {
|
||
return fmt.Errorf("\u6e05\u7406\u65e7\u7248\u672c\u5931\u8d25: %w", err)
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func (manager *Manager) resolvePackage(ctx context.Context, version string, options client.QueryOptions) (client.Package, error) {
|
||
if strings.TrimSpace(version) == "" {
|
||
return manager.config.Client.GetLatestFor(ctx, manager.config.PackageName, options)
|
||
}
|
||
versions, err := manager.config.Client.ListVersionsFor(ctx, manager.config.PackageName, options)
|
||
if err != nil {
|
||
return client.Package{}, err
|
||
}
|
||
for _, item := range versions {
|
||
if item.Version == version {
|
||
return item, nil
|
||
}
|
||
}
|
||
return client.Package{}, errors.New("\u6ca1\u6709\u627e\u5230\u6307\u5b9a\u7684\u517c\u5bb9\u7248\u672c")
|
||
}
|
||
|
||
func (manager *Manager) extractScout(downloadPath, releaseDir, fallbackEntrypoint string) (string, error) {
|
||
archive, err := scoutpkg.Open(downloadPath)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
target, extractErr := archive.Extract(manager.config.Platform, manager.config.Architecture, releaseDir)
|
||
closeErr := archive.Close()
|
||
if extractErr != nil {
|
||
return "", extractErr
|
||
}
|
||
if closeErr != nil {
|
||
return "", closeErr
|
||
}
|
||
if target.Entrypoint == "" {
|
||
return fallbackEntrypoint, nil
|
||
}
|
||
relative := strings.TrimPrefix(target.Entrypoint, strings.TrimSuffix(target.Path, "/"))
|
||
relative = strings.TrimPrefix(relative, "/")
|
||
if relative == "" {
|
||
relative = filepath.Base(filepath.FromSlash(target.Path))
|
||
}
|
||
return filepath.FromSlash(relative), nil
|
||
}
|
||
|
||
func (manager *Manager) queryOptions(currentVersion string) client.QueryOptions {
|
||
return client.QueryOptions{
|
||
Platform: manager.config.Platform,
|
||
Architecture: manager.config.Architecture,
|
||
Channel: manager.config.Channel,
|
||
CurrentVersion: currentVersion,
|
||
}
|
||
}
|
||
|
||
func (manager *Manager) currentVersion() string {
|
||
if current, err := manager.readPointer("current.json"); err == nil && current.Version != "" {
|
||
return current.Version
|
||
}
|
||
return manager.config.CurrentVersion
|
||
}
|
||
|
||
func (manager *Manager) writeReleaseMetadata(pointer ReleasePointer) error {
|
||
return manager.writeReleaseMetadataAt(pointer.Path, pointer)
|
||
}
|
||
|
||
func (manager *Manager) writeReleaseMetadataAt(directory string, pointer ReleasePointer) error {
|
||
data, err := json.MarshalIndent(pointer, "", " ")
|
||
if err != nil {
|
||
return fmt.Errorf("\u7f16\u7801\u7248\u672c\u5143\u6570\u636e\u5931\u8d25: %w", err)
|
||
}
|
||
if err := os.WriteFile(filepath.Join(directory, "scout-release.json"), data, 0o644); err != nil {
|
||
return fmt.Errorf("\u4fdd\u5b58\u7248\u672c\u5143\u6570\u636e\u5931\u8d25: %w", err)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func (manager *Manager) writeJSON(name string, value any) error {
|
||
data, err := json.MarshalIndent(value, "", " ")
|
||
if err != nil {
|
||
return fmt.Errorf("\u7f16\u7801\u66f4\u65b0\u72b6\u6001\u5931\u8d25: %w", err)
|
||
}
|
||
temp, err := os.CreateTemp(manager.config.RootDir, ".state-*")
|
||
if err != nil {
|
||
return fmt.Errorf("\u521b\u5efa\u72b6\u6001\u4e34\u65f6\u6587\u4ef6\u5931\u8d25: %w", err)
|
||
}
|
||
tempPath := temp.Name()
|
||
defer os.Remove(tempPath)
|
||
if _, err := temp.Write(data); err != nil {
|
||
temp.Close()
|
||
return fmt.Errorf("\u5199\u5165\u66f4\u65b0\u72b6\u6001\u5931\u8d25: %w", err)
|
||
}
|
||
if err := temp.Close(); err != nil {
|
||
return fmt.Errorf("\u5173\u95ed\u72b6\u6001\u4e34\u65f6\u6587\u4ef6\u5931\u8d25: %w", err)
|
||
}
|
||
if err := os.Rename(tempPath, manager.filePath(name)); err != nil {
|
||
return fmt.Errorf("\u4fdd\u5b58\u66f4\u65b0\u72b6\u6001\u5931\u8d25: %w", err)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func (manager *Manager) readPointer(name string) (ReleasePointer, error) {
|
||
data, err := os.ReadFile(manager.filePath(name))
|
||
if err != nil {
|
||
return ReleasePointer{}, err
|
||
}
|
||
var pointer ReleasePointer
|
||
if err := json.Unmarshal(data, &pointer); err != nil {
|
||
return ReleasePointer{}, fmt.Errorf("\u8bfb\u53d6\u7248\u672c\u6307\u9488\u5931\u8d25: %w", err)
|
||
}
|
||
return pointer, nil
|
||
}
|
||
|
||
func (manager *Manager) readState() (State, error) {
|
||
data, err := os.ReadFile(manager.filePath("state.json"))
|
||
if err != nil {
|
||
return State{}, err
|
||
}
|
||
var state State
|
||
if err := json.Unmarshal(data, &state); err != nil {
|
||
return State{}, fmt.Errorf("\u8bfb\u53d6\u66f4\u65b0\u72b6\u6001\u5931\u8d25: %w", err)
|
||
}
|
||
return state, nil
|
||
}
|
||
|
||
func (manager *Manager) safeReleaseDir(version string) (string, error) {
|
||
version = strings.TrimSpace(version)
|
||
if version == "" || version == "." || version == ".." || filepath.Base(version) != version || strings.ContainsAny(version, `/\\`) {
|
||
return "", errors.New("\u7248\u672c\u76ee\u5f55\u540d\u65e0\u6548")
|
||
}
|
||
releasesDir := filepath.Join(manager.config.RootDir, "releases")
|
||
target := filepath.Join(releasesDir, version)
|
||
if !isManagedRelease(releasesDir, target) {
|
||
return "", errors.New("\u7248\u672c\u76ee\u5f55\u8d85\u51fa\u66f4\u65b0\u6839\u76ee\u5f55")
|
||
}
|
||
return target, nil
|
||
}
|
||
|
||
func (manager *Manager) filePath(name string) string {
|
||
return filepath.Join(manager.config.RootDir, name)
|
||
}
|
||
|
||
func safeEntrypoint(releaseDir, entrypoint string) (string, error) {
|
||
if strings.TrimSpace(entrypoint) == "" {
|
||
return "", errors.New("\u7a0b\u5e8f\u5165\u53e3\u4e0d\u80fd\u4e3a\u7a7a")
|
||
}
|
||
releaseDir, err := filepath.Abs(releaseDir)
|
||
if err != nil {
|
||
return "", fmt.Errorf("\u89e3\u6790\u7248\u672c\u76ee\u5f55\u5931\u8d25: %w", err)
|
||
}
|
||
executable := filepath.Join(releaseDir, filepath.FromSlash(entrypoint))
|
||
relation, err := filepath.Rel(releaseDir, executable)
|
||
if err != nil || relation == ".." || strings.HasPrefix(relation, ".."+string(filepath.Separator)) || filepath.IsAbs(entrypoint) {
|
||
return "", errors.New("\u7a0b\u5e8f\u5165\u53e3\u8d85\u51fa\u7248\u672c\u76ee\u5f55")
|
||
}
|
||
return executable, nil
|
||
}
|
||
|
||
func moveDownloadedFile(source, destination string) error {
|
||
if err := os.Rename(source, destination); err == nil {
|
||
return nil
|
||
}
|
||
input, err := os.Open(source)
|
||
if err != nil {
|
||
return fmt.Errorf("\u8bfb\u53d6\u5df2\u4e0b\u8f7d\u6587\u4ef6\u5931\u8d25: %w", err)
|
||
}
|
||
defer input.Close()
|
||
output, err := os.Create(destination)
|
||
if err != nil {
|
||
return fmt.Errorf("\u521b\u5efa\u7248\u672c\u6587\u4ef6\u5931\u8d25: %w", err)
|
||
}
|
||
_, copyErr := io.Copy(output, input)
|
||
closeErr := output.Close()
|
||
if copyErr != nil {
|
||
return fmt.Errorf("\u590d\u5236\u7248\u672c\u6587\u4ef6\u5931\u8d25: %w", copyErr)
|
||
}
|
||
if closeErr != nil {
|
||
return fmt.Errorf("\u5173\u95ed\u7248\u672c\u6587\u4ef6\u5931\u8d25: %w", closeErr)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func isManagedRelease(releasesDir, candidate string) bool {
|
||
releasesDir, err := filepath.Abs(releasesDir)
|
||
if err != nil {
|
||
return false
|
||
}
|
||
candidate, err = filepath.Abs(candidate)
|
||
if err != nil {
|
||
return false
|
||
}
|
||
relation, err := filepath.Rel(releasesDir, candidate)
|
||
return err == nil && relation != "." && relation != ".." && !strings.HasPrefix(relation, ".."+string(filepath.Separator))
|
||
}
|