diff --git a/b2.go b/b2.go index e69db66..b38b86f 100644 --- a/b2.go +++ b/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 +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 }