Explorar el Código

Process JSON and form base64 strings

Andrew Swistak hace 6 años
padre
commit
a79af855e5
Se han modificado 2 ficheros con 99 adiciones y 21 borrados
  1. 98 20
      web/parse.go
  2. 1 1
      web/server_test.go

+ 98 - 20
web/parse.go

@@ -2,61 +2,139 @@ package web
 
 import (
 	"bytes"
+	"encoding/base64"
 	"io"
 	"mime/multipart"
 	"net/http"
 
 	p "github.com/ajswis/go-pkparse-server/pokemon"
-	"github.com/ajswis/go-pkparse-server/pokemon-parsing"
+	pokemonparsing "github.com/ajswis/go-pkparse-server/pokemon-parsing"
 	"github.com/gin-gonic/gin"
 )
 
 /*
+	File uploads:
   curl -X POST http://localhost:8080/parse \
-	  -F "pkmn=@/path/to/test/file1" \
-	  -F "pkmn=@/path/to/test/file2" \
-	  -H "Content-Type: multipart/form-data"
+		-H "Content-Type: multipart/form-data" \
+	  --form "pokemon=@/path/to/test/file1" \
+	  --form "pokemon=@/path/to/test/file2"
+
+	JSON POST:
+  curl -X POST http://localhost:8080/parse \
+		-H "Content-Type: application/json" \
+		-d '{"pokemon": ["base64_encoded_pokemon_file", "another_base64_encoded_pokemon_file"]}'
+
+	Form POST:
+	curl -X POST http://localhost:8080/parse \
+		-H "Content-Type: multipart/form-data" \
+		--form 'pokemon=base64_encoded_pokemon_file' \
+		--form 'pokemon=another_base64_encoded_pokemon_file'
+
+	Form POST + File upload:
+  curl -X POST http://localhost:8080/parse \
+		-H "Content-Type: multipart/form-data" \
+	  --form "pokemon=@/path/to/test/file1" \
+	  --form "pokemon=@/path/to/test/file2" \
+		--form 'pokemon=base64_encoded_pokemon_file' \
 */
 
+type pokemonParseRequest struct {
+	Pokemon []string `json:"pokemon" form:"pokemon" binding:"required"`
+}
+
 // parse grabs multipart file uploads under the key `pkmn`, aggregates them, and
 // delegates work to the parser.
-// NOTE: This can probably be improved by 1) parallelizing upload reads, or 2)
-// not accepting file uploads, preferring to service binary data as encoded
-// strings
 func (s *Server) parse(c *gin.Context) {
-	form, err := c.MultipartForm()
+	var rawPokemon []p.RawPokemon
+
+	// If the POST is `multipart/form-data`, check for file uploads and read them
+	if c.ContentType() == "multipart/form-data" {
+		fileHeaders, err := uploadedPokemonFiles(c)
+		if err != nil {
+			render(c, http.StatusInternalServerError, gin.H{"error": err.Error()})
+			return
+		}
+
+		if len(fileHeaders) > 0 {
+			err = pokemonFromFileUpload(fileHeaders, &rawPokemon)
+			if err != nil {
+				render(c, http.StatusInternalServerError, gin.H{"error": err.Error()})
+				return
+			}
+		}
+	}
+
+	// For non-file-upload form submissions or all other `Content-Type`s
+	err := pokemonFromEncodedUpload(c, &rawPokemon)
 	if err != nil {
 		render(c, http.StatusInternalServerError, gin.H{"error": err.Error()})
 		return
 	}
 
-	var rawPokemon []p.RawPokemon
-	var files []*multipart.FileHeader = form.File["pkmn[]"]
+	pkmn, err := pokemonparsing.ParseAll(rawPokemon)
+	if err != nil {
+		render(c, http.StatusUnprocessableEntity, gin.H{"error": err.Error()})
+		return
+	}
+
+	render(c, http.StatusOK, pkmn)
+}
+
+// uploadedPokemonFiles extracts any existing FileHeaders from file uploads
+func uploadedPokemonFiles(c *gin.Context) ([]*multipart.FileHeader, error) {
+	form, err := c.MultipartForm()
+	if err != nil {
+		return nil, err
+	}
+
+	return form.File["pokemon"], nil
+}
+
+// pokemonFromFileUpload reads data from uploaded files and returns the raw
+// pokemon
+func pokemonFromFileUpload(files []*multipart.FileHeader, rawPokemon *[]p.RawPokemon) error {
 	for _, file := range files {
 		var err error
 		var src multipart.File
 
 		src, err = file.Open()
 		if err != nil {
-			render(c, http.StatusInternalServerError, gin.H{"error": err.Error()})
-			return
+			return err
 		}
 		defer src.Close()
 
 		buf := bytes.NewBuffer(nil)
 		if _, err = io.Copy(buf, src); err != nil {
-			render(c, http.StatusInternalServerError, gin.H{"error": err.Error()})
-			return
+			return err
 		}
 
-		rawPokemon = append(rawPokemon, buf.Bytes())
+		*rawPokemon = append(*rawPokemon, buf.Bytes())
 	}
 
-	pkmn, err := pokemonparsing.ParseAll(rawPokemon)
-	if err != nil {
-		render(c, http.StatusUnprocessableEntity, gin.H{"error": err.Error()})
-		return
+	return nil
+}
+
+// pokemonFromEncodedUpload binds the POST data to a `pokemonParseRequest` and
+// converts each base64 string to a `[]byte` slice.
+func pokemonFromEncodedUpload(c *gin.Context, rawPokemon *[]p.RawPokemon) error {
+	var request pokemonParseRequest
+
+	if err := c.ShouldBind(&request); err != nil {
+		return err
 	}
 
-	render(c, http.StatusOK, pkmn)
+	for _, pkmn := range request.Pokemon {
+		var err error
+		var decoded []byte
+
+		decoded, err = base64.StdEncoding.DecodeString(pkmn)
+
+		if err != nil {
+			return err
+		}
+
+		*rawPokemon = append(*rawPokemon, decoded)
+	}
+
+	return nil
 }

+ 1 - 1
web/server_test.go

@@ -18,7 +18,7 @@ func TestNewServer_routes(t *testing.T) {
 		handler string
 	}{
 		{path: "/ping", method: "GET", handler: "github.com/ajswis/go-pkparse-server/web.ping"},
-		{path: "/parse", method: "POST", handler: "github.com/ajswis/go-pkparse-server/web.(*Server).(github.com/ajswis/go-pkparse-server/web.parse)-fm"},
+		{path: "/parse", method: "POST", handler: "github.com/ajswis/go-pkparse-server/web.(*Server).parse-fm"},
 	}
 	for _, tt := range tests {
 		var ran bool