-
-
Notifications
You must be signed in to change notification settings - Fork 13
Description
Some context on this:
- Drupal docs on uploading files via the API: https://www.drupal.org/docs/core-modules-and-themes/core-modules/jsonapi-module/file-uploads
- Change record: https://www.drupal.org/node/3024331
- This issue summary provides great detail as to why this works the way it does: https://www.drupal.org/project/drupal/issues/1927648
An overview:
JSON:API now:
Supports POSTing a file's contents
Supports sending file data as a binary data stream via the Content-Type: application/octet-stream request header.
This allows uploads of an arbitrary size, including uploads larger than the PHP memory limit.
Ensures that settings/validation constraints of the field to which the file is being uploaded will be respected.
Supports naming the file using the Content-Disposition: file; filename="filename.jpg" header
To use JSON:API's file uploads, you must choose to implement one or both of two different "flows":
Flow 1 requires only a single HTTP request, but will only work when the host entity already exists.
Flow 2 requires two HTTP requests but will work under all circumstances.
TL;DR
I tested uploading files using the requests session from the farmOS.py client and it works great:
client.session.post('http://localhost/api/log/input/file', data=open('data.csv', 'rb').read(), headers={'Content-Type': 'application/octet-stream', 'Content-Disposition': 'file; filename="data.csv"'})We can make this even easier in the farmOS.py client by adding a helper method. This can't be done with the existing methods because file uploads must always be a POST with special headers to unique {entity_type}/{bundle}/{file field} endpoints. This ensures proper files are uploaded to the correct location (the location can vary depending on each bundle field) and all proper validation is performed.
Solution:
# Method to be namespaced as client.file.create()
def create(entity_type, bundle, field, data, filename, id = None):
if id is not None:
path = "{entity_type}/{bundle}/{id}/{field}".format(entity_type, bundle, id, field)
else:
path = "{entity_type}/{bundle}/{field}".format(entity_type, bundle, field)
headers = {'Content-Type': 'application/octet-stream', 'Content-Disposition': "file; filename=\"{filename}\"".format(filename)}
return self.session.post(path, data, headers)
# Usage
# Flow 1: update existing log
data = open('data.csv', 'rb').read()
response = client.file.create('log', 'observation', 'file', data, 'observation_data.csv', '{log_id}')
# Flow 2: upload file then include with a new log.
data = open('data.csv', 'rb').read()
response = client.file.create('log', 'observation', 'file', data, 'observation_data.csv')
file_id = response['data']['id']
log_data = {
'attributes': {'name': 'observation log'},
'relationships': { 'file': {'data': [{'type': 'file--file', 'id': file_id}]}},
}
new_log = client.log.send('observation', log_data)