From cf24af0470e1c72985743752023780d333b78fe8 Mon Sep 17 00:00:00 2001
From: Tony Blyler <tony@blyler.cc>
Date: Sun, 22 Nov 2015 14:24:06 -0500
Subject: [PATCH] Fixed #1 added comments where necessary Fixed #2 reorganized
 methods and structs Fixed #3 hide the upload struct better and make it more
 convenient Fixed #5 parse headers from downloads into a struct

---
 b2.go => b2/b2.go | 169 +++++++++++++++++-----------------------------
 b2/bucket.go      |  65 ++++++++++++++++++
 b2/fileinfo.go    |  33 +++++++++
 b2/filename.go    |  16 +++++
 b2/upload.go      |  69 +++++++++++++++++++
 5 files changed, 246 insertions(+), 106 deletions(-)
 rename b2.go => b2/b2.go (79%)
 create mode 100644 b2/bucket.go
 create mode 100644 b2/fileinfo.go
 create mode 100644 b2/filename.go
 create mode 100644 b2/upload.go

diff --git a/b2.go b/b2/b2.go
similarity index 79%
rename from b2.go
rename to b2/b2.go
index b38b86f..231c666 100644
--- a/b2.go
+++ b/b2/b2.go
@@ -9,7 +9,8 @@ import (
 	"io/ioutil"
 	"net/http"
 	"net/url"
-	"time"
+	"strconv"
+	"strings"
 )
 
 // APIurl base address for the B2 API
@@ -21,53 +22,19 @@ const APIsuffix = "/b2api/v1"
 // GoodStatus status code for a successful API call
 const GoodStatus = 200
 
+// HeaderInfoPrefix the prefix for file header info
+const HeaderInfoPrefix = "X-Bz-Info-"
+
 // 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 {
-	AccountID   string  `json:"accountId"`
-	APIUrl      string  `json:"apiUrl"`
-	AuthToken   string  `json:"authorizationToken"`
-	DownloadURL string  `json:"downloadUrl"`
-	AppKey      string  `json:"-"`
-	Upload      *Upload `json:"-"`
-}
-
-// Upload B2 upload information
-type Upload struct {
-	BucketID  string `json:"bucketId"`
-	UploadURL string `json:"uploadUrl"`
-	AuthToken string `json:"authorizationToken"`
-}
-
-// Bucket B2 bucket type
-type Bucket struct {
-	AccountID string `json:"accountId"`
-	ID        string `json:"bucketId"`
-	Name      string `json:"bucketName"`
-	Type      string `json:"bucketType"`
-}
-
-// FileInfo B2 file information
-type FileInfo struct {
-	AccountID string            `json:"accountId"`
-	ID        string            `json:"fileId"`
-	Name      string            `json:"fileName"`
-	BucketID  string            `json:"bucketId"`
-	Length    int64             `json:"contentLength"`
-	Sha1      string            `json:"contentSha1"`
-	Type      string            `json:"contentType"`
-	Info      map[string]string `json:"fileInfo"`
-}
-
-// 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"`
+	AccountID   string `json:"accountId"`
+	APIUrl      string `json:"apiUrl"`
+	AuthToken   string `json:"authorizationToken"`
+	DownloadURL string `json:"downloadUrl"`
+	AppKey      string `json:"-"`
 }
 
 // Err B2 error information
@@ -81,6 +48,7 @@ func (b *Err) Error() string {
 	return fmt.Sprintf("code: '%s' status: '%d' message: '%s'", b.Code, b.Status, b.Message)
 }
 
