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:
parent
71d00b1a69
commit
5ada11af4c
1 changed files with 358 additions and 93 deletions
451
b2.go
451
b2.go
|
@ -6,7 +6,9 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
|
@ -59,23 +61,48 @@ type FileInfo struct {
|
|||
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"`
|
||||
Message string `json:"message"`
|
||||
Status int `json:"status"`
|
||||
}
|
||||
|
||||
func b2ErrToErr(err *b2Err) error {
|
||||
return fmt.Errorf("code: '%s' status: '%d' message: '%s'", err.Code, err.Status, err.Message)
|
||||
func (b *Err) Error() string {
|
||||
return fmt.Sprintf("code: '%s' status: '%d' message: '%s'", b.Code, b.Status, b.Message)
|
||||
}
|
||||
|
||||
func readResp(decoder *json.Decoder, output interface{}) error {
|
||||
err := decoder.Decode(output)
|
||||
if err != nil && err != io.EOF {
|
||||
func readResp(resp *http.Response, output interface{}) error {
|
||||
data, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
if resp.StatusCode == GoodStatus {
|
||||
err = json.Unmarshal(data, output)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
errb2 := &Err{}
|
||||
err = json.Unmarshal(data, errb2)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return errb2
|
||||
}
|
||||
|
||||
// NewB2 create a new B2 API handler
|
||||
|
@ -91,26 +118,14 @@ func NewB2(accountID string, applicationKey string) (*B2, error) {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
decoder := json.NewDecoder(resp.Body)
|
||||
b2 := &B2{}
|
||||
|
||||
if resp.StatusCode == GoodStatus {
|
||||
b2 := &B2{}
|
||||
|
||||
err := readResp(decoder, b2)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return b2, nil
|
||||
}
|
||||
|
||||
errb2 := &b2Err{}
|
||||
err = readResp(decoder, errb2)
|
||||
err = readResp(resp, b2)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return nil, b2ErrToErr(errb2)
|
||||
return b2, nil
|
||||
}
|
||||
|
||||
// CreateBucket creates a new bucket
|
||||
|
@ -132,25 +147,13 @@ func (b *B2) CreateBucket(bucketName string, bucketType string) (*Bucket, error)
|
|||
return nil, err
|
||||
}
|
||||
|
||||
decoder := json.NewDecoder(resp.Body)
|
||||
|
||||
if resp.StatusCode == GoodStatus {
|
||||
bucket := &Bucket{}
|
||||
err := readResp(decoder, bucket)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return bucket, nil
|
||||
}
|
||||
|
||||
errb2 := &b2Err{}
|
||||
err = readResp(decoder, errb2)
|
||||
bucket := &Bucket{}
|
||||
err = readResp(resp, bucket)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return nil, b2ErrToErr(errb2)
|
||||
return bucket, nil
|
||||
}
|
||||
|
||||
// DeleteBucket deletes the bucket specified
|
||||
|
@ -175,25 +178,14 @@ func (b *B2) DeleteBucket(bucketID string) (*Bucket, error) {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
decoder := json.NewDecoder(resp.Body)
|
||||
bucket := &Bucket{}
|
||||
|
||||
if resp.StatusCode == GoodStatus {
|
||||
bucket := &Bucket{}
|
||||
err := readResp(decoder, bucket)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return bucket, nil
|
||||
}
|
||||
|
||||
errb2 := &b2Err{}
|
||||
err = readResp(decoder, errb2)
|
||||
err = readResp(resp, bucket)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return nil, b2ErrToErr(errb2)
|
||||
return bucket, nil
|
||||
}
|
||||
|
||||
// GetUploadURL gets an URL to use for uploading files
|
||||
|
@ -217,40 +209,19 @@ func (b *B2) GetUploadURL(bucketID string) (*Upload, error) {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
decoder := json.NewDecoder(resp.Body)
|
||||
|
||||
if resp.StatusCode == GoodStatus {
|
||||
upload := &Upload{}
|
||||
|
||||
err := readResp(decoder, upload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return upload, nil
|
||||
}
|
||||
|
||||
errb2 := &b2Err{}
|
||||
err = readResp(decoder, errb2)
|
||||
upload := &Upload{}
|
||||
err = readResp(resp, upload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return nil, b2ErrToErr(errb2)
|
||||
return upload, nil
|
||||
}
|
||||
|
||||
// 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 bucketID == "" {
|
||||
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
|
||||
}
|
||||
return nil, errors.New("Must run GetUploadURL and set B2.Upload to upload")
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
req.ContentLength = fileSize
|
||||
|
||||
if contentType == "" {
|
||||
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("X-Bz-File-Name", fileName)
|
||||
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
|
||||
}
|
||||
|
||||
decoder := json.NewDecoder(resp.Body)
|
||||
|
||||
if resp.StatusCode == GoodStatus {
|
||||
info := &FileInfo{}
|
||||
|
||||
err := readResp(decoder, info)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return info, nil
|
||||
}
|
||||
|
||||
errb2 := &b2Err{}
|
||||
err = readResp(decoder, errb2)
|
||||
fileInfo := &FileInfo{}
|
||||
err = readResp(resp, fileInfo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return nil, b2ErrToErr(errb2)
|
||||
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
|
||||
}
|
||||
|
||||
info := &FileInfo{}
|
||||
err = readResp(resp, info)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return info, nil
|
||||
}
|
||||
|
||||
// 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
|
||||
func (b *B2) HideFile(bucketID string, fileName string) (*FileName, error) {
|
||||
data, err := json.Marshal(map[string]string{
|
||||
"bucketId": bucketID,
|
||||
"fileName": fileName,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue