Skip to content

Commit

Permalink
Added single retry for file upload and download methods when a non-fa…
Browse files Browse the repository at this point in the history
…tal (auth) error is returned.
  • Loading branch information
kothar committed Dec 30, 2015
1 parent 218f129 commit 2ca1dfd
Show file tree
Hide file tree
Showing 6 changed files with 208 additions and 72 deletions.
6 changes: 6 additions & 0 deletions README.md
Expand Up @@ -35,6 +35,12 @@ metadata := make(map[string]string)
file, _ := bucket.UploadFile(name, metadata, reader)
~~~

All API methods except `B2.AuthorizeAccount` and `Bucket.UploadHashedFile` will
retry once if authorization fails, which allows the operation to proceed if the current
authorization token has expired.

To disable this behaviour, set `B2.NoRetry` to `true`

## b2 command line client

A test applicaiton has been implemented using this package, and can be found in the /b2 directory.
Expand Down
9 changes: 6 additions & 3 deletions backblaze.go
Expand Up @@ -42,7 +42,7 @@ type B2 struct {
apiEndpoint string
downloadURL string
authorizationToken string
httpClient *http.Client
httpClient http.Client
}

// B2Error encapsulates an error message returned by the B2 API.
Expand Down Expand Up @@ -81,7 +81,6 @@ type authorizeAccountResponse struct {
func NewB2(creds Credentials) (*B2, error) {
c := &B2{
Credentials: creds,
httpClient: &http.Client{},
}

// Authorize account
Expand Down Expand Up @@ -200,7 +199,7 @@ func (c *B2) parseResponse(resp *http.Response, result interface{}) error {
}

if c.Debug {
println("Response: " + string(body))
log.Printf("Response: %s", body)
}

// Check response code
Expand Down Expand Up @@ -233,6 +232,10 @@ func (c *B2) apiRequest(apiMethod string, request interface{}, response interfac
if err != nil {
return err
}
if c.Debug {
log.Println("----")
log.Printf("apiRequest: %s %s", apiMethod, body)
}

// Check if we have a valid API endpoint
if c.apiEndpoint == "" {
Expand Down
58 changes: 4 additions & 54 deletions backblaze_test.go
Expand Up @@ -30,15 +30,13 @@ func prepareResponses(responses []response) (*http.Client, *httptest.Server) {

w.WriteHeader(next.code)
w.Header().Set("Content-Type", "application/json")
fmt.Fprintln(w, toJSON(next.body))
fmt.Fprint(w, toJSON(next.body))
}))

// Make a transport that reroutes all traffic to the example server
transport := &http.Transport{
Proxy: func(req *http.Request) (*url.URL, error) {
u, err := url.Parse(server.URL + req.URL.Path)
fmt.Printf("Request URL: %s\n", req.URL)
return u, err
return url.Parse(server.URL + req.URL.Path)
},
}

Expand Down Expand Up @@ -86,7 +84,7 @@ func TestAuth(T *testing.T) {
},
Debug: true,
host: server.URL,
httpClient: client,
httpClient: *client,
}

if err := b2.AuthorizeAccount(); err != nil {
Expand All @@ -106,54 +104,6 @@ func TestAuth(T *testing.T) {
}
}

func TestListBuckets(T *testing.T) {

accountID := "test"
bucketID := "bucketid"

client, server := prepareResponses([]response{
{200, authorizeAccountResponse{
AccountID: accountID,
APIEndpoint: "http://api.url",
AuthorizationToken: "testToken",
DownloadURL: "http://download.url",
}},
{200, listBucketsResponse{
Buckets: []*Bucket{
&Bucket{
ID: bucketID,
AccountID: accountID,
Name: "testbucket",
BucketType: AllPrivate,
},
},
}},
})
defer server.Close()

b2 := &B2{
Credentials: Credentials{
AccountID: accountID,
ApplicationKey: "test",
},
Debug: true,
httpClient: client,
host: server.URL,
}

buckets, err := b2.ListBuckets()
if err != nil {
T.Fatal(err)
}

if len(buckets) != 1 {
T.Errorf("Expected 1 bucket, received %d", len(buckets))
}
if buckets[0].ID != bucketID {
T.Errorf("Bucket ID does not match: expected %q, saw %q", bucketID, buckets[0].ID)
}
}

func TestReAuth(T *testing.T) {

accountID := "test"
Expand Down Expand Up @@ -197,7 +147,7 @@ func TestReAuth(T *testing.T) {
ApplicationKey: "test",
},
Debug: true,
httpClient: client,
httpClient: *client,
host: server.URL,
}

Expand Down
53 changes: 53 additions & 0 deletions buckets_test.go
@@ -0,0 +1,53 @@
package backblaze

import (
"testing"
)

func TestListBuckets(T *testing.T) {

accountID := "test"
bucketID := "bucketid"

client, server := prepareResponses([]response{
{200, authorizeAccountResponse{
AccountID: accountID,
APIEndpoint: "http://api.url",
AuthorizationToken: "testToken",
DownloadURL: "http://download.url",
}},
{200, listBucketsResponse{
Buckets: []*Bucket{
&Bucket{
ID: bucketID,
AccountID: accountID,
Name: "testbucket",
BucketType: AllPrivate,
},
},
}},
})
defer server.Close()

b2 := &B2{
Credentials: Credentials{
AccountID: accountID,
ApplicationKey: "test",
},
Debug: true,
httpClient: *client,
host: server.URL,
}

buckets, err := b2.ListBuckets()
if err != nil {
T.Fatal(err)
}

if len(buckets) != 1 {
T.Errorf("Expected 1 bucket, received %d", len(buckets))
}
if buckets[0].ID != bucketID {
T.Errorf("Bucket ID does not match: expected %q, saw %q", bucketID, buckets[0].ID)
}
}
91 changes: 76 additions & 15 deletions files.go
Expand Up @@ -139,10 +139,22 @@ func (b *Bucket) UploadFile(name string, meta map[string]string, file io.Reader)
}

