Skip to content

Commit

Permalink
Initialize project
Browse files Browse the repository at this point in the history
  • Loading branch information
eko committed Jan 6, 2019
0 parents commit 41470f6
Show file tree
Hide file tree
Showing 3 changed files with 328 additions and 0 deletions.
67 changes: 67 additions & 0 deletions README.md
@@ -0,0 +1,67 @@
graphql-go-upload
=================

[![GoDoc](https://godoc.org/github.com/eko/graphql-go-upload?status.png)](https://godoc.org/github.com/eko/graphql-go-upload)

This library exposes a middleware for the [GraphQL-Go](https://github.com/graph-gophers/graphql-go) project in order to expose a new `Upload` scalar type and allow you to send `multipart/form-data` POST requests containing files and fields data.

Installation
------------

```bash
$ dep ensure --add github.com/eko/graphql-go-upload
```

Add the middleware handler in your GraphQL project
--------------------------------------------------

Once the dependency is installed, simply update your GraphQL project code in order to add this middleware:

```go
import (
"github.com/eko/graphql-go-upload"
)

// ...

h := handler.GraphQL{
Schema: graphql.MustParseSchema(schema.String(), root, graphql.MaxParallelism(maxParallelism), graphql.MaxDepth(maxDepth)),
Handler: handler.NewHandler(conf, &m),
}

mux := mux.NewRouter()
mux.Handle("/graphql", upload.Handler(h)) // Add the middleware here (wrap the original handler)

s := &http.Server{
Addr: ":8000",
Handler: mux,
}
```

You're ready to use the new middleware!

Use the new Upload scalar type
------------------------------

In order to use the new Upload scalar type, you have to declare it in your GraphQL schema and use it in your mutations, this way:

```graphql
scalar Upload

type Mutation {
myUploadMutation(file: Upload!, title: String!): Boolean
}
```

Usage on client side
--------------------

On a client point of view, requests have to be formed this way:

```
curl http://localhost:8000/graphql \
-F operations='{ "query": "mutation DoUpload($file: Upload!, $title: String!) { upload(file: $file, title: $title) }", "variables": { "file": null, "title": null } }' \
-F map='{ "file": ["variables.file"], "title": ["variables.title"] }' \
-F file=@myfile.txt \
-F title="My content title"
```
199 changes: 199 additions & 0 deletions middleware.go
@@ -0,0 +1,199 @@
package upload

import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"mime"
"mime/multipart"
"net/http"
"os"
"path/filepath"
"strings"
"sync"
)

type postedFiles func(key string) (multipart.File, *multipart.FileHeader, error)
type graphqlParams struct {
Variables interface{} `json:"variables"`
Query interface{} `json:"query"`
Operations map[string]interface{} `json:"operations"`
Map map[string][]string `json:"map"`
}

type fileData struct {
Fields interface{}
Files postedFiles
MapEntryIndex string
EntryPaths []string
}

var (
mapEntries map[string][]string
operations map[string]interface{}
fileChannel = make(chan fileData)
wg sync.WaitGroup
)

// Handler is the middleware function that retrieves the incoming HTTP request and
// in case it is a POST and multipart/form-data request, re-maps the field values
// given in the GraphQL format and saves uploaded files.
//
// Here is how to implement the middleware handler (see upload.Handler use below):
//
// h := handler.GraphQL{
// Schema: graphql.MustParseSchema(schema.String(), root, graphql.MaxParallelism(maxParallelism), graphql.MaxDepth(maxDepth)),
// Handler: handler.NewHandler(conf, &m),
// }
//
// mux := mux.NewRouter()
// mux.Handle("/graphql", upload.Handler(h))
//
// s := &http.Server{
// Addr: ":8000",
// Handler: mux,
// }
//
func Handler(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !isMiddlewareSupported(r) {
next.ServeHTTP(w, r)
return
}

r.ParseMultipartForm((1 << 20) * 64)
m := r.PostFormValue("map")
if &m == nil {
http.Error(w, "Missing map field parameter", http.StatusBadRequest)
return
}

o := r.PostFormValue("operations")
if &o == nil {
http.Error(w, "Missing operations field parameter", http.StatusBadRequest)
return
}

err := json.Unmarshal([]byte(o), &operations)
if err != nil {
http.Error(w, "Cannot unmarshal operations: malformed query", http.StatusBadRequest)
return
}

err = json.Unmarshal([]byte(m), &mapEntries)
if err != nil {
http.Error(w, "Cannot unmarshal map entries: malformed query", http.StatusBadRequest)
return
}

mapOperations(mapEntries, operations, r)

graphqlParams := graphqlParams{
Variables: operations["variables"],
Query: operations["query"],
Operations: operations,
Map: mapEntries,
}

body, err := json.Marshal(graphqlParams)
if err == nil {
r.Body = ioutil.NopCloser(bytes.NewReader(body))
w.Header().Set("Content-Type", "application/json")
}

next.ServeHTTP(w, r)
})
}