+// readResp take an http response from the B2 API and unmarshal it to the appropriate type
 func readResp(resp *http.Response, output interface{}) error {
 	data, err := ioutil.ReadAll(resp.Body)
 	if err != nil {
@@ -96,6 +64,7 @@ func readResp(resp *http.Response, output interface{}) error {
 		return nil
 	}
 
+	// errors are generated anytime there is not a status code of GoodStatus
 	errb2 := &Err{}
 	err = json.Unmarshal(data, errb2)
 	if err != nil {
@@ -105,6 +74,34 @@ func readResp(resp *http.Response, output interface{}) error {
 	return errb2
 }
 
+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
+}
+
 // 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)
@@ -147,7 +144,7 @@ func (b *B2) CreateBucket(bucketName string, bucketType string) (*Bucket, error)
 		return nil, err
 	}
 
-	bucket := &Bucket{}
+	bucket := &Bucket{conn: b}
 	err = readResp(resp, bucket)
 	if err != nil {
 		return nil, err
@@ -178,7 +175,7 @@ func (b *B2) DeleteBucket(bucketID string) (*Bucket, error) {
 		return nil, err
 	}
 
-	bucket := &Bucket{}
+	bucket := &Bucket{conn: b}
 
 	err = readResp(resp, bucket)
 	if err != nil {
@@ -218,60 +215,8 @@ func (b *B2) GetUploadURL(bucketID string) (*Upload, error) {
 	return upload, nil
 }
 
-// UploadFile uploads one file to B2
-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 {
-		return nil, errors.New("Must run GetUploadURL and set B2.Upload to upload")
-	}
-
-	req, err := http.NewRequest("POST", b.Upload.UploadURL, data)
-	if err != nil {
-		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)
-	req.Header.Add("X-Bz-Content-Sha1", sha1)
-	if mtime != nil {
-		req.Header.Add("X-Bz-Info-src_last_modified_millis", fmt.Sprint(mtime.UnixNano()/1000000))
-	}
-
-	if info != nil {
-		for name, value := range info {
-			req.Header.Add("X-Bz-Info-"+name, value)
-		}
-	}
-
-	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
-}
-
 // DownloadFileByID Downloads one file from B2
-func (b *B2) DownloadFileByID(fileID string, output io.Writer) (http.Header, error) {
+func (b *B2) DownloadFileByID(fileID string, output io.Writer) (*FileInfo, error) {
 	req, err := http.NewRequest("GET", b.DownloadURL+APIsuffix+"/b2_download_file_by_id", nil)
 	if err != nil {
 		return nil, err
@@ -299,11 +244,11 @@ func (b *B2) DownloadFileByID(fileID string, output io.Writer) (http.Header, err
 		return nil, err
 	}
 
-	return resp.Header, nil
+	return b.readHeaderFileInfo(resp.Header)
 }
 
 // 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) {
+func (b *B2) DownloadFileByName(bucketName string, fileName string, output io.Writer) (*FileInfo, error) {
 	urlFileName, err := url.Parse(fileName)
 	if err != nil {
 		return nil, err
@@ -332,7 +277,7 @@ func (b *B2) DownloadFileByName(bucketName string, fileName string, output io.Wr
 		return nil, err
 	}
 
-	return resp.Header, nil
+	return b.readHeaderFileInfo(resp.Header)
 }
 
 // UpdateBucket update an existing bucket
@@ -358,7 +303,7 @@ func (b *B2) UpdateBucket(bucketID string, bucketType string) (*Bucket, error) {
 		return nil, err
 	}
 
-	bucket := &Bucket{}
+	bucket := &Bucket{conn: b}
 	err = readResp(resp, bucket)
 	if err != nil {
 		return nil, err
@@ -389,7 +334,7 @@ func (b *B2) DeleteFileVersion(fileName string, fileID string) (*FileInfo, error
 		return nil, err
 	}
 
-	fileInfo := &FileInfo{}
+	fileInfo := &FileInfo{conn: b}
 	err = readResp(resp, fileInfo)
 	if err != nil {
 		return nil, err
@@ -427,10 +372,14 @@ func (b *B2) ListBuckets() ([]Bucket, error) {
 		return nil, err
 	}
 
+	for i := range buckets.Buckets {
+		buckets.Buckets[i].conn = b
+	}
+
 	return buckets.Buckets, nil
 }
 
-// ListFileNames Lists the names of all files in a bucket, starting at a given name<Paste>
+// 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"`
@@ -465,6 +414,10 @@ func (b *B2) ListFileNames(bucketID string, startFileName string, maxFileCount i
 		return nil, "", err
 	}
 
+	for i := range list.Files {
+		list.Files[i].conn = b
+	}
+
 	return list.Files, list.NextFileName, nil
 }
 
@@ -506,6 +459,10 @@ func (b *B2) ListFileVersions(bucketID string, startFileName string, startFileID
 		return nil, "", "", err
 	}
 
+	for i := range list.Files {
+		list.Files[i].conn = b
+	}
+
 	return list.Files, list.NextFileID, list.NextFileName, nil
 }
 
@@ -529,7 +486,7 @@ func (b *B2) GetFileInfo(fileID string) (*FileInfo, error) {
 		return nil, err
 	}
 
-	info := &FileInfo{}
+	info := &FileInfo{conn: b}
 	err = readResp(resp, info)
 	if err != nil {
 		return nil, err
@@ -559,7 +516,7 @@ func (b *B2) HideFile(bucketID string, fileName string) (*FileName, error) {
 		return nil, err
 	}
 
-	info := &FileName{}
+	info := &FileName{conn: b}
 	err = readResp(resp, info)
 	if err != nil {
 		return nil, err
diff --git a/b2/bucket.go b/b2/bucket.go
new file mode 100644
index 0000000..af9c695
--- /dev/null
+++ b/b2/bucket.go
@@ -0,0 +1,65 @@
+package b2
+
+import (
+	"io"
+	"time"
+)
+
+// Bucket B2 bucket type
+type Bucket struct {
+	AccountID string `json:"accountId"`
+	ID        string `json:"bucketId"`
+	Name      string `json:"bucketName"`
+	Type      string `json:"bucketType"`
+	conn      *B2
+	upload    *Upload
+}
+
+// Delete deletes this bucket
+func (b *Bucket) Delete() error {
+	_, err := b.conn.DeleteBucket(b.ID)
+	return err
+}
+
+// Update updates this bucket
+func (b *Bucket) Update(bucketType string) error {
+	bucket, err := b.conn.UpdateBucket(b.ID, bucketType)
+	if err != nil {
+		return err
+	}
+
+	b.AccountID = bucket.AccountID
+	b.ID = bucket.ID
+	b.Name = bucket.Name
+	b.Type = bucket.Type
+
+	return nil
+}
+
+// ListFileNames Lists the names of all files in a bucket, starting a given name
+func (b *Bucket) ListFileNames(startFileName string, maxFileCount int) ([]FileName, string, error) {
+	return b.conn.ListFileNames(b.ID, startFileName, maxFileCount)
+}
+
+// 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 *Bucket) ListFileVersions(startFileName string, startFileID string, maxFileCount int) ([]FileName, string, string, error) {
+	return b.conn.ListFileVersions(b.ID, startFileName, startFileID, maxFileCount)
+}
+
+// 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 *Bucket) HideFile(fileName string) (*FileName, error) {
+	return b.conn.HideFile(b.ID, fileName)
+}
+
+// UploadFile uploads one file to B2
+func (b *Bucket) 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 {
+		var err error
+		b.upload, err = b.conn.GetUploadURL(b.ID)
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	return b.upload.UploadFile(data, fileName, fileSize, contentType, sha1, mtime, info)
+}
diff --git a/b2/fileinfo.go b/b2/fileinfo.go
new file mode 100644
index 0000000..c6e80fd
--- /dev/null
+++ b/b2/fileinfo.go
@@ -0,0 +1,33 @@
+package b2
+
+import (
+	"io"
+)
+
+// FileInfo B2 file information
+type FileInfo struct {
+	AccountID string            `json:"accountId"`
+	ID        string            `json:"fileId"`
+	Name      string            `json:"fileName"`
+	BucketID  string            `json:"bucketId"`
+	Length    int64             `json:"contentLength"`
+	Sha1      string            `json:"contentSha1"`
+	Type      string            `json:"contentType"`
+	Info      map[string]string `json:"fileInfo"`
+	conn      *B2
+}
+
+// Download downloads this file ID's content
+func (f *FileInfo) Download(output io.Writer) (*FileInfo, error) {
+	return f.conn.DownloadFileByID(f.ID, output)
+}
+
+// Delete deletes this version of the file
+func (f *FileInfo) Delete() (*FileInfo, error) {
+	return f.conn.DeleteFileVersion(f.Name, f.ID)
+}
+
+// Hide 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 (f *FileInfo) Hide() (*FileName, error) {
+	return f.conn.HideFile(f.BucketID, f.Name)
+}
diff --git a/b2/filename.go b/b2/filename.go
new file mode 100644
index 0000000..6f4da90
--- /dev/null
+++ b/b2/filename.go
@@ -0,0 +1,16 @@
+package b2
+
+// 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"`
+	conn      *B2
+}
+
+// GetFileInfo Gets information about one file stored in B2
+func (f *FileName) GetFileInfo() (*FileInfo, error) {
+	return f.conn.GetFileInfo(f.ID)
+}
diff --git a/b2/upload.go b/b2/upload.go
new file mode 100644
index 0000000..2dc496d
--- /dev/null
+++ b/b2/upload.go
@@ -0,0 +1,69 @@
+package b2
+
+import (
+	"fmt"
+	"io"
+	"net/http"
+	"net/url"
+	"time"
+)
+
+// Upload B2 upload information
+type Upload struct {
+	BucketID  string `json:"bucketId"`
+	UploadURL string `json:"uploadUrl"`
+	AuthToken string `json:"authorizationToken"`
+}
+
+// UploadFile uploads one file to B2
+func (u *Upload) UploadFile(data io.Reader, fileName string, fileSize int64, contentType string, sha1 string, mtime *time.Time, info map[string]string) (*FileInfo, error) {
+	req, err := http.NewRequest("POST", u.UploadURL, data)
+	if err != nil {
+		return nil, err
+	}
+
+	// content length is necessary for buffers like os.File
+	req.ContentLength = fileSize
+
+	// use B2's autodetect content type if one is not passed
+	if contentType == "" {
+		contentType = "b2/x-auto"
+	}
+
+	// encode fileName via URL encoding per B2's documentation
+	fileEncoded, err := url.Parse(fileName)
+	if err != nil {
+		return nil, err
+	}
+
+	fileName = fileEncoded.String()
+
+	req.Header.Add("Authorization", u.AuthToken)
+	req.Header.Add("X-Bz-File-Name", fileName)
+	req.Header.Add("Content-Type", contentType)
+	req.Header.Add("X-Bz-Content-Sha1", sha1)
+
+	// B2 requires time to be in UNIX milliseconds
+	if mtime != nil {
+		req.Header.Add("X-Bz-Info-src_last_modified_millis", fmt.Sprint(mtime.UnixNano()/1000000))
+	}
+
+	if info != nil {
+		for name, value := range info {
+			req.Header.Add("X-Bz-Info-"+name, value)
+		}
+	}
+
+	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
+}