This can be done with base64 encoding. The general pattern is as follows:
- Use JavaScript's FileReader's readAsDataURL to read the file selected by the file input into a String variable.
- Encode the contents using JavaScript's encodeURIComponent function.
- Call the @AuraEnabled action with the contents, content type, and the Id of the parent record.
- URI decode the contents.
- Base64 decode the contents
- 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.
- Use JavaScript's FileReader's readAsDataURL to read the file selected by the file input into a String variable.
- Call the @AuraEnabled action with the contents (URI encoded), content type, and the Id of the parent record.
- Insert the attachment (as done in the above steps) and return the Id back to the helper.
- 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;
}
}
Best Answer
You can use a standard input type="file" and parse the file on the client side. Ideally I would've like to parse the file in Apex, but I ended up converting the CSV to JSON in Javascript. I then can easily pass the JSON string to Apex for processing. Here is some sample controller code. The helper contains a convert CSVtoJSON function that can be easily found online. Here's a fiddle : http://jsfiddle.net/sturtevant/AZFvQ/