github.com/creativeprojects/go-selfupdate@v1.2.0/detect.go (about) 1 package selfupdate 2 3 import ( 4 "context" 5 "fmt" 6 "regexp" 7 "strings" 8 9 "github.com/Masterminds/semver/v3" 10 ) 11 12 var reVersion = regexp.MustCompile(`\d+\.\d+\.\d+`) 13 14 // DetectLatest tries to get the latest version from the source provider. 15 // It fetches releases information from the source provider and find out the latest release with matching the tag names and asset names. 16 // Drafts and pre-releases are ignored. 17 // Assets would be suffixed by the OS name and the arch name such as 'foo_linux_amd64' where 'foo' is a command name. 18 // '-' can also be used as a separator. File can be compressed with zip, gzip, zxip, bzip2, tar&gzip or tar&zxip. 19 // So the asset can have a file extension for the corresponding compression format such as '.zip'. 20 // On Windows, '.exe' also can be contained such as 'foo_windows_amd64.exe.zip'. 21 func (up *Updater) DetectLatest(ctx context.Context, repository Repository) (release *Release, found bool, err error) { 22 return up.DetectVersion(ctx, repository, "") 23 } 24 25 // DetectVersion tries to get the given version from the source provider. 26 // And version indicates the required version. 27 func (up *Updater) DetectVersion(ctx context.Context, repository Repository, version string) (release *Release, found bool, err error) { 28 rels, err := up.source.ListReleases(ctx, repository) 29 if err != nil { 30 return nil, false, err 31 } 32 33 rel, asset, ver, found := up.findReleaseAndAsset(rels, version) 34 if !found { 35 return nil, false, nil 36 } 37 38 return up.validateReleaseAsset(repository, rel, asset, ver) 39 } 40 41 func (up *Updater) validateReleaseAsset( 42 repository Repository, 43 rel SourceRelease, 44 asset SourceAsset, 45 ver *semver.Version, 46 ) (release *Release, found bool, err error) { 47 log.Printf("Successfully fetched release %s, name: %s, URL: %s, asset: %s", 48 rel.GetTagName(), 49 rel.GetName(), 50 rel.GetURL(), 51 asset.GetBrowserDownloadURL(), 52 ) 53 54 release = &Release{ 55 version: ver, 56 repository: repository, 57 AssetURL: asset.GetBrowserDownloadURL(), 58 AssetByteSize: asset.GetSize(), 59 AssetID: asset.GetID(), 60 AssetName: asset.GetName(), 61 ValidationAssetID: -1, 62 ValidationAssetURL: "", 63 URL: rel.GetURL(), 64 ReleaseID: rel.GetID(), 65 ReleaseNotes: rel.GetReleaseNotes(), 66 Name: rel.GetName(), 67 PublishedAt: rel.GetPublishedAt(), 68 Prerelease: rel.GetPrerelease(), 69 OS: up.os, 70 Arch: up.arch, 71 Arm: up.arm, 72 } 73 74 if up.validator != nil { 75 validationName := up.validator.GetValidationAssetName(asset.GetName()) 76 validationAsset, ok := findValidationAsset(rel, validationName) 77 if ok { 78 release.ValidationAssetID = validationAsset.GetID() 79 release.ValidationAssetURL = validationAsset.GetBrowserDownloadURL() 80 } else { 81 err = fmt.Errorf("%w: %q", ErrValidationAssetNotFound, validationName) 82 } 83 84 for err == nil { 85 release.ValidationChain = append(release.ValidationChain, struct { 86 ValidationAssetID int64 87 ValidationAssetName, ValidationAssetURL string 88 }{ 89 ValidationAssetID: validationAsset.GetID(), 90 ValidationAssetName: validationAsset.GetName(), 91 ValidationAssetURL: validationAsset.GetBrowserDownloadURL(), 92 }) 93 94 if len(release.ValidationChain) > 20 { 95 err = fmt.Errorf("failed adding validation step %q: recursive validation nesting depth exceeded", validationAsset.GetName()) 96 break 97 } 98 99 if rv, ok := up.validator.(RecursiveValidator); ok && rv.MustContinueValidation(validationAsset.GetName()) { 100 validationName = up.validator.GetValidationAssetName(validationAsset.GetName()) 101 if validationName != validationAsset.GetName() { 102 validationAsset, ok = findValidationAsset(rel, validationName) 103 if !ok { 104 err = fmt.Errorf("%w: %q", ErrValidationAssetNotFound, validationName) 105 } 106 continue 107 } 108 } 109 110 break 111 } 112 } 113 114 if found = err == nil; !found { 115 release = nil 116 } 117 return 118 } 119 120 // findValidationAsset returns the source asset used for validation 121 func findValidationAsset(rel SourceRelease, validationName string) (SourceAsset, bool) { 122 for _, asset := range rel.GetAssets() { 123 if asset.GetName() == validationName { 124 return asset, true 125 } 126 } 127 return nil, false 128 } 129 130 // findReleaseAndAsset returns the release and asset matching the target version, or latest if target version is empty 131 func (up *Updater) findReleaseAndAsset(rels []SourceRelease, targetVersion string) (SourceRelease, SourceAsset, *semver.Version, bool) { 132 // we put the detected arch at the end of the list: that's fine for ARM so far, 133 // as the additional arch are more accurate than the generic one 134 for _, arch := range append(generateAdditionalArch(up.arch, up.arm), up.arch) { 135 release, asset, version, found := up.findReleaseAndAssetForArch(arch, rels, targetVersion) 136 if found { 137 return release, asset, version, found 138 } 139 } 140 141 return nil, nil, nil, false 142 } 143 144 func (up *Updater) findReleaseAndAssetForArch(arch string, rels []SourceRelease, targetVersion string, 145 ) (SourceRelease, SourceAsset, *semver.Version, bool) { 146 var ver *semver.Version 147 var asset SourceAsset 148 var release SourceRelease 149 150 log.Printf("Searching for a possible candidate for os %q and arch %q", up.os, arch) 151 152 // Find the latest version from the list of releases. 153 // Returned list from GitHub API is in the order of the date when created. 154 for _, rel := range rels { 155 if a, v, ok := up.findAssetFromRelease(rel, up.getSuffixes(arch), targetVersion); ok { 156 // Note: any version with suffix is less than any version without suffix. 157 // e.g. 0.0.1 > 0.0.1-beta 158 if release == nil || v.GreaterThan(ver) { 159 ver = v 160 asset = a 161 release = rel 162 } 163 } 164 } 165 166 if release == nil { 167 log.Printf("Could not find any release for os %q and arch %q", up.os, arch) 168 return nil, nil, nil, false 169 } 170 171 return release, asset, ver, true 172 } 173 174 func (up *Updater) findAssetFromRelease(rel SourceRelease, suffixes []string, targetVersion string) (SourceAsset, *semver.Version, bool) { 175 if rel == nil { 176 log.Print("No source release information") 177 return nil, nil, false 178 } 179 if targetVersion != "" && targetVersion != rel.GetTagName() { 180 log.Printf("Skip %s not matching to specified version %s", rel.GetTagName(), targetVersion) 181 return nil, nil, false 182 } 183 184 if rel.GetDraft() && !up.draft && targetVersion == "" { 185 log.Printf("Skip draft version %s", rel.GetTagName()) 186 return nil, nil, false 187 } 188 if rel.GetPrerelease() && !up.prerelease && targetVersion == "" { 189 log.Printf("Skip pre-release version %s", rel.GetTagName()) 190 return nil, nil, false 191 } 192 193 verText := rel.GetTagName() 194 indices := reVersion.FindStringIndex(verText) 195 if indices == nil { 196 log.Printf("Skip version not adopting semver: %s", verText) 197 return nil, nil, false 198 } 199 if indices[0] > 0 { 200 verText = verText[indices[0]:] 201 } 202 203 // If semver cannot parse the version text, it means that the text is not adopting 204 // the semantic versioning. So it should be skipped. 205 ver, err := semver.NewVersion(verText) 206 if err != nil { 207 log.Printf("Failed to parse a semantic version: %s", verText) 208 return nil, nil, false 209 } 210 211 for _, asset := range rel.GetAssets() { 212 // try names first 213 name := asset.GetName() 214 // case insensitive search 215 name = strings.ToLower(name) 216 217 if up.hasFilters() { 218 if up.assetMatchFilters(name) { 219 return asset, ver, true 220 } 221 } else { 222 if up.assetMatchSuffixes(name, suffixes) { 223 return asset, ver, true 224 } 225 } 226 227 // then try from filename (Gitlab can assign human names to release assets) 228 name = asset.GetBrowserDownloadURL() 229 // case insensitive search 230 name = strings.ToLower(name) 231 232 if up.hasFilters() { 233 if up.assetMatchFilters(name) { 234 return asset, ver, true 235 } 236 } else { 237 if up.assetMatchSuffixes(name, suffixes) { 238 return asset, ver, true 239 } 240 } 241 } 242 243 log.Printf("No suitable asset was found in release %s", rel.GetTagName()) 244 return nil, nil, false 245 } 246 247 func (up *Updater) hasFilters() bool { 248 return len(up.filters) > 0 249 } 250 251 func (up *Updater) assetMatchFilters(name string) bool { 252 if len(up.filters) > 0 { 253 // if some filters are defined, match them: if any one matches, the asset is selected 254 for _, filter := range up.filters { 255 if filter.MatchString(name) { 256 log.Printf("Selected filtered asset: %s", name) 257 return true 258 } 259 log.Printf("Skipping asset %q not matching filter %v\n", name, filter) 260 } 261 } 262 return false 263 } 264 265 func (up *Updater) assetMatchSuffixes(name string, suffixes []string) bool { 266 for _, suffix := range suffixes { 267 if strings.HasSuffix(name, suffix) { // require version, arch etc 268 // assuming a unique artifact will be a match (or first one will do) 269 return true 270 } 271 } 272 return false 273 } 274 275 // getSuffixes returns all candidates to check against the assets 276 func (up *Updater) getSuffixes(arch string) []string { 277 suffixes := make([]string, 0) 278 for _, sep := range []rune{'_', '-'} { 279 for _, ext := range []string{".zip", ".tar.gz", ".tgz", ".gzip", ".gz", ".tar.xz", ".xz", ".bz2", ""} { 280 suffix := fmt.Sprintf("%s%c%s%s", up.os, sep, arch, ext) 281 suffixes = append(suffixes, suffix) 282 if up.os == "windows" { 283 suffix = fmt.Sprintf("%s%c%s.exe%s", up.os, sep, arch, ext) 284 suffixes = append(suffixes, suffix) 285 } 286 } 287 } 288 return suffixes 289 }