images service v2 port from rackpsace/gophercloud (#171)

* CheckByteArrayEquals funcs

* direct port from rackspace/gophercloud with minor additions to get unit tests passing

* new package for uploading and downloading image data

* updates to make imageservice v2 consistent with the rest of gophercloud/gophercloud

* add image service v2 client
diff --git a/openstack/imageservice/v2/imagedata/requests.go b/openstack/imageservice/v2/imagedata/requests.go
new file mode 100644
index 0000000..b1aac8e
--- /dev/null
+++ b/openstack/imageservice/v2/imagedata/requests.go
@@ -0,0 +1,28 @@
+package imagedata
+
+import (
+	"io"
+	"net/http"
+
+	"github.com/gophercloud/gophercloud"
+)
+
+// Upload uploads image file
+func Upload(client *gophercloud.ServiceClient, id string, data io.ReadSeeker) (r UploadResult) {
+	_, r.Err = client.Put(uploadURL(client, id), data, nil, &gophercloud.RequestOpts{
+		MoreHeaders: map[string]string{"Content-Type": "application/octet-stream"},
+		OkCodes:     []int{204},
+	})
+	return
+}
+
+// Download retrieves file
+func Download(client *gophercloud.ServiceClient, id string) (r DownloadResult) {
+	var resp *http.Response
+	resp, r.Err = client.Get(downloadURL(client, id), nil, nil)
+	if resp != nil {
+		r.Body = resp.Body
+		r.Header = resp.Header
+	}
+	return
+}
diff --git a/openstack/imageservice/v2/imagedata/results.go b/openstack/imageservice/v2/imagedata/results.go
new file mode 100644
index 0000000..970b226
--- /dev/null
+++ b/openstack/imageservice/v2/imagedata/results.go
@@ -0,0 +1,26 @@
+package imagedata
+
+import (
+	"fmt"
+	"io"
+
+	"github.com/gophercloud/gophercloud"
+)
+
+// UploadResult is the result of an upload image operation
+type UploadResult struct {
+	gophercloud.ErrResult
+}
+
+// DownloadResult is the result of a download image operation
+type DownloadResult struct {
+	gophercloud.Result
+}
+
+// Extract builds images model from io.Reader
+func (r DownloadResult) Extract() (io.Reader, error) {
+	if r, ok := r.Body.(io.Reader); ok {
+		return r, nil
+	}
+	return nil, fmt.Errorf("Expected io.Reader but got: %T(%#v)", r.Body, r.Body)
+}
diff --git a/openstack/imageservice/v2/imagedata/testing/fixtures.go b/openstack/imageservice/v2/imagedata/testing/fixtures.go
new file mode 100644
index 0000000..fe93fc9
--- /dev/null
+++ b/openstack/imageservice/v2/imagedata/testing/fixtures.go
@@ -0,0 +1,40 @@
+package testing
+
+import (
+	"io/ioutil"
+	"net/http"
+	"testing"
+
+	th "github.com/gophercloud/gophercloud/testhelper"
+	fakeclient "github.com/gophercloud/gophercloud/testhelper/client"
+)
+
+// HandlePutImageDataSuccessfully setup
+func HandlePutImageDataSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/images/da3b75d9-3f4a-40e7-8a2c-bfab23927dea/file", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "PUT")
+		th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID)
+
+		b, err := ioutil.ReadAll(r.Body)
+		if err != nil {
+			t.Errorf("Unable to read request body: %v", err)
+		}
+
+		th.AssertByteArrayEquals(t, []byte{5, 3, 7, 24}, b)
+
+		w.WriteHeader(http.StatusNoContent)
+	})
+}
+
+// HandleGetImageDataSuccessfully setup
+func HandleGetImageDataSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/images/da3b75d9-3f4a-40e7-8a2c-bfab23927dea/file", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID)
+
+		w.WriteHeader(http.StatusOK)
+
+		_, err := w.Write([]byte{34, 87, 0, 23, 23, 23, 56, 255, 254, 0})
+		th.AssertNoErr(t, err)
+	})
+}
diff --git a/openstack/imageservice/v2/imagedata/testing/requests_test.go b/openstack/imageservice/v2/imagedata/testing/requests_test.go
new file mode 100644
index 0000000..4ac42d0
--- /dev/null
+++ b/openstack/imageservice/v2/imagedata/testing/requests_test.go
@@ -0,0 +1,87 @@
+package testing
+
+import (
+	"fmt"
+	"io"
+	"io/ioutil"
+	"testing"
+
+	"github.com/gophercloud/gophercloud/openstack/imageservice/v2/imagedata"
+	th "github.com/gophercloud/gophercloud/testhelper"
+	fakeclient "github.com/gophercloud/gophercloud/testhelper/client"
+)
+
+func TestUpload(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	HandlePutImageDataSuccessfully(t)
+
+	err := imagedata.Upload(
+		fakeclient.ServiceClient(),
+		"da3b75d9-3f4a-40e7-8a2c-bfab23927dea",
+		readSeekerOfBytes([]byte{5, 3, 7, 24})).ExtractErr()
+
+	th.AssertNoErr(t, err)
+}
+
+func readSeekerOfBytes(bs []byte) io.ReadSeeker {
+	return &RS{bs: bs}
+}
+
+// implements io.ReadSeeker
+type RS struct {
+	bs     []byte
+	offset int
+}
+
+func (rs *RS) Read(p []byte) (int, error) {
+	leftToRead := len(rs.bs) - rs.offset
+
+	if 0 < leftToRead {
+		bytesToWrite := min(leftToRead, len(p))
+		for i := 0; i < bytesToWrite; i++ {
+			p[i] = rs.bs[rs.offset]
+			rs.offset++
+		}
+		return bytesToWrite, nil
+	}
+	return 0, io.EOF
+}
+
+func min(a int, b int) int {
+	if a < b {
+		return a
+	}
+	return b
+}
+
+func (rs *RS) Seek(offset int64, whence int) (int64, error) {
+	var offsetInt = int(offset)
+	if whence == 0 {
+		rs.offset = offsetInt
+	} else if whence == 1 {
+		rs.offset = rs.offset + offsetInt
+	} else if whence == 2 {
+		rs.offset = len(rs.bs) - offsetInt
+	} else {
+		return 0, fmt.Errorf("For parameter `whence`, expected value in {0,1,2} but got: %#v", whence)
+	}
+
+	return int64(rs.offset), nil
+}
+
+func TestDownload(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	HandleGetImageDataSuccessfully(t)
+
+	rdr, err := imagedata.Download(fakeclient.ServiceClient(), "da3b75d9-3f4a-40e7-8a2c-bfab23927dea").Extract()
+	th.AssertNoErr(t, err)
+
+	bs, err := ioutil.ReadAll(rdr)
+	th.AssertNoErr(t, err)
+
+	th.AssertByteArrayEquals(t, []byte{34, 87, 0, 23, 23, 23, 56, 255, 254, 0}, bs)
+}
diff --git a/openstack/imageservice/v2/imagedata/urls.go b/openstack/imageservice/v2/imagedata/urls.go
new file mode 100644
index 0000000..ccd6416
--- /dev/null
+++ b/openstack/imageservice/v2/imagedata/urls.go
@@ -0,0 +1,13 @@
+package imagedata
+
+import "github.com/gophercloud/gophercloud"
+
+// `imageDataURL(c,i)` is the URL for the binary image data for the
+// image identified by ID `i` in the service `c`.
+func uploadURL(c *gophercloud.ServiceClient, imageID string) string {
+	return c.ServiceURL("images", imageID, "file")
+}
+
+func downloadURL(c *gophercloud.ServiceClient, imageID string) string {
+	return uploadURL(c, imageID)
+}