I have a form where users can upload PDFs, which I store in the database as BLOBs. I'm showing a list of all the PDFs uploaded, all of which can be downloaded by a click. I've tried so many different workarounds to get the PDF to download properly but it will say "Failed to load PDF document" in the browser and "The file is damaged and could not be repaired" in Adobe Acrobat. Here is my code:
Instructors.cfc (form for uploading file)
<form method="post" enctype="multipart/form-data">
<input id="document_filename" name="document_filename" type="hidden">
<input id="document_title" name="document_title" type="hidden">
<input id="openFileBrowser" type="button" value="Import Data from Application PDF" onclick="document.getElementById('application_document').click();">
<input id="application_document" name="application_document" type="file" accept=".pdf" style="display:none">
<input id="upload_document" type="button" onclick="UploadDocument()" style="width:220px; display: none" value="Upload Instructor Application Form">
</form>
<script>
function UploadDocument() {
var fd = new FormData();
var theFile = document.getElementById("application_document").files[0];
fd.append('uploadedFile', theFile);
fd.append('file_name', document.getElementById("document_title").value);
$.ajax({
url: "InstructorForms.cfc?method=getApplicationPDFData",
type: "post",
data: fd,
processData: false,
contentType: false,
cache: false
});
</script>
InstructorForms.cfc (inserts PDF blob to database)
<cffunction name="getApplicationPDFData" access="remote">
<cfset uploadDirectory = "#expandPath('../UPLOADS')#">
<cfif not directoryExists(uploadDirectory)>
<cfdirectory action="create" directory="#uploadDirectory#">
</cfif>
<cfif IsDefined("uploadedFile")>
<cffile action="upload" fileField="uploadedFile" destination="#uploadDirectory#" nameConflict="overwrite" accept="application/pdf">
</cfif>
<cfif IsDefined("file_name")>
<cfset filePath = uploadDirectory & "\" & file_name>
<cfpdfform action="read" source="#filePath#" result="documentStruct" />
<cfset nameArray = documentStruct.Name.split(",")>
<cffile action="readbinary" file="#filePath#" variable="binPDF">
<cfquery name="addPDFToDB" datasource="#request.dsn#">
INSERT INTO DDMS.UPLOADED_FILES (LAST_NAME, FIRST_NAME, DOCUMENT, DOCUMENT_TYPE)
VALUES(<cfqueryparam value="#nameArray[1]#" cfsqltype="cf_sql_varchar">,
<cfqueryparam value="#ltrim(rtrim(nameArray[2]))#" cfsqltype="cf_sql_varchar">,
<cfqueryparam value="#binPDF#" cfsqltype="cf_sql_blob">,
'Instructor Application')
</cfquery>
<cffile action="delete" file="#filePath#">
</cffunction>
Instructors.cfc [again] (downloading PDF from database, where I'm having trouble)
<cffunction name="downloadPDF" access="remote" returntype="any">
<cfargument name="uploaded_file_id" required="yes" type="numeric">
<cfquery name="getInstructorApplication" datasource="#request.dsn#" result="output">
SELECT DOCUMENT, FIRST_NAME, LAST_NAME FROM DDMS.UPLOADED_FILES WHERE UPLOADED_FILE_ID = #arguments.uploaded_file_id#
</cfquery>
<cfset fileName = getInstructorApplication.LAST_NAME & "_" & getInstructorApplication.FIRST_NAME & "_application.pdf">
<cfset cfTags = "">
<cfsavecontent variable="cfTags">
<cfheader name="content-disposition" value="attachment; filename=#fileName#">
<cfcontent variable="#getInstructorApplication.DOCUMENT#" type="application/pdf" reset="yes">
</cfsavecontent>
<cfreturn cfTags>
</cffunction>
The most important section of code is the last/above snippet I included. Even when I navigate to the downloadPDF
function in the browser, it still won't download the PDF properly and gives the error messages. So cleaning that method up is step #1, and then I can actually retrieve the PDF on the user's page through an AJAX call, which I will also show in case it is helpful:
$(".pdfFile").on("click", function() {
var uploaded_file_id = $(this).data("id");
$.ajax({
url: "CFC/Instructors.cfc?method=downloadPDF",
data: { "uploaded_file_id": uploaded_file_id },
success: function(blob, status, xhr) {
var filename = "";
var disposition = xhr.getResponseHeader('Content-Disposition');
if (disposition && disposition.indexOf('attachment') !== -1) {
var filenameRegex = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/;
var matches = filenameRegex.exec(disposition);
if (matches != null && matches[1]) filename = matches[1].replace(/['"]/g, '');
}
if (typeof window.navigator.msSaveBlob !== 'undefined') {
window.navigator.msSaveBlob(blob, filename);
} else {
var URL = window.URL || window.webkitURL;
var newBlob = new Blob([blob], {type: "application/pdf"});
var downloadUrl = URL.createObjectURL(newBlob);
if (filename) {
var a = document.createElement("a");
if (typeof a.download === 'undefined') {
window.location.href = downloadUrl;
} else {
a.href = downloadUrl;
a.download = filename;
document.body.appendChild(a);
a.click();
}
} else {
window.location.href = downloadUrl;
}
}
}
});
});
Sorry for the overwhelming amount of code. But like I said, the most important part is in the downloadPDF
function, where I utilize cfcontent
and need to load the binary data properly. Any help would be greatly appreciated, as I've been stuck on this problem for a while and can't find much documentation.
UPDATE:
The PDF returned is of size 62.5 KB, and I've heard that output can be truncated to 64 KB due to a buffer in ColdFusion Admin if BLOB retrieval is disabled. I don't have access to ColdFusion Admin, yet one of my coworkers does, and perhaps he edited a wrong setting when attempting to enable BLOB retrieval globally. I'll check with him.
It turns out it was a setting in ColdFusion Administrator that was causing the problem. Although BLOB retrieval was enabled globally for the Dev environment, it wasn't enabled on my particular datasource. Enabling BLOB retrieval in the global settings is only step 1; it won't cover the datasource unless you enable it explicitly.
I did have to tweak my code a little, but not much. I changed the cfcontent
to use the file
attribute instead of variable
, which I populated the file
attribute with the path used from a <cffile action="write">
. After having written the file to the server, I create an anchor link for the file via JavaScript and call its click()
method so the PDF attachment will download. After that, I do another AJAX call in the done()
method to delete the file, as I don't need to keep that file on the server; it's a temporary thing. Here is my final code:
Instructors.cfc (file upload form stays the same)
<cfquery name="getInstructorApplications" datasource="#this.dsn#">
SELECT fm.FILELOB_ID, fm.FILEMETA_ID, fm.TITLE, fm.FILEEXT, fm.FILE_LEN, fm.CREATEDDATE
FROM DDMS.FILEMETA fm
JOIN DDMS.FILELOB fl
ON fm.FILELOB_ID = fl.FILELOB_ID
WHERE fl.DOCUMENT_TYPE = 'Instructor Application'
</cfquery>
<table class="DataTable">
<thead>
<tr>
<th style="width: 200px">File Name</th>
<th>File Extension</th>
<th>File Size</th>
<th>Date Uploaded</th>
</tr>
</thead>
<tbody>
<cfloop query="getInstructorApplications">
<cfoutput>
<tr>
<td style="width: 200px">
<a data-id="#FILELOB_ID#" data-meta-id="#FILEMETA_ID#" data-filename="#TITLE#" class="pdfFile" style="color: blue; cursor: pointer">#TITLE#</a>
</td>
<td>#FILEEXT#</td>
<td>#FILE_LEN#</td>
<td>#DATEFORMAT(LEFT(CREATEDDATE, 10))#</td>
</tr>
</cfoutput>
</cfloop>
</tbody>
</table>
<script>
$(".pdfFile").on("click", function() {
var file_id = $(this).data("id");
var file_meta_id = $(this).data("meta-id");
var file_name = $(this).data("filename");
var now = new Date();
var ticks = now.getTime();
<cfoutput>
var #ToScript(cfcPath, "cfcRoot")#;
</cfoutput>
$.ajax({
url: cfcRoot + "CFC/FileManager.cfc?method=ServeFileDownload&random=" + ticks,
type: "post",
data: { "FileID": file_id,
"FileMetaID": file_meta_id,
"fileName": file_name },
success: function() {
var a = document.createElement("a");
a.href = cfcRoot + "UPLOADS/" + file_name;
a.download = file_name;
document.body.appendChild(a);
a.click();
}
}).done(function() {
$.ajax({
url: cfcRoot + "CFC/FileManager.cfc?method=DeleteFile&random=" + ticks,
data: { "fileName": file_name }
});
});
});
</script>
FileManager.cfc (new file, takes over InstructorForms.cfc)
<cffunction name="ServeFileDownload" access="remote" returntype="void">
<cfargument name="FileID" type="numeric" required="no" default=0>
<cfargument name="FileMetaID" type="numeric" required="no" default=0>
<cfargument name="fileName" type="string" required="no" default="">
<cfif ARGUMENTS.FileID NEQ 0>
<cfset local.FileMetaID = GetCurrentMetaIDByFileID(FileID=ARGUMENTS.FileID)>
<cfelse>
<cfset local.FileMetaID = ARGUMENTS.FileMetaID>
</cfif>
<cfif local.FileMetaID NEQ 0>
<cfset ServeFile( FileMetaID=ARGUMENTS.FileMetaID, ServeType="attachment", filename="#ARGUMENTS.fileName#")>
<cfelse>
<cfreturn "">
</cfif>
</cffunction>
<cffunction name="ServeFile" access="public" returntype="void">
<cfargument name="FileMetaID" type="numeric" required="yes">
<cfargument name="ServeType" type="string" required="yes">
<cfargument name="fileName" type="string" required="no" default="">
<cfquery name="GetFileMetaData" datasource="#application.DDMS.dsn#">
SELECT fm.FILEMETA_ID, fm.FILELOB_ID, fl.FILELOB, fm.FILE_ID, fm.TITLE, fm.FILEEXT, fm.MIMETYPE_ID, fm.FILE_LEN
FROM DDMS.FILEMETA fm
JOIN DDMS.FILELOB fl
ON fm.FILELOB_ID = fl.FILELOB_ID
WHERE fm.FILEMETA_ID = <cfqueryparam cfsqltype="CF_SQL_INTEGER" value="#ARGUMENTS.FileMetaID#">
AND fm.ISACTIVE = 1
</cfquery>
<cfset MIMETypeObj = CreateObject("component","#application.global.cfcpath#.filemanager.mimetype")>
<cfset local.filename = Len(ARGUMENTS.fileName) ? "#ARGUMENTS.fileName#" : "#GetFileMetaData.TITLE#">
<cfset local.MIMETYPE = MIMETypeObj.GetMIMETypeByID(MIMETYPEID=GetFileMetaData.MIMETYPE_ID)>
<cfif ARGUMENTS.ServeType EQ "attachment">
<cfset local.MIMETYPE = "application/octet-stream">
</cfif>
<cfset uploadDirectory = "#expandPath('../UPLOADS')#">
<cfset filePath = uploadDirectory & "\" & arguments.fileName>
<cfset RecordDownloadUsage(FILEMETAID=ARGUMENTS.FileMetaID,FILENAME="#local.filename#")>
<cffile action="write" file="#filePath#" output="#GetFileMetaData.FileLOB#" >
<cfheader name="Content-Disposition" value="#ARGUMENTS.ServeType#;filename=#local.filename#;" />
<cfcontent file="#filePath#" type="#local.MIMETYPE#" />
<cfreturn ToString(uploadDirectory & "/" & arguments.fileName)>
</cffunction>
<cffunction name="DeleteFile" access="remote" returntype="void">
<cfargument name="fileName" type="string" required="yes">
<cfset uploadDirectory = "#expandPath('../UPLOADS')#">
<cfset filePath = uploadDirectory & "\" & arguments.fileName>
<cffile action="delete" file="#filePath#">
</cffunction>