Este post surgió de la necesidad de adjuntar múltiples archivos desde un componente Lightning. Salesforce limita el tamaño de los paquetes de callback desde Lightning a algo menos de 5 MB, por lo tanto, tuvimos que idear una manera de realizar llamadas sucesivas e ir insertando/completando nuestros archivos, llamada tras llamada. En la entrada anterior vimos cómo crear un componente lightning que nos permitiera seleccionar archivos desde nuestro ordenador con sus validaciones correspondientes y al cual podíamos indicarle el número máximo de ficheros a adjuntar y el tamaño máximo para cada uno de ellos. El siguiente paso sería el de ilustrar su uso y la rutina que nos va a permitir almacenar estos archivos de forma estática en la base de datos de Salesforce.
Objeto para el almacenamiento
Existen varios objetos estándar de Salesforce para representar archivos: Attachment, FeedAttachment, ContentVersion, etc. En esta entrada hemos utilizado el objeto Attachment para nuestros ficheros. Salesforce impone que para el almacenamiento de Attachments es necesario especificar un parentId, es decir el registro al cual hagan referencia. En este caso hemos escogido el Account del usuario actual.
Uso del componente
Nuestro componente FileInputLoader necesita ser utilizado dentro de otro componente padre (FilesUploader) que reciba los archivos seleccionado y gestione la subida de los mismos. La forma de que ambos componentes posean la lista de ficheros de forma reactiva es haciendo que el atributo que compartan ambos se corresponda con el atributo de salida del hijo (fileList).
FilesUploader
Nuestro componente padre se compondrá de un markup, controller y helper. Además necesitaremos desarrollar un controlador en APEX (FilesUploaderController) para gestionar las llamadas DML para el proceso de almacenamiento.
Markup
En el siguiente fragmento de código podemos ver como integrar nuestro componente, nuestro controlador en APEX y cómo enlazar los atributos entre el padre y el hijo.
1 2 3 4 5 6 7 8 |
<aura:component controller=“FilesUploaderController”> <aura:attribute name=“filesToUpload” type=“Object[]” default=“[]”/> <aura:attribute name=“accountId” type=“String” default=“”/> <aura:handler name=“init” value=“{!this}” action=“{!c.doInit}”/> <c:FileInputLoader fileList=“{!v.filesToUpload}” maxNumFiles=“2” maxFileSize=“45000000”/> <ui:button label=“Upload” press=“{!c.uploadFiles}” disabled=“{!v.filesToUpload.length==0}”/> </aura:component> |
Controller
Para conseguir el Id del account asociado al usuario actual es necesario manejar el evento init para realizar una petición a nuestra clase APEX en el proceso de «inicialización» del componente.
Para iniciar el proceso de subida incluimos un botón cuya acción la definiremos en el controller del componente padre como una función (uploadFiles):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
({ doInit: function(component,event,helper){ helper.getCurrentAccountId(component); }, uploadFiles: function(component,event,helper){ var files = component.get(‘v.filesToUpload’); var parentId = component.get(‘v.accountId’); if(files.length){ helper.saveFiles(component,files,parentId); }else{ /* * errors management */ } } }) |
Desde uploadFiles se llamará a la función saveFiles en el helper que se encargará de leer los datos de los archivos y gestionar su subida.
Helper
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
({ getCurrentAccountId: function(){ var action = component.get(‘c.getAccountId’); action.setCallback(this,function(response){ if(response.getState() === ‘SUCCESS’){ var accountId = response.getReturnValue(); component.set(‘v.accountId’,accountId); } }); $A.enqueueAction(action); }, idx: 0, saveFiles: function(component, files, parentId){ var currentFile = files[idx]; var fr = new FileReader(); var self = this; fr.onload = function(){ var fileData = fr.result; //componemos la cadena con los datos del fichero var b64Mark = ‘base64,’; var start = fileData.indexOf(b64Mark) + b64Mark.length; var fileData = fileData.substring(start); //llamamos a la función uploadFile para gestionar la subida individual de cada fichero. self.uploadFile(component,currentFile,fileData,parentId); self.idx ++; //comprobamos si queda algún fichero por leer. if(self.idx<files.length){ currentFile = files[idx]; fr.readAsDataURL(currentFile); } } fr.readAsDataURL(currentFile); } }) |
La función getCurrentAccountId realizará una llamada al controlador en APEX que devolverá el Id del Account del usuario actual y que podremos usar como parentId a la hora de crear los objetos Attachment para nuestros ficheros.
En saveFiles creamos un objeto FileReader nativo de JavaScript para leer nuestros archivos. Con el atributo idx del helper especificamos el archivo a leer en el handler onload del objeto FileReader. Cuando hemos leído los datos del fichero necesitamos quedarnos con los datos en base64 y con esa cadena de caracteres llamar a la función uploadFile para gestionar la subida individual.
A partir de este momento debemos hacer frente a dos problemas que nos propone Salesforce:
- El límite de datos de transferencia entre el controlador y el componente lightning.
- El lifecycle del evento press del button que inicia la acción.
La solución sería limitar el tamaño de los archivos a subir. Esto limita bastante nuestro componente por lo que debemos plantearnos realizar la subida partida de archivos o mediante chunks.
Según la documentación de Lightning, todas las actions en cola se ejecutarán al finalizar el lifecycle que las engloba. Nuestra acción para la subida de cada chunk al llamarse en el marco de ejecución del evento onload del FileReader ha perdido el contexto del lifecycle del evento press original por lo que deberemos llamarla de otra forma y que se ejecute fuera de este ámbito. Para ello utilizamos la función $A.getCallback.
Este proceso quedaría de la siguiente manera en nuestro helper:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
({ … MAX_SIZE_CHUNK: 1000000, //1MB uploadFile: function(component,file,fileData,parentId){ var fromPos = 0; var toPos = Math.min(fileData.length, fromPos + this.MAX_SIZE_CHUNK); var self = this; self.uploadFileChunk(component,file,fileData,fromPos,toPos,’’,parentId,CHUNK_SIZE); }, uploadFileChunk: function(component,file,fileData,fromPos,toPos,attachId,parentId,CHUNK_SIZE){ var action = component.get(‘c.saveFileChunk’); var chunk = fileData.substring(fromPos,toPos); action.setParams({ parentId: parentId, fileName: file.Name, b64Data: chunk, contentType: file.type, attachId: attachId }); var self = this; action.setCallback(this,function(response){ if(response.getState() === ‘SUCCESS’){ var attachId = response.getReturnValue(); //siguientes límites. fromPos = toPos; toPos = Math.min(fileData.length, fromPos + CHUNK_SIZE); //queda parte del archivo por subir. if(fromPos<toPos){ self.uploadFileChunk(component,file,fileData,fromPos,toPos, attachId,parentId,CHUNK_SIZE); }else{ //Proceso terminado para este fichero. } }else{ /* * Errors management */ } }); //ponemos en cola la acción fuera del lifecycle del componente. var uploadFn = $A.getCallback(function(){ $A.enqueueAction(action); }); //ejecutamos la acción. uploadFn(); } }); |
FilesUploaderController
Ya tenemos todo lo necesario en nuestro componente para realizar el proceso de subida. Ahora nos faltaría el desarrollo de los métodos de nuestro controlador en APEX.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
public with sharing class FileUploaderController{ @AuraEnabled public static String getAccountId(){ User u = [SELECT Id, ContactId FROM User WHERE Id = :UserInfo.getUserId() LIMIT 1]; Contact c = [SELECT Id, AccountId FROM Contact WHERE Id = :u.ContactId LIMIT 1]; Account acc = [SELECT Id FROM Account WHERE Id = :c.AccountId]; return String.valueOf(acc.Id); } public static String saveAttachment (Id parentId, String fileName, String b64Data, String contentType){ Attachment a = new Attachment(); a.parentId = parentId; a.Body = EncodeUtil.base64Decode(b64Data); a.Name = fileName; a.ContentType = contentType; insert a; return String.valueOf(a.Id); } @AuraEnabled public static String saveChunkFile(Id parentId, String fileName, String b64Data, String contentType, Id attachId){ if(String.isEmpty(attachId) || attachId == null){ saveAttachment(parentId, fileName,b64Data,contentType); }else{ appendToAttachment(attachId,b64Data,contentType); } } public static appendToAttachment(Id attachId, String b64Data, String contentType){ Attachment a = [SELECT Id, Body FROM Attachment WHERE Id = :attachId]; String existingBody = EncodingUtil.base64Encode(a.Body); a.Body = EncodingUtil.base64Decode(existingBody + b64Data); update a; } } |
Los métodos saveChunkFile y getAccountId poseen la cláusula @AuraEnabled para que el componente tenga acceso a ellas desde el controller y el helper.
Desde saveChunkFile se llamaría a la función saveAttachment si todavía no se ha creado un Attachment o a la función appendToAttachment para añadir el siguiente chunk de datos al body del Attachment que ya existe para el fichero en contexto. Los archivos harían referencia al Account de nuestro usuario actual por lo que sólo tendríamos que visitar el detalle para poder ver el listado.
Ya tenemos nuestro sistema de subida de ficheros listo para usar en cualquier proyecto.
Que paséis una buena semana!