2017-12-28 08:30:13 +00:00
package selfupdate
import (
2018-11-06 15:28:06 +00:00
"bytes"
2017-12-28 08:30:13 +00:00
"fmt"
2018-01-19 03:05:41 +00:00
"io"
2018-11-06 15:28:06 +00:00
"io/ioutil"
2017-12-28 08:30:13 +00:00
"net/http"
"os"
"path/filepath"
2017-12-30 04:45:19 +00:00
"runtime"
"strings"
2018-01-02 14:43:45 +00:00
"github.com/blang/semver"
"github.com/inconshreveable/go-update"
2017-12-28 08:30:13 +00:00
)
2018-11-06 15:28:06 +00:00
func uncompressAndUpdate ( src io . Reader , assetURL , cmdPath string ) error {
2018-01-19 03:05:41 +00:00
_ , cmd := filepath . Split ( cmdPath )
asset , err := UncompressCommand ( src , assetURL , cmd )
if err != nil {
return err
}
log . Println ( "Will update" , cmdPath , "to the latest downloaded from" , assetURL )
return update . Apply ( asset , update . Options {
TargetPath : cmdPath ,
} )
}
2018-01-19 06:03:47 +00:00
func ( up * Updater ) downloadDirectlyFromURL ( assetURL string ) ( io . ReadCloser , error ) {
req , err := http . NewRequest ( "GET" , assetURL , nil )
2017-12-28 08:30:13 +00:00
if err != nil {
2018-01-19 06:03:47 +00:00
return nil , fmt . Errorf ( "Failed to create HTTP request to %s: %s" , assetURL , err )
}
req . Header . Add ( "Accept" , "application/octet-stream" )
2018-01-19 08:00:30 +00:00
req = req . WithContext ( up . apiCtx )
// OAuth HTTP client is not available to download blob from URL when the URL is a redirect URL
// returned from GitHub Releases API (response status 400).
// Use default HTTP client instead.
res , err := http . DefaultClient . Do ( req )
2018-01-19 06:03:47 +00:00
if err != nil {
return nil , fmt . Errorf ( "Failed to download a release file from %s: %s" , assetURL , err )
2017-12-28 08:30:13 +00:00
}
if res . StatusCode != 200 {
2018-01-19 09:04:30 +00:00
return nil , fmt . Errorf ( "Failed to download a release file from %s: Not successful status %d" , assetURL , res . StatusCode )
2017-12-28 08:30:13 +00:00
}
2018-01-19 06:03:47 +00:00
return res . Body , nil
2018-01-19 03:05:41 +00:00
}
// UpdateTo downloads an executable from GitHub Releases API and replace current binary with the downloaded one.
// It downloads a release asset via GitHub Releases API so this function is available for update releases on private repository.
// If a redirect occurs, it fallbacks into directly downloading from the redirect URL.
func ( up * Updater ) UpdateTo ( rel * Release , cmdPath string ) error {
src , redirectURL , err := up . api . Repositories . DownloadReleaseAsset ( up . apiCtx , rel . RepoOwner , rel . RepoName , rel . AssetID )
2017-12-28 08:30:13 +00:00
if err != nil {
2018-01-19 09:38:51 +00:00
return fmt . Errorf ( "Failed to call GitHub Releases API for getting an asset(ID: %d) for repository '%s/%s': %s" , rel . AssetID , rel . RepoOwner , rel . RepoName , err )
2017-12-28 08:30:13 +00:00
}
2018-01-19 03:05:41 +00:00
if redirectURL != "" {
log . Println ( "Redirect URL was returned while trying to download a release asset from GitHub API. Falling back to downloading from asset URL directly:" , redirectURL )
2018-01-19 06:03:47 +00:00
src , err = up . downloadDirectlyFromURL ( redirectURL )
if err != nil {
return err
}
2018-01-19 03:05:41 +00:00
}
2018-11-06 15:28:06 +00:00
defer src . Close ( )
data , err := ioutil . ReadAll ( src )
if err != nil {
return fmt . Errorf ( "Failed reading asset body: %v" , err )
}
if up . validator == nil {
return uncompressAndUpdate ( bytes . NewReader ( data ) , rel . AssetURL , cmdPath )
}
validationSrc , validationRedirectURL , err := up . api . Repositories . DownloadReleaseAsset ( up . apiCtx , rel . RepoOwner , rel . RepoName , rel . ValidationAssetID )
if err != nil {
return fmt . Errorf ( "Failed to call GitHub Releases API for getting an validation asset(ID: %d) for repository '%s/%s': %s" , rel . ValidationAssetID , rel . RepoOwner , rel . RepoName , err )
}
if validationRedirectURL != "" {
log . Println ( "Redirect URL was returned while trying to download a release validation asset from GitHub API. Falling back to downloading from asset URL directly:" , redirectURL )
validationSrc , err = up . downloadDirectlyFromURL ( validationRedirectURL )
if err != nil {
return err
}
}
defer validationSrc . Close ( )
validationData , err := ioutil . ReadAll ( validationSrc )
if err != nil {
return fmt . Errorf ( "Failed reading validation asset body: %v" , err )
}
if err := up . validator . Validate ( data , validationData ) ; err != nil {
return fmt . Errorf ( "Failed validating asset content: %v" , err )
}
return uncompressAndUpdate ( bytes . NewReader ( data ) , rel . AssetURL , cmdPath )
2017-12-28 08:30:13 +00:00
}
// UpdateCommand updates a given command binary to the latest version.
// 'slug' represents 'owner/name' repository on GitHub and 'current' means the current version.
2018-01-19 02:28:59 +00:00
func ( up * Updater ) UpdateCommand ( cmdPath string , current semver . Version , slug string ) ( * Release , error ) {
2017-12-30 04:45:19 +00:00
if runtime . GOOS == "windows" && ! strings . HasSuffix ( cmdPath , ".exe" ) {
2018-01-01 07:01:42 +00:00
// Ensure to add '.exe' to given path on Windows
2017-12-30 04:45:19 +00:00
cmdPath = cmdPath + ".exe"
}
2018-01-03 14:01:33 +00:00
stat , err := os . Lstat ( cmdPath )
if err != nil {
return nil , fmt . Errorf ( "Failed to stat '%s'. File may not exist: %s" , cmdPath , err )
}
if stat . Mode ( ) & os . ModeSymlink != 0 {
p , err := filepath . EvalSymlinks ( cmdPath )
if err != nil {
return nil , fmt . Errorf ( "Failed to resolve symlink '%s' for executable: %s" , cmdPath , err )
}
cmdPath = p
}
2018-01-19 02:28:59 +00:00
rel , ok , err := up . DetectLatest ( slug )
2017-12-28 08:30:13 +00:00
if err != nil {
return nil , err
}
if ! ok {
2017-12-30 04:25:42 +00:00
log . Println ( "No release detected. Current version is considered up-to-date" )
2017-12-28 08:30:13 +00:00
return & Release { Version : current } , nil
}
if current . Equals ( rel . Version ) {
2017-12-30 04:25:42 +00:00
log . Println ( "Current version" , current , "is the latest. Update is not needed" )
2017-12-28 08:30:13 +00:00
return rel , nil
}
2017-12-30 04:25:42 +00:00
log . Println ( "Will update" , cmdPath , "to the latest version" , rel . Version )
2018-01-19 03:05:41 +00:00
if err := up . UpdateTo ( rel , cmdPath ) ; err != nil {
2017-12-28 08:30:13 +00:00
return nil , err
}
return rel , nil
}
// UpdateSelf updates the running executable itself to the latest version.
// 'slug' represents 'owner/name' repository on GitHub and 'current' means the current version.
2018-01-19 02:28:59 +00:00
func ( up * Updater ) UpdateSelf ( current semver . Version , slug string ) ( * Release , error ) {
2018-01-02 14:43:45 +00:00
cmdPath , err := os . Executable ( )
if err != nil {
return nil , err
}
2018-04-27 14:07:51 +00:00
return up . UpdateCommand ( cmdPath , current , slug )
2017-12-28 08:30:13 +00:00
}
2018-01-19 02:28:59 +00:00
2018-01-19 06:03:47 +00:00
// UpdateTo downloads an executable from assetURL and replace the current binary with the downloaded one.
// This function is low-level API to update the binary. Because it does not use GitHub API and downloads asset directly from the URL via HTTP,
// this function is not available to update a release for private repositories.
// cmdPath is a file path to command executable.
func UpdateTo ( assetURL , cmdPath string ) error {
2018-01-19 08:47:21 +00:00
up := DefaultUpdater ( )
2018-01-19 06:03:47 +00:00
src , err := up . downloadDirectlyFromURL ( assetURL )
if err != nil {
return err
}
2018-11-06 15:28:06 +00:00
defer src . Close ( )
2018-01-19 06:03:47 +00:00
return uncompressAndUpdate ( src , assetURL , cmdPath )
}
2018-01-19 02:28:59 +00:00
// UpdateCommand updates a given command binary to the latest version.
// This function is a shortcut version of updater.UpdateCommand.
func UpdateCommand ( cmdPath string , current semver . Version , slug string ) ( * Release , error ) {
2018-01-19 08:47:21 +00:00
return DefaultUpdater ( ) . UpdateCommand ( cmdPath , current , slug )
2018-01-19 02:28:59 +00:00
}
// UpdateSelf updates the running executable itself to the latest version.
// This function is a shortcut version of updater.UpdateSelf.
func UpdateSelf ( current semver . Version , slug string ) ( * Release , error ) {
2018-01-19 08:47:21 +00:00
return DefaultUpdater ( ) . UpdateSelf ( current , slug )
2018-01-19 02:28:59 +00:00
}