Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support multipart requests / file uploads #51

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 == '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