Upload multiple files in embedded form

We need to upload a number of files in the form before a process is started. The idee we have is to create new variables in the submit hook like so:

camForm.on('submit', function() {
  for (var i = 0; i < $scope.files.length; i++) {
    camForm.variableManager.createVariable({
       name: 'DOCUMENT_' + i,
       type: 'Bytes',
       value: ?
    });
};

What is the correct datatype for value (we tried byte[], Uint8Array and String).

Regards,

Casper

1 Like

Hi Casper,

you can use 'Bytes' and 'File' as type. Make sure you are processing the file data correctly. You can have a look at how the camunda form processes input fields that have the cam-variable-type="File" attribute: https://github.com/camunda/camunda-bpm-sdk-js/blob/master/lib/forms/camunda-form.js#L491-L521

Just out of curiosity: Why aren’t you using the standard way via the cam-variable-name attribute?

Cheers
Sebastian

Hello Sebastian,

Thank you for your speedy reply. I got it to work with Bytes and a byte array as value and a bit of Javascript.

To give you some additional context, this is how our form looks like (all unnecessary elements removed):

The reason we think we cannot use the cam-variable-name is because we want to set some additional attributes with each file.

I also tried with type File:

for (var i = 0; i < $scope.files.length; i++) {
   camForm.variableManager.createVariable({
      name: 'DOCUMENT_' + i,
      type: 'File',
      value: $scope.files[i].file,
      valueInfo: {
         filename: $scope.files[i].name,
      }
   })
};

but ran into this error:

27-Jul-2016 14:29:01.431 WARNING [http-nio-8080-exec-4] org.camunda.bpm.engine.rest.exception.ExceptionHandler.toResponse java.lang.IllegalArgumentException: Provided value is not of File, InputStream or byte[] type.
    at org.camunda.bpm.engine.variable.impl.type.FileValueTypeImpl.createValue(FileValueTypeImpl.java:56)
    at org.camunda.bpm.engine.rest.dto.VariableValueDto.toTypedValue(VariableValueDto.java:123)
    at org.camunda.bpm.engine.rest.dto.VariableValueDto.toMap(VariableValueDto.java:146)
    at org.camunda.bpm.engine.rest.sub.repository.impl.ProcessDefinitionResourceImpl.submitForm(ProcessDefinitionResourceImpl.java:161)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)

and it looks like the value needs to be a byte[] as with Bytes.

If would be great if we could get this to work. Less Javascript in our form, all the better!

Regards,

Casper

When using type File you have to submit the content of the file as a Base64 encoded String (source).

I assume your $scope.files[i].file is a Javascript File object. You need to get the value of the file and encode it. The link I posted earlier contains some code that does that.

Cheers
Sebastian

@c.biever

I am also looking for multiple file uploading facility. Is it successful by your own findings ?

@manmohanm

Yes it was, but we build our own solution:

Here are some code snippets to get you started:

<table class="table">
  <tbody>
    <tr ng-repeat="file in files">
      <td>
        <a role="button" ng-click="openDocument(file)">{{file.name}}</a>
      </td>
      <td>
        <span class="dropdown">
          <button class="btn btn-default dropdown-toggle" type="button" data-toggle="dropdown" required>
            {{dateitypname(file.typ)}}
            <span class="caret"></span>
          </button>
          <ul class="dropdown-menu">
            <li ng-repeat="dateityp in dateitypen" ng-click="setDateityp(file, dateityp.wert)"><a>{{dateityp.name}}</a></li>
          </ul>
        </span>
      </td>
      <td>
        <span class="glyphicon glyphicon-remove" ng-click="removeDocument(file)" style="cursor:pointer"></span>
      </td>
    </tr>
  </tbody>
</table>
<div>
  <label class="btn btn-default btn-file">
    Browse <input type="file" id="fileUpload" style="display: none;" onchange="angular.element(this).scope().upload(this, angular.element(this).scope().files, angular.element(this).scope().dateitypen[0])" multiple>
    <span class="glyphicon glyphicon-folder-open"/>
  </label>
</div>

$scope.upload = function(fileUpload, dateien, standardDateityp) {
  for (let i = 0; i < fileUpload.files.length; i++) {
    let file = fileUpload.files[i];
    let mimetype = file.type;
    if (!mimetype || mimetype == '') {
      if (file.name.toLowerCase().indexOf('.msg') > -1) {
        mimetype = 'application/vnd.ms-outlook';
      }
    }
    let datei = { name: file.name, typ: standardDateityp.wert, mimetype: mimetype, data: undefined, blob: undefined };
    dateien.push(datei);
    $scope.retrieveFiledata(file, datei);
  }
  $scope.erfa.$setDirty();
  $scope.$apply();
};

$scope.openDocument = function (file) {
  if (window.navigator.msSaveOrOpenBlob) {
    if (file.href) {
      $http.get(file.href, {responseType: 'arraybuffer'}).then(
        function successCallback(response) {
          window.navigator.msSaveOrOpenBlob(new Blob([response.data], {type: file.mimetype}), file.name);
        },
        function errorCallback(response) {
          console.log('error downloading document: ' + response);
        }
      );
    }
    else {
      window.navigator.msSaveOrOpenBlob(new Blob([file.blob], {type: file.mimetype}), file.name);
    }
  } else {
    window.open('data:' + file.mimetype + ';base64,' + file.data);
  }
};

@c.biever

Thank you for your valuable help.

When I am trying to execute your code , the following error were occurred.

TypeError: angular.element(…).scope(…).dateitypen is undefined

what all things do i need to import? Or can I use other datatypes instead of ‘dateitypen’ ?

@manmohanm

The code was meant to get you started. The mentioned variable is very specific to our project, you surely are creating something completely different. You might not even have the requirement to classify documents as we do.

Start with displaying the filenames and build on that, like storing the file content in process variables.

@c.biever

We have done ‘multiple file uploading’ by using the below mentioned form.
It stores data to db ,but we are unable to access it.
Actually What i am looking for is that I want to display the files that had uploaded from the previous process.

<form role="form">
  <input id="fileUpload"
         type="file" multiple/>
  <button ng-click="upload()">Upload</button>
  

  <script cam-script type="text/form-script">
    inject(['$http', 'Uri', function($http, Uri) {
      camForm.on('form-loaded', function() {
        $http.get(Uri.appUri('engine://engine/:engine/task/' + camForm.taskId)).success(function(result){
          $scope.upload = function() {
            var formData = new FormData();
            var filename;
            var uploadfile ;
           for( var i=0;i<document.getElementById('fileUpload').files.length;i++)
           {
               alert(result.processInstanceId);
               formData = new FormData();
               formData.append('data', document.getElementById('fileUpload').files[i]);
               filename=document.getElementById('fileUpload').files[i].name;
                
              
                $http.post(Uri.appUri('engine://engine/:engine/process-instance/' + result.processInstanceId + '/variables/'+filename+'/data'), formData, {
               transformRequest: angular.identity,
                headers: {'Content-Type': undefined}
            });
               
           }

          };
       });
      });
    }]);
  </script>
</form>

In database(in the table ACT_RU_VARIABLE), it stores data but when compare to single file upload, fields TEXT_ & TEXT2_ are having null values.

Please suggest some ideas.

Thanks & Regards
Manmohan M

@manmohanm

We choose to only upload the files in case the user decides to complete the task:

$scope.files.forEach(function (file, index) {
  camForm.variableManager.createVariable({
    name: 'DOKUMENT_' + index,
    type: 'Bytes',
    value: file.data
  });
});

They appear as BLOB’s in the ACT_RU_BYTEARRAY table.

@c.biever

I have tried with your above mentioned code. But unable to store files into db.

I am unable to find the issue.

<form role="form" id="content">
<input type="file" id="uploadFile" multiple/> 

 
   <script cam-script type="text/form-script">
    inject(['$http', 'Uri', function($http, Uri) {
        
        $scope.upload = function() {
            

            camForm.variableManager.createVariable({
            name: 'testData',
            type: 'Bytes',
            value:document.getElementById('uploadFile').files[0].data
            });
  
        }
               
    }]);
    </script>
</form>

@manmohanm

You need to hook into the Camunda events as decribed here: Camunda embedded forms lifecycle and events.

I would strongly recommend to read this documentation first. It is time well invested and will make you much more productive than the shoot and miss approach you seem to be following now.

Pack your code in the submit event like so:

camForm.on('submit', function() {
  camForm.variableManager.createVariable({
    name: 'testData',
    type: 'Bytes',
    value: document.getElementById('uploadFile').files[0].data
  });
});

@c.biever

Still its not working

<form role="form" >
<input type="file" id="uploadFile" multiple/> 

 
   <script cam-script type="text/form-script">
    inject(['$http', 'Uri', function($http, Uri) {
        

    camForm.on('submit', function() {
            camForm.variableManager.createVariable({
            name: 'testDatas',
            type: 'Bytes',
            value:document.getElementById('uploadFile').files[0].data
            });
  
        });
               
    }]);
    </script>
</form>

@c.biever

I am trying for multiple-file-uploads in several ways. One of them is mentioned below.

<form role="form">
  
<div id="fildUploads">
<input type="file"
       cam-variable-name="filee0"
       cam-variable-type="File"
       cam-max-filesize="10000000" /> 
   
       
</div>  
<button ng-click="upload()">Attach Another</button>
  <script cam-script type="text/form-script">
    inject(['$http', 'Uri', function($http, Uri) {
      var i=0;
      $scope.upload = function() {
          
          i++;
          var y = document.createElement("input");
y.setAttribute("type", "file");
y.setAttribute("cam-variable-name", "filee1");
y.setAttribute("cam-variable-type", "File");
y.setAttribute("cam-max-filesize", "10000000");
y.setAttribute("class", "ng-valid ng-valid-cam-variable-type ng-pristine");
y.setAttribute("ng-model", "filee1");


   document.getElementById('fildUploads').appendChild(y);
    }
    }]);
  </script>
</form>

Not working :frowning2:

@manmohanm

Are there any error messages? Did you try debugging with either a debugger in a browser or using alert/console.log? Is the submit code being called?

@c.biever thanks for your reply . I have tried using the following code

<form role="form">
  
<div id="fildUploads">
<input type="file"
       cam-variable-name="filee0"
       cam-variable-type="File"
       cam-max-filesize="10000000" /> 
   
       
</div>  
<button ng-click="upload()">Attach Another</button>
  <script cam-script type="text/form-script">
    inject(['$http', 'Uri', function($http, Uri) {
      var i=0;
      $scope.upload = function() {
          
          i++;
          var y = document.createElement("input");
y.setAttribute("type", "file");
y.setAttribute("cam-variable-name", "filee1");
y.setAttribute("cam-variable-type", "File");
y.setAttribute("cam-max-filesize", "10000000");
y.setAttribute("class", "ng-valid ng-valid-cam-variable-type ng-pristine");
y.setAttribute("ng-model", "filee1");


   document.getElementById('fildUploads').appendChild(y);
    }
    }]);
  </script>
</form> 

When i checked console log ,

<input type="file"
       cam-variable-name="filee0"
       cam-variable-type="File"
       cam-max-filesize="10000000" />

Above set of code is calling some onchange methods.(Event listners)

I have tried these set of codes to show another browse button (Browse button is showing but files are not saved in database). No event listeners are called this time.

var y = document.createElement(“input”);
y.setAttribute(“type”, “file”);
y.setAttribute(“cam-variable-name”, “filee1”);
y.setAttribute(“cam-variable-type”, “File”);
y.setAttribute(“cam-max-filesize”, “10000000”);
y.setAttribute(“class”, “ng-valid ng-valid-cam-variable-type ng-pristine”);
y.setAttribute(“ng-model”, “filee1”);

   document.getElementById('fildUploads').appendChild(y);

I trimmed down our code to this example:

<form name="test" xmlns="http://www.w3.org/1999/html">
  <div class="form-group">
    <label class="control-label">Documents</label>
    <table class="table">
      <tbody>
      <tr ng-repeat="file in files">
        <td>
          <a role="button" ng-click="openDocument(file)">{{file.name}}</a>
        </td>
        <td>
          <span class="glyphicon glyphicon-remove" ng-click="removeDocument(file)" style="cursor:pointer"></span>
        </td>
      </tr>
      </tbody>
    </table>
    <div>
      <label class="btn btn-default btn-file">
        Browse <input type="file" id="fileUpload" style="display: none;" onchange="angular.element(this).scope().upload(this)" multiple>
        <span class="glyphicon glyphicon-folder-open"/>
      </label>
    </div>
  </div>
  <script cam-script type="text/form-script">
    inject([ '$scope', function($scope) {

      $scope.files = [];

      $scope.upload = function(fileUpload) {
        for (let i = 0; i < fileUpload.files.length; i++) {
          let file = { name: fileUpload.files[i].name, mimetype: fileUpload.files[i].type, data: undefined, blob: undefined };
          $scope.files.push(file);
          $scope.retrieveFiledata(fileUpload.files[i], file);
        }
        $scope.test.$setDirty();
        $scope.$apply();
      };

      $scope.retrieveFiledata = function (file, fileData) {
        let reader = new FileReader();
        reader.onload = function (event) {
          fileData.blob = reader.result;
          let binary = '';
          let bytes = new Uint8Array(reader.result);
          for (let i = 0; i < bytes.length; i++) {
            binary += String.fromCharCode(bytes[i]);
          }
          fileData.data = btoa(binary);
        };
        reader.readAsArrayBuffer(file);
      };

      $scope.removeDocument = function (file) {
        $scope.files.splice($scope.files.indexOf(file), 1);
      };

      $scope.openDocument = function (file) {
        if (window.navigator.msSaveOrOpenBlob) {
          if (file.href) {
            $http.get(file.href, {responseType: 'arraybuffer'}).then(
              function successCallback(response) {
                window.navigator.msSaveOrOpenBlob(new Blob([response.data], {type: file.mimetype}), file.name);
              },
              function errorCallback(response) {
                console.log('error downloading document: ' + response);
              }
            );
          }
          else {
            window.navigator.msSaveOrOpenBlob(new Blob([file.blob], {type: file.mimetype}), file.name);
          }
        } else {
          window.open('data:' + file.mimetype + ';base64,' + file.data);
        }
      };

      camForm.on('form-loaded', function() {
      });

      camForm.on('variables-fetched', function() {
      });

      camForm.on('variables-restored', function() {
      });

      camForm.on('submit', function() {
        $scope.files.forEach(function (file, index) {
          camForm.variableManager.createVariable({
            name: 'DOCUMENT_' + index,
            type: 'Bytes',
            value: file.data
          });
        });
      });

    }]);

   </script>
</form>

@c.biever
Thank you very much.We have tried your code. It stores files to the database. But there are some issues that we had already faced earlier.

We are able to download the uploaded files by below using mentioned code

<a cam-file-download="DOCUMENT_1">Test1</a>

After that have created a variable for fetching count of files uploaded.

for(var i=0;i<$scope.fileCount; i++)
                    {
                        y = document.createElement("a");
                        y.setAttribute("cam-file-download", "DOCUMENT_"+i);
                        y.setAttribute("href"," /camunda/api/engine/engine/default/task/"+camForm.taskId+"/variables/DOCUMENT_"+i+"/data");
                        y.innerHTML="download"+i;
                        document.getElementById('fileDownloads').appendChild(y);
                    }

It works and we are able to download all files. But still there is no option for identifying the files with name
From the above code files are downloading as “data” named files. Actually i want to see the original file-name when downloading.
In database(in the table ACT_RU_VARIABLE), fields TEXT_ & TEXT2_ getting null values.

You are only storing the data as a BLOB. You need to store the filename, and any other information, separately e.g. like so:

$scope.files.forEach(function (file, index) {
  camForm.variableManager.createVariable({
    name: 'DOCUMENT_NAME_' + index,
    type: 'String',
    value: file.name
  });
});

Or you could create a json object which has references to the files and store that.

BTW: i noticed that you manipulate the DOM directly. This is unnecessary and you are better off if you use Angular’s ng-repeat.

Hi Manmohanm

I have tried this code but i am not able to download all the file.

Regards
Suhaib Sultan