forked from FiloSottile/b2
-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathupload.go
148 lines (135 loc) · 4.1 KB
/
upload.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
package b2
import (
"bytes"
"crypto/sha1"
"encoding/hex"
"encoding/json"
"io"
"io/ioutil"
"net/http"
"net/url"
)
// Upload uploads a file to a B2 bucket. If mimeType is "", "b2/x-auto" will be used.
//
// Concurrent calls to Upload will use separate upload URLs, but consequent ones
// will attempt to reuse previously obtained ones to save b2_get_upload_url calls.
// Upload URL failures are handled transparently.
//
// Since the B2 API requires a SHA1 header, normally the file will first be read
// entirely into a memory buffer. Two cases avoid the memory copy: if r is a
// bytes.Buffer, the SHA1 will be computed in place; otherwise, if r implements io.Seeker
// (like *os.File and *bytes.Reader), the file will be read twice, once to compute
// the SHA1 and once to upload.
//
// 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.ReadSeeker
switch r := r.(type) {
case *bytes.Buffer:
defer r.Reset() // we are expected to consume it
body = bytes.NewReader(r.Bytes())
case io.ReadSeeker:
body = r
default:
debugf("upload %s: buffering", name)
b, err := ioutil.ReadAll(r)
if err != nil {
return nil, err
}
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
}
fi, err = b.UploadWithSHA1(body, name, mimeType, sha1Sum, length)
if err == nil {
break
}
if err, ok := UnwrapError(err); ok && err.Status == http.StatusUnauthorized {
// We are forced to pass nil to login, risking a double login (which is
// wasteful, but not harmful) because the API does not give us access to
// the failed response (without hacks).
if err := b.c.login(nil); err != nil {
return nil, err
}
i--
}
}
return fi, err
}
type uploadURL struct {
UploadURL, AuthorizationToken string
}
func (b *Bucket) getUploadURL() (u *uploadURL, err error) {
b.uploadURLsMu.Lock()
if len(b.uploadURLs) > 0 {
u = b.uploadURLs[len(b.uploadURLs)-1]
b.uploadURLs = b.uploadURLs[:len(b.uploadURLs)-1]
}
b.uploadURLsMu.Unlock()
if u != nil {
return
}
res, err := b.c.doRequest("b2_get_upload_url", map[string]interface{}{
"bucketId": b.ID,
})
if err != nil {
return
}
defer drainAndClose(res.Body)
err = json.NewDecoder(res.Body).Decode(&u)
return
}
func (b *Bucket) putUploadURL(u *uploadURL) {
b.uploadURLsMu.Lock()
defer b.uploadURLsMu.Unlock()
b.uploadURLs = append(b.uploadURLs, u)
}
// UploadWithSHA1 is like Upload, but allows the caller to specify previously
// 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, not just error handling, is
// mandatory by the B2 API documentation. If the error Status is Unauthorized,
// a call to (*Client).LoginInfo(true) should be performed first.
//
// sha1Sum should be the hex encoding of the SHA1 sum of what will be read from r.
//
// 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 int64) (*FileInfo, error) {
uurl, err := b.getUploadURL()
if err != nil {
return nil, err
}
req, err := http.NewRequest("POST", uurl.UploadURL, ioutil.NopCloser(r))
if err != nil {
return nil, err
}
req.ContentLength = length
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("X-Bz-Content-Sha1", sha1Sum)
res, err := b.c.hc.Do(req)
if err != nil {
debugf("upload %s: %s", name, err)
return nil, err
}
debugf("upload %s (%d %s)", name, length, sha1Sum)
defer drainAndClose(res.Body)
fi := fileInfoObj{}
if err = json.NewDecoder(res.Body).Decode(&fi); err != nil {
return nil, err
}
b.putUploadURL(uurl)
return fi.makeFileInfo(), nil
}