Skip to content

Commit 4231784

Browse files
authored
Support multipart requests (#110)
* 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
1 parent f78ce07 commit 4231784

File tree

10 files changed

+1015
-194
lines changed

10 files changed

+1015
-194
lines changed

examples/auth/go.mod

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,9 @@ module github.com/nautilus/gateway/examples/auth
22

33
require (
44
github.com/graph-gophers/graphql-go v0.0.0-20190108123631-d5b7dc6be53b
5-
github.com/kr/pretty v0.1.0 // indirect
65
github.com/nautilus/gateway v0.1.4
76
github.com/nautilus/graphql v0.0.9
87
github.com/vektah/gqlparser v1.1.0
9-
github.com/vektah/gqlparser/v2 v2.0.1 // indirect
108
)
119

1210
go 1.13

examples/fileupload/README.md

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# File uploads
2+
3+
This example demonstrates proxying file upload requests according to specification:
4+
- https://github.com/jaydenseric/graphql-multipart-request-spec
5+
6+
## How to test
7+
8+
1. Run file upload service
9+
10+
```
11+
cd examples/fileupload/
12+
13+
go run serviceUpload.go
14+
```
15+
16+
2. Run the gateway
17+
```
18+
go run ./cmd/ start --port 4000 --services http://localhost:5000
19+
```
20+
21+
3. Execute file upload query:
22+
23+
```
24+
curl localhost:4000/graphql \
25+
-F operations='{ "query": "mutation ($someFile: Upload!) { upload(file: $someFile) }", "variables": { "someFile": null } }' \
26+
-F map='{ "0": ["variables.someFile"] }' \
27+
28+
```
29+
30+
4. Validate that the file is uploaded to temporary folder:
31+
32+
```
33+
ls examples/fileupload/tmp/
34+
> cd4c0810-d5d7-4adf-9edb-bea74eadae4e
35+
36+
head -n3 examples/fileupload/tmp/*
37+
> # nautilus/gateway
38+
>
39+
> ![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)
40+
```
41+
42+
5. Execute multi file upload query:
43+
```
44+
curl localhost:4000/graphql \
45+
-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"}' \
46+
-F map='{"0":["variables.someFile"],"1":["variables.allFiles.0"],"2":["variables.allFiles.1"]}' \
47+
48+
49+
50+
```
51+
52+
6. Validate that more files are created in the folder:
53+
54+
```
55+
ls -la examples/fileupload/tmp/
56+
57+
> -rw-rw-r-- 1 user user 924 Sep 3 00:12 343b9067-f2be-4ea9-b73b-4e8390ed55c7
58+
> -rw-rw-r-- 1 user user 1557 Sep 3 00:12 5417f766-8e7d-44ef-afb6-90ec0b4c548c
59+
> -rw-rw-r-- 1 user user 15089 Sep 3 00:12 a590196f-6450-4785-8998-8013ff7c8cf3
60+
```
61+
62+
7. Testing batch mode
63+
64+
```
65+
curl localhost:4000/graphql \
66+
-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"}]' \
67+
-F map='{"0":["0.variables.someFile"],"1":["1.variables.someFile"],"2":["1.variables.allFiles.0"],"3":["1.variables.allFiles.1"]}' \
68+
69+
70+
71+
72+
```

examples/fileupload/go.mod

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
module github.com/nautilus/gateway/example/fileupload
2+
3+
go 1.14
4+
5+
require (
6+
github.com/graphql-go/graphql v0.7.9
7+
github.com/jpascal/graphql-upload v0.0.0-20200219114743-2a693c100233
8+
github.com/satori/go.uuid v1.2.0
9+
)

examples/fileupload/go.sum

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
github.com/graphql-go/graphql v0.7.9 h1:5Va/Rt4l5g3YjwDnid3vFfn43faaQBq7rMcIZ0VnV34=
2+
github.com/graphql-go/graphql v0.7.9/go.mod h1:k6yrAYQaSP59DC5UVxbgxESlmVyojThKdORUqGDGmrI=
3+
github.com/jpascal/graphql-upload v0.0.0-20200219114743-2a693c100233 h1:6tY2KAHlytnGu9s7ceFZURenjqmxbVHwGembFi6sz+c=
4+
github.com/jpascal/graphql-upload v0.0.0-20200219114743-2a693c100233/go.mod h1:I/WT/Xwt4isoRE43Sr1LG8T0bGkxmYEaQRvKbXSdVSo=
5+
github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
6+
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
package main
2+
3+
import (
4+
"crypto/sha1"
5+
"encoding/hex"
6+
"errors"
7+
"fmt"
8+
"github.com/graphql-go/graphql"
9+
handler "github.com/jpascal/graphql-upload"
10+
uuid "github.com/satori/go.uuid"
11+
"io"
12+
"io/ioutil"
13+
"log"
14+
"net/http"
15+
"os"
16+
"path"
17+
)
18+
19+
var UploadType = graphql.NewScalar(graphql.ScalarConfig{
20+
Name: "Upload",
21+
Description: "Scalar upload object",
22+
})
23+
24+
type FileWrapper struct {
25+
File *os.File
26+
Name string
27+
}
28+
29+
var File = graphql.NewObject(graphql.ObjectConfig{
30+
Name: "File",
31+
Description: "File object",
32+
Fields: graphql.Fields{
33+
"name": &graphql.Field{
34+
Type: graphql.String,
35+
Resolve: func(params graphql.ResolveParams) (interface{}, error) {
36+
file := params.Source.(*FileWrapper)
37+
name := path.Base(file.Name)
38+
39+
return name, nil
40+
},
41+
},
42+
"hash": &graphql.Field{
43+
Type: graphql.String,
44+
Resolve: func(params graphql.ResolveParams) (interface{}, error) {
45+
file := params.Source.(*FileWrapper)
46+
if data, err := ioutil.ReadAll(file.File); err == nil {
47+
fileHash := sha1.Sum(data)
48+
49+
return hex.EncodeToString(fileHash[:]), nil
50+
} else {
51+
return nil, err
52+
}
53+
54+
},
55+
},
56+
"size": &graphql.Field{
57+
Type: graphql.Int,
58+
Resolve: func(params graphql.ResolveParams) (interface{}, error) {
59+
file := params.Source.(*FileWrapper)
60+
if info, err := file.File.Stat(); err != nil {
61+
return nil, err
62+
} else {
63+
return info.Size(), nil
64+
}
65+
},
66+
},
67+
},
68+
})
69+
70+
func main() {
71+
schema, err := graphql.NewSchema(
72+
graphql.SchemaConfig{
73+
Query: graphql.NewObject(
74+
graphql.ObjectConfig{
75+
Name: "Query",
76+
Fields: graphql.Fields{
77+
"file": &graphql.Field{
78+
Type: File,
79+
Args: graphql.FieldConfigArgument{
80+
"id": &graphql.ArgumentConfig{
81+
Type: graphql.NewNonNull(graphql.String),
82+
},
83+
},
84+
Resolve: func(params graphql.ResolveParams) (interface{}, error) {
85+
if fileId, ok := params.Args["id"].(string); ok {
86+
fileUuid, err := uuid.FromString(fileId)
87+
if err != nil {
88+
return nil, err
89+
}
90+
91+
file, err := os.Open("tmp/" + fileUuid.String())
92+
if err != nil {
93+
return nil, err
94+
}
95+
96+
return &FileWrapper{File: file, Name: fileUuid.String()}, nil
97+
} else {
98+
return nil, errors.New("file id is not provided")
99+
}
100+
},
101+
},
102+
},
103+
}),
104+
Mutation: graphql.NewObject(
105+
graphql.ObjectConfig{
106+
Name: "Mutation",
107+
Fields: graphql.Fields{
108+
"upload": &graphql.Field{
109+
Type: graphql.NewNonNull(graphql.String),
110+
Args: graphql.FieldConfigArgument{
111+
"file": &graphql.ArgumentConfig{
112+
Type: graphql.NewNonNull(UploadType),
113+
},
114+
},
115+
Resolve: func(params graphql.ResolveParams) (interface{}, error) {
116+
upload, uploadPresent := params.Args["file"].(handler.File)
117+
if uploadPresent {
118+
id := uuid.NewV4().String()
119+
targetFile, err := os.Create("tmp/" + id)
120+
if err != nil {
121+
return nil, err
122+
}
123+
124+
defer targetFile.Close()
125+
nBytes, err := io.Copy(targetFile, upload.File)
126+
if err != nil {
127+
return nil, err
128+
}
129+
130+
log.Println("File saved nBytes: ", nBytes)
131+
return id, nil
132+
} else {
133+
return nil, errors.New("no file found in request")
134+
}
135+
136+
},
137+
},
138+
"uploadMulti": &graphql.Field{
139+
Type: graphql.NewNonNull(graphql.NewList(graphql.NewNonNull(graphql.String))),
140+
Args: graphql.FieldConfigArgument{
141+
"files": &graphql.ArgumentConfig{
142+
Type: graphql.NewNonNull(graphql.NewList(graphql.NewNonNull(UploadType))),
143+
},
144+
},
145+
Resolve: func(params graphql.ResolveParams) (interface{}, error) {
146+
uploads, uploadPresent := params.Args["files"].([]interface{})
147+
if uploadPresent {
148+
var result []string
149+
for i, uploadItem := range uploads {
150+
151+
upload, ok := uploadItem.(handler.File)
152+
if !ok {
153+
return nil, errors.New(fmt.Sprintf("type of file %d is wrong", i))
154+
}
155+
156+
id := uuid.NewV4().String()
157+
targetFile, err := os.Create("tmp/" + id)
158+
if err != nil {
159+
return nil, err
160+
}
161+
162+
defer targetFile.Close()
163+
nBytes, err := io.Copy(targetFile, upload.File)
164+
if err != nil {
165+
return nil, err
166+
}
167+
168+
log.Println("File saved nBytes: ", nBytes)
169+
result = append(result, id)
170+
}
171+
172+
return result, nil
173+
} else {
174+
return nil, errors.New("no file found in request")
175+
}
176+
177+
},
178+
},
179+
},
180+
}),
181+
})
182+
if err != nil {
183+
panic(err)
184+
}
185+
186+
server := &http.Server{Addr: "0.0.0.0:5000", Handler: handler.New(func(request *handler.Request) interface{} {
187+
return graphql.Do(graphql.Params{
188+
RequestString: request.Query,
189+
OperationName: request.OperationName,
190+
VariableValues: request.Variables,
191+
Schema: schema,
192+
Context: request.Context,
193+
})
194+
}, &handler.Config{
195+
MaxBodySize: 1024,
196+
}),
197+
}
198+
server.ListenAndServe()
199+
}

examples/fileupload/tmp/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
*
2+
!.gitignore

go.mod

Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,34 +2,18 @@ module github.com/nautilus/gateway
22

33
require (
44
github.com/99designs/gqlgen v0.11.3
5-
github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect
6-
github.com/gorilla/websocket v1.4.1 // indirect
75
github.com/graph-gophers/graphql-go v0.0.0-20190108123631-d5b7dc6be53b
86
github.com/graphql-go/graphql v0.7.9 // indirect
9-
github.com/hashicorp/golang-lru v0.5.4 // indirect
10-
github.com/inconshreveable/mousetrap v1.0.0 // indirect
117
github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
12-
github.com/matryer/moq v0.0.0-20200125112110-7615cbe60268 // indirect
13-
github.com/mitchellh/gox v1.0.1 // indirect
148
github.com/mitchellh/mapstructure v1.2.2
15-
github.com/nautilus/graphql v0.0.10
9+
github.com/nautilus/graphql v0.0.11
1610
github.com/opentracing/opentracing-go v1.1.0 // indirect
17-
github.com/pkg/errors v0.9.1 // indirect
1811
github.com/sirupsen/logrus v1.4.2
19-
github.com/spf13/cobra v0.0.3
20-
github.com/spf13/pflag v1.0.3 // indirect
12+
github.com/spf13/cobra v0.0.5
2113
github.com/stretchr/testify v1.4.0
22-
github.com/tcnksm/ghr v0.13.0 // indirect
23-
github.com/urfave/cli v1.22.2 // indirect
24-
github.com/vektah/dataloaden v0.3.0 // indirect
25-
github.com/vektah/gqlparser v1.1.0
2614
github.com/vektah/gqlparser/v2 v2.0.1
27-
golang.org/x/crypto v0.0.0-20200320181102-891825fb96df // indirect
28-
golang.org/x/mod v0.2.0 // indirect
2915
golang.org/x/net v0.0.0-20200320220750-118fecf932d8
3016
golang.org/x/sys v0.0.0-20200321134203-328b4cd54aae // indirect
31-
golang.org/x/tools v0.0.0-20200225022059-a0ec867d517c // indirect
32-
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 // indirect
3317
gopkg.in/yaml.v2 v2.2.8 // indirect
3418
)
3519

0 commit comments

Comments
 (0)