在 ColdFusion 中显示数据库中的 BLOB 文件

Display BLOB file from database in ColdFusion

我有一个用户可以上传 PDF 的表单,我将其作为 BLOB 存储在数据库中。我正在显示所有上传的 PDF 的列表,所有这些都可以通过单击下载。我已经尝试了很多不同的解决方法来让 PDF 正确下载,但它会在浏览器中显示“无法加载 PDF 文档”,在 Adob​​e Acrobat 中显示“文件已损坏且无法修复”。这是我的代码:

Instructors.cfc(上传文件格式)

<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(将 PDF blob 插入数据库)

<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 [再次](从数据库下载 PDF,我遇到了问题)

<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>

最重要的代码部分是我包含的 last/above 片段。即使我在浏览器中导航到 downloadPDF 功能,它仍然无法正确下载 PDF 并给出错误消息。因此,清理该方法是第 1 步,然后我实际上可以通过 AJAX 调用检索用户页面上的 PDF,如果有帮助,我也会展示它:

$(".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]*=((['"]).*?|[^;\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;
                }
            }
        }
    });
});

对于大量的代码,我们深表歉意。但正如我所说,最重要的部分是 downloadPDF 函数,我在其中使用 cfcontent 并需要正确加载二进制数据。任何帮助将不胜感激,因为我已经在这个问题上停留了一段时间并且找不到太多文档。

更新:

返回的 PDF 大小为 62.5 KB,我听说如果禁用 BLOB 检索,由于 ColdFusion Admin 中的缓冲区,输出可能会被截断为 64 KB。我无权访问 ColdFusion Admin,但我的一位同事可以访问,也许他在尝试全局启用 BLOB 检索时编辑了错误的设置。我会和他核实一下。

原来是 ColdFusion Administrator 中的设置导致了问题。尽管在 Dev 环境中 globally 启用了 BLOB 检索,但在我的特定 datasource 上并未启用它。在全局设置中启用 BLOB 检索只是第一步;除非您明确启用它,否则它不会覆盖数据源。

我确实需要稍微调整一下我的代码,但幅度不大。我将 cfcontent 更改为使用 file 属性而不是 variable,我使用 <cffile action="write"> 中使用的路径填充了 file 属性。将文件写入服务器后,我通过 JavaScript 为文件创建锚点 link 并调用其 click() 方法,以便下载 PDF 附件。之后,我在 done() 方法中执行另一个 AJAX 调用来删除文件,因为我不需要将该文件保留在服务器上;这是暂时的。这是我的最终代码:

Instructors.cfc(文件上传形式保持不变)

<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(新建文件,接管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>