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, &params); 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(), &params)
   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  }