Add check version functionality (#296)

* Add check version functionality
 * add backend support
 * add ui support
 * minor fixes
 * cosmetic fixes
 * workaround query refetch not working after network error
 * revert changes to Makefile after testing is complete
 * switch to Releases Github API endpoint, add latest Release URL to UI
 * latest version is only shown in UI when version is available and data is ready
 * resolve conflict , squash rebase
This commit is contained in:
bnkai
2020-01-08 00:21:23 +02:00
committed by Leopere
parent f8aa0433a3
commit 87f0b667b5
8 changed files with 311 additions and 55 deletions

View File

@@ -66,3 +66,10 @@ query Version {
build_time build_time
} }
} }
query LatestVersion {
latestversion {
shorthash
url
}
}

View File

@@ -96,6 +96,9 @@ type Query {
# Version # Version
version: Version! version: Version!
# LatestVersion
latestversion: ShortVersion!
} }
type Mutation { type Mutation {

View File

@@ -3,3 +3,8 @@ type Version {
hash: String! hash: String!
build_time: String! build_time: String!
} }
type ShortVersion {
shorthash: String!
url: String!
}

179
pkg/api/check_version.go Normal file
View File

@@ -0,0 +1,179 @@
package api
import (
"encoding/json"
"fmt"
"github.com/stashapp/stash/pkg/logger"
"io/ioutil"
"net/http"
"runtime"
"time"
)
//we use the github REST V3 API as no login is required
const apiURL string = "https://api.github.com/repos/stashapp/stash/tags"
const apiReleases string = "https://api.github.com/repos/stashapp/stash/releases"
const apiAcceptHeader string = "application/vnd.github.v3+json"
var stashReleases = func() map[string]string {
return map[string]string{
"windows/amd64": "stash-win.exe",
"linux/amd64": "stash-linux",
"darwin/amd64": "stash-osx",
"linux/arm": "stash-pi",
}
}
type githubTagResponse struct {
Name string
Zipball_url string
Tarball_url string
Commit struct {
Sha string
Url string
}
Node_id string
}
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
}
//gets latest version (git commit hash) from github API
//the repo's tags are used to find the latest version
//of the "master" or "develop" branch
func GetLatestVersion(shortHash bool) (latestVersion string, latestRelease string, err error) {
platform := fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH)
wantedRelease := stashReleases()[platform]
branch, _, _ := GetVersion()
if branch == "" {
return "", "", fmt.Errorf("Stash doesn't have a version. Version check not supported.")
}
client := &http.Client{
Timeout: 3 * time.Second,
}
req, _ := http.NewRequest("GET", apiReleases, nil)
req.Header.Add("Accept", apiAcceptHeader) // gh api recommendation , send header with api version
response, err := client.Do(req)
input := make([]githubReleasesResponse, 0)
if err != nil {
return "", "", fmt.Errorf("Github API request failed: %s", err)
} else {
defer response.Body.Close()
data, err := ioutil.ReadAll(response.Body)
if err != nil {
return "", "", fmt.Errorf("Github API read response failed: %s", err)
} else {
err = json.Unmarshal(data, &input)
if err != nil {
return "", "", fmt.Errorf("Unmarshalling Github API response failed: %s", err)
} else {
for _, ghApi := range input {
if ghApi.Tag_name == branch {
if shortHash {
latestVersion = ghApi.Target_commitish[0:7] //shorthash is first 7 digits of git commit hash
} else {
latestVersion = ghApi.Target_commitish
}
if wantedRelease != "" {
for _, asset := range ghApi.Assets {
if asset.Name == wantedRelease {
latestRelease = asset.Browser_download_url
break
}
}
}
break
}
}
}
}
if latestVersion == "" {
return "", "", fmt.Errorf("No version found for \"%s\"", branch)
}
}
return latestVersion, latestRelease, nil
}
func printLatestVersion() {
_, githash, _ = GetVersion()
latest, _, err := GetLatestVersion(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)
}
}
}

View File

