如何使用API路由在Next.js上下载文件



我使用的是next.js。我有一个第三方服务,需要从中检索PDF文件。该服务需要一个API密钥,我不希望在客户端公开该密钥。

这是我的文件

/api/getPDFFile.js…

const options = {
method: 'GET',
encoding: 'binary',
headers: {
'Subscription-Key': process.env.GUIDE_STAR_CHARITY_CHECK_API_PDF_KEY,
'Content-Type': 'application/json',
},
rejectUnauthorized: false,
};
const binaryStream = await fetch(
'https://apidata.guidestar.org/charitycheckpdf/v1/pdf/26-4775012',
options
);

return res.status(200).send({body: { data: binaryStream}}); 

pages/getPDF.js

<button type="button" onClick={() => {
fetch('http://localhost:3000/api/guidestar/charitycheckpdf',
{
method: 'GET',
encoding: 'binary',
responseType: 'blob',
}).then(response => {
if (response.status !== 200) {
throw new Error('Sorry, I could not find that file.');
}
return response.blob();
}).then(blob => {
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.style.display = 'none';
a.href = url;
a.setAttribute('download', 'test.pdf');
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
})}}>Click to Download</button>

点击按钮下载一个文件,但当我打开它时,我看到错误消息,";未能加载PDF文档">

您似乎正在使用node-fetch。所以,你可以这样做:

// /pages/api/getAPI.js
import stream from 'stream';
import { promisify } from 'util';
import fetch from 'node-fetch';
const pipeline = promisify(stream.pipeline);
const url = 'https://w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf';
const handler = async (req, res) => {
const response = await fetch(url); // replace this with your API call & options
if (!response.ok) throw new Error(`unexpected response ${response.statusText}`);
res.setHeader('Content-Type', 'application/pdf');
res.setHeader('Content-Disposition', 'attachment; filename=dummy.pdf');
await pipeline(response.body, res);
};
export default handler;

然后从客户端:

// /pages/index.js
const IndexPage = () => <a href="/api/getPDF">Download PDF</a>;
export default IndexPage;

CodeSandbox链接(在新选项卡中打开已部署的URL以查看其工作情况(

参考文献:

  • API路线|下一个.js
  • 流|节点提取
  • 如何从Node/Express应用程序向浏览器发送pdf文件
  • <a>:Anchor元素|MDN

PS:我认为在这种情况下不需要太多的错误处理。如果你想向你的用户提供更多信息,你可以。但这么多代码也可以正常工作。在出现错误的情况下,文件下载将失败;服务器错误";。此外,我认为没有必要首先创建blobURL。你可以直接在你的应用程序中下载,因为API在同一个来源。


早些时候我使用了request,也在这里发布,以备有人需要:

import request from 'request';
const url = 'https://w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf';
export default (_, res) => { request.get(url).pipe(res); };

@brc-dd在这个问题上救了我。我必须添加的一件事是动态生成的链接元素(请参阅代码中的var link(,一旦我们从API获得文件数据,它就会单击自己。这对于获得一致的下载非常重要(这是我之前没有得到的(。

创建下载文件链接的页面代码如下所示:

// the fileProps variable used below looks like {"file_name":"test.png", "file_type":"image/png", "file_size": 748833}
import Button from 'react-bootstrap/Button'
import { toast } from 'react-toastify';
const DataGridCell = ({ filename, filetype, filesize }) => {
const [objFileState, setFileDownload] = useState({})
// handle POST request here
useEffect(() => {
async function retrieveFileBlob() {
try {
const ftch = await fetch( // this will request the file information for the download (whether an image, PDF, etc.)
`/api/request-file`,
{
method: "POST",
headers: {
"Content-type": "application/json"
},
body: JSON.stringify(objFileState)
},
)
const fileBlob = await ftch.blob()
// this works and prompts for download
var link = document.createElement('a')  // once we have the file buffer BLOB from the post request we simply need to send a GET request to retrieve the file data
link.href = window.URL.createObjectURL(fileBlob)
link.download = objFileState.strFileName
link.click()
link.remove();  //afterwards we remove the element  
} catch (e) {
console.log({ "message": e, status: 400 })  // handle error
}
}
if (objFileState !== {} && objFileState.strFileId) retrieveFileBlob()   // request the file from our file server
}, [objFileState])
// NOTE: it is important that the objFile is properly formatted otherwise the useEffect will just start firing off without warning
const objFile = {
"objFileProps": { "file_name": filename, "file_type": filetype, "file_size": filesize }
}
return <Button onClick={() => {toast("File download started"); setFileDownload(objFile) }} className="btn btn-primary m-2">Download {filename}</Button>
}

链接调用的本地NextJs API端点(/API/qualtrics/retrieve-file(如下:

/**
* @abstract This API endpoint requests an uploaded file from a Qualtrics response
* (see Qualtrics API reference for more info: 
https://api.qualtrics.com/guides/reference/singleResponses.json/paths/~1surveys~1%7BsurveyId%7D~1responses~1%7BresponseId%7D~1uploaded-files~1%7BfileId%7D/get)
* For this API endpoint the parameters we will be:
* Param 0 = Survey ID
* Param 1 = Response ID
* Param 2 = File ID
* Param 3 = Header object (properties of the file needed to return the file to the client)
*
*/
// This is a protected API route
import { getSession } from 'next-auth/client'
export default async function API(req, res) {
// parse the API query
const { params } = await req.query  // NOTE: we must await the assignment of params from the request query
const session = await getSession({ req })
const strSurveyId = await params[0]
const strResponseId = await params[1]
const strFileId = await params[2]
const objFileProps = JSON.parse(decodeURIComponent(await params[3]))    // file properties
// this if condition simply checks that a user is logged into the app in order to get data from this API
if (session) {
// ****** IMPORTANT: wrap your fetch to Qualtrics in a try statement to help prevent errors of headers already set **************
try {
const response = await fetch(
`${process.env.QUALTRICS_SERVER_URL}/API/v3/surveys/${strSurveyId}/responses/${strResponseId}/uploaded-files/${strFileId}`,
{
method: "get",
headers: {
"X-API-TOKEN": process.env.QUALTRICS_API_TOKEN
}
}
);
// get the file information from the external API
const resBlob = await response.blob();
const resBufferArray = await resBlob.arrayBuffer();
const resBuffer = Buffer.from(resBufferArray);
if (!response.ok) throw new Error(`unexpected response ${response.statusText}`);
// write the file to the response (should prompt user to download or open the file)
res.setHeader('Content-Type', objFileProps.file_type);
res.setHeader('Content-Length', objFileProps.file_size);
res.setHeader('Content-Disposition', `attachment; filename=${objFileProps.file_name}`);
res.write(resBuffer, 'binary');
res.end();
} catch (error) {
return res.send({ error: `You made an invalid request to download a file ${error}`, status: 400 })
}
} else {
return res.send({ error: 'You must sign in to view the protected content on this page...', status: 401 })
}
}