forked from FiloSottile/b2
-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathfile.go
238 lines (216 loc) · 6.42 KB
/
file.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
package b2
import (
"encoding/json"
"errors"
"time"
)
// DeleteFile deletes a file version.
func (c *Client) DeleteFile(id, name string) error {
res, err := c.doRequest("b2_delete_file_version", map[string]interface{}{
"fileId": id, "fileName": name,
})
if err != nil {
return err
}
drainAndClose(res.Body)
return nil
}
// A FileInfo is the metadata associated with a specific file version.
type FileInfo struct {
ID string
Name string
// Had to remove BucketID since it is not returned by b2_download_file_by_*
// BucketID string
ContentSHA1 string // hex encoded
ContentLength int
ContentType string
CustomMetadata map[string]interface{}
UploadTimestamp time.Time
// If Action is "hide", this ID does not refer to a file version
// but to an hiding action. Otherwise "upload".
Action string
}
type fileInfoObj struct {
AccountID string `json:"accountId"`
BucketID string `json:"bucketId"`
ContentLength int `json:"contentLength"`
ContentSHA1 string `json:"contentSha1"`
ContentType string `json:"contentType"`
FileID string `json:"fileId"`
FileInfo map[string]interface{} `json:"fileInfo"`
FileName string `json:"fileName"`
UploadTimestamp int64 `json:"uploadTimestamp"`
Action string `json:"action"`
}
func (fi *fileInfoObj) makeFileInfo() *FileInfo {
return &FileInfo{
ID: fi.FileID,
Name: fi.FileName,
ContentLength: fi.ContentLength,
ContentSHA1: fi.ContentSHA1,
ContentType: fi.ContentType,
CustomMetadata: fi.FileInfo,
Action: fi.Action,
UploadTimestamp: time.Unix(fi.UploadTimestamp/1e3, fi.UploadTimestamp%1e3*1e6),
}
}
// GetFileInfoByID obtains a FileInfo for a given ID.
//
// The ID can refer to any file version or "hide" action in any bucket.
func (c *Client) GetFileInfoByID(id string) (*FileInfo, error) {
res, err := c.doRequest("b2_get_file_info", map[string]interface{}{
"fileId": id,
})
if err != nil {
return nil, err
}
defer drainAndClose(res.Body)
var fi *fileInfoObj
if err := json.NewDecoder(res.Body).Decode(&fi); err != nil {
return nil, err
}
return fi.makeFileInfo(), nil
}
var FileNotFoundError = errors.New("no file with the given name in the bucket")
// GetFileInfoByName obtains a FileInfo for a given name.
//
// If the file doesn't exist, FileNotFoundError is returned.
// If multiple versions of the file exist, only the latest is returned.
func (b *Bucket) GetFileInfoByName(name string) (*FileInfo, error) {
l := b.ListFiles(name)
l.SetPageCount(1)
if l.Next() {
if l.FileInfo().Name == name {
return l.FileInfo(), nil
}
}
if err := l.Err(); err != nil {
return nil, l.Err()
}
return nil, FileNotFoundError
}
// A Listing is the result of (*Bucket).ListFiles[Versions].
// It works like sql.Rows: use Next to advance and then FileInfo.
// Check Err once Next returns false.
//
// l := b.ListFiles("")
// for l.Next() {
// fi := l.FileInfo()
// ...
// }
// if err := l.Err(); err != nil {
// ...
// }
//
// A Listing handles pagination transparently, so it iterates until
// the last file in the bucket. To limit the number of results, do this.
//
// for i := 0; i < limit && l.Next(); i++ {
//
type Listing struct {
b *Bucket
versions bool
nextPageCount int
nextName, nextID *string
objects []*FileInfo // in reverse order
err error
}
// SetPageCount controls the number of results to be fetched with each API
// call. The maximum n is 1000, higher values are automatically limited to 1000.
//
// SetPageCount does not limit the number of results returned by a Listing.
func (l *Listing) SetPageCount(n int) {
if n > 1000 {
n = 1000
}
l.nextPageCount = n
}
// Next calls the list API if needed and prepares the FileInfo results.
// It returns true on success, or false if there is no next result
// or an error happened while preparing it. Err should be
// consulted to distinguish between the two cases.
func (l *Listing) Next() bool {
if l.err != nil {
return false
}
if len(l.objects) > 0 {
l.objects = l.objects[:len(l.objects)-1]
}
if len(l.objects) > 0 {
return true
}
if l.nextName == nil {
return false // end of iteration
}
data := map[string]interface{}{
"bucketId": l.b.ID,
"startFileName": *l.nextName,
"maxFileCount": l.nextPageCount,
}
endpoint := "b2_list_file_names"
if l.versions {
endpoint = "b2_list_file_versions"
}
if l.nextID != nil && *l.nextID != "" {
data["startFileId"] = *l.nextID
}
r, err := l.b.c.doRequest(endpoint, data)
if err != nil {
l.err = err
return false
}
defer drainAndClose(r.Body)
var x struct {
Files []fileInfoObj
NextFileName *string
NextFileID *string
}
if l.err = json.NewDecoder(r.Body).Decode(&x); l.err != nil {
return false
}
l.objects = make([]*FileInfo, len(x.Files))
for i, f := range x.Files {
l.objects[len(l.objects)-1-i] = f.makeFileInfo()
}
l.nextName, l.nextID = x.NextFileName, x.NextFileID
return len(l.objects) > 0
}
// FileInfo returns the FileInfo object made available by Next.
//
// FileInfo must only be called after a call to Next returned true.
func (l *Listing) FileInfo() *FileInfo {
return l.objects[len(l.objects)-1]
}
// Err returns the error, if any, that was encountered while listing.
func (l *Listing) Err() error {
return l.err
}
// ListFiles returns a Listing of files in the Bucket, alphabetically sorted,
// starting from the file named fromName (included if it exists). To start from
// the first file in the bucket, set fileName to "".
//
// ListFiles only returns the most recent version of each (non-hidden) file.
// If you want to fetch all versions, use ListFilesVersions.
func (b *Bucket) ListFiles(fromName string) *Listing {
return &Listing{
b: b,
nextName: &fromName,
}
}
// ListFilesVersions is like ListFiles, but returns all file versions,
// alphabetically sorted first, and by reverse of date/time uploaded then.
//
// If fromID is specified, the name-and-id pair is the starting point.
func (b *Bucket) ListFilesVersions(fromName, fromID string) *Listing {
if fromName == "" && fromID != "" {
return &Listing{
err: errors.New("can't set fromID if fromName is not set"),
}
}
return &Listing{
b: b,
versions: true,
nextName: &fromName,
nextID: &fromID,
}
}