github.com/creativeprojects/go-selfupdate@v1.2.0/update.go (about)

     1  package selfupdate
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"fmt"
     7  	"io"
     8  	"os"
     9  	"path/filepath"
    10  	"strings"
    11  
    12  	"github.com/Masterminds/semver/v3"
    13  	"github.com/creativeprojects/go-selfupdate/update"
    14  )
    15  
    16  // UpdateTo downloads an executable from the source provider and replace current binary with the downloaded one.
    17  // It downloads a release asset via the source provider so this function is available for update releases on private repository.
    18  func (up *Updater) UpdateTo(ctx context.Context, rel *Release, cmdPath string) error {
    19  	if rel == nil {
    20  		return ErrInvalidRelease
    21  	}
    22  
    23  	data, err := up.download(ctx, rel, rel.AssetID)
    24  	if err != nil {
    25  		return fmt.Errorf("failed to read asset %q: %w", rel.AssetName, err)
    26  	}
    27  
    28  	if up.validator != nil {
    29  		err = up.validate(ctx, rel, data)
    30  		if err != nil {
    31  			return err
    32  		}
    33  	}
    34  
    35  	return up.decompressAndUpdate(bytes.NewReader(data), rel.AssetName, rel.AssetURL, cmdPath)
    36  }
    37  
    38  // UpdateCommand updates a given command binary to the latest version.
    39  // 'current' is used to check the latest version against the current version.
    40  func (up *Updater) UpdateCommand(ctx context.Context, cmdPath string, current string, repository Repository) (*Release, error) {
    41  	version, err := semver.NewVersion(current)
    42  	if err != nil {
    43  		return nil, fmt.Errorf("incorrect version %q: %w", current, err)
    44  	}
    45  
    46  	if up.os == "windows" && !strings.HasSuffix(cmdPath, ".exe") {
    47  		// Ensure to add '.exe' to given path on Windows
    48  		cmdPath = cmdPath + ".exe"
    49  	}
    50  
    51  	stat, err := os.Lstat(cmdPath)
    52  	if err != nil {
    53  		return nil, fmt.Errorf("failed to stat '%s'. file may not exist: %s", cmdPath, err)
    54  	}
    55  	if stat.Mode()&os.ModeSymlink != 0 {
    56  		p, err := filepath.EvalSymlinks(cmdPath)
    57  		if err != nil {
    58  			return nil, fmt.Errorf("failed to resolve symlink '%s' for executable: %s", cmdPath, err)
    59  		}
    60  		cmdPath = p
    61  	}
    62  
    63  	rel, ok, err := up.DetectLatest(ctx, repository)
    64  	if err != nil {
    65  		return nil, err
    66  	}
    67  	if !ok {
    68  		log.Print("No release detected. Current version is considered up-to-date")
    69  		return &Release{version: version}, nil
    70  	}
    71  	if version.Equal(rel.version) {
    72  		log.Printf("Current version %s is the latest. Update is not needed", version.String())
    73  		return rel, nil
    74  	}
    75  	log.Printf("Will update %s to the latest version %s", cmdPath, rel.Version())
    76  	if err := up.UpdateTo(ctx, rel, cmdPath); err != nil {
    77  		return nil, err
    78  	}
    79  	return rel, nil
    80  }
    81  
    82  // UpdateSelf updates the running executable itself to the latest version.
    83  // 'current' is used to check the latest version against the current version.
    84  func (up *Updater) UpdateSelf(ctx context.Context, current string, repository Repository) (*Release, error) {
    85  	cmdPath, err := os.Executable()
    86  	if err != nil {
    87  		return nil, err
    88  	}
    89  	return up.UpdateCommand(ctx, cmdPath, current, repository)
    90  }
    91  
    92  func (up *Updater) decompressAndUpdate(src io.Reader, assetName, assetURL, cmdPath string) error {
    93  	_, cmd := filepath.Split(cmdPath)
    94  	asset, err := DecompressCommand(src, assetName, cmd, up.os, up.arch)
    95  	if err != nil {
    96  		return err
    97  	}
    98  
    99  	log.Printf("Will update %s to the latest downloaded from %s", cmdPath, assetURL)
   100  	return update.Apply(asset, update.Options{
   101  		TargetPath: cmdPath,
   102  	})
   103  }
   104  
   105  // validate loads the validation file and passes it to the validator.
   106  // The validation is successful if no error was returned
   107  func (up *Updater) validate(ctx context.Context, rel *Release, data []byte) error {
   108  	if rel == nil {
   109  		return ErrInvalidRelease
   110  	}
   111  
   112  	// compatibility with setting rel.ValidationAssetID directly
   113  	if len(rel.ValidationChain) == 0 {
   114  		rel.ValidationChain = append(rel.ValidationChain, struct {
   115  			ValidationAssetID                       int64
   116  			ValidationAssetName, ValidationAssetURL string
   117  		}{
   118  			ValidationAssetID:   rel.ValidationAssetID,
   119  			ValidationAssetName: "",
   120  			ValidationAssetURL:  rel.ValidationAssetURL,
   121  		})
   122  	}
   123  
   124  	validationName := rel.AssetName
   125  
   126  	for _, va := range rel.ValidationChain {
   127  		validationData, err := up.download(ctx, rel, va.ValidationAssetID)
   128  		if err != nil {
   129  			return fmt.Errorf("failed reading validation data %q: %w", va.ValidationAssetName, err)
   130  		}
   131  
   132  		if err = up.validator.Validate(validationName, data, validationData); err != nil {
   133  			return fmt.Errorf("failed validating asset content %q: %w", validationName, err)
   134  		}
   135  
   136  		// Select what next to validate
   137  		validationName = va.ValidationAssetName
   138  		data = validationData
   139  	}
   140  	return nil
   141  }
   142  
   143  func (up *Updater) download(ctx context.Context, rel *Release, assetId int64) (data []byte, err error) {
   144  	var reader io.ReadCloser
   145  	if reader, err = up.source.DownloadReleaseAsset(ctx, rel, assetId); err == nil {
   146  		defer func() { _ = reader.Close() }()
   147  		data, err = io.ReadAll(reader)
   148  	}
   149  	return
   150  }