FileMaker 17: Data API Multi-File Upload

The FileMaker 17 Data API, also known as fmrest, includes a new capability to upload data to a container field. Prior to 17 the Data API only allowed container data to be downloaded. Uploading data from a website to FileMaker had to be done using plugins, Base64 encoding, or by saving a temporary copy of the file in a spot where a FileMaker script could find it. Now we can upload to FileMaker container fields using only the Data API. This blog post describes how to use the new upload capability with HTML and JavaScript.

Before uploading to FileMaker, we need to obtain an access token. We’ll use this token to authenticate our requests to FileMaker. The 17 Data API allows you to obtain an access token using either a basic authentication header or an OAuth identity provider like Google. In this blog I’ll use basic authentication.

To simplify the code I’m using jQuery. Google makes jQuery easily available through their content delivery network. Load the library by including the following script tag at the top of your HTML:

<html>
   <head>
       <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
   </head>

 

To configure basic authentication we prompt the web user for a username and password. The user needs to enter credentials for an account that has been assigned the “fmrest” extended privilege. Once the user submits this form, we concatenate their username and password, separated by a colon. Then we Base64 encode this concatenated string, which can be done in a web browser using the JavaScript method btoa. Here’s what this looks like:

   <form id="login">
       <label for="username">Username</label>
       <input name="username" type="text" />
       <label for="password">Password</label>
       <input name="password" type="password" />

       <input type="submit" value="Login" />
   </form>
  
   <script>
   let request;

   $("#login").submit(function(event){
       event.preventDefault();
       if(request) {
           request.abort();
       }

       let $form = $(this);

       let $inputs = $form.find("input");

       const auth = btoa($form.find('input[name="username"]').val() + ':' +  
$form.find('input[name="password"]').val());

       $inputs.prop('disabled', true);

 

With our newly encoded string, we’re ready to make our login request. If you used the Data API in FileMaker 16, the format of 17 API calls will be familiar to you. Note however that many of the URIs have changed. The URI for logging in has changed to “/fmi/data/v1/databases/filename/sessions”, for example.

Our login request looks like this:

       request = $.ajax({
           url: 'https://17.app.works/fmi/data/v1/databases/MultiUpload/sessions',
           type: 'post',
           headers: {
               'Content-Type': 'application/json',
               'Authorization': 'Basic ' + auth
           },
           data: {}
       });

 

If our login request is successful we store the access token in localStorage so we can use it in subsequent requests, then load the upload page. Otherwise we log an error message.

       request.done((response) => {
           localStorage.setItem('fmrest_token', response.response.token)
           $inputs.prop('disabled', false);
           location = 'blog-demo-upload.html'
       });

       request.fail((jqXHR, textStatus, errorThrown) => {
           console.log(errorThrown)
           $inputs.prop('disabled', false);
       })
   });
   </script>
</html>

 

Now that we have an access token saved in localStorage, we can begin making other requests to FileMaker, including uploading files to container fields.

My FileMaker demo file contains a simple table and layout called “Image”. The Image table contains 17’s default fields and two fields of my own: a container field named File and a text field named Name. We’ll upload a file by creating a new record in the Image table, obtaining the recordId in the response, then using the recordId in a call to insert the file into the container field.

To get started, let’s load jQuery and set up our form:

<html>
   <head>
       <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
   </head>

   <form id="uploadForm">
       <label for="upload">Upload</label>
       <input name="upload" type="file" />

       <label for="name">Name</label>
       <input name="name" type="text" />

       <input type="submit" value="Upload" />
   </form>

 

Next let’s get our access token from localStorage. If we find that the token does not exist, we redirect the user back to the login page.

   <script>
       const token = localStorage.getItem('fmrest_token');
       if (token === null) {
           location = 'blog-demo-login.html'
       }

 

