farmOS / farmOS.py

A Python library for interacting with farmOS over API.
GNU General Public License v3.0
27 stars 12 forks source link

Add helper method to support file uploads #49

Open paul121 opened 3 years ago

paul121 commented 3 years ago

Some context on this:

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)