Scout/pkg/updater/updater.go
2026-08-31 17:50:00 +08:00

734 lines
24 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 = &current
}
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 = &current
} 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))
}