Skip to content

Commit

Permalink
Correctly handle multipart/form-data requests
Browse files Browse the repository at this point in the history
  • Loading branch information
rshk committed Jul 25, 2018
1 parent 4183613 commit 52e647f
Show file tree
Hide file tree
Showing 4 changed files with 334 additions and 16 deletions.
98 changes: 98 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,101 @@ class UserRootValue(GraphQLView):
return request.user

```

### File upload support

File uploads are supported via [multipart requests](https://github.com/jaydenseric/graphql-multipart-request-spec).

You can simply define a ``FileUpload`` field in your schema, and use
it to receive data from uploaded files.


Example using ``graphql-core``:

```python
from collections import NamedTuple
from graphql.type.definition import GraphQLScalarType


GraphQLFileUpload = GraphQLScalarType(
name='FileUpload',
description='File upload',
serialize=lambda x: None,
parse_value=lambda value: value,
parse_literal=lambda node: None,
)


FileEchoResult = namedtuple('FileEchoResult', 'data,name,type')


FileEchoResultSchema = GraphQLObjectType(
name='FileEchoResult,
fields={
'data': GraphQLField(GraphQLString),
'name': GraphQLField(GraphQLString),
'type': GraphQLField(GraphQLString),
}
)


def resolve_file_echo(obj, info, file):
data = file.stream.read().decode()
return FileEchoResult(
data=data,
name=file.filename,
type=file.content_type)


MutationRootType = GraphQLObjectType(
name='MutationRoot',
fields={
# ...
'fileEcho': GraphQLField(
type=FileUploadTestResultSchema,
args={'file': GraphQLArgument(GraphQLFileUpload)},
resolver=resolve_file_echo,
),
# ...
}
)
```


Example using ``graphene``:

```python
import graphene

class FileUpload(graphene.Scalar):

@staticmethod
def serialize(value):
return None

@staticmethod
def parse_literal(node):
return None

@staticmethod
def parse_value(value):
return value # IMPORTANT


class FileEcho(graphene.Mutation):

class Arguments:
myfile = FileUpload(required=True)

ok = graphene.Boolean()
name = graphene.String()
data = graphene.String()
type = graphene.String()

def mutate(self, info, myfile):
return FileEcho(
ok=True
name=myfile.filename
data=myfile.stream.read(),
type=myfile.content_type)
```
92 changes: 91 additions & 1 deletion flask_graphql/graphqlview.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,9 +135,22 @@ def parse_body(self):
elif content_type == 'application/json':
return load_json_body(request.data.decode('utf8'))

elif content_type in ('application/x-www-form-urlencoded', 'multipart/form-data'):
elif content_type in 'application/x-www-form-urlencoded':
return request.form

elif content_type == 'multipart/form-data':
# --------------------------------------------------------
# See spec: https://github.com/jaydenseric/graphql-multipart-request-spec
#
# When processing multipart/form-data, we need to take
# files (from "parts") and place them in the "operations"
# data structure (list or dict) according to the "map".
# --------------------------------------------------------
operations = load_json_body(request.form['operations'])
files_map = load_json_body(request.form['map'])
return place_files_in_operations(
operations, files_map, request.files)

return {}

def should_display_graphiql(self):
Expand All @@ -152,3 +165,80 @@ def request_wants_html(self):
return best == 'text/html' and \
request.accept_mimetypes[best] > \
request.accept_mimetypes['application/json']


def place_files_in_operations(operations, files_map, files):
"""Place files from multipart reuqests inside operations.
Args:
operations:
Either a dict or a list of dicts, containing GraphQL
operations to be run.
files_map:
A dictionary defining the mapping of files into "paths"
inside the operations data structure.
Keys are file names from the "files" dict, values are
lists of dotted paths describing where files should be
placed.
files:
A dictionary mapping file names to FileStorage instances.
Returns:
A structure similar to operations, but with FileStorage
instances placed appropriately.
"""

# operations: dict or list
# files_map: {filename: [path, path, ...]}
# files: {filename: FileStorage}

fmap = []
for key, values in files_map.items():
for val in values:
path = val.split('.')
fmap.append((path, key))

return _place_files_in_operations(operations, fmap, files)


def _place_files_in_operations(ops, fmap, fobjs):
for path, fkey in fmap:
ops = _place_file_in_operations(ops, path, fobjs[fkey])
return ops


def _place_file_in_operations(ops, path, obj):

if len(path) == 0:
return obj

if isinstance(ops, list):
key = int(path[0])
sub = _place_file_in_operations(ops[key], path[1:], obj)
return _insert_in_list(ops, key, sub)

if isinstance(ops, dict):
key = path[0]
sub = _place_file_in_operations(ops[key], path[1:], obj)
return _insert_in_dict(ops, key, sub)

raise TypeError('Expected ops to be list or dict')


def _insert_in_dict(dct, key, val):
new_dict = dct.copy()
new_dict[key] = val
return new_dict


def _insert_in_list(lst, key, val):
new_list = []
new_list.extend(lst[:key])
new_list.append(val)
new_list.extend(lst[key + 1:])
return new_list
51 changes: 49 additions & 2 deletions tests/schema.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from graphql.type.definition import GraphQLArgument, GraphQLField, GraphQLNonNull, GraphQLObjectType
from graphql.type.definition import (
GraphQLArgument, GraphQLField, GraphQLNonNull, GraphQLObjectType,
GraphQLScalarType)
from graphql.type.scalars import GraphQLString
from graphql.type.schema import GraphQLSchema

Expand All @@ -25,13 +27,58 @@ def resolve_raises(*_):
}
)


FileUploadTestResult = GraphQLObjectType(
name='FileUploadTestResult',
fields={
'data': GraphQLField(GraphQLString),
'name': GraphQLField(GraphQLString),
'type': GraphQLField(GraphQLString),
}
)

GraphQLFileUpload = GraphQLScalarType(
name='FileUpload',
description='File upload',
serialize=lambda x: None,
parse_value=lambda value: value,
parse_literal=lambda node: None,
)


def to_object(dct):
class MyObject(object):
pass

obj = MyObject()
for key, val in dct.items():
setattr(obj, key, val)
return obj


def resolve_file_upload_test(obj, info, file):
data = file.stream.read().decode()

# Need to return an object, not a dict
return to_object({
'data': data,
'name': file.filename,
'type': file.content_type,
})


MutationRootType = GraphQLObjectType(
name='MutationRoot',
fields={
'writeTest': GraphQLField(
type=QueryRootType,
resolver=lambda *_: QueryRootType
)
),
'fileUploadTest': GraphQLField(
type=FileUploadTestResult,
args={'file': GraphQLArgument(GraphQLFileUpload)},
resolver=resolve_file_upload_test,
),
}
)

Expand Down
Loading

0 comments on commit 52e647f

Please sign in to comment.