Skip to content

Commit 3512bf9

Browse files
jwilderaktau
authored andcommitted
Add download support
This adds a new "download" command that will download a named asset from a release to the current directory. Both public and private repos are supported. Info command now also supports private repos to simplifly listing assets to download over the command-line.
1 parent 78cb36c commit 3512bf9

7 files changed

Lines changed: 153 additions & 36 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
github-release
12
go-app
23
bin/
34

api.go

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,11 @@ import (
1111

1212
const (
1313
API_URL = "https://api.github.com"
14+
GH_URL = "https://github.com"
1415
)
1516

1617
/* create a new request that sends the auth token */
17-
func NewAuthRequest(method, url, bodyType, token string, body io.Reader) (*http.Request, error) {
18+
func NewAuthRequest(method, url, bodyType, token string, headers map[string]string, body io.Reader) (*http.Request, error) {
1819
vprintln("creating request:", method, url, bodyType, token)
1920

2021
req, err := http.NewRequest(method, url, body)
@@ -43,14 +44,20 @@ func NewAuthRequest(method, url, bodyType, token string, body io.Reader) (*http.
4344
}
4445
}
4546

46-
req.Header.Set("Content-Type", bodyType)
47+
if bodyType != "" {
48+
req.Header.Set("Content-Type", bodyType)
49+
}
4750
req.Header.Set("Authorization", fmt.Sprintf("token %s", token))
4851

52+
for k, v := range headers {
53+
req.Header.Set(k, v)
54+
}
55+
4956
return req, nil
5057
}
5158

52-
func DoAuthRequest(method, url, bodyType, token string, body io.Reader) (*http.Response, error) {
53-
req, err := NewAuthRequest(method, url, bodyType, token, body)
59+
func DoAuthRequest(method, url, bodyType, token string, headers map[string]string, body io.Reader) (*http.Response, error) {
60+
req, err := NewAuthRequest(method, url, bodyType, token, headers, body)
5461
if err != nil {
5562
return nil, err
5663
}
@@ -98,4 +105,4 @@ func ApiURL() string {
98105
} else {
99106
return EnvApiEndpoint
100107
}
101-
}
108+
}

assets.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ import (
44
"time"
55
)
66

7+
const (
8+
ASSET_DOWNLOAD_URI = "/repos/%s/%s/releases/assets/%d"
9+
)
10+
711
type Asset struct {
812
Url string `json:"url"`
913
Id int `json:"id"`

cmd.go

Lines changed: 97 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,26 @@ import (
44
"bytes"
55
"encoding/json"
66
"fmt"
7+
"io"
78
"io/ioutil"
89
"net/http"
910
"net/url"
11+
"os"
12+
"strconv"
1013
)
1114

1215
func infocmd(opt Options) error {
1316
user := nvls(opt.Info.User, EnvUser)
1417
repo := nvls(opt.Info.Repo, EnvRepo)
1518
tag := opt.Upload.Tag
19+
token := nvls(opt.Info.Token, EnvToken)
1620

1721
if user == "" || repo == "" {
1822
return fmt.Errorf("user and repo need to be passed as arguments")
1923
}
2024

2125
/* list all tags */
22-
tags, err := Tags(user, repo)
26+
tags, err := Tags(user, repo, token)
2327
if err != nil {
2428
return fmt.Errorf("could not fetch tags, %v", err)
2529
}
@@ -35,14 +39,14 @@ func infocmd(opt Options) error {
3539
if tag == "" {
3640
/* get all releases */
3741
vprintf("%v/%v: getting information for all releases\n", user, repo)
38-
releases, err = Releases(user, repo)
42+
releases, err = Releases(user, repo, token)
3943
if err != nil {
4044
return err
4145
}
4246
} else {
4347
/* get only one release */
4448
vprintf("%v/%v/%v: getting information for the release\n", user, repo, tag)
45-
release, err := ReleaseOfTag(user, repo, tag)
49+
release, err := ReleaseOfTag(user, repo, tag, token)
4650
if err != nil {
4751
return err
4852
}
@@ -75,7 +79,7 @@ func uploadcmd(opt Options) error {
7579
}
7680

7781
/* find the release corresponding to the entered tag, if any */
78-
rel, err := ReleaseOfTag(user, repo, tag)
82+
rel, err := ReleaseOfTag(user, repo, tag, token)
7983
if err != nil {
8084
return err
8185
}
@@ -86,7 +90,7 @@ func uploadcmd(opt Options) error {
8690
url := rel.CleanUploadUrl() + "?" + v.Encode()
8791

8892
resp, err := DoAuthRequest("POST", url, "application/octet-stream",
89-
token, file)
93+
token, nil, file)
9094
if err != nil {
9195
return fmt.Errorf("can't create upload request to %v, %v", url, err)
9296
}
@@ -113,7 +117,81 @@ func uploadcmd(opt Options) error {
113117
return nil
114118
}
115119

116-
func ValidateCredentials(user, repo, token, tag string) error {
120+
func downloadcmd(opt Options) error {
121+
user := nvls(opt.Download.User, EnvUser)
122+
repo := nvls(opt.Download.Repo, EnvRepo)
123+
token := nvls(opt.Download.Token, EnvToken)
124+
tag := opt.Download.Tag
125+
name := opt.Download.Name
126+
127+
vprintln("downloading...")
128+
129+
if err := ValidateTarget(user, repo, tag); err != nil {
130+
return err
131+
}
132+
133+
/* find the release corresponding to the entered tag, if any */
134+
rel, err := ReleaseOfTag(user, repo, tag, token)
135+
if err != nil {
136+
return err
137+
}
138+
139+
assetId := 0
140+
for _, asset := range rel.Assets {
141+
if asset.Name == name {
142+
assetId = asset.Id
143+
}
144+
}
145+
146+
if assetId == 0 {
147+
return fmt.Errorf("coud not find asset named %s", name)
148+
}
149+
150+
var resp *http.Response
151+
var url string
152+
if token == "" {
153+
url = GH_URL + fmt.Sprintf("/%s/%s/releases/download/%s/%s", user, repo, tag, name)
154+
resp, err = http.Get(url)
155+
156+
} else {
157+
url = ApiURL() + fmt.Sprintf(ASSET_DOWNLOAD_URI, user, repo, assetId)
158+
159+
resp, err = DoAuthRequest("GET", url, "", token, map[string]string{
160+
"Accept": "application/octet-stream",
161+
}, nil)
162+
}
163+
164+
if err != nil {
165+
return fmt.Errorf("could not fetch releases, %v", err)
166+
}
167+
168+
defer resp.Body.Close()
169+
vprintln("GET", url, "->", resp)
170+
171+
contentLength, err := strconv.ParseInt(resp.Header.Get("Content-Length"), 10, 64)
172+
if err != nil {
173+
return err
174+
}
175+
176+
if resp.StatusCode != http.StatusOK {
177+
return fmt.Errorf("github did not respond with 200 OK but with %v", resp.Status)
178+
}
179+
180+
out, err := os.Create(name)
181+
defer out.Close()
182+
183+
n, err := io.Copy(out, resp.Body)
184+
if err != nil {
185+
return err
186+
}
187+
if n != contentLength {
188+
return fmt.Errorf("downloaded data did not match content length %s != %s", contentLength, n)
189+
}
190+
191+
return nil
192+
}
193+
194+
func ValidateTarget(user, repo, tag string) error {
117195
if user == "" {
118196
return fmt.Errorf("empty user")
119197
}
@@ -123,6 +201,14 @@ func ValidateCredentials(user, repo, token, tag string) error {
123201
if tag == "" {
124202
return fmt.Errorf("empty tag")
125203
}
204+
return nil
205+
}
206+
207+
func ValidateCredentials(user, repo, token, tag string) error {
208+
err := ValidateTarget(user, repo, tag)
209+
if err != nil {
210+
return err
211+
}
126212
if token == "" {
127213
return fmt.Errorf("empty token")
128214
}
@@ -163,7 +249,7 @@ func releasecmd(opt Options) error {
163249

164250
uri := fmt.Sprintf("/repos/%s/%s/releases", user, repo)
165251
resp, err := DoAuthRequest("POST", ApiURL()+uri, "application/json",
166-
token, reader)
252+
token, nil, reader)
167253
if err != nil {
168254
return fmt.Errorf("while submitting %v, %v", string(payload), err)
169255
}
@@ -206,7 +292,7 @@ func editcmd(opt Options) error {
206292
return err
207293
}
208294

209-
id, err := IdOfTag(user, repo, tag)
295+
id, err := IdOfTag(user, repo, tag, token)
210296
if err != nil {
211297
return err
212298
}
@@ -230,7 +316,7 @@ func editcmd(opt Options) error {
230316

231317
uri := fmt.Sprintf("/repos/%s/%s/releases/%d", user, repo, id)
232318
resp, err := DoAuthRequest("PATCH", ApiURL()+uri, "application/json",
233-
token, bytes.NewReader(payload))
319+
token, nil, bytes.NewReader(payload))
234320
if err != nil {
235321
return fmt.Errorf("while submitting %v, %v", string(payload), err)
236322
}
@@ -263,7 +349,7 @@ func deletecmd(opt Options) error {
263349
opt.Delete.Tag
264350
vprintln("deleting...")
265351

266-
id, err := IdOfTag(user, repo, tag)
352+
id, err := IdOfTag(user, repo, tag, token)
267353
if err != nil {
268354
return err
269355
}
@@ -286,7 +372,7 @@ func deletecmd(opt Options) error {
286372
}
287373

288374
func httpDelete(url, token string) (*http.Response, error) {
289-
resp, err := DoAuthRequest("DELETE", url, "application/json", token, nil)
375+
resp, err := DoAuthRequest("DELETE", url, "application/json", token, nil, nil)
290376
if err != nil {
291377
return nil, err
292378
}

github-release.go

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@ package main
22

33
import (
44
"fmt"
5-
"github.com/voxelbrain/goptions"
65
"os"
6+
7+
"github.com/voxelbrain/goptions"
78
)
89

910
type Options struct {
@@ -12,6 +13,13 @@ type Options struct {
1213
Quiet bool `goptions:"-q, --quiet, description='Do not print anything, even errors (except if --verbose is specified)'"`
1314

1415
goptions.Verbs
16+
Download struct {
17+
Token string `goptions:"-s, --security-token, description='Github token ($GITHUB_TOKEN if set). required if repo is private.'"`
18+
User string `goptions:"-u, --user, description='Github user (required if $GITHUB_USER not set)'"`
19+
Repo string `goptions:"-r, --repo, description='Github repo (required if $GITHUB_REPO not set)'"`
20+
Tag string `goptions:"-t, --tag, description='Git tag to upload for', obligatory"`
21+
Name string `goptions:"-n, --name, description='Name of the file', obligatory"`
22+
} `goptions:"download"`
1523
Upload struct {
1624
Token string `goptions:"-s, --security-token, description='Github token (required if $GITHUB_TOKEN not set)'"`
1725
User string `goptions:"-u, --user, description='Github user (required if $GITHUB_USER not set)'"`
@@ -47,20 +55,22 @@ type Options struct {
4755
Tag string `goptions:"-t, --tag, obligatory, description='Git tag of release to delete'"`
4856
} `goptions:"delete"`
4957
Info struct {
50-
User string `goptions:"-u, --user, description='Github user (required if $GITHUB_USER not set)'"`
51-
Repo string `goptions:"-r, --repo, description='Github repo (required if $GITHUB_REPO not set)'"`
52-
Tag string `goptions:"-t, --tag, description='Git tag to query (optional)'"`
58+
Token string `goptions:"-s, --security-token, description='Github token ($GITHUB_TOKEN if set). required if repo is private.'"`
59+
User string `goptions:"-u, --user, description='Github user (required if $GITHUB_USER not set)'"`
60+
Repo string `goptions:"-r, --repo, description='Github repo (required if $GITHUB_REPO not set)'"`
61+
Tag string `goptions:"-t, --tag, description='Git tag to query (optional)'"`
5362
} `goptions:"info"`
5463
}
5564

5665
type Command func(Options) error
5766

5867
var commands = map[goptions.Verbs]Command{
59-
"upload": uploadcmd,
60-
"release": releasecmd,
61-
"edit": editcmd,
62-
"delete": deletecmd,
63-
"info": infocmd,
68+
"download": downloadcmd,
69+
"upload": uploadcmd,
70+
"release": releasecmd,
71+
"edit": editcmd,
72+
"delete": deletecmd,
73+
"info": infocmd,
6474
}
6575

6676
var (

releases.go

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@ package main
22

33
import (
44
"fmt"
5-
"github.com/dustin/go-humanize"
65
"strings"
76
"time"
7+
8+
"github.com/dustin/go-humanize"
89
)
910

1011
const (
11-
RELEASE_LIST_URI = "/repos/%s/%s/releases"
12+
RELEASE_LIST_URI = "/repos/%s/%s/releases%s"
1213
)
1314

1415
type Release struct {
@@ -65,18 +66,21 @@ type ReleaseCreate struct {
6566
Prerelease bool `json:"prerelease"`
6667
}
6768

68-
func Releases(user, repo string) ([]Release, error) {
69+
func Releases(user, repo, token string) ([]Release, error) {
70+
if token != "" {
71+
token = "?access_token=" + token
72+
}
6973
var releases []Release
70-
err := GithubGet(fmt.Sprintf(RELEASE_LIST_URI, user, repo), &releases)
74+
err := GithubGet(fmt.Sprintf(RELEASE_LIST_URI, user, repo, token), &releases)
7175
if err != nil {
7276
return nil, err
7377
}
7478

7579
return releases, nil
7680
}
7781

78-
func ReleaseOfTag(user, repo, tag string) (*Release, error) {
79-
releases, err := Releases(user, repo)
82+
func ReleaseOfTag(user, repo, tag, token string) (*Release, error) {
83+
releases, err := Releases(user, repo, token)
8084
if err != nil {
8185
return nil, err
8286
}
@@ -91,8 +95,8 @@ func ReleaseOfTag(user, repo, tag string) (*Release, error) {
9195
}
9296

9397
/* find the release-id of the specified tag */
94-
func IdOfTag(user, repo, tag string) (int, error) {
95-
release, err := ReleaseOfTag(user, repo, tag)
98+
func IdOfTag(user, repo, tag, token string) (int, error) {
99+
release, err := ReleaseOfTag(user, repo, tag, token)
96100
if err != nil {
97101
return 0, err
98102
}

0 commit comments

Comments
 (0)