From 086cd572479fb818542695a0ec0368234e5e1c0d Mon Sep 17 00:00:00 2001
From: zanbin168 <zanbin168@qq.com>
Date: 星期二, 16 五月 2023 22:30:47 +0800
Subject: [PATCH] update

---
 _example/specific/specific.go |   61 ++++
 stores/apex/apex.go           |  124 ++++++++
 go.sum                        |  123 ++++++++
 stores/github/github.go       |   83 +++++
 _example/latest/latest.go     |   70 +++++
 go.mod                        |   14 +
 store.go                      |   18 +
 update.go                     |  232 ++++++++++++++++
 README.md                     |   14 
 progress/progress.go          |   47 +++
 10 files changed, 784 insertions(+), 2 deletions(-)

diff --git a/README.md b/README.md
index ab83ef6..d6460f6 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,12 @@
-# goupdate
-Forked and adapted from https://github.com/tj/go-update
+## go-update
+
+Package update provides tooling to auto-update binary releases
+from GitHub based on the user's current version and operating system. Used by command-line tools such as [Up](https://github.com/apex/up) and [Apex](https://github.com/apex/apex).
+
+---
+
+[![GoDoc](https://godoc.org/github.com/zan8in/go-update?status.svg)](https://godoc.org/github.com/zan8in/go-update)
+![](https://img.shields.io/badge/license-MIT-blue.svg)
+![](https://img.shields.io/badge/status-stable-green.svg)
+
+<a href="https://apex.sh"><img src="http://tjholowaychuk.com:6000/svg/sponsor"></a>
diff --git a/_example/latest/latest.go b/_example/latest/latest.go
new file mode 100644
index 0000000..b661cf5
--- /dev/null
+++ b/_example/latest/latest.go
@@ -0,0 +1,70 @@
+package main
+
+import (
+	"fmt"
+	"runtime"
+
+	"github.com/apex/log"
+	"github.com/kierdavis/ansi"
+
+	"github.com/zan8in/go-update"
+	"github.com/zan8in/go-update/progress"
+	"github.com/zan8in/go-update/stores/github"
+)
+
+func init() {
+	log.SetLevel(log.DebugLevel)
+}
+
+func main() {
+	ansi.HideCursor()
+	defer ansi.ShowCursor()
+
+	// update polls(1) from tj/gh-polls on github
+	m := &update.Manager{
+		Command: "polls",
+		Store: &github.Store{
+			Owner:   "tj",
+			Repo:    "gh-polls",
+			Version: "0.0.3",
+		},
+	}
+
+	// fetch the new releases
+	releases, err := m.LatestReleases()
+	if err != nil {
+		log.Fatalf("error fetching releases: %s", err)
+	}
+
+	// no updates
+	if len(releases) == 0 {
+		log.Info("no updates")
+		return
+	}
+
+	// latest release
+	latest := releases[0]
+
+	// find the tarball for this system
+	a := latest.FindTarball(runtime.GOOS, runtime.GOARCH)
+	if a == nil {
+		log.Info("no binary for your system")
+		return
+	}
+
+	// whitespace
+	fmt.Println()
+
+	// download tarball to a tmp dir
+	tarball, err := a.DownloadProxy(progress.Reader)
+	if err != nil {
+		log.Fatalf("error downloading: %s", err)
+	}
+
+	// install it
+	if err := m.Install(tarball); err != nil {
+		log.Fatalf("error installing: %s", err)
+	}
+
+	fmt.Printf("Updated to %s\n", latest.Version)
+}
diff --git a/_example/specific/specific.go b/_example/specific/specific.go
new file mode 100644
index 0000000..b7d890d
--- /dev/null
+++ b/_example/specific/specific.go
@@ -0,0 +1,61 @@
+package main
+
+import (
+	"fmt"
+	"runtime"
+
+	"github.com/apex/log"
+	"github.com/kierdavis/ansi"
+
+	"github.com/zan8in/go-update"
+	"github.com/zan8in/go-update/progress"
+	"github.com/zan8in/go-update/stores/github"
+)
+
+func init() {
+	log.SetLevel(log.DebugLevel)
+}
+
+func main() {
+	ansi.HideCursor()
+	defer ansi.ShowCursor()
+
+	// update polls(1) from tj/gh-polls on github
+	m := &update.Manager{
+		Command: "up",
+		Store: &github.Store{
+			Owner:   "apex",
+			Repo:    "up",
+			Version: "0.4.6",
+		},
+	}
+
+	// fetch the target release
+	release, err := m.GetRelease("0.4.5")
+	if err != nil {
+		log.Fatalf("error fetching release: %s", err)
+	}
+
+	// find the tarball for this system
+	a := release.FindTarball(runtime.GOOS, runtime.GOARCH)
+	if a == nil {
+		log.Info("no binary for your system")
+		return
+	}
+
+	// whitespace
+	fmt.Println()
+
+	// download tarball to a tmp dir
+	tarball, err := a.DownloadProxy(progress.Reader)
+	if err != nil {
+		log.Fatalf("error downloading: %s", err)
+	}
+
+	// install it
+	if err := m.Install(tarball); err != nil {
+		log.Fatalf("error installing: %s", err)
+	}
+
+	fmt.Printf("Updated to %s\n", release.Version)
+}
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..626502c
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,14 @@
+module github.com/zan8in/go-update
+
+go 1.16
+
+require (
+	github.com/apex/log v1.9.0
+	github.com/c4milo/unpackit v1.0.0
+	github.com/google/go-github v17.0.0+incompatible
+	github.com/google/go-querystring v1.1.0 // indirect
+	github.com/gosuri/uilive v0.0.4 // indirect
+	github.com/gosuri/uiprogress v0.0.1
+	github.com/pkg/errors v0.9.1
+	github.com/tj/go v1.8.7
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..41180d4
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,123 @@
+github.com/apex/log v1.3.0/go.mod h1:jd8Vpsr46WAe3EZSQ/IUMs2qQD/GOycT5rPWCO1yGcs=
+github.com/apex/log v1.9.0 h1:FHtw/xuaM8AgmvDDTI9fiwoAL25Sq2cxojnZICUU8l0=
+github.com/apex/log v1.9.0/go.mod h1:m82fZlWIuiWzWP04XCTXmnX0xRkYYbCdYn8jbJeLBEA=
+github.com/apex/logs v0.0.4/go.mod h1:XzxuLZ5myVHDy9SAmYpamKKRNApGj54PfYLcFrXqDwo=
+github.com/apex/logs v1.0.0/go.mod h1:XzxuLZ5myVHDy9SAmYpamKKRNApGj54PfYLcFrXqDwo=
+github.com/aphistic/golf v0.0.0-20180712155816-02c07f170c5a/go.mod h1:3NqKYiepwy8kCu4PNA+aP7WUV72eXWJeP9/r3/K9aLE=
+github.com/aphistic/sweet v0.2.0/go.mod h1:fWDlIh/isSE9n6EPsRmC0det+whmX6dJid3stzu0Xys=
+github.com/atotto/clipboard v0.1.2/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
+github.com/aws/aws-sdk-go v1.20.6/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
+github.com/aws/aws-sdk-go v1.31.9/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0=
+github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59/go.mod h1:q/89r3U2H7sSsE2t6Kca0lfwTK8JdoNGS/yzM/4iH5I=
+github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8 h1:GKTyiRCL6zVf5wWaqKnf+7Qs6GbEPfd4iMOitWzXJx8=
+github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8/go.mod h1:spo1JLcs67NmW1aVLEgtA8Yy1elc+X8y5SRW1sFW4Og=
+github.com/buger/goterm v0.0.0-20181115115552-c206103e1f37/go.mod h1:u9UyCz2eTrSGy6fbupqJ54eY5c4IC8gREQ1053dK12U=
+github.com/c4milo/unpackit v1.0.0 h1:Umce1lwtFvEHNFQev+xENObYiiYxdSmKhvGlkcufUGE=
+github.com/c4milo/unpackit v1.0.0/go.mod h1:0cXRaRz5pMcJm7o9jYQmPAeBl6y1na9BKy3K+og0UJY=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/dsnet/compress v0.0.1 h1:PlZu0n3Tuv04TzpfPbrnI0HW/YwodEXDS+oPKahKF0Q=
+github.com/dsnet/compress v0.0.1/go.mod h1:Aw8dCMJ7RioblQeTqt88akK31OvO8Dhf5JflhBbQEHo=
+github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
+github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
+github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
+github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
+github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM=
+github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY=
+github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
+github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
+github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
+github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/gosuri/uilive v0.0.4 h1:hUEBpQDj8D8jXgtCdBu7sWsy5sbW/5GhuO8KBwJ2jyY=
+github.com/gosuri/uilive v0.0.4/go.mod h1:V/epo5LjjlDE5RJUcqx8dbw+zc93y5Ya3yg8tfZ74VI=
+github.com/gosuri/uiprogress v0.0.1 h1:0kpv/XY/qTmFWl/SkaJykZXrBBzwwadmW8fRb7RJSxw=
+github.com/gosuri/uiprogress v0.0.1/go.mod h1:C1RTYn4Sc7iEyf6j8ft5dyoZ4212h8G1ol9QQluh5+0=
+github.com/hooklift/assert v0.1.0 h1:UZzFxx5dSb9aBtvMHTtnPuvFnBvcEhHTPb9+0+jpEjs=
+github.com/hooklift/assert v0.1.0/go.mod h1:pfexfvIHnKCdjh6CkkIZv5ic6dQ6aU2jhKghBlXuwwY=
+github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
+github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
+github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik=
+github.com/jpillora/backoff v0.0.0-20180909062703-3050d21c67d7/go.mod h1:2iMrUgbbvHEiQClaW2NsSzMyGHqN+rDFqY705q49KG0=
+github.com/klauspost/compress v1.4.1 h1:8VMb5+0wMgdBykOV96DwNwKFQ+WTI4pzYURP99CcB9E=
+github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
+github.com/klauspost/cpuid v1.2.0 h1:NMpwD2G9JSFOE1/TJjGSo5zG7Yb2bTe7eq1jH+irmeE=
+github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
+github.com/klauspost/pgzip v1.2.5 h1:qnWYvvKqedOF2ulHpMG72XQol4ILEJ8k2wwRl/Km8oE=
+github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
+github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
+github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
+github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
+github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
+github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
+github.com/mattn/go-isatty v0.0.9 h1:d5US/mDsogSGW37IV293h//ZFaeajb69h+EHFsv2xGg=
+github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
+github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
+github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
+github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA=
+github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/rogpeppe/fastuuid v1.1.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
+github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
+github.com/smartystreets/assertions v1.0.0/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM=
+github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9/go.mod h1:SnhjPscd9TpLiy1LpzGSKh3bXCfxxXuqd9xmQJy3slM=
+github.com/smartystreets/gunit v1.0.0/go.mod h1:qwPWnhz6pn0NnRBP++URONOVyNkPyr4SauJk4cUOwJs=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
+github.com/stretchr/testify v1.6.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
+github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stripe/stripe-go v70.15.0+incompatible/go.mod h1:A1dQZmO/QypXmsL0T8axYZkSN/uA/T/A64pfKdBAMiY=
+github.com/tj/assert v0.0.0-20171129193455-018094318fb0/go.mod h1:mZ9/Rh9oLWpLLDRpvE+3b7gP/C2YyLFYxNmcLnPTMe0=
+github.com/tj/assert v0.0.1/go.mod h1:lsg+GHQ0XplTcWKGxFLf/XPcPxWO8x2ut5jminoR2rA=
+github.com/tj/assert v0.0.3 h1:Df/BlaZ20mq6kuai7f5z2TvPFiwC3xaWJSDQNiIS3Rk=
+github.com/tj/assert v0.0.3/go.mod h1:Ne6X72Q+TB1AteidzQncjw9PabbMp4PBMZ1k+vd1Pvk=
+github.com/tj/go v1.8.7 h1:a7M1Xo+QKmlUHEzZj2LX0LHqkh7/LpOa6Or8luBvY/c=
+github.com/tj/go v1.8.7/go.mod h1:88DQADQo0c0fHmWNcr88pIGUHlV5du8aGtON+S1jr5A=
+github.com/tj/go-buffer v1.1.0/go.mod h1:iyiJpfFcR2B9sXu7KvjbT9fpM4mOelRSDTbntVj52Uc=
+github.com/tj/go-elastic v0.0.0-20171221160941-36157cbbebc2/go.mod h1:WjeM0Oo1eNAjXGDx2yma7uG2XoyRZTq1uv3M/o7imD0=
+github.com/tj/go-kinesis v0.0.0-20171128231115-08b17f58cb1b/go.mod h1:/yhzCV0xPfx6jb1bBgRFjl5lytqVqZXEaeqWP8lTEao=
+github.com/tj/go-spin v1.1.0/go.mod h1:Mg1mzmePZm4dva8Qz60H2lHwmJ2loum4VIrLgVnKwh4=
+github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8=
+github.com/ulikunitz/xz v0.5.10 h1:t92gobL9l3HE202wg3rlk19F6X+JOxl9BBrCCMYEYd8=
+github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a h1:aYOabOQFp6Vj6W1F80affTUvO9UxmJRx8K0gsfABByQ=
+golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
+gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.0-20200602174320-3e3e88ca92fa/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c h1:grhR+C34yXImVGp7EzNk+DTIk+323eIUWOmEevy6bDo=
+gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/progress/progress.go b/progress/progress.go
new file mode 100644
index 0000000..e7972b1
--- /dev/null
+++ b/progress/progress.go
@@ -0,0 +1,47 @@
+// Package progress provides a proxy for download progress.
+package progress
+
+import (
+	"io"
+	"time"
+
+	pb "github.com/gosuri/uiprogress"
+)
+
+// TODO: refactor to just check EOF
+
+// reader wrapping a progress bar.
+type reader struct {
+	bars    *pb.Progress
+	r       io.ReadCloser
+	written int
+}
+
+// Read implementation.
+func (r *reader) Read(b []byte) (int, error) {
+	n, err := r.r.Read(b)
+	r.written += n
+	r.bars.Bars[0].Set(r.written)
+	return n, err
+}
+
+// Close implementation.
+func (r *reader) Close() error {
+	r.bars.Stop()
+	return r.r.Close()
+}
+
+// Reader returns a progress bar reader.
+func Reader(size int, r io.ReadCloser) io.ReadCloser {
+	bars := pb.New()
+	bars.Width = 50
+	bars.AddBar(size)
+	bars.Start()
+	bars.SetRefreshInterval(50 * time.Millisecond)
+	bars.Bars[0].AppendCompleted()
+
+	return &reader{
+		bars: bars,
+		r:    r,
+	}
+}
diff --git a/store.go b/store.go
new file mode 100644
index 0000000..7c541be
--- /dev/null
+++ b/store.go
@@ -0,0 +1,18 @@
+package update
+
+import "github.com/pkg/errors"
+
+// TODO: the platform resolution should also be
+// in the interface...
+
+// Errors.
+var (
+	// ErrNotFound is returned from GetRelease if the release is not found.
+	ErrNotFound = errors.New("release not found")
+)
+
+// Store is the interface used for listing and fetching releases.
+type Store interface {
+	GetRelease(version string) (*Release, error)
+	LatestReleases() ([]*Release, error)
+}
diff --git a/stores/apex/apex.go b/stores/apex/apex.go
new file mode 100644
index 0000000..64ff334
--- /dev/null
+++ b/stores/apex/apex.go
@@ -0,0 +1,124 @@
+// Package apex provides an Apex release store.
+package apex
+
+import (
+	"encoding/json"
+	"fmt"
+	"net/http"
+	"time"
+
+	"github.com/pkg/errors"
+	"github.com/tj/go/http/request"
+	"github.com/zan8in/go-update"
+)
+
+// Store is the store implementation.
+type Store struct {
+	URL       string
+	Product   string
+	Plan      string
+	Version   string
+	AccessKey string
+}
+
+// Release model.
+type Release struct {
+	Version   string    `json:"version"`
+	Notes     string    `json:"notes"`
+	Files     []*File   `json:"files"`
+	CreatedAt time.Time `json:"created_at"`
+}
+
+// File model.
+type File struct {
+	Name string `json:"name"`
+	Key  string `json:"key"`
+	Size int64  `json:"size"`
+	URL  string `json:"url"`
+}
+
+// GetRelease returns the specified release or ErrNotFound.
+func (s *Store) GetRelease(version string) (*update.Release, error) {
+	releases, err := s.releases()
+	if err != nil {
+		return nil, err
+	}
+
+	for _, r := range releases {
+		if r.Version == version {
+			return r, nil
+		}
+	}
+
+	return nil, update.ErrNotFound
+}
+
+// LatestReleases returns releases newer than Version, or nil.
+func (s *Store) LatestReleases() (latest []*update.Release, err error) {
+	releases, err := s.releases()
+	if err != nil {
+		return
+	}
+
+	for _, r := range releases {
+		if r.Version == s.Version {
+			break
+		}
+
+		latest = append(latest, r)
+	}
+
+	return
+}
+
+// releases returns all releases.
+func (s *Store) releases() (all []*update.Release, err error) {
+	url := fmt.Sprintf("%s/%s/%s", s.URL, s.Product, s.Plan)
+
+	req, err := http.NewRequest("GET", url, nil)
+	if err != nil {
+		return nil, errors.Wrap(err, "creating request")
+	}
+	req.Header.Set("Authorization", "Bearer "+s.AccessKey)
+
+	res, err := http.DefaultClient.Do(req)
+	if err != nil {
+		return nil, errors.Wrap(err, "requesting")
+	}
+	defer res.Body.Close()
+
+	if res.StatusCode >= 400 {
+		return nil, request.Error(res.StatusCode)
+	}
+
+	var releases []*Release
+
+	if err := json.NewDecoder(res.Body).Decode(&releases); err != nil {
+		return nil, errors.Wrap(err, "unmarshaling")
+	}
+
+	for _, r := range releases {
+		all = append(all, toRelease(r))
+	}
+
+	return
+}
+
+// toRelease returns a Release.
+func toRelease(r *Release) *update.Release {
+	out := &update.Release{
+		Version:     r.Version,
+		Notes:       r.Notes,
+		PublishedAt: r.CreatedAt,
+	}
+
+	for _, f := range r.Files {
+		out.Assets = append(out.Assets, &update.Asset{
+			Name: f.Name,
+			Size: int(f.Size),
+			URL:  f.URL,
+		})
+	}
+
+	return out
+}
diff --git a/stores/github/github.go b/stores/github/github.go
new file mode 100644
index 0000000..220d5b3
--- /dev/null
+++ b/stores/github/github.go
@@ -0,0 +1,83 @@
+// Package github provides a GitHub release store.
+package github
+
+import (
+	"context"
+	"time"
+
+	"github.com/google/go-github/github"
+	"github.com/zan8in/go-update"
+)
+
+// Store is the store implementation.
+type Store struct {
+	Owner   string
+	Repo    string
+	Version string
+}
+
+// GetRelease returns the specified release or ErrNotFound.
+func (s *Store) GetRelease(version string) (*update.Release, error) {
+	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+	defer cancel()
+
+	gh := github.NewClient(nil)
+
+	r, res, err := gh.Repositories.GetReleaseByTag(ctx, s.Owner, s.Repo, "v"+version)
+
+	if res.StatusCode == 404 {
+		return nil, update.ErrNotFound
+	}
+
+	if err != nil {
+		return nil, err
+	}
+
+	return githubRelease(r), nil
+}
+
+// LatestReleases returns releases newer than Version, or nil.
+func (s *Store) LatestReleases() (latest []*update.Release, err error) {
+	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+	defer cancel()
+
+	gh := github.NewClient(nil)
+
+	releases, _, err := gh.Repositories.ListReleases(ctx, s.Owner, s.Repo, nil)
+	if err != nil {
+		return nil, err
+	}
+
+	for _, r := range releases {
+		tag := r.GetTagName()
+
+		if tag == s.Version || "v"+s.Version == tag {
+			break
+		}
+
+		latest = append(latest, githubRelease(r))
+	}
+
+	return
+}
+
+// githubRelease returns a Release.
+func githubRelease(r *github.RepositoryRelease) *update.Release {
+	out := &update.Release{
+		Version:     r.GetTagName(),
+		Notes:       r.GetBody(),
+		PublishedAt: r.GetPublishedAt().Time,
+		URL:         r.GetURL(),
+	}
+
+	for _, a := range r.Assets {
+		out.Assets = append(out.Assets, &update.Asset{
+			Name:      a.GetName(),
+			Size:      a.GetSize(),
+			URL:       a.GetBrowserDownloadURL(),
+			Downloads: a.GetDownloadCount(),
+		})
+	}
+
+	return out
+}
diff --git a/update.go b/update.go
new file mode 100644
index 0000000..bed4c9e
--- /dev/null
+++ b/update.go
@@ -0,0 +1,232 @@
+// Package update provides tooling to auto-update binary releases
+// from GitHub based on the user's current version and operating system.
+package update
+
+import (
+	"fmt"
+	"io"
+	"io/ioutil"
+	"net/http"
+	"os"
+	"path/filepath"
+	"runtime"
+	"strconv"
+	"strings"
+	"time"
+
+	"github.com/apex/log"
+	"github.com/c4milo/unpackit"
+	"github.com/pkg/errors"
+)
+
+// Proxy is used to proxy a reader, for example
+// using https://github.com/cheggaaa/pb to provide
+// progress updates.
+type Proxy func(int, io.ReadCloser) io.ReadCloser
+
+// NopProxy does nothing.
+var NopProxy = func(size int, r io.ReadCloser) io.ReadCloser {
+	return r
+}
+
+// Manager is the update manager.
+type Manager struct {
+	Store          // Store for releases such as Github or a custom private store.
+	Command string // Command is the executable's name.
+}
+
+// Release represents a project release.
+type Release struct {
+	Version     string    // Version is the release version.
+	Notes       string    // Notes is the markdown release notes.
+	URL         string    // URL is the notes url.
+	PublishedAt time.Time // PublishedAt is the publish time.
+	Assets      []*Asset  // Assets is the release assets.
+}
+
+// Asset represents a project release asset.
+type Asset struct {
+	Name      string // Name of the asset.
+	Size      int    // Size of the asset.
+	URL       string // URL of the asset.
+	Downloads int    // Downloads count.
+}
+
+// InstallTo binary to the given dir.
+func (m *Manager) InstallTo(path, dir string) error {
+	log.Debugf("unpacking %q", path)
+
+	f, err := os.Open(path)
+	if err != nil {
+		return errors.Wrap(err, "opening tarball")
+	}
+
+	err = unpackit.Unpack(f, "")
+	if err != nil {
+		f.Close()
+		return errors.Wrap(err, "unpacking tarball")
+	}
+
+	if err := f.Close(); err != nil {
+		return errors.Wrap(err, "closing tarball")
+	}
+
+	bin := filepath.Join(path, m.Command)
+
+	if err := os.Chmod(bin, 0755); err != nil {
+		return errors.Wrap(err, "chmod")
+	}
+
+	dst := filepath.Join(dir, m.Command)
+	tmp := dst + ".tmp"
+
+	log.Debugf("copy %q to %q", bin, tmp)
+	if err := copyFile(tmp, bin); err != nil {
+		return errors.Wrap(err, "copying")
+	}
+
+	if runtime.GOOS == "windows" {
+		old := dst + ".old"
+		log.Debugf("windows workaround renaming %q to %q", dst, old)
+		if err := os.Rename(dst, old); err != nil {
+			return errors.Wrap(err, "windows renaming")
+		}
+	}
+
+	log.Debugf("renaming %q to %q", tmp, dst)
+	if err := os.Rename(tmp, dst); err != nil {
+		return errors.Wrap(err, "renaming")
+	}
+
+	return nil
+}
+
+// Install binary to replace the current version.
+func (m *Manager) Install(path string) error {
+	bin, err := os.Executable()
+	if err != nil {
+		return errors.Wrapf(err, "looking up path of %q", m.Command)
+	}
+
+	dir := filepath.Dir(bin)
+	return m.InstallTo(path, dir)
+}
+
+// FindTarball returns a tarball matching os and arch, or nil.
+func (r *Release) FindTarball(os, arch string) *Asset {
+	s := fmt.Sprintf("%s_%s", os, arch)
+	for _, a := range r.Assets {
+		ext := filepath.Ext(a.Name)
+		if strings.Contains(a.Name, s) && ext == ".gz" {
+			return a
+		}
+	}
+
+	return nil
+}
+
+// FindZip returns a zipfile matching os and arch, or nil.
+func (r *Release) FindZip(os, arch string) *Asset {
+	s := fmt.Sprintf("%s_%s", os, arch)
+	for _, a := range r.Assets {
+		ext := filepath.Ext(a.Name)
+		if strings.Contains(a.Name, s) && ext == ".zip" {
+			return a
+		}
+	}
+
+	return nil
+}
+
+// Download the asset to a tmp directory and return its path.
+func (a *Asset) Download() (string, error) {
+	return a.DownloadProxy(NopProxy)
+}
+
+// DownloadProxy the asset to a tmp directory and return its path.
+func (a *Asset) DownloadProxy(proxy Proxy) (string, error) {
+	f, err := ioutil.TempFile(os.TempDir(), "update-")
+	if err != nil {
+		return "", errors.Wrap(err, "creating temp file")
+	}
+
+	log.Debugf("fetch %q", a.URL)
+	res, err := http.Get(a.URL)
+	if err != nil {
+		return "", errors.Wrap(err, "fetching asset")
+	}
+
+	kind := res.Header.Get("Content-Type")
+	size, _ := strconv.Atoi(res.Header.Get("Content-Length"))
+	log.Debugf("response %s – %s (%d KiB)", res.Status, kind, size/1024)
+
+	body := proxy(size, res.Body)
+
+	if res.StatusCode >= 400 {
+		body.Close()
+		return "", errors.Wrap(err, res.Status)
+	}
+
+	log.Debugf("copy to %q", f.Name())
+	if _, err := io.Copy(f, body); err != nil {
+		body.Close()
+		return "", errors.Wrap(err, "copying body")
+	}
+
+	if err := body.Close(); err != nil {
+		return "", errors.Wrap(err, "closing body")
+	}
+
+	if err := f.Close(); err != nil {
+		return "", errors.Wrap(err, "closing file")
+	}
+
+	log.Debugf("copied")
+	return f.Name(), nil
+}
+
+// copyFile copies the contents of the file named src to the file named
+// by dst. The file will be created if it does not already exist. If the
+// destination file exists, all it's contents will be replaced by the contents
+// of the source file. The file mode will be copied from the source and
+// the copied data is synced/flushed to stable storage.
+func copyFile(dst, src string) (err error) {
+	in, err := os.Open(src)
+	if err != nil {
+		return
+	}
+	defer in.Close()
+
+	out, err := os.Create(dst)
+	if err != nil {
+		return
+	}
+
+	defer func() {
+		if e := out.Close(); e != nil {
+			err = e
+		}
+	}()
+
+	_, err = io.Copy(out, in)
+	if err != nil {
+		return
+	}
+
+	err = out.Sync()
+	if err != nil {
+		return
+	}
+
+	si, err := os.Stat(src)
+	if err != nil {
+		return
+	}
+
+	err = os.Chmod(dst, si.Mode())
+	if err != nil {
+		return
+	}
+
+	return
+}

--
Gitblit v1.8.0