[SalesForce] How to upload Files as Guest Community User

According to Spring 19, it should now be possible to upload files as a Guest Community user.

But if you try either of,

  1. <lightning:fileUpload>
  2. . <lightning:input aura:id="fileId" onchange="{!c.handleFilesChange}" type="file" name="file" label="{!v.label}"> along with custom Apex (Without sharing)

it fails to upload files, I tried this against a custom object.

Simple Lightning component example below:

<lightning:fileUpload  aura:id="fileUploader" label= "Documents" multiple="true" 
 recordId="{!v.fdw.app.Id}" onuploadfinished="{! c.handleUploadFinished }"/>

Debug logs for Community guest user seems to be giving error while creating ContentDocumentLink.

Related Debug log trace as below.

02:15:05.0 (6412865)|HEAP_ALLOCATE|[32]|Bytes:4 02:15:05.0
(14733563)|VARIABLE_ASSIGNMENT|[32]|this.Title|"2019-05-02 13_04_59-
(10 more) …"|0x3459e1ce 02:15:05.0
(14800526)|VARIABLE_SCOPE_BEGIN|[32]|cv|ContentVersion|true|false
02:15:05.0 (14824748)|VARIABLE_ASSIGNMENT|[32]|cv|{"Title":"2019-05-02
13_04_59- (10 more) …"}|0x3459e1ce 02:15:05.0
(14831351)|STATEMENT_EXECUTE|[33] 02:15:05.0
(14863568)|METHOD_ENTRY|[33]||System.EncodingUtil.base64Decode(String)
02:15:05.0
(15366339)|METHOD_EXIT|[33]||System.EncodingUtil.base64Decode(String)
02:15:05.0
(15476425)|VARIABLE_ASSIGNMENT|[33]|this.VersionData|BLOB(14076
bytes)|0x3459e1ce 02:15:05.0 (15484691)|STATEMENT_EXECUTE|[34]
02:15:05.0 (15489332)|HEAP_ALLOCATE|[34]|Bytes:1 02:15:05.0
(15509894)|HEAP_ALLOCATE|[34]|Bytes:31 02:15:05.0
(15577773)|VARIABLE_ASSIGNMENT|[34]|this.PathOnClient|"/2019-05-02
13_04_59 (11 more) …"|0x3459e1ce 02:15:05.0
(15584954)|STATEMENT_EXECUTE|[35] 02:15:05.0
(15614660)|HEAP_ALLOCATE|[35]|Bytes:8 02:15:05.0
(15625148)|DML_BEGIN|[35]|Op:Insert|Type:ContentVersion|Rows:1
02:15:05.0 (15666572)|HEAP_ALLOCATE|[EXTERNAL]|Bytes:8 02:15:05.0
(202795736)|DML_END|[35] 02:15:05.0 (202824238)|STATEMENT_EXECUTE|[37]
02:15:05.0 (202834036)|HEAP_ALLOCATE|[37]|Bytes:64 02:15:05.0
(202876533)|HEAP_ALLOCATE|[37]|Bytes:4 02:15:05.0
(202897343)|HEAP_ALLOCATE|[37]|Bytes:7 02:15:05.0
(203255763)|SOQL_EXECUTE_BEGIN|[37]|Aggregations:0|SELECT
ContentDocumentId FROM ContentVersion WHERE id = :tmpVar1 02:15:05.0
(219893445)|SOQL_EXECUTE_END|[37]|Rows:1 02:15:05.0
(219933351)|HEAP_ALLOCATE|[37]|Bytes:8 02:15:05.0
(219955124)|HEAP_ALLOCATE|[37]|Bytes:68 02:15:05.0
(220089165)|HEAP_ALLOCATE|[37]|Bytes:8 02:15:05.0
(220109080)|HEAP_ALLOCATE|[37]|Bytes:40 02:15:05.0
(220166834)|HEAP_ALLOCATE|[37]|Bytes:12 02:15:05.0
(220206632)|VARIABLE_ASSIGNMENT|[37]|cv|{"ContentDocumentId":"069N0000001RiTUIA0","Id":"068N0000001RW4sIAG"}|0x6f11b49f
02:15:05.0 (220215545)|STATEMENT_EXECUTE|[38] 02:15:05.0
(220277460)|HEAP_ALLOCATE|[38]|Bytes:4 02:15:05.0
(220480051)|VARIABLE_ASSIGNMENT|[38]|this.ContentDocumentId|"069N0000001RiTUIA0"|0x1b3e677a
02:15:05.0 (220490200)|HEAP_ALLOCATE|[38]|Bytes:1 02:15:05.0
(220525784)|VARIABLE_ASSIGNMENT|[38]|this.ShareType|"V"|0x1b3e677a
02:15:05.0
(220571846)|VARIABLE_ASSIGNMENT|[38]|this.LinkedEntityId|"a0jN0000009bzCbIAI"|0x1b3e677a
02:15:05.0
(220612000)|VARIABLE_SCOPE_BEGIN|[38]|contentLinkDoc|ContentDocumentLink|true|false
02:15:05.0
(220634097)|VARIABLE_ASSIGNMENT|[38]|contentLinkDoc|{"ContentDocumentId":"069N0000001RiTUIA0","ShareType":"V","LinkedEntityId":"a0jN0000009bzCbIAI"}|0x1b3e677a
02:15:05.0 (220640696)|STATEMENT_EXECUTE|[40] 02:15:05.0
(220672740)|HEAP_ALLOCATE|[40]|Bytes:8 02:15:05.0
(220682700)|DML_BEGIN|[40]|Op:Insert|Type:ContentDocumentLink|Rows:1
02:15:05.0 (220713085)|HEAP_ALLOCATE|[EXTERNAL]|Bytes:8 02:15:05.0
(263227829)|DML_END|[40] 02:15:05.0
(263322419)|EXCEPTION_THROWN|[40]|System.DmlException: Insert failed.
First exception on row 0; first error:
INSUFFICIENT_ACCESS_OR_READONLY, You do not have the level of access
necessary to perform the operation you requested. Please contact the
owner of the record or your administrator if access is necessary.: []
02:15:05.0 (263712544)|HEAP_ALLOCATE|[40]|Bytes:265 02:15:05.0
(263763674)|METHOD_EXIT|[11]|01pN0000001tiGM|FileUploadController.saveTheFile(Id,
String, String, String) 02:15:05.0 (263778390)|SYSTEM_MODE_EXIT|true
02:15:05.0 (264564654)|FATAL_ERROR|System.DmlException: Insert failed.
First exception on row 0; first error:
INSUFFICIENT_ACCESS_OR_READONLY, You do not have the level of access
necessary to perform the operation you requested. Please contact the
owner of the record or your administrator if access is necessary.: []

Class.FileUploadController.saveTheFile: line 40, column 1
Class.FileUploadController.saveChunk: line 11, column 1 02:15:05.0
(264581079)|FATAL_ERROR|System.DmlException: Insert failed. First
exception on row 0; first error: INSUFFICIENT_ACCESS_OR_READONLY, You
do not have the level of access necessary to perform the operation you
requested. Please contact the owner of the record or your
administrator if access is necessary.: []

Any helpful thoughts to get File upload working for Community guest users? Much Appreciated.
Thanks!

Best Answer

Found below workaround example which seems to work,

But notably,

  1. Owner need to be assigned to internal user
  2. This workaround failed for any image files (pdf, doc, excel, ppt, txt seems to work)

Component markup:

<!-- configurable attribute -->
<aura:attribute name="parentId" type="String" required="true"/>
<aura:attribute name="fieldLabel" type="String" default="Upload Attachment"/>
<aura:attribute name="instantUpload" type="boolean" default="false" />
<aura:attribute name="buttonLabel" type="String" default="Upload"/>
<aura:attribute name="refreshAfterUpload" type="boolean" default="false"/>

<!-- internal use -->
<aura:attribute name="showLoadingSpinner" type="boolean" default="false" />
<aura:attribute name="fileName" type="String" default="No File Selected.." />

<b>Custom File upload</b>

<aura:if isTrue="{!v.showLoadingSpinner}">
    <div class="slds-text-body_x-small">Uploading... 
        <lightning:spinner aura:id="spinner" alternativeText="Loading" size="medium" title="Uploading..."/>
    </div>
</aura:if>

<div class="maincontent">
    <lightning:input aura:id="fileId" onchange="{!c.handleFilesChange}" type="file" name="file" label="{!v.fieldLabel}"  multiple="false" />
    <div class="filenameholder slds-text-body_small slds-text-color_error">
        {!v.fileName}
    </div>

    <aura:if isTrue="{!not(v.instantUpload)}">
        <div class="buttonholder">
            <lightning:button variant="brand" label="{!v.buttonLabel}" title="{!v.buttonLabel}" onclick="{!c.handleManualUpload}"></lightning:button>
        </div>
    </aura:if>
</div>    

Javascript Controller:

({
    handleFilesChange: function(cmp, event, helper) {
        var fileName = 'No File Selected..';

        if (event.getSource().get("v.files").length > 0) {
            fileName = event.getSource().get("v.files")[0]['name'];

            if(cmp.get("v.instantUpload")){
                //we only invoke upload the file here if instant upload is enabled
                helper.uploadFile(cmp, event);
            }
        }

        cmp.set("v.fileName", fileName);
    },

    handleManualUpload: function(cmp, event, helper) {

        if (cmp.get("v.fileName").length > 0 && cmp.find("fileId").get("v.files") != null && cmp.find("fileId").get("v.files").length > 0) {
            helper.uploadFile(cmp, event);
        } else {

            var toastEvent = $A.get("e.force:showToast");

            toastEvent.setParams({
                "title": "Warning",
                "message": "Please select a valid file.",
                "type": "error",
                "mode": "dismissible"
            });

            toastEvent.fire();
        }
    },

    handleUploadFinished : function(component, event, helper) {
        alert("uploaded ....");       
    }

})

Javascript Helper:

({
    MAX_FILE_SIZE: 4500000, //Max file size 4.5 MB 
    CHUNK_SIZE: 750000,     //Chunk Max size 750Kb 

    uploadFile: function(cmp, event) {

        this.toggleSpinner(cmp, true);

        // get the selected files using aura:id [return array of files]
        var fileInput = cmp.find("fileId").get("v.files");

        // get the first file using array index[0]  
        var file = fileInput[0];
        var self = this;
        // check the selected file size, if select file size greter then MAX_FILE_SIZE,
        // then show a alert msg to user,hide the loading spinner and return from function  
        if (file.size > self.MAX_FILE_SIZE) {

            this.toggleSpinner(cmp, false);
            cmp.set("v.fileName", 'File size cannot exceed ' + self.MAX_FILE_SIZE + ' bytes.\n' + ' Selected file size: ' + file.size);
            return;
        }

        // create a FileReader object 
        var objFileReader = new FileReader();
        // set onload function of FileReader object   
        objFileReader.onload = $A.getCallback(function() {
            var fileContents = objFileReader.result;
            var base64 = 'base64,';
            var dataStart = fileContents.indexOf(base64) + base64.length;

            fileContents = fileContents.substring(dataStart);
            // call the uploadProcess method 
            self.uploadProcess(cmp, file, fileContents);
        });

        objFileReader.readAsDataURL(file);
    },

    uploadProcess: function(cmp, file, fileContents) {
        // set a default size or startpostiton as 0 
        var startPosition = 0;
        // calculate the end size or endPostion using Math.min() function which is return the min. value   
        var endPosition = Math.min(fileContents.length, startPosition + this.CHUNK_SIZE);

        // start with the initial chunk, and set the attachId(last parameter)is null in begin
        this.uploadInChunk(cmp, file, fileContents, startPosition, endPosition, '');
    },

    uploadInChunk: function(cmp, file, fileContents, startPosition, endPosition, attachId) {

        // call the apex method 'saveChunk'
        var getchunk = fileContents.substring(startPosition, endPosition);
        var action = cmp.get("c.saveChunk");
        action.setParams({
            parentId: this.getParentId(cmp),
            fileName: file.name,
            base64Data: encodeURIComponent(getchunk),
            contentType: file.type,
            fileId: attachId
        });

        action.setCallback(this, function(response) {
            // store the response / Attachment Id   
            attachId = response.getReturnValue();
            var state = response.getState();
            console.log('state: '+state +' attachId '+attachId);
            if (state === "SUCCESS") {
                // update the start position with end postion
                startPosition = endPosition;
                endPosition = Math.min(fileContents.length, startPosition + this.CHUNK_SIZE);
                // check if the start postion is still less then end postion 
                // then call again 'uploadInChunk' method , 
                // else, diaply alert msg and hide the loading spinner
                if (startPosition < endPosition) {
                    this.uploadInChunk(cmp, file, fileContents, startPosition, endPosition, attachId);
                } else {

                    cmp.find("fileId").set("v.files",[]);
                    cmp.set("v.fileName", cmp.get("v.fileName") + ' is uploaded.' );

                    var toastEvent = $A.get("e.force:showToast");

                    if(toastEvent){
                        toastEvent.setParams({
                            "title": "Success!",
                            "message": "File " + file.name + " was uploaded successfully.",
                            "type": "success",
                            "mode": "dismissible"
                        });

                        toastEvent.fire();
                    } else {
                        alert("File " + file.name + " was uploaded successfully.");
                    }

                    //refresh the page after uploading
                    if(cmp.get("v.refreshAfterUpload")){
                        var refreshEvent = $A.get('e.force:refreshView');

                        if(refreshEvent){
                            refreshEvent.fire();
                        }
                    }

                }

            } else if (state === "INCOMPLETE") {

                var toastEvent = $A.get("e.force:showToast");
                if(toastEvent){
                    toastEvent.setParams({
                        "title": "Fail!",
                        "message": "File " + file.name + " was not uploaded due to " + response.getReturnValue(),
                        "type": "error",
                        "mode": "dismissible"
                    });

                    toastEvent.fire();
                } else {
                    alert("File " + file.name + " was not uploaded due to " + response.getReturnValue());
                }

            } else if (state === "ERROR") {

                var error = "Unknwown error.";

                var errors = response.getError();
                console.log('errors '+errors);
                if (errors) {
                    if (errors[0] && errors[0].message) {
                        error = errors[0].message;
                    }
                }

                var toastEvent = $A.get("e.force:showToast");
                if(toastEvent){
                    toastEvent.setParams({
                        "title": "Fail!",
                        "message": "File " + file.name + " was not uploaded due to " + error,
                        "type": "error",
                        "mode": "dismissible",
                        mode: 'sticky'
                    });

                    toastEvent.fire();
                } else {
                    alert("File " + file.name + " was not uploaded due to " + error);
                }

            }

            this.toggleSpinner(cmp, false);
        });

        $A.enqueueAction(action);
    },

    toggleSpinner: function(cmp, isLoading){
        if(isLoading){
            cmp.set("v.showLoadingSpinner", true);
        } else {
            cmp.set("v.showLoadingSpinner", false);
        }
    },

    getParentId: function(cmp){
        var parentIdParam = cmp.get("v.parentId");

        if(parentIdParam === "{record.Id}"){
            return cmp.get("v.recordId");
        } else {
            return cmp.get("v.parentId");
        }
    }
})

Apex Controller:

public without sharing class FileUploader_LCTRL {

    @AuraEnabled
    public static Id saveChunk(Id parentId, String fileName, String base64Data, String contentType, String fileId) {
        // check if fileId id ''(Always blank in first chunk), then call the saveTheFile method,
        //  which is save the check data and return the attachemnt Id after insert, 
        //  next time (in else) we are call the appentTOFile() method
        //   for update the attachment with reamins chunks   
        System.debug('fileId--------------: '+fileId);
        if (fileId == '') {
            fileId = saveTheFile(parentId, fileName, base64Data, contentType);

        } else {
            appendToFile(fileId, base64Data);       // small files used to test scenario
        }
        System.debug('file id-------------------- '+fileId);        
        return fileId;//Id.valueOf(fileId);
    }

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

        /*Attachment newAtt = new Attachment();
        newAtt.parentId = parentId;

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

        insert newAtt;

        return newAtt.Id;*/
        try{
            ContentVersion cv =new ContentVersion(Title = fileName); 
            cv.VersionData = EncodingUtil.base64Decode(base64Data);
            cv.PathOnClient='/' + fileName ;
            cv.OwnerId = '0057F0000010x2R';     // Admin user
            cv.FirstPublishLocationId = parentId; // cv.OwnerId;

            System.debug('Before insert cv ');

            insert cv;

            System.debug('after insert cv');

            /*cv = [select ContentDocumentId from ContentVersion where id=:cv.Id];
            ContentDocumentLink cDL = new ContentDocumentLink(ContentDocumentId = cv.ContentDocumentId,  LinkedEntityId = parentId, ShareType = 'V'); //ShareType = 'V'
            System.debug('Before insert cdl');
            insert cDL;
            System.debug('After insert cdl');
            return cDL.Id;*/
            System.debug('id------------------------------------------- '+cv.Id);
            return String.valueOf(cv.Id);
        }
        catch(Exception ex){
            System.debug('Error: '+ex.getLineNumber()+' - '+ex.getMessage());
            System.debug('Stack: '+ex.getStackTraceString());
            return null;
        }
    }

    private static void appendToFile(Id fileId, String base64Data) {
        System.debug(' Update: ' + '----------------Appending data to fileid: '+fileId);
        /*base64Data = EncodingUtil.urlDecode(base64Data, 'UTF-8');

        Attachment att = [select Id, Body from Attachment where Id =: fileId];

        String existingBody = EncodingUtil.base64Encode(att.Body);

        att.Body = EncodingUtil.base64Decode(existingBody + base64Data);

        update att;*/

        ContentVersion cvUploaded = [select VersionData from ContentVersion where Id = :fileId];

        String existingBody = EncodingUtil.base64Encode(cvUploaded.VersionData);
        cvUploaded.VersionData = EncodingUtil.base64Decode(existingBody + base64Data);

        update cvUploaded;

    }

}

Please leave any comments, better workarounds are welcome and much appreciated!

Cheers!