When the user submits the form, we need to create a new record in the Image table and obtain it’s recordId from FileMaker’s response. First let’s grab what the user typed in the “Name” field:

       let request;
       $("#uploadForm").submit(function(event) {
           event.preventDefault();
           if(request) {
               request.abort();
           }

           let $form = $(this);

           let $inputs = $form.find("input");

           $inputs.prop('disabled', true);

           let name = $('#name').prop('value');

 

To create a record using the 17 Data API, we need to send a JSON object containing the fields and values we want to set. Let’s create our JSON object:

            const fieldData = {"fieldData": {"Name": name } }
            const data = JSON.stringify(fieldData)

 

With our JSON object created, we’re ready to make our request. The URI for creating records is in the format “/fmi/data/v1/databases/database-name/layouts/layout-name/records”. Notice that in our request, we include an Authorization header set to our token, with the prefix Bearer:

           request = $.ajax({
               url: 'https://17.app.works/fmi/data/v1/databases/MultiUpload/layouts/Image/records',
               type: 'post',
               headers: {
                   'Content-Type': 'application/json',
                   'Authorization': 'Bearer ' + token
               },
               data: data
           });

 

If our request succeeded, we obtain the recordId from FileMaker’s response and pass it to our insertFile function (see below). Otherwise, we display an error.

           request.done((response) => {
               insertFile(response.response.recordId)
               $inputs.prop('disabled', false);
           });

           request.fail((jqXHR, textStatus, errorThrown) => {
               console.log(errorThrown)
               $inputs.prop('disabled', false);
           })
       })

 

Our insertFile function builds a FormData object from the file upload input box:

        const insertFile = (recordId) => {
           let formData = new FormData();
           formData.append('upload', ($("#upload"))[0].files[0]);
           console.log(formData)
           event.preventDefault();
           if(request) {
               request.abort();
           }

 

Then it sends the object to FileMaker and uses recordId to identify which record it’s trying to modify:

           request = $.ajax({
               url: 'https://17.app.works/fmi/data/v1/databases/MultiUpload/layouts/Image/records/9/containers/File/1',
               type: 'post',
               headers: {
                   'Authorization': 'Bearer ' + token
               },
               data: formData,
               cache : false,
               contentType: false,
               processData: false
           });

       }

 

Knowing what we know now about uploading a single file to FileMaker, how can we change our code to allow us to upload multiple files at once? The answer lies in the HTML Drag and Drop API. This API is built into modern browsers and allows us to assign drag and drop event listeners to our DOM elements. A property of these events is dataTransfer, which is an object containing whatever files the user dropped on our DOM element.

The following chunk of code is borrowed from Mozilla’s documentation.

First let’s set up a div on the page where users can drop their files:

<div id="dropbox" style="height:200px; width:200px; background:rgba(0,0,0,0.2)">Drop files here.</div>

 

When “dropbox” hears a “drop” event, it assigns a dataTransfer object to dt. Then we select the files from dt and pass them to a handler function, handleFiles. Our handler function simply needs to iterate through our list of files and pass them off one by one to functions that call the Data API:

       const handleFiles = (files) => {
           for(i = 0; i < files.length; i++){
               createRecord(files[i], multiInsert)
           }
       }

 

Like our process when uploading a single file, we first create a record in the Image table using createRecord then call multiInsert to upload the file:

       const createRecord = (file, callback) => {

           const fieldData = {"fieldData": {"Name": file.name } }
           const data = JSON.stringify(fieldData)

           request = $.ajax({
               url: 'https://17.app.works/fmi/data/v1/databases/MultiUpload/layouts/Image/records',
               type: 'post',
               headers: {
                   'Content-Type': 'application/json',
                   'Authorization': 'Bearer ' + token
               },
               data: data
           });

           request.done((response) => {
               callback(file, response.response.recordId)
           });

           request.fail((jqXHR, textStatus, errorThrown) => {
               console.log(errorThrown)
           });

       }


       const multiInsert = (file, recordId) => {
           const formData = new FormData();
           formData.append('upload', file);
           event.preventDefault();

           request = $.ajax({
               url: 'https://17.app.works/fmi/data/v1/databases/MultiUpload/layouts/Image/records/' + recordId + '/containers/File/1',
               type: 'post',
               headers: {
                   'Authorization': 'Bearer ' + token
               },
               data: formData,
               cache : false,
               contentType: false,
               processData: false
           });
       }

 

These code snippets should hopefully get you started working with the Data API’s container features. They can be refined and built upon to do things like validation (only accept certain file types), relate an uploaded file to a record in another table, and even traverse a folder structure. If you’re interested in learning more about 17’s features, check out our other recent blog posts.