DevCon, DevCon 2019, FileMaker, FileMaker Data API

FileMaker Data API Workshop – activity eight

Uploading files

Dropzone is already enabled for the file upload, it just needs something to do with the files which it receives.

The Postman example File upload gives us a good starting point because it shows us the name the file needs to be given when it’s uploaded, but it masks one very important aspect which is covered in the FileMaker Data API documentation:

I’ve highlighed the important aspect of this – we need to send this as multipart/form-data which means we have to structure our POST carefully so that we get that set properly.

The other thing you’ll notice from the documentation is that we already need to have an existing record to upload to, so we need to create an empty record first so that we have a recordId to use in the upload query.

One final note in regards creating that record – FileMaker returns the internal record ID as the key recordId – note the lowercase ‘r’ and ‘d’ which you may not expect given that within FileMaker itself it uses get(RecordID)

To begin with we need to update our interface file to add in a new table reference to receive our uploaded images.

  1. Add the Images table from the data file to the graph of your interface file.
  2. Create a layout based on that table. Call it ImagesAPI. Add the following fields to the layout
    1. Image
    2. PrimaryKey
    3. Latitude
    4. Longitude

As with previous activities it doesn't matter here if you're using jQuery or fetch as your transport layer because we don't actually interact directly with the FileMaker Data API, only with our performRequest method.

To complete this activity everything that needs to be implemented is in scripts/app.js. There are a number of TODO items in there which in combination with the documentation and the Postman examples should get you well on the way to having this working.

You'll notice that the most important aspect of this, creating the FileMaker record and setting the upload url correctly all happen inside an accept callback function which receives two parameters, the file which is being uploaded and done which is itself a callback. This is crucial since we need the upload process to wait until we've created the record before trying to upload the file (otherwise there's no record waiting to receive the image). In the success callback from your performRequest to create the record you'll need to call done(); to tell Dropzone that it should upload the image at that point.

You'll need to make changes in two places to complete this task. Firstly in src/Service/ImageUploadService.php you'll need to populate createImageRecord.

There are a number of TODOs in there to guide you through, but one thing which might trip you up is that the fieldData must be an object, even though it's empty. In PHP when you use json_encode() it will generally cast associative arrays, an array like

[
    'who' => 'hah'
    'foo' => 'bar',
]

to a JSON object, however if the array is empty (as will be the case on this occasion because we have no data to send) then json_encode will decide this doesn't need to be an object and leave it as an array, which the FileMaker Data API most assuredly doesn't like. To get round this we need to 'cast' the empty array to an object using (object)[]. In PHP the (object) says "It doesn't matter what comes next, I want you to turn it into an object for me please!" and because PHP can do that sensibly with an array, it will.

The second place you need to make changes is in src/Service/FileMakerAPI.php where you'll need to complete the method performContainerInsert. Again there are a lots of TODOs in there to help.

In the options that you set for this method you'll need to ensure you set the headers so that you overwrite the default headers in your performrequest method because as per the FileMaker Data API documentation you don't want to set the Content-Type header. The Guzzle documentation section on uploading data includes details on how to include file uploads if you find yourself stuck.

Solutions

You'll see that we need to create the empty record first. Even though we're not submitting any other information at this time, we still have to have an empty fieldData object in the data that we post, otherwise FileMaker will object. Once we have the record, we extract the recordId and update the Dropzone URL, then call the done() callback to tell DropZone to 'carry on'. As per the FileMaker Data API documentation the paramName needs to be set to upload. We also need to get the current API token so that the request can be authenticated.

var dz = new Dropzone("div#ImageUpload", {
    accept: function (file, done) {
        let data = {
           fieldData: {}
        };
        FileMaker.performRequest('POST', '/layouts/ImagesAPI/records', data, function (record) {
            dz.options.url = Config.baseURI + '/layouts/ImagesAPI/records/' + record.recordId + '/containers/Image/1';

            done();
       }, App.error);
   },
   url: '/replace/me',
   clickable: true,
   acceptedFiles: ".png,.jpg,.jpeg",
   paramName: 'upload',
   addRemoveLinks: true,
   maxFilesize: 10,
   headers: { "Authorization": "Bearer " + FileMaker.retrieveToken() },
});

You'll need to make changes in two places to complete this task. Firstly in src/Service/ImageUploadService.php replace createImageRecord with the following method.

The line (object) in 'fieldData' => (object)[] is important because it tells PHP that you want to convert the empty array (represented by []) to an object. The FileMaker Data API is very particular about this - in order to create a record you must have a body, it must have a key fieldData and even if that's empty it must be an object - any empty string or array there will fail!

/**
 * @return array
 *
 * @throws Exception
 */
private function createImageRecord()
{
    // To create a record you have to POST fieldData, even though we don't want any, AND it has to be
    // and object, so we cast an empty array as an object.
    $body = [
        'fieldData' => (object)[]
    ];

    // The body always needs to exist, and it always needs to be JSON, so encode that
    $params = ['body' => json_encode($body)];

    return $this->fm->performRequest('POST', 'layouts/ImagesAPI/records', $params);
}

In src/Service/FileMakerAPI.php replace the method performContainerInsert with the below.

/**
 * @param string $layout    The layout to upload content to
 * @param int $recId        The FileMaker internal recordId of the record to add the content to
 * @param string $field     The name of the container field
 * @param string $file      The path to the file to be inserted into the container
 * @param int $repetition   Which field repetition to use, defaults to 1
 *
 * @return array
 *
 * @throws Exception
 */
public function performContainerInsert($layout, $recId, $field, $file, $repetition = 1)
{
    // Generate the URI required for this request
    $uri = sprintf('layouts/%s/records/%s/containers/%s/%s', $layout, $recId, $field, $repetition);

    // Se the options specific to uploading a file. This means that we do NOT set the Content-Type
    // header, but we do still need the Authorization header.
    // We also have to set a form multipart, which has the name 'upload' as specified in the Data API
    // documentation, with a 'contents' node containing the actual file data which we read off disk
    $options = [
        'headers' => [
            'Authorization' => sprintf('Bearer %s', $this->getStoredToken()),
        ],
        'multipart' => [
            [
                'name' => 'upload',
                'contents' => fopen($file, 'r')
            ],
        ]
    ];

    return $this->performRequest('POST', $uri, $options);
}

You'll see that this method sets it's own headers as well as creating the necessary multipart so that the FileMaker Data API knows how to handle the uploaded file.

The headers need to be set here so that they override the default headers in the performRequest method because that also includes Content-Type = "application/json" which isn't correct for container uploads and will cause them to fail if present.

< Back to activity seven

Leave A Comment

*
*