├── .gitignore
├── Gopkg.lock
├── Gopkg.toml
├── README.md
├── example
└── contact
│ ├── cmd
│ └── main.go
│ ├── contact.go
│ └── graphiql.html
├── middleware.go
└── scalar.go
/.gitignore:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/smithaitufe/go-graphql-upload/6442a1320276fdd1f4e651d532016c183da717b4/.gitignore
--------------------------------------------------------------------------------
/Gopkg.lock:
--------------------------------------------------------------------------------
1 | # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
2 |
3 |
4 | [solve-meta]
5 | analyzer-name = "dep"
6 | analyzer-version = 1
7 | input-imports = []
8 | solver-name = "gps-cdcl"
9 | solver-version = 1
10 |
--------------------------------------------------------------------------------
/Gopkg.toml:
--------------------------------------------------------------------------------
1 | # Gopkg.toml example
2 | #
3 | # Refer to https://golang.github.io/dep/docs/Gopkg.toml.html
4 | # for detailed Gopkg.toml documentation.
5 | #
6 | # required = ["github.com/user/thing/cmd/thing"]
7 | # ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
8 | #
9 | # [[constraint]]
10 | # name = "github.com/user/project"
11 | # version = "1.0.0"
12 | #
13 | # [[constraint]]
14 | # name = "github.com/user/project2"
15 | # branch = "dev"
16 | # source = "github.com/myfork/project2"
17 | #
18 | # [[override]]
19 | # name = "github.com/x/y"
20 | # version = "2.4.0"
21 | #
22 | # [prune]
23 | # non-go = false
24 | # go-tests = true
25 | # unused-packages = true
26 |
27 |
28 | [prune]
29 | go-tests = true
30 | unused-packages = true
31 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # go-graphqlupload
2 |
3 | A file upload middleware for gophers. It enables file upload operations to be done in [Go](https://github.com/google/) using [apollo-upload-client](https://github.com/jaydenseric/apollo-upload-client) and [graph-gophers/graphql-go](https://github.com/graph-gophers/graphql-go) packages.
4 |
5 |
6 | ## Usage
7 |
8 | Add the line below to the list of imports
9 |
10 | `github.com/smithaitufe/go-graphqlupload`
11 |
12 | Pass the graphql endpoint handler to the `graphqlupload.Handler` middleware function.
13 | Eg.
14 |
15 | `http.Handler("/graphql", graphqlupload.Handler(relay.Handler...))`
16 |
17 | In your input struct declaration, simply use the `graphqlupload.GraphQLUpload` type as datatype against the file input variable.
18 |
19 | Eg.
20 |
21 | ```og
22 | type contactInput struct {
23 | photo graphqlupload.GraphQLUpload
24 | }
25 | ```
26 | In the schema definition, add the following `scalar Upload` and you are good to go.
27 |
28 |
29 | The `graphqlupload.GraphQLUpload` has two methods
30 |
31 | `CreateReadStream` returns a reader to the upload file(s)
32 | `WriteFile` accepts a `name` string that indicates where the file should be saved into
33 |
34 | In the example folder, there is a working sample.
35 |
36 | Thanks.
37 |
38 | I hope to improve on it in the coming days
39 |
40 |
--------------------------------------------------------------------------------
/example/contact/cmd/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import "github.com/smithaitufe/go-graphql-upload/example/contact"
4 |
5 | func main() {
6 | contact.StartAndListenGraphQL(8099)
7 | }
8 |
--------------------------------------------------------------------------------
/example/contact/contact.go:
--------------------------------------------------------------------------------
1 | package contact
2 |
3 | import (
4 | "context"
5 | "encoding/json"
6 | "fmt"
7 | "io/ioutil"
8 | "log"
9 | "net/http"
10 |
11 | graphql "github.com/graph-gophers/graphql-go"
12 | "github.com/rs/cors"
13 | "github.com/smithaitufe/go-graphql-upload"
14 | )
15 |
16 | var schemastring = `
17 | schema {
18 | query: Query
19 | mutation: Mutation
20 | }
21 | type Query {
22 | contacts: [Contact]!
23 | }
24 | type Mutation {
25 | createContact(input: ContactInput!): Contact
26 | }
27 |
28 | scalar Upload
29 |
30 | input ContactInput {
31 | firstName: String!
32 | lastName: String!
33 | photo: Upload!
34 | }
35 |
36 | type Contact {
37 | firstName: String!
38 | lastName: String!
39 | }
40 | `
41 |
42 | func StartAndListenGraphQL(port int) {
43 | schema := graphql.MustParseSchema(schemastring, &schemaResolver{})
44 |
45 | h := handler{Schema: schema}
46 | http.Handle("/graphiql", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
47 | http.ServeFile(w, r, "../graphiql.html")
48 | }))
49 | http.Handle("/graphql", cors.Default().Handler(graphqlupload.Handler(h)))
50 | if err := http.ListenAndServe(fmt.Sprintf(":%d", port), nil); err != nil {
51 | log.Fatalf("could not start server. reason: %v", err)
52 | }
53 |
54 | }
55 |
56 | type handler struct {
57 | Schema *graphql.Schema
58 | }
59 |
60 | func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
61 | var params struct {
62 | OperationName string `json:"operationName"`
63 | Variables map[string]interface{} `json:"variables"`
64 | Query string `json:"query"`
65 | }
66 | if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil {
67 | fmt.Printf("bad request. %v", err.Error())
68 | http.Error(w, err.Error(), http.StatusBadRequest)
69 | return
70 | }
71 | response := h.Schema.Exec(r.Context(), params.Query, params.OperationName, params.Variables)
72 | responseJSON, err := json.Marshal(response)
73 | if err != nil {
74 | fmt.Println(err.Error())
75 | http.Error(w, err.Error(), http.StatusInternalServerError)
76 | return
77 | }
78 | w.Header().Set("Content-Type", "application/json")
79 | w.Write(responseJSON)
80 |
81 | }
82 |
83 | type schemaResolver struct{}
84 |
85 | type contactInput struct {
86 | FirstName string
87 | LastName string
88 | Photo graphqlupload.GraphQLUpload
89 | }
90 | type contact struct {
91 | FirstName string
92 | LastName string
93 | }
94 | type contactResolver struct {
95 | c contact
96 | }
97 |
98 | func (r *contactResolver) FirstName() string {
99 | return r.c.FirstName
100 | }
101 | func (r *contactResolver) LastName() string {
102 | return r.c.LastName
103 | }
104 | func (r *schemaResolver) Contacts(ctx context.Context) ([]*contactResolver, error) {
105 | contacts := []contact{
106 | contact{FirstName: "Smith", LastName: "Samuel"},
107 | contact{FirstName: "Friday", LastName: "Gabs"},
108 | contact{FirstName: "Miriam", LastName: "Jude"},
109 | contact{FirstName: "Stephen", LastName: "Stoke"},
110 | contact{FirstName: "Rachael", LastName: "Magdalene"},
111 | contact{FirstName: "Joseph", LastName: "Brown"},
112 | contact{FirstName: "Sonia", LastName: "Fish"},
113 | contact{FirstName: "Cynthia", LastName: "Gray"},
114 | contact{FirstName: "Saint", LastName: "Rose"},
115 | }
116 | resolvers := make([]*contactResolver, len(contacts))
117 | for k, v := range contacts {
118 | resolvers[k] = &contactResolver{v}
119 | }
120 | return resolvers, nil
121 | }
122 | func (r *schemaResolver) CreateContact(ctx context.Context, args struct {
123 | Input contactInput
124 | }) (*contactResolver, error) {
125 | // method 1: use the CreateReadStream to get a reader and manipulate it whichever way you want
126 | rd, _ := args.Input.Photo.CreateReadStream()
127 | b2, err := ioutil.ReadAll(rd)
128 | if err != nil {
129 | panic(err)
130 | }
131 | ioutil.WriteFile(args.Input.Photo.FileName, b2[:], 0666)
132 |
133 | // method 2: using WriteFile function. Easily write to any location in the local file system
134 | args.Input.Photo.WriteFile(args.Input.Photo.FileName)
135 | return &contactResolver{contact{FirstName: "Smithies", LastName: "Frank"}}, nil
136 | }
137 |
--------------------------------------------------------------------------------
/example/contact/graphiql.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | Loading...
13 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/middleware.go:
--------------------------------------------------------------------------------
1 | package graphqlupload
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "fmt"
7 | "io/ioutil"
8 | "mime"
9 | "mime/multipart"
10 | "net/http"
11 | "os"
12 | "path/filepath"
13 | "strconv"
14 | "strings"
15 | "sync"
16 | )
17 |
18 | type postedFileCollection func(key string) (multipart.File, *multipart.FileHeader, error)
19 | type params struct {
20 | OperationName string `json:"operationName"`
21 | Variables interface{} `json:"variables"`
22 | Query interface{} `json:"query"`
23 | Operations map[string]interface{} `json:"operations"`
24 | Map map[string][]string `json:"map"`
25 | }
26 | type fileOperation struct {
27 | Fields interface{}
28 | FileCollection postedFileCollection
29 | MapEntryIndex string
30 | SplittedPath []string
31 | }
32 |
33 | type graphqlUploadError struct {
34 | errorString string
35 | }
36 |
37 | func (e graphqlUploadError) Error() string {
38 | return e.errorString
39 | }
40 |
41 | var (
42 | errMissingOperationsParam = &graphqlUploadError{"Missing operations parameter"}
43 | errMissingMapParam = &graphqlUploadError{"Missing operations parameter"}
44 | errInvalidMapParam = &graphqlUploadError{"Invalid map parameter"}
45 | errIncompleteRequestProcessing = &graphqlUploadError{"Could not process request"}
46 | addFileChannel = make(chan fileOperation)
47 | )
48 |
49 | var mapEntries map[string][]string
50 | var singleOperations map[string]interface{}
51 | var wg sync.WaitGroup
52 |
53 | func Handler(next http.Handler) http.Handler {
54 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
55 | if r.Method == http.MethodPost {
56 | v := r.Header.Get("Content-Type")
57 | if v != "" {
58 | mediatype, _, _ := mime.ParseMediaType(v)
59 | if mediatype == "multipart/form-data" {
60 | r.ParseMultipartForm((1 << 20) * 64)
61 | m := r.PostFormValue("map")
62 | o := r.PostFormValue("operations")
63 |
64 | if &o == nil {
65 | http.Error(w, errMissingOperationsParam.Error(), http.StatusBadRequest)
66 | return
67 | }
68 | if &m == nil {
69 | http.Error(w, errMissingMapParam.Error(), http.StatusBadRequest)
70 | return
71 | }
72 | err := json.Unmarshal([]byte(o), &singleOperations)
73 | if err == nil {
74 | err = json.Unmarshal([]byte(m), &mapEntries)
75 | if err == nil {
76 | mo, err := singleTransformation(mapEntries, singleOperations, r.FormFile)
77 | if err != nil {
78 | http.Error(w, errIncompleteRequestProcessing.Error(), http.StatusBadRequest)
79 | return
80 | }
81 | p := params{
82 | OperationName: r.PostFormValue("operationName"),
83 | Variables: mo["variables"],
84 | Query: mo["query"],
85 | Operations: singleOperations,
86 | Map: mapEntries,
87 | }
88 | body, err := json.Marshal(p)
89 | if err == nil {
90 | r.Body = ioutil.NopCloser(bytes.NewReader(body))
91 | w.Header().Set("Content-Type", "application/json")
92 | }
93 | } else {
94 | http.Error(w, errInvalidMapParam.Error(), http.StatusBadRequest)
95 | return
96 | }
97 |
98 | } else {
99 | var batchOperations []map[string]interface{}
100 | err := json.Unmarshal([]byte(o), &batchOperations)
101 | if err == nil {
102 | if err := json.Unmarshal([]byte(m), &mapEntries); err == nil {
103 | _ = batchTransformation(mapEntries, batchOperations, r.FormFile)
104 | // p := params{
105 | // OperationName: r.PostFormValue("operationName"),
106 | // Variables: mo["variables"],
107 | // Query: mo["query"],
108 | // Operations: singleOperations,
109 | // Map: mapEntries,
110 | // }
111 | // body, err := json.Marshal(p)
112 | // if err == nil {
113 | // r.Body = ioutil.NopCloser(bytes.NewReader(body))
114 | // w.Header().Set("Content-Type", "application/json")
115 | // }
116 | }
117 | }
118 | }
119 |
120 | }
121 | }
122 | }
123 | next.ServeHTTP(w, r)
124 | })
125 | }
126 |
127 | func singleTransformation(mapEntries map[string][]string, operations map[string]interface{}, p postedFileCollection) (map[string]interface{}, error) {
128 |
129 | for idx, mapEntry := range mapEntries {
130 | for _, entry := range mapEntry {
131 | wg.Add(1)
132 | go func(entry, idx string, operations map[string]interface{}) {
133 | defer wg.Done()
134 |
135 | wg.Add(1)
136 | go func() {
137 | defer wg.Done()
138 | _, _ = addFile()
139 | }()
140 |
141 | entryPaths := strings.Split(entry, ".")
142 | fields := findField(operations, entryPaths[:len(entryPaths)-1])
143 |
144 | addFileChannel <- fileOperation{
145 | Fields: fields,
146 | FileCollection: p,
147 | MapEntryIndex: idx,
148 | SplittedPath: entryPaths,
149 | }
150 | }(entry, idx, operations)
151 |
152 | }
153 | }
154 | wg.Wait()
155 |
156 | return operations, nil
157 | }
158 | func batchTransformation(mapEntries map[string][]string, batchOperations []map[string]interface{}, p postedFileCollection) []map[string]interface{} {
159 | for _, mapEntry := range mapEntries {
160 | for _, entry := range mapEntry {
161 | entryPaths := strings.Split(entry, ".")
162 | opIdx, _ := strconv.Atoi(entryPaths[0])
163 | operations := batchOperations[opIdx]
164 | _ = findField(operations, entryPaths[:len(entryPaths)-1])
165 | // _ = addFileToOperations(fields, p, idx, entryPaths)
166 |
167 | }
168 | }
169 | return batchOperations
170 | }
171 | func findField(operations interface{}, entryPaths []string) map[string]interface{} {
172 | for i := 0; i < len(entryPaths); i++ {
173 | if arr, ok := operations.([]map[string]interface{}); ok {
174 | operations = arr[i]
175 | return findField(operations, entryPaths)
176 | } else if op, ok := operations.(map[string]interface{}); ok {
177 | operations = op[entryPaths[i]]
178 | }
179 | }
180 | return operations.(map[string]interface{})
181 | }
182 |
183 | func addFile() (interface{}, error) {
184 | params := <-addFileChannel
185 | file, handle, err := params.FileCollection(params.MapEntryIndex)
186 | if err != nil {
187 | return nil, fmt.Errorf("could not access multipart file. reason: %v", err)
188 | }
189 | defer file.Close()
190 |
191 | data, err := ioutil.ReadAll(file)
192 | if err != nil {
193 | return nil, fmt.Errorf("could not read multipart file. reason: %v", err)
194 | }
195 | f, err := ioutil.TempFile(os.TempDir(), fmt.Sprintf("graphql-upload-%s%s", handle.Filename, filepath.Ext(handle.Filename)))
196 | if err != nil {
197 | return nil, fmt.Errorf("unable to create temp file. reason: %v", err)
198 | }
199 |
200 | _, err = f.Write(data)
201 | if err != nil {
202 | return nil, fmt.Errorf("could not write file. reason: %v", err)
203 | }
204 | mimeType := handle.Header.Get("Content-Type")
205 | if op, ok := params.Fields.([]map[string]interface{}); ok {
206 | fidx, _ := strconv.Atoi(params.SplittedPath[len(params.SplittedPath)-1])
207 | upload := &GraphQLUpload{
208 | MIMEType: mimeType,
209 | FileName: handle.Filename,
210 | FilePath: f.Name(),
211 | }
212 | fmt.Printf("%#v", *upload)
213 | op[fidx][params.SplittedPath[len(params.SplittedPath)-1]] = upload
214 | return op, nil
215 | } else if op, ok := params.Fields.(map[string]interface{}); ok {
216 | op[params.SplittedPath[len(params.SplittedPath)-1]] = &GraphQLUpload{
217 | MIMEType: mimeType,
218 | FileName: handle.Filename,
219 | FilePath: f.Name(),
220 | }
221 | return op, nil
222 | }
223 | return nil, nil
224 | }
225 |
--------------------------------------------------------------------------------
/scalar.go:
--------------------------------------------------------------------------------
1 | package graphqlupload
2 |
3 | import (
4 | "bufio"
5 | "encoding/json"
6 | "fmt"
7 | "io"
8 | "os"
9 | )
10 |
11 | type GraphQLUpload struct {
12 | FileName string `json:"filename"`
13 | MIMEType string `json:"mimetype"`
14 | FilePath string `json:"filepath"`
15 | }
16 |
17 | func (_ GraphQLUpload) ImplementsGraphQLType(name string) bool { return name == "Upload" }
18 | func (u *GraphQLUpload) UnmarshalGraphQL(input interface{}) error {
19 | switch input := input.(type) {
20 | case map[string]interface{}:
21 | b, err := json.Marshal(input)
22 | if err != nil {
23 | u = &GraphQLUpload{}
24 | } else {
25 | json.Unmarshal(b, u)
26 | }
27 | return nil
28 | default:
29 | return fmt.Errorf("no implementation for the type specified")
30 | }
31 | }
32 | func createReadStream(u *GraphQLUpload) (io.Reader, error) {
33 | f, err := os.Open(u.FilePath)
34 | if err == nil {
35 | return bufio.NewReader(f), nil
36 | }
37 | return nil, err
38 | }
39 | func (u *GraphQLUpload) CreateReadStream() (io.Reader, error) {
40 | return createReadStream(u)
41 | }
42 |
43 | func (u *GraphQLUpload) WriteFile(name string) error {
44 | rdr, err := createReadStream(u)
45 | if err != nil {
46 | return err
47 | }
48 | fo, err := os.Create(name)
49 | if err != nil {
50 | return err
51 | }
52 | defer func() {
53 | if err := fo.Close(); err != nil {
54 | panic(err)
55 | }
56 | }()
57 | w := bufio.NewWriter(fo)
58 | buf := make([]byte, 1024*1024)
59 | for {
60 | n, err := rdr.Read(buf)
61 | if err != nil && err != io.EOF {
62 | return err
63 | }
64 | if n == 0 {
65 | break
66 | }
67 | if _, err := w.Write(buf[:n]); err != nil {
68 | return err
69 | }
70 | }
71 |
72 | if err = w.Flush(); err != nil {
73 | return err
74 | }
75 | return nil
76 | }
77 |
--------------------------------------------------------------------------------