forked from FiloSottile/b2
-
Notifications
You must be signed in to change notification settings - Fork 2
/
b2.go
391 lines (347 loc) · 10.5 KB
/
b2.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
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
// Package b2 provides an idiomatic, efficient interface to Backblaze B2 Cloud Storage.
//
// Uploading
//
// Uploads to B2 require a SHA1 header, so the hash of the file must be known
// before the upload starts. The (*Bucket).Upload API tries its best not to
// buffer the entire file in memory, and it will avoid it if passed either a
// bytes.Buffer or a io.ReadSeeker.
//
// If you know the SHA1 and the length of the file in advance, you can use
// (*Bucket).UploadWithSHA1 but you are responsible for retrying on
// transient errors.
//
// Downloading
//
// Downloads from B2 are simple GETs, so if you want more control than the
// standard functions you can build your own URL according to the API docs.
// All the information you need is returned by Client.LoginInfo().
//
// Hidden files and versions
//
// There is no first-class support for versions in this library, but most
// behaviors are transparently exposed. Upload can be used multiple times
// with the same name, ListFiles will only return the latest version of
// non-hidden files, and ListFilesVersions will return all files and versions.
//
// Unsupported APIs
//
// Large files (b2_*_large_file, b2_*_part), b2_get_download_authorization,
// b2_hide_file, b2_update_bucket.
//
// Debug mode
//
// If the B2_DEBUG environment variable is set to 1, all API calls will be
// logged. On Go 1.7 and later, it will also log when new (non-reused)
// connections are established.
package b2
import (
"bytes"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"log"
"net/http"
"net/url"
"os"
"sync"
"sync/atomic"
)
// Error is the decoded B2 JSON error return value. It's not the only type of
// error returned by this package, and it is mostly returned wrapped in a
// url.Error. Use UnwrapError to access it.
type Error struct {
Code string
Message string
Status int
}
func (e *Error) Error() string {
return fmt.Sprintf("b2 remote error [%s]: %s", e.Code, e.Message)
}
// UnwrapError attempts to extract the Error that caused err. If there is no
// Error object to unwrap, ok is false and err is nil. That does not mean that
// the original error should be ignored.
func UnwrapError(err error) (b2Err *Error, ok bool) {
if e, ok := err.(*url.Error); ok {
err = e.Err
}
if e, ok := err.(*Error); ok {
return e, true
}
return nil, false
}
const (
defaultAPIURL = "https://api.backblaze.com"
apiPath = "/b2api/v1/"
)
// LoginInfo holds the information obtained upon login, which are sufficient
// to interact with the API directly.
type LoginInfo struct {
AccountID string
ApiURL string
// DownloadURL is the base URL for file downloads. It is supposed
// to never change for the same account.
DownloadURL string
// AuthorizationToken is the value to pass in the Authorization
// header of all private calls. This is valid for at most 24 hours.
AuthorizationToken string
}
// LoginInfo returns the LoginInfo object currently in use. If refresh is
// true, it obtains a new one before returning.
//
// Note that once you obtain this there is no guarantee on its freshness,
// and it will eventually expire.
func (c *Client) LoginInfo(refresh bool) (*LoginInfo, error) {
if refresh {
if err := c.login(nil); err != nil {
return nil, err
}
}
return c.loginInfo.Load().(*LoginInfo), nil
}
// A Client is an authenticated API client. It is safe for concurrent use and should
// be reused to take advantage of connection and URL reuse.
//
// The Client handles refreshing authorization tokens transparently.
type Client struct {
accountID, applicationKey string
loginInfo atomic.Value // *LoginInfo
// loginMu is held to avoid multiple logins in flight at the same time
loginMu sync.Mutex
hc *http.Client
}
// NewClient calls b2_authorize_account and returns an authenticated Client.
// httpClient can be nil, in which case http.DefaultClient will be used.
func NewClient(accountID, applicationKey string, httpClient *http.Client) (*Client, error) {
if httpClient == nil {
httpClient = http.DefaultClient
}
c := &Client{
accountID: accountID,
applicationKey: applicationKey,
hc: httpClient,
}
if err := c.login(nil); err != nil {
return nil, err
}
c.hc.Transport = &transport{t: c.hc.Transport, c: c}
return c, nil
}
func (c *Client) login(failedRes *http.Response) error {
c.loginMu.Lock()
defer c.loginMu.Unlock()
// check under the lock that another login didn't beat us
if failedRes != nil && c.loginInfo.Load() != nil {
current := c.loginInfo.Load().(*LoginInfo).AuthorizationToken
failed := failedRes.Request.Header.Get("Authorization")
if current != failed {
debugf("another login call succeeded concurrently")
return nil
}
}
r, err := http.NewRequest("GET", defaultAPIURL+apiPath+"b2_authorize_account", nil)
if err != nil {
return err
}
r.Header.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString(
[]byte(c.accountID+":"+c.applicationKey)))
res, err := c.hc.Do(r)
if err != nil {
return err
}
defer drainAndClose(res.Body)
debugf("login: %d", res.StatusCode)
if res.StatusCode != 200 {
b2Err := &Error{}
if err := json.NewDecoder(res.Body).Decode(b2Err); err != nil {
return fmt.Errorf("unknown error during b2_authorize_account: %d", res.StatusCode)
}
return b2Err
}
li := &LoginInfo{}
if err := json.NewDecoder(res.Body).Decode(li); err != nil {
return fmt.Errorf("failed to decode b2_authorize_account answer: %s", err)
}
c.loginInfo.Store(li)
return nil
}
// transport is a wrapper providing authentication, tracing and error handling.
type transport struct {
t http.RoundTripper
c *Client
}
// requestExtFunc is implemented in the go1.7 file, to add httptrace
var requestExtFunc func(*http.Request) *http.Request
func (t *transport) RoundTrip(req *http.Request) (res *http.Response, err error) {
if req.Header.Get("Authorization") == "" {
req.Header.Set("Authorization", t.c.loginInfo.Load().(*LoginInfo).AuthorizationToken)
}
if requestExtFunc != nil {
req = requestExtFunc(req)
}
if t.t == nil {
res, err = http.DefaultTransport.RoundTrip(req)
} else {
res, err = t.t.RoundTrip(req)
}
if err == nil && res.StatusCode != 200 {
return nil, parseB2Error(res)
}
return res, err
}
var debug = os.Getenv("B2_DEBUG") == "1"
func debugf(format string, a ...interface{}) {
if debug {
log.Printf("[b2] "+format, a...)
}
}
func (c *Client) doRequest(endpoint string, params map[string]interface{}) (*http.Response, error) {
body, err := json.Marshal(params)
if err != nil {
return nil, err
}
// Reduce debug log noise
// delete(params, "accountID")
delete(params, "bucketID")
apiURL := c.loginInfo.Load().(*LoginInfo).ApiURL
res, err := c.hc.Post(apiURL+apiPath+endpoint, "application/json", bytes.NewBuffer(body))
if e, ok := UnwrapError(err); ok && e.Status == http.StatusUnauthorized {
if err = c.login(res); err == nil {
res, err = c.hc.Post(apiURL+apiPath+endpoint, "application/json", bytes.NewBuffer(body))
}
}
if err != nil {
debugf("%s (%v): %v", endpoint, params, err)
} else {
debugf("%s (%v)", endpoint, params)
}
return res, err
}
func parseB2Error(res *http.Response) error {
defer drainAndClose(res.Body)
b2Err := &Error{}
if err := json.NewDecoder(res.Body).Decode(b2Err); err != nil {
return fmt.Errorf("unknown error during b2_authorize_account: %d", res.StatusCode)
}
return b2Err
}
// drainAndClose will make an attempt at flushing and closing the body so that the
// underlying connection can be reused. It will not read more than 10KB.
func drainAndClose(body io.ReadCloser) {
io.CopyN(ioutil.Discard, body, 10*1024)
body.Close()
}
// A Bucket is bound to the Client that created it. It is safe for concurrent use and
// should be reused to take advantage of connection and URL reuse.
type Bucket struct {
ID string
c *Client
uploadURLs []*uploadURL
uploadURLsMu sync.Mutex
}
// BucketInfo is an extended Bucket object with metadata.
type BucketInfo struct {
Bucket
Name string
Type string
}
// BucketByID returns a Bucket bound to the Client. It does NOT check that the
// bucket actually exists, or perform any network operation.
func (c *Client) BucketByID(id string) *Bucket {
return &Bucket{ID: id, c: c}
}
// BucketByName returns the Bucket with the given name. If such a bucket is not
// found and createIfNotExists is true, CreateBucket is called with allPublic set
// to false. Otherwise, an error is returned.
func (c *Client) BucketByName(name string, createIfNotExists bool) (*BucketInfo, error) {
bs, err := c.Buckets(name)
if err != nil {
return nil, err
}
for _, b := range bs {
if b.Name == name {
return b, nil
}
}
if !createIfNotExists {
return nil, errors.New("bucket not found: " + name)
}
return c.CreateBucket(name, false)
}
// Buckets returns a list of buckets sorted by name.
func (c *Client) Buckets(name string) ([]*BucketInfo, error) {
res, err := c.doRequest("b2_list_buckets", map[string]interface{}{
"accountId": c.loginInfo.Load().(*LoginInfo).AccountID,
"bucketName": name,
})
if err != nil {
return nil, err
}
defer drainAndClose(res.Body)
var buckets struct {
Buckets []struct {
BucketID, BucketName, BucketType string
}
}
if err := json.NewDecoder(res.Body).Decode(&buckets); err != nil {
return nil, err
}
var r []*BucketInfo
for _, b := range buckets.Buckets {
r = append(r, &BucketInfo{
Bucket: Bucket{
ID: b.BucketID,
c: c,
},
Name: b.BucketName,
Type: b.BucketType,
})
}
return r, nil
}
// CreateBucket creates a bucket with b2_create_bucket. If allPublic is true,
// files in this bucket can be downloaded by anybody.
func (c *Client) CreateBucket(name string, allPublic bool) (*BucketInfo, error) {
bucketType := "allPrivate"
if allPublic {
bucketType = "allPublic"
}
res, err := c.doRequest("b2_create_bucket", map[string]interface{}{
"accountId": c.loginInfo.Load().(*LoginInfo).AccountID,
"bucketName": name,
"bucketType": bucketType,
})
if err != nil {
return nil, err
}
defer drainAndClose(res.Body)
var bucket struct {
BucketID string
}
if err := json.NewDecoder(res.Body).Decode(&bucket); err != nil {
return nil, err
}
return &BucketInfo{
Bucket: Bucket{
c: c, ID: bucket.BucketID,
},
Name: name,
Type: bucketType,
}, nil
}
// Delete calls b2_delete_bucket. After this call succeeds the Bucket object
// becomes invalid and any other calls will fail.
func (b *Bucket) Delete() error {
res, err := b.c.doRequest("b2_delete_bucket", map[string]interface{}{
"accountId": b.c.loginInfo.Load().(*LoginInfo).AccountID,
"bucketId": b.ID,
})
if err != nil {
return err
}
drainAndClose(res.Body)
return nil
}