@@ -135,9 +135,22 @@ def parse_body(self):
135135 elif content_type == 'application/json' :
136136 return load_json_body (request .data .decode ('utf8' ))
137137
138- elif content_type in ( 'application/x-www-form-urlencoded' , 'multipart/form-data' ) :
138+ elif content_type in 'application/x-www-form-urlencoded' :
139139 return request .form
140140
141+ elif content_type == 'multipart/form-data' :
142+ # --------------------------------------------------------
143+ # See spec: https://github.com/jaydenseric/graphql-multipart-request-spec
144+ #
145+ # When processing multipart/form-data, we need to take
146+ # files (from "parts") and place them in the "operations"
147+ # data structure (list or dict) according to the "map".
148+ # --------------------------------------------------------
149+ operations = load_json_body (request .form ['operations' ])
150+ files_map = load_json_body (request .form ['map' ])
151+ return place_files_in_operations (
152+ operations , files_map , request .files )
153+
141154 return {}
142155
143156 def should_display_graphiql (self ):
@@ -152,3 +165,80 @@ def request_wants_html(self):
152165 return best == 'text/html' and \
153166 request .accept_mimetypes [best ] > \
154167 request .accept_mimetypes ['application/json' ]
168+
169+
170+ def place_files_in_operations (operations , files_map , files ):
171+ """Place files from multipart reuqests inside operations.
172+
173+ Args:
174+
175+ operations:
176+ Either a dict or a list of dicts, containing GraphQL
177+ operations to be run.
178+
179+ files_map:
180+ A dictionary defining the mapping of files into "paths"
181+ inside the operations data structure.
182+
183+ Keys are file names from the "files" dict, values are
184+ lists of dotted paths describing where files should be
185+ placed.
186+
187+ files:
188+ A dictionary mapping file names to FileStorage instances.
189+
190+ Returns:
191+
192+ A structure similar to operations, but with FileStorage
193+ instances placed appropriately.
194+ """
195+
196+ # operations: dict or list
197+ # files_map: {filename: [path, path, ...]}
198+ # files: {filename: FileStorage}
199+
200+ fmap = []
201+ for key , values in files_map .items ():
202+ for val in values :
203+ path = val .split ('.' )
204+ fmap .append ((path , key ))
205+
206+ return _place_files_in_operations (operations , fmap , files )
207+
208+
209+ def _place_files_in_operations (ops , fmap , fobjs ):
210+ for path , fkey in fmap :
211+ ops = _place_file_in_operations (ops , path , fobjs [fkey ])
212+ return ops
213+
214+
215+ def _place_file_in_operations (ops , path , obj ):
216+
217+ if len (path ) == 0 :
218+ return obj
219+
220+ if isinstance (ops , list ):
221+ key = int (path [0 ])
222+ sub = _place_file_in_operations (ops [key ], path [1 :], obj )
223+ return _insert_in_list (ops , key , sub )
224+
225+ if isinstance (ops , dict ):
226+ key = path [0 ]
227+ sub = _place_file_in_operations (ops [key ], path [1 :], obj )
228+ return _insert_in_dict (ops , key , sub )
229+
230+ raise TypeError ('Expected ops to be list or dict' )
231+
232+
233+ def _insert_in_dict (dct , key , val ):
234+ new_dict = dct .copy ()
235+ new_dict [key ] = val
236+ return new_dict
237+
238+
239+ def _insert_in_list (lst , key , val ):
240+ new_list = []
241+ new_list .extend (lst [:key ])
242+ new_list .append (val )
243+ new_list .extend (lst [key + 1 :])
244+ return new_list
0 commit comments