mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-15 09:22:43 +03:00
181 lines
4.5 KiB
Go
181 lines
4.5 KiB
Go
|
package update
|
||
|
|
||
|
// adapted from
|
||
|
// https://github.com/inconshreveable/go-update
|
||
|
|
||
|
import (
|
||
|
"encoding/json"
|
||
|
"fmt"
|
||
|
"io"
|
||
|
"net/http"
|
||
|
"os"
|
||
|
"path/filepath"
|
||
|
"runtime"
|
||
|
"time"
|
||
|
|
||
|
"github.com/kardianos/osext"
|
||
|
|
||
|
"github.com/Masterminds/semver"
|
||
|
"github.com/pkg/errors"
|
||
|
)
|
||
|
|
||
|
const updateCheckURL = "https://releases.hasura.io/graphql-engine?agent=cli"
|
||
|
|
||
|
type updateCheckResponse struct {
|
||
|
Latest string `json:"latest"`
|
||
|
}
|
||
|
|
||
|
func getLatestVersion() (*semver.Version, error) {
|
||
|
res, err := http.Get(updateCheckURL)
|
||
|
if err != nil {
|
||
|
return nil, errors.Wrap(err, "update check request")
|
||
|
}
|
||
|
|
||
|
defer res.Body.Close()
|
||
|
var response updateCheckResponse
|
||
|
err = json.NewDecoder(res.Body).Decode(&response)
|
||
|
if err != nil {
|
||
|
return nil, errors.Wrap(err, "decoding update check response")
|
||
|
}
|
||
|
|
||
|
v, err := semver.NewVersion(response.Latest)
|
||
|
if err != nil {
|
||
|
return nil, errors.Wrap(err, "semver parsing")
|
||
|
}
|
||
|
|
||
|
return v, nil
|
||
|
}
|
||
|
|
||
|
func buildAssetURL(v string) string {
|
||
|
os := runtime.GOOS
|
||
|
arch := runtime.GOARCH
|
||
|
extension := ""
|
||
|
if os == "windows" {
|
||
|
extension = ".exe"
|
||
|
}
|
||
|
return fmt.Sprintf(
|
||
|
"https://github.com/hasura/graphql-engine/releases/download/v%s/cli-hasura-%s-%s%s",
|
||
|
v, os, arch, extension,
|
||
|
)
|
||
|
}
|
||
|
|
||
|
func downloadAsset(url, fileName, filePath string) (*os.File, error) {
|
||
|
res, err := http.Get(url)
|
||
|
if err != nil {
|
||
|
return nil, errors.Wrap(err, "downloading asset")
|
||
|
}
|
||
|
defer res.Body.Close()
|
||
|
|
||
|
if res.StatusCode != 200 {
|
||
|
return nil, errors.New("could not find the release asset")
|
||
|
}
|
||
|
|
||
|
asset, err := os.OpenFile(
|
||
|
filepath.Join(filePath, fileName),
|
||
|
os.O_CREATE|os.O_WRONLY|os.O_TRUNC,
|
||
|
0755,
|
||
|
)
|
||
|
if err != nil {
|
||
|
return nil, errors.Wrap(err, "creating new binary file")
|
||
|
}
|
||
|
defer asset.Close()
|
||
|
|
||
|
_, err = io.Copy(asset, res.Body)
|
||
|
if err != nil {
|
||
|
return nil, errors.Wrap(err, "saving downloaded file")
|
||
|
}
|
||
|
|
||
|
return asset, nil
|
||
|
}
|
||
|
|
||
|
// HasUpdate tells us if there is a new update available.
|
||
|
func HasUpdate(currentVersion *semver.Version, timeFile string) (bool, *semver.Version, error) {
|
||
|
if timeFile != "" {
|
||
|
defer writeTimeToFile(timeFile, time.Now().UTC())
|
||
|
}
|
||
|
|
||
|
latestVersion, err := getLatestVersion()
|
||
|
if err != nil {
|
||
|
return false, nil, errors.Wrap(err, "get latest version")
|
||
|
}
|
||
|
|
||
|
c, err := semver.NewConstraint(fmt.Sprintf("> %s", currentVersion.String()))
|
||
|
if err != nil {
|
||
|
return false, nil, errors.Wrap(err, "semver constraint build")
|
||
|
}
|
||
|
|
||
|
return c.Check(latestVersion), latestVersion, nil
|
||
|
}
|
||
|
|
||
|
// ApplyUpdate downloads and applies the update indicated by version v.
|
||
|
func ApplyUpdate(v *semver.Version) error {
|
||
|
// get the current executable
|
||
|
exe, err := osext.Executable()
|
||
|
if err != nil {
|
||
|
return errors.Wrap(err, "find executable")
|
||
|
}
|
||
|
|
||
|
// extract the filename and path
|
||
|
exePath := filepath.Dir(exe)
|
||
|
exeName := filepath.Base(exe)
|
||
|
|
||
|
// download the new binary
|
||
|
asset, err := downloadAsset(
|
||
|
buildAssetURL(v.String()), "."+exeName+".new", exePath,
|
||
|
)
|
||
|
if err != nil {
|
||
|
return errors.Wrap(err, "download asset")
|
||
|
}
|
||
|
|
||
|
// get the downloaded binary name and build the absolute path
|
||
|
newExe := asset.Name()
|
||
|
|
||
|
// build name and absolute path for saving old binary
|
||
|
oldExeName := "." + exeName + ".old"
|
||
|
oldExe := filepath.Join(exePath, oldExeName)
|
||
|
|
||
|
// delete any existing old binary file - this is necessary on Windows for two reasons:
|
||
|
// 1. after a successful update, Windows can't remove the .old file because the process is still running
|
||
|
// 2. windows rename operations fail if the destination file already exists
|
||
|
_ = os.Remove(oldExe)
|
||
|
|
||
|
// rename the current binary as old binary
|
||
|
err = os.Rename(exe, oldExe)
|
||
|
if err != nil {
|
||
|
return errors.Wrap(err, "rename exe to old")
|
||
|
}
|
||
|
|
||
|
// rename the new binary as the current binary
|
||
|
err = os.Rename(newExe, exe)
|
||
|
if err != nil {
|
||
|
// rename unsuccessfull
|
||
|
//
|
||
|
// The filesystem is now in a bad state. We have successfully
|
||
|
// moved the existing binary to a new location, but we couldn't move the new
|
||
|
// binary to take its place. That means there is no file where the current executable binary
|
||
|
// used to be!
|
||
|
// Try to rollback by restoring the old binary to its original path.
|
||
|
rerr := os.Rename(oldExe, exe)
|
||
|
if rerr != nil {
|
||
|
// rolling back failed, ask user to re-install cli
|
||
|
return errors.Wrap(
|
||
|
rerr,
|
||
|
"rename old to exe: inconsistent state, re-install cli",
|
||
|
)
|
||
|
}
|
||
|
// rolled back, throw update error
|
||
|
return errors.Wrap(err, "rename new to exe")
|
||
|
}
|
||
|
|
||
|
// rename success, remove the old binary
|
||
|
errRemove := os.Remove(oldExe)
|
||
|
|
||
|
// windows has trouble removing old binaries, so hide it instead
|
||
|
// it will be removed next time this code runs.
|
||
|
if errRemove != nil {
|
||
|
_ = hideFile(oldExe)
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|