package transport import ( "encoding/json" "io" "mime" "net/http" "os" "github.com/99designs/gqlgen/graphql" ) // MultipartForm the Multipart request spec https://github.com/jaydenseric/graphql-multipart-request-spec type MultipartForm struct { // MaxUploadSize sets the maximum number of bytes used to parse a request body // as multipart/form-data. MaxUploadSize int64 // MaxMemory defines the maximum number of bytes used to parse a request body // as multipart/form-data in memory, with the remainder stored on disk in // temporary files. MaxMemory int64 // Map of all headers that are added to graphql response. If not // set, only one header: Content-Type: application/json will be set. ResponseHeaders map[string][]string } var _ graphql.Transport = MultipartForm{} func (f MultipartForm) Supports(r *http.Request) bool { if r.Header.Get("Upgrade") != "" { return false } mediaType, _, err := mime.ParseMediaType(r.Header.Get("Content-Type")) if err != nil { return false } return r.Method == "POST" && mediaType == "multipart/form-data" } func (f MultipartForm) maxUploadSize() int64 { if f.MaxUploadSize == 0 { return 32 << 20 } return f.MaxUploadSize } func (f MultipartForm) maxMemory() int64 { if f.MaxMemory == 0 { return 32 << 20 } return f.MaxMemory } func (f MultipartForm) Do(w http.ResponseWriter, r *http.Request, exec graphql.GraphExecutor) { writeHeaders(w, f.ResponseHeaders) start := graphql.Now() var err error if r.ContentLength > f.maxUploadSize() { writeJsonError(w, "failed to parse multipart form, request body too large") return } r.Body = http.MaxBytesReader(w, r.Body, f.maxUploadSize()) defer r.Body.Close() mr, err := r.MultipartReader() if err != nil { w.WriteHeader(http.StatusUnprocessableEntity) writeJsonError(w, "failed to parse multipart form") return } part, err := mr.NextPart() if err != nil || part.FormName() != "operations" { w.WriteHeader(http.StatusUnprocessableEntity) writeJsonError(w, "first part must be operations") return } var params graphql.RawParams if err = jsonDecode(part, ¶ms); err != nil { w.WriteHeader(http.StatusUnprocessableEntity) writeJsonError(w, "operations form field could not be decoded") return } part, err = mr.NextPart() if err != nil || part.FormName() != "map" { w.WriteHeader(http.StatusUnprocessableEntity) writeJsonError(w, "second part must be map") return } uploadsMap := map[string][]string{} if err = json.NewDecoder(part).Decode(&uploadsMap); err != nil { w.WriteHeader(http.StatusUnprocessableEntity) writeJsonError(w, "map form field could not be decoded") return } for { part, err = mr.NextPart() if err == io.EOF { break } else if err != nil { w.WriteHeader(http.StatusUnprocessableEntity) writeJsonErrorf(w, "failed to parse part") return } key := part.FormName() filename := part.FileName() contentType := part.Header.Get("Content-Type") paths := uploadsMap[key] if len(paths) == 0 { w.WriteHeader(http.StatusUnprocessableEntity) writeJsonErrorf(w, "invalid empty operations paths list for key %s", key) return } delete(uploadsMap, key) var upload graphql.Upload if r.ContentLength < f.maxMemory() { fileBytes, err := io.ReadAll(part) if err != nil { w.WriteHeader(http.StatusUnprocessableEntity) writeJsonErrorf(w, "failed to read file for key %s", key) return } for _, path := range paths { upload = graphql.Upload{ File: &bytesReader{s: &fileBytes, i: 0}, Size: int64(len(fileBytes)), Filename: filename, ContentType: contentType, } if err := params.AddUpload(upload, key, path); err != nil { w.WriteHeader(http.StatusUnprocessableEntity) writeJsonGraphqlError(w, err) return } } } else { tmpFile, err := os.CreateTemp(os.TempDir(), "gqlgen-") if err != nil { w.WriteHeader(http.StatusUnprocessableEntity) writeJsonErrorf(w, "failed to create temp file for key %s", key) return } tmpName := tmpFile.Name() defer func() { _ = os.Remove(tmpName) }() fileSize, err := io.Copy(tmpFile, part) if err != nil { w.WriteHeader(http.StatusUnprocessableEntity) if err := tmpFile.Close(); err != nil { writeJsonErrorf(w, "failed to copy to temp file and close temp file for key %s", key) return } writeJsonErrorf(w, "failed to copy to temp file for key %s", key) return } if err := tmpFile.Close(); err != nil { w.WriteHeader(http.StatusUnprocessableEntity) writeJsonErrorf(w, "failed to close temp file for key %s", key) return } for _, path := range paths { pathTmpFile, err := os.Open(tmpName) if err != nil { w.WriteHeader(http.StatusUnprocessableEntity) writeJsonErrorf(w, "failed to open temp file for key %s", key) return } defer pathTmpFile.Close() upload = graphql.Upload{ File: pathTmpFile, Size: fileSize, Filename: filename, ContentType: contentType, } if err := params.AddUpload(upload, key, path); err != nil { w.WriteHeader(http.StatusUnprocessableEntity) writeJsonGraphqlError(w, err) return } } } } for key := range uploadsMap { w.WriteHeader(http.StatusUnprocessableEntity) writeJsonErrorf(w, "failed to get key %s from form", key) return } params.Headers = r.Header params.ReadTime = graphql.TraceTiming{ Start: start, End: graphql.Now(), } rc, gerr := exec.CreateOperationContext(r.Context(), ¶ms) if gerr != nil { resp := exec.DispatchError(graphql.WithOperationContext(r.Context(), rc), gerr) w.WriteHeader(statusFor(gerr)) writeJson(w, resp) return } responses, ctx := exec.DispatchOperation(r.Context(), rc) writeJson(w, responses(ctx)) }