Update B2 API to support all documented API methods. Fairly stable

* Needs more comments
* Reorganize methods and code
* Fix the way an upload struct is passed around
* Write reproducible tests
This commit is contained in:
Tony Blyler 2015-11-21 10:52:05 -05:00
parent 71d00b1a69
commit 5ada11af4c

419
b2.go
View file

@ -6,7 +6,9 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"io/ioutil"
"net/http" "net/http"
"net/url"
"time" "time"
) )
@ -59,23 +61,48 @@ type FileInfo struct {
Info map[string]string `json:"fileInfo"` Info map[string]string `json:"fileInfo"`
} }
type b2Err struct { // FileName B2 file name
type FileName struct {
ID string `json:"fileId"`
Name string `json:"fileName"`
Action string `json:"action"`
Size int64 `json:"size"`
Timestamp int64 `json:"uploadTimestamp"`
}
// Err B2 error information
type Err struct {
Code string `json:"code"` Code string `json:"code"`
Message string `json:"message"` Message string `json:"message"`
Status int `json:"status"` Status int `json:"status"`
} }
func b2ErrToErr(err *b2Err) error { func (b *Err) Error() string {
return fmt.Errorf("code: '%s' status: '%d' message: '%s'", err.Code, err.Status, err.Message) return fmt.Sprintf("code: '%s' status: '%d' message: '%s'", b.Code, b.Status, b.Message)
} }
func readResp(decoder *json.Decoder, output interface{}) error { func readResp(resp *http.Response, output interface{}) error {
err := decoder.Decode(output) data, err := ioutil.ReadAll(resp.Body)
if err != nil && err != io.EOF { if err != nil {
return err
}
if resp.StatusCode == GoodStatus {
err = json.Unmarshal(data, output)
if err != nil {
return err return err
} }
return nil return nil
}
errb2 := &Err{}
err = json.Unmarshal(data, errb2)
if err != nil {
return err
}
return errb2
} }
// NewB2 create a new B2 API handler // NewB2 create a new B2 API handler
@ -91,26 +118,14 @@ func NewB2(accountID string, applicationKey string) (*B2, error) {
return nil, err return nil, err
} }
decoder := json.NewDecoder(resp.Body)
if resp.StatusCode == GoodStatus {
b2 := &B2{} b2 := &B2{}
err := readResp(decoder, b2) err = readResp(resp, b2)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return b2, nil return b2, nil
}
errb2 := &b2Err{}
err = readResp(decoder, errb2)
if err != nil {
return nil, err
}
return nil, b2ErrToErr(errb2)
} }
// CreateBucket creates a new bucket // CreateBucket creates a new bucket
@ -132,25 +147,13 @@ func (b *B2) CreateBucket(bucketName string, bucketType string) (*Bucket, error)
return nil, err return nil, err
} }
decoder := json.NewDecoder(resp.Body)
if resp.StatusCode == GoodStatus {
bucket := &Bucket{} bucket := &Bucket{}
err := readResp(decoder, bucket) err = readResp(resp, bucket)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return bucket, nil return bucket, nil
}
errb2 := &b2Err{}
err = readResp(decoder, errb2)
if err != nil {
return nil, err
}
return nil, b2ErrToErr(errb2)
} }
// DeleteBucket deletes the bucket specified // DeleteBucket deletes the bucket specified
@ -175,25 +178,14 @@ func (b *B2) DeleteBucket(bucketID string) (*Bucket, error) {
return nil, err return nil, err
} }
decoder := json.NewDecoder(resp.Body)
if resp.StatusCode == GoodStatus {
bucket := &Bucket{} bucket := &Bucket{}
err := readResp(decoder, bucket)
err = readResp(resp, bucket)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return bucket, nil return bucket, nil
}
errb2 := &b2Err{}
err = readResp(decoder, errb2)
if err != nil {
return nil, err
}
return nil, b2ErrToErr(errb2)
} }
// GetUploadURL gets an URL to use for uploading files // GetUploadURL gets an URL to use for uploading files
@ -217,40 +209,19 @@ func (b *B2) GetUploadURL(bucketID string) (*Upload, error) {
return nil, err return nil, err
} }
decoder := json.NewDecoder(resp.Body)
if resp.StatusCode == GoodStatus {
upload := &Upload{} upload := &Upload{}
err = readResp(resp, upload)
err := readResp(decoder, upload)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return upload, nil return upload, nil
}
errb2 := &b2Err{}
err = readResp(decoder, errb2)
if err != nil {
return nil, err
}
return nil, b2ErrToErr(errb2)
} }
// UploadFile uploads one file to B2 // UploadFile uploads one file to B2
func (b *B2) UploadFile(data io.Reader, fileName string, contentType string, sha1 string, mtime *time.Time, info map[string]string, bucketID string) (*FileInfo, error) { func (b *B2) UploadFile(data io.Reader, fileName string, fileSize int64, contentType string, sha1 string, mtime *time.Time, info map[string]string) (*FileInfo, error) {
if b.Upload == nil { if b.Upload == nil {
if bucketID == "" { return nil, errors.New("Must run GetUploadURL and set B2.Upload to upload")
return nil, errors.New("Must run GetUploadURL and set B2.Upload, or provide bucket id to upload")
}
var err error
b.Upload, err = b.GetUploadURL(bucketID)
if err != nil {
return nil, err
}
} }
req, err := http.NewRequest("POST", b.Upload.UploadURL, data) req, err := http.NewRequest("POST", b.Upload.UploadURL, data)
@ -258,10 +229,19 @@ func (b *B2) UploadFile(data io.Reader, fileName string, contentType string, sha
return nil, err return nil, err
} }
req.ContentLength = fileSize
if contentType == "" { if contentType == "" {
contentType = "b2/x-auto" contentType = "b2/x-auto"
} }
fileEncoded, err := url.Parse(fileName)
if err != nil {
return nil, err
}
fileName = fileEncoded.String()
req.Header.Add("Authorization", b.Upload.AuthToken) req.Header.Add("Authorization", b.Upload.AuthToken)
req.Header.Add("X-Bz-File-Name", fileName) req.Header.Add("X-Bz-File-Name", fileName)
req.Header.Add("Content-Type", contentType) req.Header.Add("Content-Type", contentType)
@ -281,24 +261,309 @@ func (b *B2) UploadFile(data io.Reader, fileName string, contentType string, sha
return nil, err return nil, err
} }
decoder := json.NewDecoder(resp.Body) fileInfo := &FileInfo{}
err = readResp(resp, fileInfo)
if err != nil {
return nil, err
}
return fileInfo, nil
}
// DownloadFileByID Downloads one file from B2
func (b *B2) DownloadFileByID(fileID string, output io.Writer) (http.Header, error) {
req, err := http.NewRequest("GET", b.DownloadURL+APIsuffix+"/b2_download_file_by_id", nil)
if err != nil {
return nil, err
}
req.Header.Add("Authorization", b.AuthToken)
q := req.URL.Query()
q.Add("fileId", fileID)
req.URL.RawQuery = q.Encode()
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
if resp.StatusCode != GoodStatus {
return nil, readResp(resp, nil)
}
defer resp.Body.Close()
_, err = io.Copy(output, resp.Body)
if err != nil {
return nil, err
}
return resp.Header, nil
}
// DownloadFileByName downloads one file by providing the name of the bucket and the name of the file
func (b *B2) DownloadFileByName(bucketName string, fileName string, output io.Writer) (http.Header, error) {
urlFileName, err := url.Parse(fileName)
if err != nil {
return nil, err
}
req, err := http.NewRequest("GET", b.DownloadURL+"/file/"+bucketName+"/"+urlFileName.String(), nil)
if err != nil {
return nil, err
}
req.Header.Add("Authorization", b.AuthToken)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
if resp.StatusCode != GoodStatus {
return nil, readResp(resp, nil)
}
defer resp.Body.Close()
_, err = io.Copy(output, resp.Body)
if err != nil {
return nil, err
}
return resp.Header, nil
}
// UpdateBucket update an existing bucket
func (b *B2) UpdateBucket(bucketID string, bucketType string) (*Bucket, error) {
data, err := json.Marshal(map[string]string{
"accountId": b.AccountID,
"bucketId": bucketID,
"bucketType": bucketType,
})
if err != nil {
return nil, err
}
req, err := http.NewRequest("POST", b.APIUrl+APIsuffix+"/b2_update_bucket", bytes.NewReader(data))
if err != nil {
return nil, err
}
req.Header.Add("Authorization", b.AuthToken)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
bucket := &Bucket{}
err = readResp(resp, bucket)
if err != nil {
return nil, err
}
return bucket, nil
}
// DeleteFileVersion deletes one version of a file from B2
func (b *B2) DeleteFileVersion(fileName string, fileID string) (*FileInfo, error) {
data, err := json.Marshal(map[string]string{
"fileName": fileName,
"fileId": fileID,
})
if err != nil {
return nil, err
}
req, err := http.NewRequest("POST", b.APIUrl+APIsuffix+"/b2_delete_file_version", bytes.NewReader(data))
if err != nil {
return nil, err
}
req.Header.Add("Authorization", b.AuthToken)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
fileInfo := &FileInfo{}
err = readResp(resp, fileInfo)
if err != nil {
return nil, err
}
return fileInfo, nil
}
// ListBuckets lists buckets associated with an account, in alphabetical order by bucket ID
func (b *B2) ListBuckets() ([]Bucket, error) {
data, err := json.Marshal(map[string]string{
"accountId": b.AccountID,
})
if err != nil {
return nil, err
}
req, err := http.NewRequest("POST", b.APIUrl+APIsuffix+"/b2_list_buckets", bytes.NewReader(data))
if err != nil {
return nil, err
}
req.Header.Add("Authorization", b.AuthToken)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
buckets := &struct {
Buckets []Bucket `json:"buckets"`
}{}
err = readResp(resp, buckets)
if err != nil {
return nil, err
}
return buckets.Buckets, nil
}
// ListFileNames Lists the names of all files in a bucket, starting at a given name<Paste>
func (b *B2) ListFileNames(bucketID string, startFileName string, maxFileCount int) ([]FileName, string, error) {
data, err := json.Marshal(struct {
BucketID string `json:"bucketId"`
StartFileName string `json:"startFileName,omitempty"`
MaxFileCount int `json:"maxFileCount,omitempty"`
}{
BucketID: bucketID,
StartFileName: startFileName,
MaxFileCount: maxFileCount,
})
if err != nil {
return nil, "", err
}
req, err := http.NewRequest("POST", b.APIUrl+APIsuffix+"/b2_list_file_names", bytes.NewReader(data))
if err != nil {
return nil, "", err
}
req.Header.Add("Authorization", b.AuthToken)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, "", err
}
list := &struct {
Files []FileName `json:"files"`
NextFileName string `json:"nextFileName"`
}{}
err = readResp(resp, list)
if err != nil {
return nil, "", err
}
return list.Files, list.NextFileName, nil
}
// ListFileVersions lists all of the versions of all of the files contained in one bucket, in alphabetical order by file name, and by reverse of date/time uploaded for versions of files with the same name
func (b *B2) ListFileVersions(bucketID string, startFileName string, startFileID string, maxFileCount int) ([]FileName, string, string, error) {
data, err := json.Marshal(struct {
BucketID string `json:"bucketId"`
StartFileName string `json:"startFileName,omitempty"`
StartFileID string `json:"startFileId,omitempty"`
MaxFileCount int `json:"maxFileCount,omitempty"`
}{
BucketID: bucketID,
StartFileName: startFileName,
StartFileID: startFileID,
MaxFileCount: maxFileCount,
})
if err != nil {
return nil, "", "", err
}
req, err := http.NewRequest("POST", b.APIUrl+APIsuffix+"/b2_list_file_versions", bytes.NewReader(data))
if err != nil {
return nil, "", "", err
}
req.Header.Add("Authorization", b.AuthToken)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, "", "", err
}
list := &struct {
Files []FileName `json:"files"`
NextFileID string `json:"nextFileId"`
NextFileName string `json:"nextFileName"`
}{}
err = readResp(resp, list)
if err != nil {
return nil, "", "", err
}
return list.Files, list.NextFileID, list.NextFileName, nil
}
// GetFileInfo Gets information about one file stored in B2
func (b *B2) GetFileInfo(fileID string) (*FileInfo, error) {
data, err := json.Marshal(map[string]string{
"fileId": fileID,
})
if err != nil {
return nil, err
}
req, err := http.NewRequest("POST", b.APIUrl+APIsuffix+"/b2_get_file_info", bytes.NewReader(data))
if err != nil {
return nil, err
}
req.Header.Add("Authorization", b.AuthToken)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
if resp.StatusCode == GoodStatus {
info := &FileInfo{} info := &FileInfo{}
err = readResp(resp, info)
err := readResp(decoder, info)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return info, nil return info, nil
} }
errb2 := &b2Err{} // HideFile hides a file so that downloading by name will not find the file, but previous versions of the file are still stored. See File Versions about what it means to hide a file
err = readResp(decoder, errb2) func (b *B2) HideFile(bucketID string, fileName string) (*FileName, error) {
data, err := json.Marshal(map[string]string{
"bucketId": bucketID,
"fileName": fileName,
})
if err != nil { if err != nil {
return nil, err return nil, err
} }
return nil, b2ErrToErr(errb2) req, err := http.NewRequest("POST", b.APIUrl+APIsuffix+"/b2_hide_file", bytes.NewReader(data))
if err != nil {
return nil, err
}
req.Header.Add("Authorization", b.AuthToken)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
info := &FileName{}
err = readResp(resp, info)
if err != nil {
return nil, err
}
return info, nil
} }