Skip to content

Commit

Permalink
Support multipart requests (#110)
Browse files Browse the repository at this point in the history
* Example file service

* Parse multipart request

* Inject files to values

* Finalize upload integration

* Add README.md

* Add multi upload endpoint

* Fix multiple file upload

* Add multi upload instructions

* Add batch operations instructions

* Update branch dependency

* Accept empty content-type header

* Reorganise request parsing

* Add test for positive scenarios

* Some  negative tests

* Convert negative tests to table tests

* Drop todo in example's todo

* Update dependecy to the main library
  • Loading branch information
obukhov authored Sep 22, 2020
1 parent f78ce07 commit 4231784
Show file tree
Hide file tree
Showing 10 changed files with 1,015 additions and 194 deletions.
2 changes: 0 additions & 2 deletions examples/auth/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,9 @@ module github.com/nautilus/gateway/examples/auth

require (
github.com/graph-gophers/graphql-go v0.0.0-20190108123631-d5b7dc6be53b
github.com/kr/pretty v0.1.0 // indirect
github.com/nautilus/gateway v0.1.4
github.com/nautilus/graphql v0.0.9
github.com/vektah/gqlparser v1.1.0
github.com/vektah/gqlparser/v2 v2.0.1 // indirect
)

go 1.13
72 changes: 72 additions & 0 deletions examples/fileupload/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# File uploads

This example demonstrates proxying file upload requests according to specification:
- https://github.com/jaydenseric/graphql-multipart-request-spec

## How to test

1. Run file upload service

```
cd examples/fileupload/
go run serviceUpload.go
```

2. Run the gateway
```
go run ./cmd/ start --port 4000 --services http://localhost:5000
```

3. Execute file upload query:

```
curl localhost:4000/graphql \
-F operations='{ "query": "mutation ($someFile: Upload!) { upload(file: $someFile) }", "variables": { "someFile": null } }' \
-F map='{ "0": ["variables.someFile"] }' \
-F [email protected]
```

4. Validate that the file is uploaded to temporary folder:

```
ls examples/fileupload/tmp/
> cd4c0810-d5d7-4adf-9edb-bea74eadae4e
head -n3 examples/fileupload/tmp/*
> # nautilus/gateway
>
> ![CI Checks](https://github.com/nautilus/gateway/workflows/CI%20Checks/badge.svg?branch=master) [![Coverage Status](https://coveralls.io/repos/github/nautilus/gateway/badge.svg?branch=master)](https://coveralls.io/github/nautilus/gateway?branch=master) [![Go Report Card](https://goreportcard.com/badge/github.com/nautilus/gateway)](https://goreportcard.com/report/github.com/nautilus/gateway)
```

5. Execute multi file upload query:
```
curl localhost:4000/graphql \
-F operations='{"query":"mutation TestFileUpload(\n $someFile: Upload!,\n\t$allFiles: [Upload!]!\n) {\n upload(file: $someFile)\n uploadMulti(files: $allFiles)\n}","variables":{"someFile":null,"allFiles":[null,null]},"operationName":"TestFileUpload"}' \
-F map='{"0":["variables.someFile"],"1":["variables.allFiles.0"],"2":["variables.allFiles.1"]}' \
-F [email protected] \
-F [email protected] \
-F [email protected]
```

6. Validate that more files are created in the folder:

```
ls -la examples/fileupload/tmp/
> -rw-rw-r-- 1 user user 924 Sep 3 00:12 343b9067-f2be-4ea9-b73b-4e8390ed55c7
> -rw-rw-r-- 1 user user 1557 Sep 3 00:12 5417f766-8e7d-44ef-afb6-90ec0b4c548c
> -rw-rw-r-- 1 user user 15089 Sep 3 00:12 a590196f-6450-4785-8998-8013ff7c8cf3
```

7. Testing batch mode

```
curl localhost:4000/graphql \
-F operations='[{"query":"mutation ($someFile: Upload!) { upload(file: $someFile) }","variables":{"someFile":null}}, {"query":"mutation TestFileUpload(\n $someFile: Upload!,\n\t$allFiles: [Upload!]!\n) {\n upload(file: $someFile)\n uploadMulti(files: $allFiles)\n}","variables":{"someFile":null,"allFiles":[null,null]},"operationName":"TestFileUpload"}]' \
-F map='{"0":["0.variables.someFile"],"1":["1.variables.someFile"],"2":["1.variables.allFiles.0"],"3":["1.variables.allFiles.1"]}' \
-F [email protected] \
-F [email protected] \
-F [email protected] \
-F [email protected]
```
9 changes: 9 additions & 0 deletions examples/fileupload/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
module github.com/nautilus/gateway/example/fileupload

go 1.14

require (
github.com/graphql-go/graphql v0.7.9
github.com/jpascal/graphql-upload v0.0.0-20200219114743-2a693c100233
github.com/satori/go.uuid v1.2.0
)
6 changes: 6 additions & 0 deletions examples/fileupload/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
github.com/graphql-go/graphql v0.7.9 h1:5Va/Rt4l5g3YjwDnid3vFfn43faaQBq7rMcIZ0VnV34=
github.com/graphql-go/graphql v0.7.9/go.mod h1:k6yrAYQaSP59DC5UVxbgxESlmVyojThKdORUqGDGmrI=
github.com/jpascal/graphql-upload v0.0.0-20200219114743-2a693c100233 h1:6tY2KAHlytnGu9s7ceFZURenjqmxbVHwGembFi6sz+c=
github.com/jpascal/graphql-upload v0.0.0-20200219114743-2a693c100233/go.mod h1:I/WT/Xwt4isoRE43Sr1LG8T0bGkxmYEaQRvKbXSdVSo=
github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
199 changes: 199 additions & 0 deletions examples/fileupload/serviceUpload.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
package main

import (
"crypto/sha1"
"encoding/hex"
"errors"
"fmt"
"github.com/graphql-go/graphql"
handler "github.com/jpascal/graphql-upload"
uuid "github.com/satori/go.uuid"
"io"
"io/ioutil"
"log"
"net/http"
"os"
"path"
)

var UploadType = graphql.NewScalar(graphql.ScalarConfig{
Name: "Upload",
Description: "Scalar upload object",
})

type FileWrapper struct {
File *os.File
Name string
}

var File = graphql.NewObject(graphql.ObjectConfig{
Name: "File",
Description: "File object",
Fields: graphql.Fields{
"name": &graphql.Field{
Type: graphql.String,
Resolve: func(params graphql.ResolveParams) (interface{}, error) {
file := params.Source.(*FileWrapper)
name := path.Base(file.Name)

return name, nil
},
},
"hash": &graphql.Field{
Type: graphql.String,
Resolve: func(params graphql.ResolveParams) (interface{}, error) {
file := params.Source.(*FileWrapper)
if data, err := ioutil.ReadAll(file.File); err == nil {
fileHash := sha1.Sum(data)

return hex.EncodeToString(fileHash[:]), nil
} else {
return nil, err
}

},
},
"size": &graphql.Field{
Type: graphql.Int,
Resolve: func(params graphql.ResolveParams) (interface{}, error) {
file := params.Source.(*FileWrapper)
if info, err := file.File.Stat(); err != nil {
return nil, err
} else {
return info.Size(), nil
}
},
},
},
})

func main() {
schema, err := graphql.NewSchema(
graphql.SchemaConfig{
Query: graphql.NewObject(
graphql.ObjectConfig{
Name: "Query",
Fields: graphql.Fields{
"file": &graphql.Field{
Type: File,
Args: graphql.FieldConfigArgument{
"id": &graphql.ArgumentConfig{
Type: graphql.NewNonNull(graphql.String),
},
},
Resolve: func(params graphql.ResolveParams) (interface{}, error) {
if fileId, ok := params.Args["id"].(string); ok {
fileUuid, err := uuid.FromString(fileId)
if err != nil {
return nil, err
}

file, err := os.Open("tmp/" + fileUuid.String())
if err != nil {
return nil, err
}

return &FileWrapper{File: file, Name: fileUuid.String()}, nil
} else {
return nil, errors.New("file id is not provided")
}
},
},
},
}),
Mutation: graphql.NewObject(
graphql.ObjectConfig{
Name: "Mutation",
Fields: graphql.Fields{
"upload": &graphql.Field{
Type: graphql.NewNonNull(graphql.String),
Args: graphql.FieldConfigArgument{
"file": &graphql.ArgumentConfig{
Type: graphql.NewNonNull(UploadType),
},
},
Resolve: func(params graphql.ResolveParams) (interface{}, error) {
upload, uploadPresent := params.Args["file"].(handler.File)
if uploadPresent {
id := uuid.NewV4().String()
targetFile, err := os.Create("tmp/" + id)
if err != nil {
return nil, err
}

defer targetFile.Close()
nBytes, err := io.Copy(targetFile, upload.File)
if err != nil {
return nil, err
}

log.Println("File saved nBytes: ", nBytes)
return id, nil
} else {
return nil, errors.New("no file found in request")
}

},
},
"uploadMulti": &graphql.Field{
Type: graphql.NewNonNull(graphql.NewList(graphql.NewNonNull(graphql.String))),
Args: graphql.FieldConfigArgument{
"files": &graphql.ArgumentConfig{
Type: graphql.NewNonNull(graphql.NewList(graphql.NewNonNull(UploadType))),
},
},
Resolve: func(params graphql.ResolveParams) (interface{}, error) {
uploads, uploadPresent := params.Args["files"].([]interface{})
if uploadPresent {
var result []string
for i, uploadItem := range uploads {

upload, ok := uploadItem.(handler.File)
if !ok {
return nil, errors.New(fmt.Sprintf("type of file %d is wrong", i))
}

id := uuid.NewV4().String()
targetFile, err := os.Create("tmp/" + id)
if err != nil {
return nil, err
}

defer targetFile.Close()
nBytes, err := io.Copy(targetFile, upload.File)
if err != nil {
return nil, err
}

log.Println("File saved nBytes: ", nBytes)
result = append(result, id)
}

return result, nil
} else {
return nil, errors.New("no file found in request")
}

},
},
},
}),
})
if err != nil {
panic(err)
}

server := &http.Server{Addr: "0.0.0.0:5000", Handler: handler.New(func(request *handler.Request) interface{} {
return graphql.Do(graphql.Params{
RequestString: request.Query,
OperationName: request.OperationName,
VariableValues: request.Variables,
Schema: schema,
Context: request.Context,
})
}, &handler.Config{
MaxBodySize: 1024,
}),
}
server.ListenAndServe()
}
2 changes: 2 additions & 0 deletions examples/fileupload/tmp/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
*
!.gitignore
20 changes: 2 additions & 18 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,34 +2,18 @@ module github.com/nautilus/gateway

require (
github.com/99designs/gqlgen v0.11.3
github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect
github.com/gorilla/websocket v1.4.1 // indirect
github.com/graph-gophers/graphql-go v0.0.0-20190108123631-d5b7dc6be53b
github.com/graphql-go/graphql v0.7.9 // indirect
github.com/hashicorp/golang-lru v0.5.4 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
github.com/matryer/moq v0.0.0-20200125112110-7615cbe60268 // indirect
github.com/mitchellh/gox v1.0.1 // indirect
github.com/mitchellh/mapstructure v1.2.2
github.com/nautilus/graphql v0.0.10
github.com/nautilus/graphql v0.0.11
github.com/opentracing/opentracing-go v1.1.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/sirupsen/logrus v1.4.2
github.com/spf13/cobra v0.0.3
github.com/spf13/pflag v1.0.3 // indirect
github.com/spf13/cobra v0.0.5
github.com/stretchr/testify v1.4.0
github.com/tcnksm/ghr v0.13.0 // indirect
github.com/urfave/cli v1.22.2 // indirect
github.com/vektah/dataloaden v0.3.0 // indirect
github.com/vektah/gqlparser v1.1.0
github.com/vektah/gqlparser/v2 v2.0.1
golang.org/x/crypto v0.0.0-20200320181102-891825fb96df // indirect
golang.org/x/mod v0.2.0 // indirect
golang.org/x/net v0.0.0-20200320220750-118fecf932d8
golang.org/x/sys v0.0.0-20200321134203-328b4cd54aae // indirect
golang.org/x/tools v0.0.0-20200225022059-a0ec867d517c // indirect
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 // indirect
gopkg.in/yaml.v2 v2.2.8 // indirect
)

Expand Down
Loading

0 comments on commit 4231784

Please sign in to comment.