sha1Hash := hex.EncodeToString(hash.Sum(nil))
return b.UploadHashedFile(name, meta, reader, sha1Hash, contentLength)
f, err := b.UploadHashedFile(name, meta, reader, sha1Hash, contentLength)

// Retry after non-fatal errors
if b2err, ok := err.(*B2Error); ok {
if !b2err.IsFatal() && !b.b2.NoRetry {
f, err = b.UploadHashedFile(name, meta, reader, sha1Hash, contentLength)
}
}
return f, err
}

// UploadHashedFile Uploads a file to B2, returning its unique file ID.
//
// This method will not retry if the upload fails, as the reader may have consumed
// some bytes. If the error type is B2Error and IsFatal returns false, you may retry the
// upload and expect it to succeed eventually.
func (b *Bucket) UploadHashedFile(name string, meta map[string]string, file io.Reader, sha1Hash string, contentLength int64) (*File, error) {

_, err := b.getUploadURL()
Expand Down Expand Up @@ -211,44 +223,87 @@ func (b *Bucket) GetFileInfo(fileID string) (*File, error) {
}

// DownloadFileByID downloads a file from B2 using its unique ID
func (b *B2) DownloadFileByID(fileID string) (*File, io.ReadCloser, error) {
func (c *B2) DownloadFileByID(fileID string) (*File, io.ReadCloser, error) {

if c.apiEndpoint == "" {
if err := c.AuthorizeAccount(); err != nil {
return nil, nil, err
}
}

request := &fileRequest{
ID: fileID,
}
body, err := json.Marshal(request)
requestBody, err := json.Marshal(request)
if err != nil {
return nil, nil, err
}

resp, err := b.post(b.apiEndpoint+v1+"b2_download_file_by_id", bytes.NewReader(body))
resp, err := c.post(c.apiEndpoint+v1+"b2_download_file_by_id", bytes.NewReader(requestBody))
if err != nil {
return nil, nil, err
}

return b.downloadFile(resp)
f, body, err := c.downloadFile(resp)

// Retry after non-fatal errors
if b2err, ok := err.(*B2Error); ok {
if !b2err.IsFatal() && !c.NoRetry {
resp, err = c.post(c.apiEndpoint+v1+"b2_download_file_by_id", bytes.NewReader(requestBody))
if err != nil {
return nil, nil, err
}
f, body, err = c.downloadFile(resp)
}
}
return f, body, err
}

// FileURL returns a URL which may be used to dowload the laterst version of a file.
// This will only work for public URLs unless the correct authorization header is provided.
func (b *Bucket) FileURL(fileName string) string {
return b.b2.downloadURL + "/file/" + b.Name + "/" + fileName
// FileURL returns a URL which may be used to dowload the latest version of a file.
// This returned URL will only work for public buckets unless the correct authorization header is provided.
func (b *Bucket) FileURL(fileName string) (string, error) {
if b.b2.downloadURL == "" {
if err := b.b2.AuthorizeAccount(); err != nil {
return "", err
}
}
return b.b2.downloadURL + "/file/" + b.Name + "/" + fileName, nil
}

// DownloadFileByName Downloads one file by providing the name of the bucket and the name of the
// file.
func (b *Bucket) DownloadFileByName(fileName string) (*File, io.ReadCloser, error) {

url := b.FileURL(fileName)
// Locate the file
url, err := b.FileURL(fileName)
if err != nil {
return nil, nil, err
}

// Make the download request
resp, err := b.b2.get(url)
if err != nil {
return nil, nil, err
}

return b.b2.downloadFile(resp)
// Handle the response
f, body, err := b.b2.downloadFile(resp)

// Retry after non-fatal errors
if b2err, ok := err.(*B2Error); ok {
if !b2err.IsFatal() && !b.b2.NoRetry {
resp, err = b.b2.get(url)
if err != nil {
return nil, nil, err
}

f, body, err = b.b2.downloadFile(resp)
}
}
return f, body, err
}

func (b *B2) downloadFile(resp *http.Response) (*File, io.ReadCloser, error) {
func (c *B2) downloadFile(resp *http.Response) (*File, io.ReadCloser, error) {
success := false
defer func() {
if !success {
Expand All @@ -258,16 +313,22 @@ func (b *B2) downloadFile(resp *http.Response) (*File, io.ReadCloser, error) {

switch resp.StatusCode {
case 200:
case 401:
c.authorizationToken = ""
return nil, nil, &B2Error{
Code: "UNAUTHORIZED",
Message: "The account ID is wrong, the account does not have B2 enabled, or the application key is not valid",
Status: resp.StatusCode,
}
default:
body, err := ioutil.ReadAll(resp.Body)

if err != nil {
return nil, nil, err
}

if err := b.parseError(body); err != nil {
if err := c.parseError(body); err != nil {
return nil, nil, err
}

return nil, nil, fmt.Errorf("Unrecognised status code: %d", resp.StatusCode)
}

Expand Down

0 comments on commit 2ca1dfd

Please sign in to comment.