-
Notifications
You must be signed in to change notification settings - Fork 19
/
Copy pathpkgcloud.go
243 lines (208 loc) · 6.37 KB
/
pkgcloud.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
// Package pkgcloud allows you to talk to the packagecloud API.
// See https://packagecloud.io/docs/api
package pkgcloud
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"os"
"regexp"
"strconv"
"github.com/mlafeldt/pkgcloud/upload"
"github.com/peterhellberg/link"
pb "gopkg.in/cheggaaa/pb.v1"
)
//go:generate bash -c "./gendistros.py supportedDistros | gofmt > distros.go"
// ServiceURL is the URL of packagecloud's API.
const ServiceURL = "https://packagecloud.io/api/v1"
const UserAgent = "pkgcloud Go client"
// A Client is a packagecloud client.
type Client struct {
token string
progressBar bool
}
// NewClient creates a packagecloud client. API requests are authenticated
// using an API token. If no token is passed, it will be read from the
// PACKAGECLOUD_TOKEN environment variable.
func NewClient(token string) (*Client, error) {
if token == "" {
token = os.Getenv("PACKAGECLOUD_TOKEN")
if token == "" {
return nil, errors.New("PACKAGECLOUD_TOKEN unset")
}
}
return &Client{token, false}, nil
}
// Print a progress bar of paginated API requests when show is set to true
func (c *Client) ShowProgress(show bool) {
c.progressBar = show
}
// decodeResponse checks http status code and tries to decode json body
func decodeResponse(resp *http.Response, respJson interface{}) error {
switch resp.StatusCode {
case http.StatusOK, http.StatusCreated:
return json.NewDecoder(resp.Body).Decode(respJson)
case http.StatusUnauthorized, http.StatusNotFound:
return fmt.Errorf("HTTP status: %s", http.StatusText(resp.StatusCode))
case 422: // Unprocessable Entity
var v map[string][]string
if err := json.NewDecoder(resp.Body).Decode(&v); err != nil {
return err
}
for _, messages := range v {
for _, msg := range messages {
// Only return the very first error message
return errors.New(msg)
}
break
}
return fmt.Errorf("invalid HTTP body")
default:
return fmt.Errorf("unexpected HTTP status: %d", resp.StatusCode)
}
}
// CreatePackage pushes a new package to packagecloud.
func (c Client) CreatePackage(repo, distro, pkgFile string) error {
var extraParams map[string]string
if distro != "" {
distID, ok := supportedDistros[distro]
if !ok {
return fmt.Errorf("invalid distro name: %s", distro)
}
extraParams = map[string]string{
"package[distro_version_id]": strconv.Itoa(distID),
}
}
endpoint := fmt.Sprintf("%s/repos/%s/packages.json", ServiceURL, repo)
request, err := upload.NewRequest(endpoint, extraParams, "package[package_file]", pkgFile)
if err != nil {
return err
}
request.SetBasicAuth(c.token, "")
request.Header.Add("User-Agent", UserAgent)
client := &http.Client{}
resp, err := client.Do(request)
if err != nil {
return err
}
defer resp.Body.Close()
return decodeResponse(resp, &struct{}{})
}
type Package struct {
Name string `json:"name"`
Filename string `json:"filename"`
DistroVersion string `json:"distro_version"`
Version string `json:"version"`
Release string `json:"release"`
Type string `json:"type"`
PackageUrl string `json:"package_url"`
PackageHtmlUrl string `json:"package_html_url"`
}
// All list all packages in repository
func (c Client) All(repo string) ([]Package, error) {
endpoint := fmt.Sprintf("%s/repos/%s/packages.json", ServiceURL, repo)
return c.paginatedRequest(http.MethodGet, endpoint)
}
// Destroy removes package from repository.
//
// repo should be full path to repository
// (e.g. youruser/repository/ubuntu/xenial).
func (c Client) Destroy(repo, packageFilename string) error {
endpoint := fmt.Sprintf("%s/repos/%s/%s", ServiceURL, repo, packageFilename)
_, err := c.apiRequest(http.MethodDelete, endpoint, &struct{}{})
return err
}
// Search searches packages from repository.
// repo should be full path to repository
// (e.g. youruser/repository/ubuntu/xenial).
// q: The query string to search for package filename. If empty string is passed, all packages are returned
// filter: Search by package type (RPMs, Debs, DSCs, Gem, Python) - Ignored when dist != ""
// dist: The name of the distribution the package is in. (i.e. ubuntu, el/6) - Overrides filter.
// perPage: The number of packages to return from the results set. If nothing passed, default is 30
func (c Client) Search(repo, q, filter, dist string, perPage int) ([]Package, error) {
endpoint := fmt.Sprintf("%s/repos/%s/search.json?q=%s&filter=%s&dist=%s&per_page=%d", ServiceURL, repo, q, filter, dist, perPage)
return c.paginatedRequest(http.MethodGet, endpoint)
}
func (c Client) paginatedRequest(method string, endpoint string) ([]Package, error) {
var (
allPackages []Package
getLastPage bool
)
newPage := make(chan bool)
total := make(chan int)
if c.progressBar {
getLastPage = true
progBar := pb.New(0)
progBar.SetMaxWidth(100)
progBar.Start()
defer func() {
progBar.Increment()
progBar.Finish()
}()
go func() {
for {
select {
case np := <-newPage:
if !np {
return
}
progBar.Increment()
case t := <-total:
progBar.SetTotal(t)
}
}
}()
}
for {
var pkgs []Package
var group link.Group
group, err := c.apiRequest(method, endpoint, &pkgs)
if err != nil {
return nil, err
}
allPackages = append(allPackages, pkgs...)
next, found := group["next"]
newPage <- found
if !found {
break
}
endpoint = next.URI
if !getLastPage {
continue
}
if last, found := group["last"]; found {
// expect format similar to https://packagecloud.io/api/v1/repos/foo/bar/search.json?dist=&filter=&page=57&per_page=10&q=querystring
// or sometimes page=<number> is the end of the string
re := regexp.MustCompile(`page=(\d+)($|&)`)
pages, err := strconv.Atoi(re.FindStringSubmatch(last.URI)[1])
if err != nil {
continue
}
getLastPage = false
total <- pages
}
}
return allPackages, nil
}
func (c Client) apiRequest(method string, endpoint string, decodedResp interface{}) (link.Group, error) {
req, err := http.NewRequest(method, endpoint, nil)
if err != nil {
return nil, err
}
req.SetBasicAuth(c.token, "")
req.Header.Add("User-Agent", UserAgent)
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
err = decodeResponse(resp, decodedResp)
if err != nil {
return nil, err
}
// API are paginated, with next page in the response header, as "Link"
// element
return link.ParseResponse(resp), nil
}