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 }