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