func isMiddlewareSupported(r *http.Request) bool {
if r.Method != http.MethodPost {
return false
}

contentType := r.Header.Get("Content-Type")
mediatype, _, _ := mime.ParseMediaType(contentType)
if contentType == "" || mediatype != "multipart/form-data" {
return false
}

return true
}

func mapOperations(mapEntries map[string][]string, operations map[string]interface{}, r *http.Request) {
for idx, mapEntry := range mapEntries {
for _, entry := range mapEntry {
entryPaths := strings.Split(entry, ".")
fields := findFields(operations, entryPaths[:len(entryPaths)-1])

if value := r.PostForm.Get(idx); value != "" { // Form field values
entryPaths := strings.Split(entry, ".")
operations[entryPaths[0]].(map[string]interface{})[entryPaths[1]] = value
} else { // Try to catch an uploaded file
wg.Add(1)
go func() {
defer wg.Done()
mapTemporaryFileToOperations()
}()

fileChannel <- fileData{
Fields: fields,
Files: r.FormFile,
MapEntryIndex: idx,
EntryPaths: entryPaths,
}
}
}
}
wg.Wait()
}

func findFields(operations interface{}, entryPaths []string) map[string]interface{} {
for i := 0; i < len(entryPaths); i++ {
if arr, ok := operations.([]map[string]interface{}); ok {
operations = arr[i]

return findFields(operations, entryPaths)
} else if op, ok := operations.(map[string]interface{}); ok {
operations = op[entryPaths[i]]
}
}

return operations.(map[string]interface{})
}

func mapTemporaryFileToOperations() error {
params := <-fileChannel
file, handle, err := params.Files(params.MapEntryIndex)
if err != nil {
return fmt.Errorf("Could not access multipart file. Reason: %v", err)
}
defer file.Close()

data, err := ioutil.ReadAll(file)
if err != nil {
return fmt.Errorf("Could not read multipart file. Reason: %v", err)
}

f, err := ioutil.TempFile(os.TempDir(), fmt.Sprintf("graphqlupload-*%s", filepath.Ext(handle.Filename)))
if err != nil {
return fmt.Errorf("Unable to create temporary file. Reason: %v", err)
}

_, err = f.Write(data)
if err != nil {
return fmt.Errorf("Could not write temporary file. Reason: %v", err)
}

upload := &GraphQLUpload{
MIMEType: handle.Header.Get("Content-Type"),
Filename: handle.Filename,
Filepath: f.Name(),
}

if op, ok := params.Fields.(map[string]interface{}); ok {
op[params.EntryPaths[len(params.EntryPaths)-1]] = upload
}

return nil
}
62 changes: 62 additions & 0 deletions scalar.go
@@ -0,0 +1,62 @@
package upload

import (
"bufio"
"encoding/json"
"errors"
"io"
"os"
)

// GraphQLUpload is the struct used for the new "Upload" GraphQL scalar type
//
// It allows you to use the Upload type in your GraphQL schema, this way:
//
// scalar Upload
//
// type Mutation {
// upload(file: Upload!, title: String!, description: String!): Boolean
// }
type GraphQLUpload struct {
Filename string `json:"filename"`
MIMEType string `json:"mimetype"`
Filepath string `json:"filepath"`
}

// ImplementsGraphQLType is implemented to respect the GraphQL-Go Unmarshaler interface.
// It allows to chose the name of the GraphQL scalar type you want to implement
//
// Reference: https://github.com/graph-gophers/graphql-go/blob/bb9738501bd42a6536227b96068349b814379d6e/internal/exec/packer/packer.go#L319
func (u GraphQLUpload) ImplementsGraphQLType(name string) bool {
return name == "Upload"
}

// UnmarshalGraphQL is implemented to respect the GraphQL-Go Unmarshaler interface.
// It hydrates the GraphQLUpload struct with input data
//
// Reference: https://github.com/graph-gophers/graphql-go/blob/bb9738501bd42a6536227b96068349b814379d6e/internal/exec/packer/packer.go#L319
func (u *GraphQLUpload) UnmarshalGraphQL(input interface{}) error {
switch input := input.(type) {
case map[string]interface{}:
data, err := json.Marshal(input)
if err != nil {
u = &GraphQLUpload{}
} else {
json.Unmarshal(data, u)
}

return nil
default:
return errors.New("Cannot unmarshal received type as a GraphQLUpload type")
}
}

// GetReader returns the buffer of the uploaded (and temporary saved) file.
func (u *GraphQLUpload) GetReader() (io.Reader, error) {
f, err := os.Open(u.Filepath)
if err == nil {
return bufio.NewReader(f), nil
}

return nil, err
}

0 comments on commit 41470f6

Please sign in to comment.