launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #15148
[Merge] lp:~rvb/gomaasapi/upload-files2 into lp:gomaasapi
Raphaël Badin has proposed merging lp:~rvb/gomaasapi/upload-files2 into lp:gomaasapi.
Commit message:
Add a method to upload files to the API. Exercise the files API in the example package.
Requested reviews:
MAAS Maintainers (maas-maintainers)
For more details, see:
https://code.launchpad.net/~rvb/gomaasapi/upload-files2/+merge/147920
The meat of this branch is the change to nonIdempotentRequest: it now accept a new parameter, 'files'. If this parameter is non-nil, it will issue an multipart request with the given files instead of the standard post request.
I've also tested that this works against a real MAAS server (see the changes to live_example.go). Try it out with:
(first, get a MAAS dev server started)
go run example/live_example.go <<EOF
your:api:key
http://0.0.0.0:5240/api/1.0/
EOF
--
https://code.launchpad.net/~rvb/gomaasapi/upload-files2/+merge/147920
Your team MAAS Maintainers is requested to review the proposed merge of lp:~rvb/gomaasapi/upload-files2 into lp:gomaasapi.
=== modified file 'client.go'
--- client.go 2013-02-11 12:15:14 +0000
+++ client.go 2013-02-12 14:00:27 +0000
@@ -4,9 +4,12 @@
package gomaasapi
import (
+ "bytes"
"errors"
"fmt"
+ "io"
"io/ioutil"
+ "mime/multipart"
"net/http"
"net/url"
"strings"
@@ -65,30 +68,76 @@
return client.dispatchRequest(request)
}
+// writeMultiPartFiles writes the given files as parts of a multipart message
+// using the given writer.
+func writeMultiPartFiles(writer *multipart.Writer, files map[string][]byte) {
+ for fileName, fileContent := range files {
+
+ fw, err := writer.CreateFormFile(fileName, fileName)
+ if err != nil {
+ panic(err)
+ }
+ io.Copy(fw, bytes.NewBuffer(fileContent))
+ }
+}
+
+// writeMultiPartParams writes the given parameters as parts of a multipart
+// message using the given writer.
+func writeMultiPartParams(writer *multipart.Writer, parameters url.Values) {
+ for key, values := range parameters {
+ for _, value := range values {
+ fw, err := writer.CreateFormField(key)
+ if err != nil {
+ panic(err)
+ }
+ buffer := bytes.NewBufferString(value)
+ io.Copy(fw, buffer)
+ }
+ }
+
+}
+
// nonIdempotentRequest implements the common functionality of PUT and POST
// requests (but not GET or DELETE requests).
-func (client Client) nonIdempotentRequest(method string, uri *url.URL, parameters url.Values) ([]byte, error) {
- url := client.GetURL(uri)
- request, err := http.NewRequest(method, url.String(), strings.NewReader(string(parameters.Encode())))
- if err != nil {
- return nil, err
+func (client Client) nonIdempotentRequest(method string, uri *url.URL, parameters url.Values, files map[string][]byte) ([]byte, error) {
+ var request *http.Request
+ var err error
+ if files != nil {
+ // files is not nil, create a multipart request.
+ buf := new(bytes.Buffer)
+ writer := multipart.NewWriter(buf)
+ writeMultiPartFiles(writer, files)
+ writeMultiPartParams(writer, parameters)
+ writer.Close()
+ url := client.GetURL(uri)
+ request, err = http.NewRequest(method, url.String(), buf)
+ if err != nil {
+ return nil, err
+ }
+ request.Header.Set("Content-Type", writer.FormDataContentType())
+ } else {
+ url := client.GetURL(uri)
+ request, err = http.NewRequest(method, url.String(), strings.NewReader(string(parameters.Encode())))
+ if err != nil {
+ return nil, err
+ }
+ request.Header.Set("Content-Type", "application/x-www-form-urlencoded")
}
- request.Header.Set("Content-Type", "application/x-www-form-urlencoded")
return client.dispatchRequest(request)
}
// Post performs an HTTP "POST" to the API. This may be either an API method
// invocation (if you pass its name in "operation") or plain resource
// retrieval (if you leave "operation" blank).
-func (client Client) Post(uri *url.URL, operation string, parameters url.Values) ([]byte, error) {
+func (client Client) Post(uri *url.URL, operation string, parameters url.Values, files map[string][]byte) ([]byte, error) {
queryParams := url.Values{"op": {operation}}
uri.RawQuery = queryParams.Encode()
- return client.nonIdempotentRequest("POST", uri, parameters)
+ return client.nonIdempotentRequest("POST", uri, parameters, files)
}
// Put updates an object on the API, using an HTTP "PUT" request.
func (client Client) Put(uri *url.URL, parameters url.Values) ([]byte, error) {
- return client.nonIdempotentRequest("PUT", uri, parameters)
+ return client.nonIdempotentRequest("PUT", uri, parameters, nil)
}
// Delete deletes an object on the API, using an HTTP "DELETE" request.
=== modified file 'client_test.go'
--- client_test.go 2013-02-05 14:39:14 +0000
+++ client_test.go 2013-02-12 14:00:27 +0000
@@ -4,6 +4,8 @@
package gomaasapi
import (
+ "bytes"
+ "io/ioutil"
. "launchpad.net/gocheck"
"net/http"
"net/url"
@@ -72,7 +74,7 @@
c.Check(string(result), Equals, expectedResult)
}
-func (suite *ClientSuite) TestClientPostSendsRequest(c *C) {
+func (suite *ClientSuite) TestClientPostSendsRequestWithParams(c *C) {
URI, _ := url.Parse("/some/url")
expectedResult := "expected:result"
fullURI := URI.String() + "?op=list"
@@ -81,11 +83,40 @@
defer server.Close()
client, _ := NewAnonymousClient(server.URL)
- result, err := client.Post(URI, "list", params)
-
- c.Check(err, IsNil)
- c.Check(string(result), Equals, expectedResult)
- c.Check(*server.requestContent, Equals, "test=123")
+ result, err := client.Post(URI, "list", params, nil)
+
+ c.Check(err, IsNil)
+ c.Check(string(result), Equals, expectedResult)
+ postedValues, err := url.ParseQuery(*server.requestContent)
+ c.Check(err, IsNil)
+ expectedPostedValues, _ := url.ParseQuery("test=123")
+ c.Check(postedValues, DeepEquals, expectedPostedValues)
+}
+
+func (suite *ClientSuite) TestClientPostSendsMultipartRequest(c *C) {
+ URI, _ := url.Parse("/some/url")
+ expectedResult := "expected:result"
+ fullURI := URI.String() + "?op=add"
+ server := newSingleServingServer(fullURI, expectedResult, http.StatusOK)
+ defer server.Close()
+ client, _ := NewAnonymousClient(server.URL)
+ fileContent := []byte("content")
+ files := map[string][]byte{"testfile": fileContent}
+
+ result, err := client.Post(URI, "add", url.Values{}, files)
+
+ c.Check(err, IsNil)
+ c.Check(string(result), Equals, expectedResult)
+ // Recreate the request from server.requestContent to use the parsing
+ // utility from the http package (http.Request.FormFile).
+ request, err := http.NewRequest("POST", fullURI, bytes.NewBufferString(*server.requestContent))
+ c.Assert(err, IsNil)
+ request.Header.Set("Content-Type", server.requestHeader.Get("Content-Type"))
+
+ file, _, err := request.FormFile("testfile")
+ c.Check(err, IsNil)
+ receivedFileContent, err := ioutil.ReadAll(file)
+ c.Check(receivedFileContent, DeepEquals, fileContent)
}
func (suite *ClientSuite) TestClientPutSendsRequest(c *C) {
=== modified file 'example/live_example.go'
--- example/live_example.go 2013-02-12 05:42:32 +0000
+++ example/live_example.go 2013-02-12 14:00:27 +0000
@@ -10,6 +10,7 @@
import (
"fmt"
+ "bytes"
"launchpad.net/gomaasapi"
"net/url"
)
@@ -39,11 +40,49 @@
func main() {
getParams()
+
+ // Create API server endpoint.
authClient, err := gomaasapi.NewAuthenticatedClient(apiURL, apiKey)
checkError(err)
-
maas := gomaasapi.NewMAAS(*authClient)
+ // Exercice the API.
+ ManipulateNodes(maas)
+ ManipulateFiles(maas)
+
+ fmt.Println("All done.")
+}
+
+// ManipulateFiles exercices the /api/1.0/files/ API endpoint. Most precisely,
+// it uploads a files and then fetches it, making sure the received content
+// is the same as the one that was sent.
+func ManipulateFiles(maas gomaasapi.MAASObject) {
+ files := maas.GetSubObject("files")
+ fileContent := []byte("test file content")
+ filesToUpload := map[string][]byte{"file": fileContent}
+
+ // Upload a file.
+ fmt.Println("Uploading a file...")
+ _, err := files.CallPostFiles("add", url.Values{"filename": {"filename"}}, filesToUpload)
+ checkError(err)
+ fmt.Println("File sent.")
+
+ // Fetch the file.
+ fmt.Println("Fetching the file...")
+ fileResult, err := files.CallGet("get", url.Values{"filename": {"filename2"}})
+ checkError(err)
+ receivedFileContent, err := fileResult.GetBytes()
+ checkError(err)
+ if bytes.Compare(receivedFileContent, fileContent) != 0 {
+ panic("Received content differs from the content sent!")
+ }
+ fmt.Println("Got file.")
+}
+
+// ManipulateFiles exercices the /api/1.0/nodes/ API endpoint. Most precisely,
+// it lists the existing nodes, creates a new node, updates it and then
+// deletes it.
+func ManipulateNodes(maas gomaasapi.MAASObject) {
nodeListing := maas.GetSubObject("nodes")
// List nodes.
@@ -99,6 +138,4 @@
listNodes3, err := listNodeObjects3.GetArray()
checkError(err)
fmt.Printf("We've got %v nodes\n", len(listNodes3))
-
- fmt.Println("All done.")
}
=== modified file 'maasobject.go'
--- maasobject.go 2013-02-12 07:17:17 +0000
+++ maasobject.go 2013-02-12 14:00:27 +0000
@@ -144,7 +144,7 @@
// in "params." It returns the object's new value as received from the API.
func (obj MAASObject) Post(params url.Values) (JSONObject, error) {
uri := obj.URI()
- result, err := obj.client.Post(uri, "", params)
+ result, err := obj.client.Post(uri, "", params, nil)
if err != nil {
return JSONObject{}, err
}
@@ -184,8 +184,15 @@
// CallPost invokes a non-idempotent API method on this object.
func (obj MAASObject) CallPost(operation string, params url.Values) (JSONObject, error) {
+ return obj.CallPostFiles(operation, params, nil)
+}
+
+// CallPostFiles invokes a non-idempotent API method on this object. It is
+// similar to CallPost but has an extra parameter, 'files', which should
+// contain the files that will be uploaded to the API.
+func (obj MAASObject) CallPostFiles(operation string, params url.Values, files map[string][]byte) (JSONObject, error) {
uri := obj.URI()
- result, err := obj.client.Post(uri, operation, params)
+ result, err := obj.client.Post(uri, operation, params, files)
if err != nil {
return JSONObject{}, err
}