Skip to content

Commit

Permalink
Move retrying out of UploadWithSHA1 since it can't seek
Browse files Browse the repository at this point in the history
The B2 API is explicit about having to retry on transient upload errors,
but obviously that's not something we can do with a plain io.Reader.

Moved the retry code in Upload, where we have ways of seeking since we
have to do that already for the SHA1 calculation.
  • Loading branch information
FiloSottile committed Oct 17, 2016
1 parent ff2f66f commit 970cc75
Show file tree
Hide file tree
Showing 2 changed files with 57 additions and 51 deletions.
3 changes: 2 additions & 1 deletion b2.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
// bytes.Buffer or a io.ReadSeeker.
//
// If you know the SHA1 and the length of the file in advance, you can use
// (*Bucket).UploadWithSHA1.
// (*Bucket).UploadWithSHA1 but you are responsible for retrying on
// transient errors.
//
// Downloading
//
Expand Down
105 changes: 55 additions & 50 deletions upload.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"encoding/hex"
"encoding/json"
"io"
"io/ioutil"
"net/http"
"net/url"
"strconv"
Expand All @@ -25,32 +26,40 @@ import (
//
// If a file by this name already exist, a new version will be created.
func (b *Bucket) Upload(r io.Reader, name, mimeType string) (*FileInfo, error) {
var body io.Reader
var length int
h := sha1.New()

var body io.ReadSeeker
switch r := r.(type) {
case *bytes.Buffer:
h.Write(r.Bytes())
body, length = r, r.Len()
defer r.Reset() // we are expected to consume it
body = bytes.NewReader(r.Bytes())
case io.ReadSeeker:
n, err := io.Copy(h, r)
body = r
default:
b, err := ioutil.ReadAll(r)
if err != nil {
return nil, err
}
if _, err := r.Seek(0, io.SeekStart); err != nil {
body = bytes.NewReader(b)
}

h := sha1.New()
length, err := io.Copy(h, body)
if err != nil {
return nil, err
}
sha1Sum := hex.EncodeToString(h.Sum(nil))

var fi *FileInfo
for i := 0; i < 5; i++ {
if _, err = body.Seek(0, io.SeekStart); err != nil {
return nil, err
}
body, length = r, int(n)
default:
buf := &bytes.Buffer{}
if _, err := buf.ReadFrom(io.TeeReader(r, h)); err != nil {
return nil, err

fi, err = b.UploadWithSHA1(body, name, mimeType, sha1Sum, int(length))
if err == nil {
break
}
body, length = buf, buf.Len()
}

return b.UploadWithSHA1(body, name, mimeType, hex.EncodeToString(h.Sum(nil)), length)
return fi, err
}

type uploadURL struct {
Expand Down Expand Up @@ -86,49 +95,45 @@ func (b *Bucket) putUploadURL(u *uploadURL) {
}

// UploadWithSHA1 is like Upload, but allows the caller to specify previously
// known SHA1 and length of the file. It never does any buffering.
// known SHA1 and length of the file. It never does any buffering, nor does it
// retry on failure.
//
// Note that retrying on most upload failures is mandatory by the B2 API
// documentation, and not just error condition handling.
//
// sha1sum should be the hex encoding of the SHA1 sum of what will be read from r.
// sha1Sum should be the hex encoding of the SHA1 sum of what will be read from r.
//
// This is an advanced interface, common clients should use Upload, and consider
// This is an advanced interface, most clients should use Upload, and consider
// passing it a bytes.Buffer or io.ReadSeeker to avoid buffering.
func (b *Bucket) UploadWithSHA1(r io.Reader, name, mimeType, sha1sum string, length int) (*FileInfo, error) {
var res *http.Response
failedTries := 0
for {
uurl, err := b.getUploadURL()
if err != nil {
return nil, err
}
func (b *Bucket) UploadWithSHA1(r io.Reader, name, mimeType, sha1Sum string, length int) (*FileInfo, error) {
uurl, err := b.getUploadURL()
if err != nil {
return nil, err
}

req, err := http.NewRequest("POST", uurl.UploadURL, r)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", uurl.AuthorizationToken)
req.Header.Set("X-Bz-File-Name", url.QueryEscape(name))
req.Header.Set("Content-Type", mimeType)
req.Header.Set("Content-Length", strconv.Itoa(length))
req.Header.Set("X-Bz-Content-Sha1", sha1sum)
req, err := http.NewRequest("POST", uurl.UploadURL, r)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", uurl.AuthorizationToken)
req.Header.Set("X-Bz-File-Name", url.QueryEscape(name))
req.Header.Set("Content-Type", mimeType)
req.Header.Set("Content-Length", strconv.Itoa(length))
req.Header.Set("X-Bz-Content-Sha1", sha1Sum)

res, err = b.c.hc.Do(req)
if err == nil && res.StatusCode != 200 {
err = parseB2Error(res)
}
if err == nil {
defer b.putUploadURL(uurl)
break
} else if failedTries < 4 {
// Upload URLs are allowed to fail a few times.
failedTries++
} else {
return nil, err
}
res, err := b.c.hc.Do(req)
if err != nil {
return nil, err
}
if res.StatusCode != 200 {
return nil, parseB2Error(res)
}
defer drainAndClose(res.Body)

fi := fileInfoObj{}
if err := json.NewDecoder(res.Body).Decode(&fi); err != nil {
if err = json.NewDecoder(res.Body).Decode(&fi); err != nil {
return nil, err
}
b.putUploadURL(uurl)
return fi.makeFileInfo(), nil
}

0 comments on commit 970cc75

Please sign in to comment.