github.com/tomwright/dasel@v1.27.3/internal/selfupdate/release.go (about)

     1  package selfupdate
     2  
     3  import (
     4  	"fmt"
     5  	"net/http"
     6  	"os"
     7  	"path/filepath"
     8  	"time"
     9  )
    10  
    11  const (
    12  	gitHubUsername = "TomWright"
    13  	gitHubRepo     = "dasel"
    14  )
    15  
    16  // NewUpdater returns an Updater.
    17  func NewUpdater(currentVersion string) *Updater {
    18  	return &Updater{
    19  		httpClient: &http.Client{
    20  			Timeout: time.Second * 10,
    21  		},
    22  		currentVersion: currentVersion,
    23  
    24  		FetchReleaseFn: fetchGitHubRelease,
    25  		DownloadFileFn: downloadFile,
    26  		ChmodFn:        os.Chmod,
    27  		ExecuteCmdFn:   executeCmd,
    28  		ExecutableFn:   os.Executable,
    29  		RenameFn:       os.Rename,
    30  		RemoveFn:       os.Remove,
    31  	}
    32  }
    33  
    34  // Release is a github release.
    35  type Release struct {
    36  	URL     string          `json:"url"`
    37  	Assets  []*ReleaseAsset `json:"assets"`
    38  	Name    string          `json:"name"`
    39  	TagName string          `json:"tag_name"`
    40  }
    41  
    42  // FindAssetForSystem searches returns the asset for the given OS and arch.
    43  func (r *Release) FindAssetForSystem(os string, arch string) *ReleaseAsset {
    44  	ext := ""
    45  	if os == "windows" {
    46  		ext = ".exe"
    47  	}
    48  	matches := []string{fmt.Sprintf("dasel_%s_%s%s", os, arch, ext)}
    49  	if os == "darwin" {
    50  		matches = append(matches, fmt.Sprintf("dasel_%s_%s%s", "macos", arch, ext))
    51  	}
    52  
    53  	for _, a := range r.Assets {
    54  		for _, m := range matches {
    55  			if a.Name == m {
    56  				return a
    57  			}
    58  		}
    59  	}
    60  
    61  	return nil
    62  }
    63  
    64  // Version returns the version of the release.
    65  func (r *Release) Version() *Version {
    66  	return versionFromString(r.TagName)
    67  }
    68  
    69  // ReleaseAsset is an asset of a Release.
    70  type ReleaseAsset struct {
    71  	URL                string `json:"url"`
    72  	Name               string `json:"name"`
    73  	BrowserDownloadURL string `json:"browser_download_url"`
    74  }
    75  
    76  // Updater provides functionality that allows you to update a binary from github.
    77  type Updater struct {
    78  	httpClient     *http.Client
    79  	currentVersion string
    80  
    81  	// FetchReleaseFn is used to fetch github release information.
    82  	FetchReleaseFn func(httpClient *http.Client, user string, repo string, tag string) (*Release, error)
    83  	// DownloadFileFn is used to download a file.
    84  	DownloadFileFn func(url string, dest string) error
    85  	// ChmodFn is used to change the permissions of a file.
    86  	ChmodFn func(name string, mode os.FileMode) error
    87  	// ExecuteCmdFn executes the given command and returns the output.
    88  	ExecuteCmdFn func(name string, arg ...string) ([]byte, error)
    89  	// ExecutableFn is used to return the path of the current executable.
    90  	ExecutableFn func() (string, error)
    91  	// RenameFn is used to rename a file.
    92  	RenameFn func(src string, dst string) error
    93  	// RemoveFn is used to remove a file.
    94  	RemoveFn func(path string) error
    95  }
    96  
    97  // FindLatestRelease returns the latest release on the given repo.
    98  func (u *Updater) FindLatestRelease() (*Release, error) {
    99  	return u.FetchReleaseFn(u.httpClient, gitHubUsername, gitHubRepo, "latest")
   100  }
   101  
   102  // DownloadAsset downloads and returns a path to the given asset.
   103  func (u *Updater) DownloadAsset(asset *ReleaseAsset) (string, error) {
   104  	path, err := filepath.Abs(fmt.Sprintf("./%s", asset.Name))
   105  	if err != nil {
   106  		return "", err
   107  	}
   108  
   109  	if err := u.DownloadFileFn(asset.BrowserDownloadURL, path); err != nil {
   110  		return "", fmt.Errorf("could not download file: %w", err)
   111  	}
   112  
   113  	if err := u.ChmodFn(path, os.ModePerm); err != nil {
   114  		return "", fmt.Errorf("could not make executable: %w", err)
   115  	}
   116  
   117  	return path, nil
   118  }
   119  
   120  // CurrentVersion returns the current version.
   121  func (u *Updater) CurrentVersion() *Version {
   122  	return versionFromString(u.currentVersion)
   123  }
   124  
   125  // GetVersion returns the version of the given executable.
   126  func (u *Updater) GetVersion(path string) (*Version, error) {
   127  	versionOutput, err := u.ExecuteCmdFn(path, "--version")
   128  	if err != nil {
   129  		return nil, fmt.Errorf("could not get new version: %w", err)
   130  	}
   131  
   132  	latest := versionFromString(string(versionOutput))
   133  
   134  	return latest, nil
   135  }
   136  
   137  // Replace replaces the current executable with the given executable.
   138  func (u *Updater) Replace(path string) error {
   139  	currentPath, err := u.ExecutableFn()
   140  	if err != nil {
   141  		return fmt.Errorf("cannot get current executable path: %w", err)
   142  	}
   143  
   144  	if err := u.RenameFn(path, currentPath); err != nil {
   145  		return fmt.Errorf("could not replace old executable: %w", err)
   146  	}
   147  
   148  	return nil
   149  }
   150  
   151  // CleanUp cleans up the given path.
   152  // This should be deferred once a download has completed.
   153  func (u *Updater) CleanUp(path string) {
   154  	_ = u.RemoveFn(path)
   155  }