5

I wanted to attach a file to an objects's notes & attachments list. Since Aura's inputFile ui tag is in Prototype stage, Is there any other way to implement this? How to pass the Blob value received from FileReader (Javascript). Which attribute type in Lightning has to be used?

7

This can be done with base64 encoding. The general pattern is as follows:

  1. Use JavaScript's FileReader's readAsDataURL to read the file selected by the file input into a String variable.
  2. Encode the contents using JavaScript's encodeURIComponent function.
  3. Call the @AuraEnabled action with the contents, content type, and the Id of the parent record.
  4. URI decode the contents.
  5. Base64 decode the contents
  6. Insert the attachment.

There are limitations in the size of the file that can be transferred. I observed that limitation to be 1,000,000 characters. The solution to that is to chunk the file in to different pieces. That algorithm is slightly different, but basically boils down to iterating over the contents in chunks, and then concatenating them to the existing Attachment body.

  1. Use JavaScript's FileReader's readAsDataURL to read the file selected by the file input into a String variable.
  2. Call the @AuraEnabled action with the contents (URI encoded), content type, and the Id of the parent record.
  3. Insert the attachment (as done in the above steps) and return the Id back to the helper.
  4. For each chunk left:
    • Call the @AuraEnabled action with the contents (URI encoded), and the Attachment Id.
    • Retrieve the Attachment out of the DB (SOQL) and base64 encode its contents.
    • Append the base64 chunk passed into the method with the existing body.
    • Base64 decode it back into the body of the attachment.
    • Update the attachment.

You have to watch out for the String size limitations in the concatenation and of course the Attachment file size limitation. The String size limitation is roughly a bit less than 4.5 MB which isn't that far off from the 5 MB Attachment limit.

I just wrote a detailed blog article about this here. Below is the helper's upload code and Apex controller, copied and pasted from my article for convenience and answer completeness.

The fileUploadHelper.js code:

({
    MAX_FILE_SIZE: 4 500 000, /* 6 000 000 * 3/4 to account for base64 */
    CHUNK_SIZE: 950 000, /* Use a multiple of 4 */

    save : function(component) {
        var fileInput = component.find("file").getElement();
        var file = fileInput.files[0];

        if (file.size > this.MAX_FILE_SIZE) {
            alert('File size cannot exceed ' + this.MAX_FILE_SIZE + ' bytes.\n' +
                    'Selected file size: ' + file.size);
            return;
        }

        var fr = new FileReader();

        var self = this;
        fr.onload = $A.getCallback(function() {
            var fileContents = fr.result;
            var base64Mark = 'base64,';
            var dataStart = fileContents.indexOf(base64Mark) + base64Mark.length;

            fileContents = fileContents.substring(dataStart);

            self.upload(component, file, fileContents);
        });

        fr.readAsDataURL(file);
    },

    upload: function(component, file, fileContents) {
        var fromPos = 0;
        var toPos = Math.min(fileContents.length, fromPos + this.CHUNK_SIZE);

        // start with the initial chunk
        this.uploadChunk(component, file, fileContents, fromPos, toPos, '');   
    },

    uploadChunk : function(component, file, fileContents, fromPos, toPos, attachId) {
        var action = component.get("c.saveTheChunk"); 
        var chunk = fileContents.substring(fromPos, toPos);

        action.setParams({
            parentId: component.get("v.parentId"),
            fileName: file.name,
            base64Data: encodeURIComponent(chunk), 
            contentType: file.type,
            fileId: attachId
        });

        var self = this;
        action.setCallback(this, function(a) {
            attachId = a.getReturnValue();

            fromPos = toPos;
            toPos = Math.min(fileContents.length, fromPos + self.CHUNK_SIZE);    
            if (fromPos < toPos) {
                self.uploadChunk(component, file, fileContents, fromPos, toPos, attachId);  
            }
        });

        $A.enqueueAction(action); 
    }
})

The FileController.cls code:

public class FileController {

    @AuraEnabled
    public static Id saveTheFile(Id parentId, String fileName, String base64Data, String contentType) { 
        base64Data = EncodingUtil.urlDecode(base64Data, 'UTF-8');

        Attachment a = new Attachment();
        a.parentId = parentId;

        a.Body = EncodingUtil.base64Decode(base64Data);
        a.Name = fileName;
        a.ContentType = contentType;

        insert a;

        return a.Id;
    }

    @AuraEnabled
    public static Id saveTheChunk(Id parentId, String fileName, String base64Data, String contentType, String fileId) { 
        if (fileId == '') {
            fileId = saveTheFile(parentId, fileName, base64Data, contentType);
        } else {
            appendToFile(fileId, base64Data);
        }

        return Id.valueOf(fileId);
    }

    private static void appendToFile(Id fileId, String base64Data) {
        base64Data = EncodingUtil.urlDecode(base64Data, 'UTF-8');

        Attachment a = [
            SELECT Id, Body
            FROM Attachment
            WHERE Id = :fileId
        ];

        String existingBody = EncodingUtil.base64Encode(a.Body);
        a.Body = EncodingUtil.base64Decode(existingBody + base64Data); 

        update a;
    }
}
  • Great solution :). As an FYI, this seems to work for me without url-encoding/decoding the data, but I haven't done extensive testing. – dana Jul 29 '16 at 1:57
  • Peter, Thanks for this post -- I have been able to successfully implement the first method in your blog post with the 750k limitation. Haven't tried the chunking method yet, but looking at your code there is no mechanism for telling when the chunk upload is complete? I need to be able to send the uploaded Attachment to another function once all the chunks are complete -- any ideas? – mplunket Nov 29 '16 at 21:20
  • Hi Peter, i am struggling uploading file in contentVersion using lightning component. I followed all the steps given in your blog but dont know whats wrong with my code. Can you please guide me with this. Refer to below link for details : salesforce.stackexchange.com/questions/157024/… – Muzammil Bajaria Jan 23 '17 at 8:50

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.