Scout/pkg/updater/updater.go
2026-08-31 15:47:55 +08:00

695 lines
22 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
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
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)
}
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 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
}
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) {
executable, current, err := manager.ResolveCurrent()
if err != nil {
return nil, err
}
command := exec.CommandContext(ctx, executable, args...)
command.Dir = current.Path
command.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,
)
if err := command.Start(); err != nil {
return nil, fmt.Errorf("\u542f\u52a8\u5f53\u524d\u7248\u672c\u5931\u8d25: %w", err)
}
return command, nil
}
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))
}