2015-11-21 00:13:07 -05:00
package b2
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
2015-11-21 10:52:05 -05:00
"io/ioutil"
2015-11-21 00:13:07 -05:00
"net/http"
2015-11-21 10:52:05 -05:00
"net/url"
2015-11-22 14:24:06 -05:00
"strconv"
"strings"
2015-11-21 00:13:07 -05:00
)
// APIurl base address for the B2 API
const APIurl = "https://api.backblaze.com"
// APIsuffix the version of the API
const APIsuffix = "/b2api/v1"
// GoodStatus status code for a successful API call
const GoodStatus = 200
2015-11-22 14:24:06 -05:00
// HeaderInfoPrefix the prefix for file header info
const HeaderInfoPrefix = "X-Bz-Info-"
2015-11-21 00:13:07 -05:00
// ErrGeneric generic error from API
var ErrGeneric = errors . New ( "Received invalid response from B2 API" )
// B2 communicates to B2 API and holds information for the connection
type B2 struct {
2015-11-22 14:24:06 -05:00
AccountID string ` json:"accountId" `
APIUrl string ` json:"apiUrl" `
AuthToken string ` json:"authorizationToken" `
DownloadURL string ` json:"downloadUrl" `
AppKey string ` json:"-" `
2015-11-21 10:52:05 -05:00
}
// Err B2 error information
type Err struct {
2015-11-21 00:13:07 -05:00
Code string ` json:"code" `
Message string ` json:"message" `
Status int ` json:"status" `
}
2015-11-21 10:52:05 -05:00
func ( b * Err ) Error ( ) string {
return fmt . Sprintf ( "code: '%s' status: '%d' message: '%s'" , b . Code , b . Status , b . Message )
2015-11-21 00:13:07 -05:00
}
2015-11-22 14:24:06 -05:00
// readResp take an http response from the B2 API and unmarshal it to the appropriate type
2015-11-21 10:52:05 -05:00
func readResp ( resp * http . Response , output interface { } ) error {
data , err := ioutil . ReadAll ( resp . Body )
if err != nil {
return err
}
if resp . StatusCode == GoodStatus {
err = json . Unmarshal ( data , output )
if err != nil {
return err
}
return nil
}
2015-11-22 14:24:06 -05:00
// errors are generated anytime there is not a status code of GoodStatus
2015-11-21 10:52:05 -05:00
errb2 := & Err { }
err = json . Unmarshal ( data , errb2 )
if err != nil {
2015-11-21 00:13:07 -05:00
return err
}
2015-11-21 10:52:05 -05:00
return errb2
2015-11-21 00:13:07 -05:00
}
2015-11-22 14:24:06 -05:00
func ( b * B2 ) readHeaderFileInfo ( header http . Header ) ( * FileInfo , error ) {
var err error
info := & FileInfo { conn : b }
info . AccountID = b . AccountID
info . Type = header . Get ( "Content-Type" )
info . ID = header . Get ( "X-Bz-File-Id" )
info . Length , err = strconv . ParseInt ( header . Get ( "Content-Length" ) , 10 , 64 )
if err != nil {
return nil , err
}
info . Name , err = url . QueryUnescape ( header . Get ( "X-Bz-File-Name" ) )
if err != nil {
return nil , err
}
info . Sha1 = header . Get ( "X-Bz-Content-Sha1" )
for headerName , val := range header {
if ! strings . HasPrefix ( headerName , HeaderInfoPrefix ) {
continue
}
// B2 does not support multiple values per header
info . Info [ headerName [ len ( HeaderInfoPrefix ) : ] ] = val [ 0 ]
}
return info , nil
}
2015-11-21 00:13:07 -05:00
// NewB2 create a new B2 API handler
func NewB2 ( accountID string , applicationKey string ) ( * B2 , error ) {
req , err := http . NewRequest ( "GET" , APIurl + APIsuffix + "/b2_authorize_account" , nil )
if err != nil {
return nil , err
}
req . SetBasicAuth ( accountID , applicationKey )
resp , err := http . DefaultClient . Do ( req )
if err != nil {
return nil , err
}
2015-11-21 10:52:05 -05:00
b2 := & B2 { }
2015-11-21 00:13:07 -05:00
2015-11-21 10:52:05 -05:00
err = readResp ( resp , b2 )
2015-11-21 00:13:07 -05:00
if err != nil {
return nil , err
}
2015-11-21 10:52:05 -05:00
return b2 , nil
2015-11-21 00:13:07 -05:00
}
// CreateBucket creates a new bucket
func ( b * B2 ) CreateBucket ( bucketName string , bucketType string ) ( * Bucket , error ) {
req , err := http . NewRequest ( "GET" , b . APIUrl + APIsuffix + "/b2_create_bucket" , nil )
if err != nil {
return nil , err
}
req . Header . Add ( "Authorization" , b . AuthToken )
q := req . URL . Query ( )
q . Add ( "accountId" , b . AccountID )
q . Add ( "bucketName" , bucketName )
q . Add ( "bucketType" , bucketType )
req . URL . RawQuery = q . Encode ( )
resp , err := http . DefaultClient . Do ( req )
if err != nil {
return nil , err
}
2015-11-22 14:24:06 -05:00
bucket := & Bucket { conn : b }
2015-11-21 10:52:05 -05:00
err = readResp ( resp , bucket )
2015-11-21 00:13:07 -05:00
if err != nil {
return nil , err
}
2015-11-21 10:52:05 -05:00
return bucket , nil
2015-11-21 00:13:07 -05:00
}
// DeleteBucket deletes the bucket specified
func ( b * B2 ) DeleteBucket ( bucketID string ) ( * Bucket , error ) {
data , err := json . Marshal ( map [ string ] string {
"accountId" : b . AccountID ,
"bucketId" : bucketID ,
} )
if err != nil {
return nil , err
}
req , err := http . NewRequest ( "POST" , b . APIUrl + APIsuffix + "/b2_delete_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
}
2015-11-22 14:24:06 -05:00
bucket := & Bucket { conn : b }
2015-11-21 00:13:07 -05:00
2015-11-21 10:52:05 -05:00
err = readResp ( resp , bucket )
2015-11-21 00:13:07 -05:00
if err != nil {
return nil , err
}
2015-11-21 10:52:05 -05:00
return bucket , nil
2015-11-21 00:13:07 -05:00
}
// GetUploadURL gets an URL to use for uploading files
func ( b * B2 ) GetUploadURL ( bucketID string ) ( * Upload , error ) {
data , err := json . Marshal ( map [ string ] string {
"bucketId" : bucketID ,
} )
if err != nil {
return nil , err
}
req , err := http . NewRequest ( "POST" , b . APIUrl + APIsuffix + "/b2_get_upload_url" , 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
}
2015-11-21 10:52:05 -05:00
upload := & Upload { }
err = readResp ( resp , upload )
2015-11-21 00:13:07 -05:00
if err != nil {
return nil , err
}
2015-11-21 10:52:05 -05:00
return upload , nil
2015-11-21 00:13:07 -05:00
}
2015-11-21 10:52:05 -05:00
// DownloadFileByID Downloads one file from B2
2015-11-22 14:24:06 -05:00
func ( b * B2 ) DownloadFileByID ( fileID string , output io . Writer ) ( * FileInfo , error ) {
2015-11-21 10:52:05 -05:00
req , err := http . NewRequest ( "GET" , b . DownloadURL + APIsuffix + "/b2_download_file_by_id" , nil )
if err != nil {
return nil , err
}
2015-11-21 00:13:07 -05:00
2015-11-21 10:52:05 -05:00
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
}
2015-11-22 14:24:06 -05:00
return b . readHeaderFileInfo ( resp . Header )
2015-11-21 10:52:05 -05:00
}
// DownloadFileByName downloads one file by providing the name of the bucket and the name of the file
2015-11-22 14:24:06 -05:00
func ( b * B2 ) DownloadFileByName ( bucketName string , fileName string , output io . Writer ) ( * FileInfo , error ) {
2015-11-21 10:52:05 -05:00
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
}
2015-11-22 14:24:06 -05:00
return b . readHeaderFileInfo ( resp . Header )
2015-11-21 10:52:05 -05:00
}
// 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
}
2015-11-22 14:24:06 -05:00
bucket := & Bucket { conn : b }
2015-11-21 10:52:05 -05:00
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
}
2015-11-22 14:24:06 -05:00
fileInfo := & FileInfo { conn : b }
2015-11-21 10:52:05 -05:00
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
}
2015-11-22 14:24:06 -05:00
for i := range buckets . Buckets {
buckets . Buckets [ i ] . conn = b
}
2015-11-21 10:52:05 -05:00
return buckets . Buckets , nil
}
2015-11-22 14:24:06 -05:00
// ListFileNames Lists the names of all files in a bucket, starting at a given name
2015-11-21 10:52:05 -05:00
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
}
2015-11-22 14:24:06 -05:00
for i := range list . Files {
list . Files [ i ] . conn = b
}
2015-11-21 10:52:05 -05:00
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
}
2015-11-22 14:24:06 -05:00
for i := range list . Files {
list . Files [ i ] . conn = b
}
2015-11-21 10:52:05 -05:00
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
}
2015-11-22 14:24:06 -05:00
info := & FileInfo { conn : b }
2015-11-21 10:52:05 -05:00
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
2015-11-21 00:13:07 -05:00
}
2015-11-22 14:24:06 -05:00
info := & FileName { conn : b }
2015-11-21 10:52:05 -05:00
err = readResp ( resp , info )
2015-11-21 00:13:07 -05:00
if err != nil {
return nil , err
}
2015-11-21 10:52:05 -05:00
return info , nil
2015-11-21 00:13:07 -05:00
}