@@ -6,7 +6,7 @@ import (
"strconv" "strconv"
"github.com/99designs/gqlgen/graphql" "github.com/99designs/gqlgen/graphql"
"github.com/stashapp/stash/pkg/logger"
"github.com/stashapp/stash/pkg/models" "github.com/stashapp/stash/pkg/models"
) )
@@ -116,6 +116,21 @@ func (r *queryResolver) Version(ctx context.Context) (*models.Version, error) {
}, nil }, nil
} }
//Gets latest version (git shorthash commit for now)
func (r *queryResolver) Latestversion(ctx context.Context) (*models.ShortVersion, error) {
ver, url, err := GetLatestVersion(true)
if err == nil {
logger.Infof("Retrieved latest hash: %s", ver)
} else {
logger.Errorf("Error while retrieving latest hash: %s", err)
}
return &models.ShortVersion{
Shorthash: ver,
URL: url,
}, err
}
// Get scene marker tags which show up under the video. // Get scene marker tags which show up under the video.
func (r *queryResolver) SceneMarkerTags(ctx context.Context, scene_id string) ([]*models.SceneMarkerTag, error) { func (r *queryResolver) SceneMarkerTags(ctx context.Context, scene_id string) ([]*models.SceneMarkerTag, error) {
sceneID, _ := strconv.Atoi(scene_id) sceneID, _ := strconv.Atoi(scene_id)

View File

@@ -216,6 +216,7 @@ func Start() {
go func() { go func() {
printVersion() printVersion()
printLatestVersion()
logger.Infof("stash is listening on " + address) logger.Infof("stash is listening on " + address)
logger.Infof("stash is running at https://" + displayAddress + "/") logger.Infof("stash is running at https://" + displayAddress + "/")
logger.Fatal(httpsServer.ListenAndServeTLS("", "")) logger.Fatal(httpsServer.ListenAndServeTLS("", ""))
@@ -228,6 +229,7 @@ func Start() {
go func() { go func() {
printVersion() printVersion()
printLatestVersion()
logger.Infof("stash is listening on " + address) logger.Infof("stash is listening on " + address)
logger.Infof("stash is running at http://" + displayAddress + "/") logger.Infof("stash is running at http://" + displayAddress + "/")
logger.Fatal(server.ListenAndServe()) logger.Fatal(server.ListenAndServe())

View File

@@ -1,4 +1,5 @@
import { import {
Button,
H1, H1,
H4, H4,
H6, H6,
@@ -15,6 +16,7 @@ interface IProps {}
export const SettingsAboutPanel: FunctionComponent<IProps> = (props: IProps) => { export const SettingsAboutPanel: FunctionComponent<IProps> = (props: IProps) => {
const { data, error, loading } = StashService.useVersion(); const { data, error, loading } = StashService.useVersion();
const { data: dataLatest, error: errorLatest, loading: loadingLatest, refetch, networkStatus } = StashService.useLatestVersion();
function maybeRenderTag() { function maybeRenderTag() {
if (!data || !data.version || !data.version.version) { return; } if (!data || !data.version || !data.version.version) { return; }
@@ -26,6 +28,44 @@ export const SettingsAboutPanel: FunctionComponent<IProps> = (props: IProps) =>
); );
} }
function maybeRenderLatestVersion() {
if (!dataLatest || !dataLatest.latestversion || !dataLatest.latestversion.shorthash || !dataLatest.latestversion.url) { return; }
if (!data || !data.version || !data.version.hash) {
return (
<>{dataLatest.latestversion.shorthash}</>
);
}
if (data.version.hash !== dataLatest.latestversion.shorthash) {
return (
<>
<strong>{dataLatest.latestversion.shorthash} [NEW] </strong><a href={dataLatest.latestversion.url}>Download</a>
</>
);
}
return (
<>{dataLatest.latestversion.shorthash}</>
);
}
function renderLatestVersion() {
if (!data || !data.version || !data.version.version) { return; } //if there is no "version" latest version check is obviously not supported
return (
<HTMLTable>
<tbody>
<tr>
<td>Latest Version Build Hash: </td>
<td>{maybeRenderLatestVersion()} </td>
</tr>
<tr>
<td><Button onClick={() => refetch()} text="Check for new version" /></td>
</tr>
</tbody>
</HTMLTable>
);
}
function renderVersion() { function renderVersion() {
if (!data || !data.version) { return; } if (!data || !data.version) { return; }
return ( return (
@@ -50,8 +90,11 @@ export const SettingsAboutPanel: FunctionComponent<IProps> = (props: IProps) =>
<> <>
<H4>About</H4> <H4>About</H4>
{!data || loading ? <Spinner size={Spinner.SIZE_LARGE} /> : undefined} {!data || loading ? <Spinner size={Spinner.SIZE_LARGE} /> : undefined}
{!!error ? <span>error.message</span> : undefined} {!!error ? <span>{error.message}</span> : undefined}
{!!errorLatest ? <span>{errorLatest.message}</span> : undefined}
{renderVersion()} {renderVersion()}
{!dataLatest || loadingLatest || networkStatus == 4 ? <Spinner size={Spinner.SIZE_SMALL} /> : <>{renderLatestVersion()}</>}
</> </>
); );
}; };

View File

@@ -249,6 +249,7 @@ export class StashService {
} }
public static useStats() { return GQL.useStats(); } public static useStats() { return GQL.useStats(); }
public static useVersion() { return GQL.useVersion(); } public static useVersion() { return GQL.useVersion(); }
public static useLatestVersion() { return GQL.useLatestVersion({ notifyOnNetworkStatusChange: true, errorPolicy: 'ignore' }); }
public static useConfiguration() { return GQL.useConfiguration(); } public static useConfiguration() { return GQL.useConfiguration(); }
public static useDirectories(path?: string) { return GQL.useDirectories({ variables: { path } }); } public static useDirectories(path?: string) { return GQL.useDirectories({ variables: { path } }); }
@@ -260,6 +261,7 @@ export class StashService {
"allPerformers" "allPerformers"
]; ];
public static usePerformerCreate() { public static usePerformerCreate() {
return GQL.usePerformerCreate({ return GQL.usePerformerCreate({
update: () => StashService.invalidateQueries(StashService.performerMutationImpactedQueries) update: () => StashService.invalidateQueries(StashService.performerMutationImpactedQueries)