Skip to content

Commit cbde5b3

Browse files
committed
Correctly handle multipart/form-data requests
1 parent 4183613 commit cbde5b3

File tree

2 files changed

+136
-1
lines changed

2 files changed

+136
-1
lines changed

flask_graphql/fields.py

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
"""Provide field definitions for file uploads
2+
3+
TODO: this should be supported upstream, in graphql-core
4+
and graphene. Shall we just remove it from here and only
5+
provide a reference definition instead?
6+
"""
7+
8+
try:
9+
from graphql.type.definition import GraphQLScalarType
10+
11+
except ImportError:
12+
pass
13+
14+
else:
15+
16+
GraphQLFileUpload = GraphQLScalarType(
17+
name='FileUpload',
18+
description='File upload',
19+
serialize=lambda x: None,
20+
parse_value=lambda value: value,
21+
parse_literal=lambda node: None,
22+
)
23+
24+
25+
try:
26+
import graphene
27+
28+
except ImportError:
29+
pass
30+
31+
else:
32+
33+
class FileUpload(graphene.Scalar):
34+
35+
@staticmethod
36+
def serialize(value):
37+
return None
38+
39+
@staticmethod
40+
def parse_literal(node):
41+
return None
42+
43+
@staticmethod
44+
def parse_value(value):
45+
return value # IMPORTANT

flask_graphql/graphqlview.py

+91-1
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)