1// Copyright 2013 The go-github AUTHORS. All rights reserved.
2//
3// Use of this source code is governed by a BSD-style
4// license that can be found in the LICENSE file.
5
6package github
7
8import (
9	"context"
10	"errors"
11	"fmt"
12	"io"
13	"mime"
14	"net/http"
15	"os"
16	"path/filepath"
17	"strings"
18)
19
20// RepositoryRelease represents a GitHub release in a repository.
21type RepositoryRelease struct {
22	ID              *int64         `json:"id,omitempty"`
23	TagName         *string        `json:"tag_name,omitempty"`
24	TargetCommitish *string        `json:"target_commitish,omitempty"`
25	Name            *string        `json:"name,omitempty"`
26	Body            *string        `json:"body,omitempty"`
27	Draft           *bool          `json:"draft,omitempty"`
28	Prerelease      *bool          `json:"prerelease,omitempty"`
29	CreatedAt       *Timestamp     `json:"created_at,omitempty"`
30	PublishedAt     *Timestamp     `json:"published_at,omitempty"`
31	URL             *string        `json:"url,omitempty"`
32	HTMLURL         *string        `json:"html_url,omitempty"`
33	AssetsURL       *string        `json:"assets_url,omitempty"`
34	Assets          []ReleaseAsset `json:"assets,omitempty"`
35	UploadURL       *string        `json:"upload_url,omitempty"`
36	ZipballURL      *string        `json:"zipball_url,omitempty"`
37	TarballURL      *string        `json:"tarball_url,omitempty"`
38	Author          *User          `json:"author,omitempty"`
39	NodeID          *string        `json:"node_id,omitempty"`
40}
41
42func (r RepositoryRelease) String() string {
43	return Stringify(r)
44}
45
46// ReleaseAsset represents a GitHub release asset in a repository.
47type ReleaseAsset struct {
48	ID                 *int64     `json:"id,omitempty"`
49	URL                *string    `json:"url,omitempty"`
50	Name               *string    `json:"name,omitempty"`
51	Label              *string    `json:"label,omitempty"`
52	State              *string    `json:"state,omitempty"`
53	ContentType        *string    `json:"content_type,omitempty"`
54	Size               *int       `json:"size,omitempty"`
55	DownloadCount      *int       `json:"download_count,omitempty"`
56	CreatedAt          *Timestamp `json:"created_at,omitempty"`
57	UpdatedAt          *Timestamp `json:"updated_at,omitempty"`
58	BrowserDownloadURL *string    `json:"browser_download_url,omitempty"`
59	Uploader           *User      `json:"uploader,omitempty"`
60	NodeID             *string    `json:"node_id,omitempty"`
61}
62
63func (r ReleaseAsset) String() string {
64	return Stringify(r)
65}
66
67// ListReleases lists the releases for a repository.
68//
69// GitHub API docs: https://developer.github.com/v3/repos/releases/#list-releases-for-a-repository
70func (s *RepositoriesService) ListReleases(ctx context.Context, owner, repo string, opt *ListOptions) ([]*RepositoryRelease, *Response, error) {
71	u := fmt.Sprintf("repos/%s/%s/releases", owner, repo)
72	u, err := addOptions(u, opt)
73	if err != nil {
74		return nil, nil, err
75	}
76
77	req, err := s.client.NewRequest("GET", u, nil)
78	if err != nil {
79		return nil, nil, err
80	}
81
82	var releases []*RepositoryRelease
83	resp, err := s.client.Do(ctx, req, &releases)
84	if err != nil {
85		return nil, resp, err
86	}
87	return releases, resp, nil
88}
89
90// GetRelease fetches a single release.
91//
92// GitHub API docs: https://developer.github.com/v3/repos/releases/#get-a-single-release
93func (s *RepositoriesService) GetRelease(ctx context.Context, owner, repo string, id int64) (*RepositoryRelease, *Response, error) {
94	u := fmt.Sprintf("repos/%s/%s/releases/%d", owner, repo, id)
95	return s.getSingleRelease(ctx, u)
96}
97
98// GetLatestRelease fetches the latest published release for the repository.
99//
100// GitHub API docs: https://developer.github.com/v3/repos/releases/#get-the-latest-release
101func (s *RepositoriesService) GetLatestRelease(ctx context.Context, owner, repo string) (*RepositoryRelease, *Response, error) {
102	u := fmt.Sprintf("repos/%s/%s/releases/latest", owner, repo)
103	return s.getSingleRelease(ctx, u)
104}
105
106// GetReleaseByTag fetches a release with the specified tag.
107//
108// GitHub API docs: https://developer.github.com/v3/repos/releases/#get-a-release-by-tag-name
109func (s *RepositoriesService) GetReleaseByTag(ctx context.Context, owner, repo, tag string) (*RepositoryRelease, *Response, error) {
110	u := fmt.Sprintf("repos/%s/%s/releases/tags/%s", owner, repo, tag)
111	return s.getSingleRelease(ctx, u)
112}
113
114func (s *RepositoriesService) getSingleRelease(ctx context.Context, url string) (*RepositoryRelease, *Response, error) {
115	req, err := s.client.NewRequest("GET", url, nil)
116	if err != nil {
117		return nil, nil, err
118	}
119
120	release := new(RepositoryRelease)
121	resp, err := s.client.Do(ctx, req, release)
122	if err != nil {
123		return nil, resp, err
124	}
125	return release, resp, nil
126}
127
128// CreateRelease adds a new release for a repository.
129//
130// GitHub API docs: https://developer.github.com/v3/repos/releases/#create-a-release
131func (s *RepositoriesService) CreateRelease(ctx context.Context, owner, repo string, release *RepositoryRelease) (*RepositoryRelease, *Response, error) {
132	u := fmt.Sprintf("repos/%s/%s/releases", owner, repo)
133
134	req, err := s.client.NewRequest("POST", u, release)
135	if err != nil {
136		return nil, nil, err
137	}
138
139	r := new(RepositoryRelease)
140	resp, err := s.client.Do(ctx, req, r)
141	if err != nil {
142		return nil, resp, err
143	}
144	return r, resp, nil
145}
146
147// EditRelease edits a repository release.
148//
149// GitHub API docs: https://developer.github.com/v3/repos/releases/#edit-a-release
150func (s *RepositoriesService) EditRelease(ctx context.Context, owner, repo string, id int64, release *RepositoryRelease) (*RepositoryRelease, *Response, error) {
151	u := fmt.Sprintf("repos/%s/%s/releases/%d", owner, repo, id)
152
153	req, err := s.client.NewRequest("PATCH", u, release)
154	if err != nil {
155		return nil, nil, err
156	}
157
158	r := new(RepositoryRelease)
159	resp, err := s.client.Do(ctx, req, r)
160	if err != nil {
161		return nil, resp, err
162	}
163	return r, resp, nil
164}
165
166// DeleteRelease delete a single release from a repository.
167//
168// GitHub API docs: https://developer.github.com/v3/repos/releases/#delete-a-release
169func (s *RepositoriesService) DeleteRelease(ctx context.Context, owner, repo string, id int64) (*Response, error) {
170	u := fmt.Sprintf("repos/%s/%s/releases/%d", owner, repo, id)
171
172	req, err := s.client.NewRequest("DELETE", u, nil)
173	if err != nil {
174		return nil, err
175	}
176	return s.client.Do(ctx, req, nil)
177}
178
179// ListReleaseAssets lists the release's assets.
180//
181// GitHub API docs: https://developer.github.com/v3/repos/releases/#list-assets-for-a-release
182func (s *RepositoriesService) ListReleaseAssets(ctx context.Context, owner, repo string, id int64, opt *ListOptions) ([]*ReleaseAsset, *Response, error) {
183	u := fmt.Sprintf("repos/%s/%s/releases/%d/assets", owner, repo, id)
184	u, err := addOptions(u, opt)
185	if err != nil {
186		return nil, nil, err
187	}
188
189	req, err := s.client.NewRequest("GET", u, nil)
190	if err != nil {
191		return nil, nil, err
192	}
193
194	var assets []*ReleaseAsset
195	resp, err := s.client.Do(ctx, req, &assets)
196	if err != nil {
197		return nil, resp, err
198	}
199	return assets, resp, nil
200}
201
202// GetReleaseAsset fetches a single release asset.
203//
204// GitHub API docs: https://developer.github.com/v3/repos/releases/#get-a-single-release-asset
205func (s *RepositoriesService) GetReleaseAsset(ctx context.Context, owner, repo string, id int64) (*ReleaseAsset, *Response, error) {
206	u := fmt.Sprintf("repos/%s/%s/releases/assets/%d", owner, repo, id)
207
208	req, err := s.client.NewRequest("GET", u, nil)
209	if err != nil {
210		return nil, nil, err
211	}
212
213	asset := new(ReleaseAsset)
214	resp, err := s.client.Do(ctx, req, asset)
215	if err != nil {
216		return nil, resp, err
217	}
218	return asset, resp, nil
219}
220
221// DownloadReleaseAsset downloads a release asset or returns a redirect URL.
222//
223// DownloadReleaseAsset returns an io.ReadCloser that reads the contents of the
224// specified release asset. It is the caller's responsibility to close the ReadCloser.
225// If a redirect is returned, the redirect URL will be returned as a string instead
226// of the io.ReadCloser. Exactly one of rc and redirectURL will be zero.
227//
228// GitHub API docs: https://developer.github.com/v3/repos/releases/#get-a-single-release-asset
229func (s *RepositoriesService) DownloadReleaseAsset(ctx context.Context, owner, repo string, id int64) (rc io.ReadCloser, redirectURL string, err error) {
230	u := fmt.Sprintf("repos/%s/%s/releases/assets/%d", owner, repo, id)
231
232	req, err := s.client.NewRequest("GET", u, nil)
233	if err != nil {
234		return nil, "", err
235	}
236	req.Header.Set("Accept", defaultMediaType)
237
238	s.client.clientMu.Lock()
239	defer s.client.clientMu.Unlock()
240
241	var loc string
242	saveRedirect := s.client.client.CheckRedirect
243	s.client.client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
244		loc = req.URL.String()
245		return errors.New("disable redirect")
246	}
247	defer func() { s.client.client.CheckRedirect = saveRedirect }()
248
249	req = withContext(ctx, req)
250	resp, err := s.client.client.Do(req)
251	if err != nil {
252		if !strings.Contains(err.Error(), "disable redirect") {
253			return nil, "", err
254		}
255		return nil, loc, nil // Intentionally return no error with valid redirect URL.
256	}
257
258	if err := CheckResponse(resp); err != nil {
259		resp.Body.Close()
260		return nil, "", err
261	}
262
263	return resp.Body, "", nil
264}
265
266// EditReleaseAsset edits a repository release asset.
267//
268// GitHub API docs: https://developer.github.com/v3/repos/releases/#edit-a-release-asset
269func (s *RepositoriesService) EditReleaseAsset(ctx context.Context, owner, repo string, id int64, release *ReleaseAsset) (*ReleaseAsset, *Response, error) {
270	u := fmt.Sprintf("repos/%s/%s/releases/assets/%d", owner, repo, id)
271
272	req, err := s.client.NewRequest("PATCH", u, release)
273	if err != nil {
274		return nil, nil, err
275	}
276
277	asset := new(ReleaseAsset)
278	resp, err := s.client.Do(ctx, req, asset)
279	if err != nil {
280		return nil, resp, err
281	}
282	return asset, resp, nil
283}
284
285// DeleteReleaseAsset delete a single release asset from a repository.
286//
287// GitHub API docs: https://developer.github.com/v3/repos/releases/#delete-a-release-asset
288func (s *RepositoriesService) DeleteReleaseAsset(ctx context.Context, owner, repo string, id int64) (*Response, error) {
289	u := fmt.Sprintf("repos/%s/%s/releases/assets/%d", owner, repo, id)
290
291	req, err := s.client.NewRequest("DELETE", u, nil)
292	if err != nil {
293		return nil, err
294	}
295	return s.client.Do(ctx, req, nil)
296}
297
298// UploadReleaseAsset creates an asset by uploading a file into a release repository.
299// To upload assets that cannot be represented by an os.File, call NewUploadRequest directly.
300//
301// GitHub API docs: https://developer.github.com/v3/repos/releases/#upload-a-release-asset
302func (s *RepositoriesService) UploadReleaseAsset(ctx context.Context, owner, repo string, id int64, opt *UploadOptions, file *os.File) (*ReleaseAsset, *Response, error) {
303	u := fmt.Sprintf("repos/%s/%s/releases/%d/assets", owner, repo, id)
304	u, err := addOptions(u, opt)
305	if err != nil {
306		return nil, nil, err
307	}
308
309	stat, err := file.Stat()
310	if err != nil {
311		return nil, nil, err
312	}
313	if stat.IsDir() {
314		return nil, nil, errors.New("the asset to upload can't be a directory")
315	}
316
317	mediaType := mime.TypeByExtension(filepath.Ext(file.Name()))
318	req, err := s.client.NewUploadRequest(u, file, stat.Size(), mediaType)
319	if err != nil {
320		return nil, nil, err
321	}
322
323	asset := new(ReleaseAsset)
324	resp, err := s.client.Do(ctx, req, asset)
325	if err != nil {
326		return nil, resp, err
327	}
328	return asset, resp, nil
329}
330