1 package transport
2
3 import (
4 "encoding/json"
5 "io"
6 "mime"
7 "net/http"
8 "os"
9
10 "github.com/99designs/gqlgen/graphql"
11 )
12
13
14 type MultipartForm struct {
15
16
17 MaxUploadSize int64
18
19
20
21
22 MaxMemory int64
23
24
25
26 ResponseHeaders map[string][]string
27 }
28
29 var _ graphql.Transport = MultipartForm{}
30
31 func (f MultipartForm) Supports(r *http.Request) bool {
32 if r.Header.Get("Upgrade") != "" {
33 return false
34 }
35
36 mediaType, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
37 if err != nil {
38 return false
39 }
40
41 return r.Method == "POST" && mediaType == "multipart/form-data"
42 }
43
44 func (f MultipartForm) maxUploadSize() int64 {
45 if f.MaxUploadSize == 0 {
46 return 32 << 20
47 }
48 return f.MaxUploadSize
49 }
50
51 func (f MultipartForm) maxMemory() int64 {
52 if f.MaxMemory == 0 {
53 return 32 << 20
54 }
55 return f.MaxMemory
56 }
57
58 func (f MultipartForm) Do(w http.ResponseWriter, r *http.Request, exec graphql.GraphExecutor) {
59 writeHeaders(w, f.ResponseHeaders)
60
61 start := graphql.Now()
62
63 var err error
64 if r.ContentLength > f.maxUploadSize() {
65 writeJsonError(w, "failed to parse multipart form, request body too large")
66 return
67 }
68 r.Body = http.MaxBytesReader(w, r.Body, f.maxUploadSize())
69 defer r.Body.Close()
70
71 mr, err := r.MultipartReader()
72 if err != nil {
73 w.WriteHeader(http.StatusUnprocessableEntity)
74 writeJsonError(w, "failed to parse multipart form")
75 return
76 }
77
78 part, err := mr.NextPart()
79 if err != nil || part.FormName() != "operations" {
80 w.WriteHeader(http.StatusUnprocessableEntity)
81 writeJsonError(w, "first part must be operations")
82 return
83 }
84
85 var params graphql.RawParams
86 if err = jsonDecode(part, ¶ms); err != nil {
87 w.WriteHeader(http.StatusUnprocessableEntity)
88 writeJsonError(w, "operations form field could not be decoded")
89 return
90 }
91
92 part, err = mr.NextPart()
93 if err != nil || part.FormName() != "map" {
94 w.WriteHeader(http.StatusUnprocessableEntity)
95 writeJsonError(w, "second part must be map")
96 return
97 }
98
99 uploadsMap := map[string][]string{}
100 if err = json.NewDecoder(part).Decode(&uploadsMap); err != nil {
101 w.WriteHeader(http.StatusUnprocessableEntity)
102 writeJsonError(w, "map form field could not be decoded")
103 return
104 }
105
106 for {
107 part, err = mr.NextPart()
108 if err == io.EOF {
109 break
110 } else if err != nil {
111 w.WriteHeader(http.StatusUnprocessableEntity)
112 writeJsonErrorf(w, "failed to parse part")
113 return
114 }
115
116 key := part.FormName()
117 filename := part.FileName()
118 contentType := part.Header.Get("Content-Type")
119
120 paths := uploadsMap[key]
121 if len(paths) == 0 {
122 w.WriteHeader(http.StatusUnprocessableEntity)
123 writeJsonErrorf(w, "invalid empty operations paths list for key %s", key)
124 return
125 }
126 delete(uploadsMap, key)
127
128 var upload graphql.Upload
129 if r.ContentLength < f.maxMemory() {
130 fileBytes, err := io.ReadAll(part)
131 if err != nil {
132 w.WriteHeader(http.StatusUnprocessableEntity)
133 writeJsonErrorf(w, "failed to read file for key %s", key)
134 return
135 }
136 for _, path := range paths {
137 upload = graphql.Upload{
138 File: &bytesReader{s: &fileBytes, i: 0},
139 Size: int64(len(fileBytes)),
140 Filename: filename,
141 ContentType: contentType,
142 }
143
144 if err := params.AddUpload(upload, key, path); err != nil {
145 w.WriteHeader(http.StatusUnprocessableEntity)
146 writeJsonGraphqlError(w, err)
147 return
148 }
149 }
150 } else {
151 tmpFile, err := os.CreateTemp(os.TempDir(), "gqlgen-")
152 if err != nil {
153 w.WriteHeader(http.StatusUnprocessableEntity)
154 writeJsonErrorf(w, "failed to create temp file for key %s", key)
155 return
156 }
157 tmpName := tmpFile.Name()
158 defer func() {
159 _ = os.Remove(tmpName)
160 }()
161 fileSize, err := io.Copy(tmpFile, part)
162 if err != nil {
163 w.WriteHeader(http.StatusUnprocessableEntity)
164 if err := tmpFile.Close(); err != nil {
165 writeJsonErrorf(w, "failed to copy to temp file and close temp file for key %s", key)
166 return
167 }
168 writeJsonErrorf(w, "failed to copy to temp file for key %s", key)
169 return
170 }
171 if err := tmpFile.Close(); err != nil {
172 w.WriteHeader(http.StatusUnprocessableEntity)
173 writeJsonErrorf(w, "failed to close temp file for key %s", key)
174 return
175 }
176 for _, path := range paths {
177 pathTmpFile, err := os.Open(tmpName)
178 if err != nil {
179 w.WriteHeader(http.StatusUnprocessableEntity)
180 writeJsonErrorf(w, "failed to open temp file for key %s", key)
181 return
182 }
183 defer pathTmpFile.Close()
184 upload = graphql.Upload{
185 File: pathTmpFile,
186 Size: fileSize,
187 Filename: filename,
188 ContentType: contentType,
189 }
190
191 if err := params.AddUpload(upload, key, path); err != nil {
192 w.WriteHeader(http.StatusUnprocessableEntity)
193 writeJsonGraphqlError(w, err)
194 return
195 }
196 }
197 }
198 }
199
200 for key := range uploadsMap {
201 w.WriteHeader(http.StatusUnprocessableEntity)
202 writeJsonErrorf(w, "failed to get key %s from form", key)
203 return
204 }
205
206 params.Headers = r.Header
207
208 params.ReadTime = graphql.TraceTiming{
209 Start: start,
210 End: graphql.Now(),
211 }
212
213 rc, gerr := exec.CreateOperationContext(r.Context(), ¶ms)
214 if gerr != nil {
215 resp := exec.DispatchError(graphql.WithOperationContext(r.Context(), rc), gerr)
216 w.WriteHeader(statusFor(gerr))
217 writeJson(w, resp)
218 return
219 }
220 responses, ctx := exec.DispatchOperation(r.Context(), rc)
221 writeJson(w, responses(ctx))
222 }
223
View as plain text