stash/pkg/api/check_version.go
SmallCoccinelle 214a15bc40
Fixups + enable the commentFormatting linter (#1866)
* Add a space after // comments

For consistency, the commentFormatting lint checker suggests a space
after each // comment block. This commit handles all the spots in
the code where that is needed.

* Rewrite documentation on functions

Use the Go idiom of commenting:

* First sentence declares the purpose.
* First word is the name being declared

The reason this style is preferred is such that grep is able to find
names the user might be interested in. Consider e.g.,

    go doc -all pkg/ffmpeg | grep -i transcode

in which case a match will tell you the name of the function you are
interested in.

* Remove old code comment-blocks

There are some commented out old code blocks in the code base. These are
either 3 years old, or 2 years old. By now, I don't think their use is
going to come back any time soon, and Git will track old pieces of
deleted code anyway.

Opt for deletion.

* Reorder imports

Split stdlib imports from non-stdlib imports in files we are touching.

* Use a range over an iteration variable

Probably more go-idiomatic, and the code needed comment-fixing anyway.

* Use time.After rather than rolling our own

The idiom here is common enough that the stdlib contains a function for
it. Use the stdlib function over our own variant.

* Enable the commentFormatting linter
2021-10-20 16:10:46 +11:00

275 lines
7.5 KiB
Go

package api
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"regexp"
"runtime"
"time"
"golang.org/x/sys/cpu"
"github.com/stashapp/stash/pkg/logger"
)
// we use the github REST V3 API as no login is required
const apiReleases string = "https://api.github.com/repos/stashapp/stash/releases"
const apiTags string = "https://api.github.com/repos/stashapp/stash/tags"
const apiAcceptHeader string = "application/vnd.github.v3+json"
const developmentTag string = "latest_develop"
const defaultSHLength int = 7 // default length of SHA short hash returned by <git rev-parse --short HEAD>
// ErrNoVersion indicates that no version information has been embedded in the
// stash binary
var ErrNoVersion = errors.New("no stash version")
var stashReleases = func() map[string]string {
return map[string]string{
"darwin/amd64": "stash-osx",
"darwin/arm64": "stash-osx-applesilicon",
"linux/amd64": "stash-linux",
"windows/amd64": "stash-win.exe",
"linux/arm": "stash-pi",
"linux/arm64": "stash-linux-arm64v8",
"linux/armv7": "stash-linux-arm32v7",
}
}
type githubReleasesResponse struct {
Url string
Assets_url string
Upload_url string
Html_url string
Id int64
Node_id string
Tag_name string
Target_commitish string
Name string
Draft bool
Author githubAuthor
Prerelease bool
Created_at string
Published_at string
Assets []githubAsset
Tarball_url string
Zipball_url string
Body string
}
type githubAuthor struct {
Login string
Id int64
Node_id string
Avatar_url string
Gravatar_id string
Url string
Html_url string
Followers_url string
Following_url string
Gists_url string
Starred_url string
Subscriptions_url string
Organizations_url string
Repos_url string
Events_url string
Received_events_url string
Type string
Site_admin bool
}
type githubAsset struct {
Url string
Id int64
Node_id string
Name string
Label string
Uploader githubAuthor
Content_type string
State string
Size int64
Download_count int64
Created_at string
Updated_at string
Browser_download_url string
}
type githubTagResponse struct {
Name string
Zipball_url string
Tarball_url string
Commit struct {
Sha string
Url string
}
Node_id string
}
func makeGithubRequest(ctx context.Context, url string, output interface{}) error {
client := &http.Client{
Timeout: 3 * time.Second,
}
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
req.Header.Add("Accept", apiAcceptHeader) // gh api recommendation , send header with api version
response, err := client.Do(req)
if err != nil {
//lint:ignore ST1005 Github is a proper capitalized noun
return fmt.Errorf("Github API request failed: %s", err)
}
if response.StatusCode != http.StatusOK {
//lint:ignore ST1005 Github is a proper capitalized noun
return fmt.Errorf("Github API request failed: %s", response.Status)
}
defer response.Body.Close()
data, err := io.ReadAll(response.Body)
if err != nil {
//lint:ignore ST1005 Github is a proper capitalized noun
return fmt.Errorf("Github API read response failed: %s", err)
}
err = json.Unmarshal(data, output)
if err != nil {
return fmt.Errorf("unmarshalling Github API response failed: %s", err)
}
return nil
}
// GetLatestVersion gets latest version (git commit hash) from github API
// If running a build from the "master" branch, then the latest full release
// is used, otherwise it uses the release that is tagged with "latest_develop"
// which is the latest pre-release build.
func GetLatestVersion(ctx context.Context, shortHash bool) (latestVersion string, latestRelease string, err error) {
arch := runtime.GOARCH // https://en.wikipedia.org/wiki/Comparison_of_ARM_cores
isARMv7 := cpu.ARM.HasNEON || cpu.ARM.HasVFPv3 || cpu.ARM.HasVFPv3D16 || cpu.ARM.HasVFPv4 // armv6 doesn't support any of these features
if arch == "arm" && isARMv7 {
arch = "armv7"
}
platform := fmt.Sprintf("%s/%s", runtime.GOOS, arch)
wantedRelease := stashReleases()[platform]
version, _, _ := GetVersion()
if version == "" {
return "", "", ErrNoVersion
}
// if the version is suffixed with -x-xxxx, then we are running a development build
usePreRelease := false
re := regexp.MustCompile(`-\d+-g\w+$`)
if re.MatchString(version) {
usePreRelease = true
}
url := apiReleases
if !usePreRelease {
// just get the latest full release
url += "/latest"
} else {
// get the release tagged with the development tag
url += "/tags/" + developmentTag
}
release := githubReleasesResponse{}
err = makeGithubRequest(ctx, url, &release)
if err != nil {
return "", "", err
}
if release.Prerelease == usePreRelease {
latestVersion = getReleaseHash(ctx, release, shortHash, usePreRelease)
if wantedRelease != "" {
for _, asset := range release.Assets {
if asset.Name == wantedRelease {
latestRelease = asset.Browser_download_url
break
}
}
}
}
if latestVersion == "" {
return "", "", fmt.Errorf("no version found for \"%s\"", version)
}
return latestVersion, latestRelease, nil
}
func getReleaseHash(ctx context.Context, release githubReleasesResponse, shortHash bool, usePreRelease bool) string {
shaLength := len(release.Target_commitish)
// the /latest API call doesn't return the hash in target_commitish
// also add sanity check in case Target_commitish is not 40 characters
if !usePreRelease || shaLength != 40 {
return getShaFromTags(ctx, shortHash, release.Tag_name)
}
if shortHash {
last := defaultSHLength // default length of git short hash
_, gitShort, _ := GetVersion() // retrieve it to check actual length
if len(gitShort) > last && len(gitShort) < shaLength { // sometimes short hash is longer
last = len(gitShort)
}
return release.Target_commitish[0:last]
}
return release.Target_commitish
}
func printLatestVersion(ctx context.Context) {
_, githash, _ = GetVersion()
latest, _, err := GetLatestVersion(ctx, true)
if err != nil {
logger.Errorf("Couldn't find latest version: %s", err)
} else {
if githash == latest {
logger.Infof("Version: (%s) is already the latest released.", latest)
} else {
logger.Infof("New version: (%s) available.", latest)
}
}
}
// get sha from the github api tags endpoint
// returns the sha1 hash/shorthash or "" if something's wrong
func getShaFromTags(ctx context.Context, shortHash bool, name string) string {
url := apiTags
tags := []githubTagResponse{}
err := makeGithubRequest(ctx, url, &tags)
if err != nil {
logger.Errorf("Github Tags Api %v", err)
return ""
}
_, gitShort, _ := GetVersion() // retrieve short hash to check actual length
for _, tag := range tags {
if tag.Name == name {
shaLength := len(tag.Commit.Sha)
if shaLength != 40 {
return ""
}
if shortHash {
last := defaultSHLength // default length of git short hash
if len(gitShort) > last && len(gitShort) < shaLength { // sometimes short hash is longer
last = len(gitShort)
}
return tag.Commit.Sha[0:last]
}
return tag.Commit.Sha
}
}
return ""
}