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

26

我正在使用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}}); 


页面/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 文档。”


@brc-dd 我仍然得到相同的结果。我也尝试找到有关Express下载的内容,几乎所有文章都是指从服务器下载静态文件,而不是从API下载。 - JoshJoe
2个回答

38
你似乎在使用 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以查看其工作情况)

参考文献:

PS:我认为在这种情况下不需要太多错误处理。如果您希望对用户更加详细,可以添加错误处理。但是这样的代码也可以正常工作。如果出现错误,文件下载将失败,并显示“服务器错误”。此外,我认为没有必要首先创建一个 blob URL。您可以直接在应用程序中下载它,因为 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); };

谢谢!请求已被弃用,所以我尝试不使用它。你的另一种方法很好用。 - JoshJoe
1
你的回答启发了我编写自己的API端点,从内存(流)生成可下载的CSV文件。https://dev59.com/D1IG5IYBdhLWcg3wwkSM#70089343 谢谢! - Vadorequest
1
这对于较小的文件效果很好。我遇到的问题是Nextjs警告:“/api/download的API响应超过4MB。这将导致请求在未来版本中失败。https://nextjs.org/docs/messages/api-routes-body-size-limit”。继续使用这种方法让我有点紧张,尽管我真的不明白为什么会有问题。我正在流式传输数据。 - crice1988
这个对我来说出了些问题... 有什么想法吗? - Primoz Rome
@PrimozRome 考虑提出一个带有调试细节的新问题。如果没有先查看一些代码,我们无法确定出现了什么问题。为了调试,您可能需要检查处理程序是否被执行,并且在获取特定文件时是否没有错误。 - brc-dd
@brc-dd 我在这里开了一个新问题 -> https://stackoverflow.com/questions/74688722/how-to-download-a-string-returned-from-3rd-party-api-as-xml-file-using-next-js-a - Primoz Rome

3
@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 })
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接