Node.js Axios下载文件流和writeFile

87

我想使用axios下载PDF文件,并使用fs.writeFile保存到磁盘上(服务端),我已经尝试了:

axios.get('https://xxx/my.pdf', {responseType: 'blob'}).then(response => {
    fs.writeFile('/temp/my.pdf', response.data, (err) => {
        if (err) throw err;
        console.log('The file has been saved!');
    });
});

文件已保存,但内容损坏...

如何正确保存文件?


你会得到控制台日志“文件已保存”,并且文件已创建,但内容不正确? - Roland Starke
你在哪里调用axios.get?它不会等待文件被写入。最好将fs promisify或使用fs-extra或使用fs的promisfied方法。并且像这样使用:return fs.writeFile(...) - AZ_
@RolandStarke 是的,文件已保存。 - ar099968
1
我在下面发布了一种更加简洁的解决方案,使用了Node流管道。它基于被接受的答案提出的概念。 stackoverflow.com/a/64925465/3476378 - Aman Saraf
13个回答

142

实际上,我认为之前被接受的答案有些缺陷,因为它无法正确处理写入流,所以如果你在Axios给出响应后调用"then()",你最终会得到一个部分下载的文件。

在下载稍大的文件时,以下是一种更合适的解决方案:

export async function downloadFile(fileUrl: string, outputLocationPath: string) {
  const writer = createWriteStream(outputLocationPath);

  return Axios({
    method: 'get',
    url: fileUrl,
    responseType: 'stream',
  }).then(response => {

    //ensure that the user can call `then()` only when the file has
    //been downloaded entirely.

    return new Promise((resolve, reject) => {
      response.data.pipe(writer);
      let error = null;
      writer.on('error', err => {
        error = err;
        writer.close();
        reject(err);
      });
      writer.on('close', () => {
        if (!error) {
          resolve(true);
        }
        //no need to call the reject here, as it will have been called in the
        //'error' stream;
      });
    });
  });
}

这样,您可以调用downloadFile(),在返回的Promise上调用then(),并确保已完成处理下载的文件。

或者,如果您使用更现代化的NodeJS版本,可以尝试使用以下方法:

import * as stream from 'stream';
import { promisify } from 'util';

const finished = promisify(stream.finished);

export async function downloadFile(fileUrl: string, outputLocationPath: string): Promise<any> {
  const writer = createWriteStream(outputLocationPath);
  return Axios({
    method: 'get',
    url: fileUrl,
    responseType: 'stream',
  }).then(response => {
    response.data.pipe(writer);
    return finished(writer); //this is a Promise
  });
}

2
这应该是被接受的答案。它修复了部分下载错误。 - ariezona
2
我在下面的链接中发布了一个更干净的方法,使用流管道来实现与您相同的概念:https://dev59.com/qFMI5IYBdhLWcg3wxudI#64925465。 - Aman Saraf
1
这个代码会等待文件完全下载后才开始使用“writer”进行写入吗?也就是说,在这种情况下,它似乎并不像真正的流式读取和写入一样,因为它在一个完成之前等待另一个开始。 - 1252748
2
我不确定我理解了。当字节被下载时,它们被流式传输到文件中,一旦所有字节都被流式传输,Promise 就会结束,应用程序的其余流程就会继续。在示例中的“then”在文件下载完成之前被调用 - 请查看 axios 的 stream responseType 的文档。 - csotiriou
7
response.data.pipe 不是一个函数。 - rendom
显示剩余6条评论

92

你可以简单地使用 response.data.pipefs.createWriteStream 将响应数据导向文件。

axios({
    method: "get",
    url: "https://xxx/my.pdf",
    responseType: "stream"
}).then(function (response) {
    response.data.pipe(fs.createWriteStream("/temp/my.pdf"));
});

非常感谢!我一直在寻找这个。 - Harrison Cramer
1
这个答案不完整,因为当你下载一些较大的文件时,管道会给你多个事件。这段代码在可以调用then之前并不等待整个文件被下载。请看我的解决方案,找到我认为更完整的解决方案。 - csotiriou
33
response.data.pipe不是一个函数。 - Murat Serdar Akkuş
如果不想将文件下载到本地存储,该怎么做呢?我在Node.js中尝试了res.sendFile。 - s.j
2
要对这个解决方案进行批评,您必须将responseType设置为“stream”。如果不这样做,当您尝试将其管道传输到另一个流时会导致错误。 - RashadRivera

33

文件损坏的问题是由于在节点流中的反压。您可以阅读此链接:https://nodejs.org/es/docs/guides/backpressuring-in-streams/

我不太喜欢在JS代码中使用基于Promise的声明对象,因为我觉得它会污染实际的核心逻辑并使代码难以阅读。除此之外,您需要提供事件处理程序和侦听器以确保代码完成。

与所接受的答案提出的相同逻辑的更清晰的方法如下所示。它使用了流管道的概念。

const util = require('util');
const stream = require('stream');
const pipeline = util.promisify(stream.pipeline);

const downloadFile = async () => {
  try {
    const request = await axios.get('https://xxx/my.pdf', {
      responseType: 'stream',
    });
    await pipeline(request.data, fs.createWriteStream('/temp/my.pdf'));
    console.log('download pdf pipeline successful');   
  } catch (error) {
    console.error('download pdf pipeline failed', error);
  }
}

exports.downloadFile = downloadFile

我希望你觉得这很有用。


为什么使用响应类型 blob 而不是 stream? - 1252748
1
我使用这种方法时出现了“stream.on不是函数”的错误。 - dz210
我是这样让它工作的: const resp = await axios.get(....); await pipeline(resp.data, fs.createWriteStream(...)) - dz210
@1252748 blob 是仅限浏览器选项。 - B45i

14

1
非常感谢,我不想使用流(streams),因为我需要将此文件上传到我的云端! - Itay Ben Shmuel

12
// This works perfectly well! 
const axios = require('axios'); 

axios.get('http://www.sclance.com/pngs/png-file-download/png_file_download_1057991.png', {responseType: "stream"} )  
.then(response => {  
// Saving file to working directory  
    response.data.pipe(fs.createWriteStream("todays_picture.png"));  
})  
    .catch(error => {  
    console.log(error);  
});  

10
欢迎来到StackOverflow!您可能希望在代码示例中提供一些解释。 - Airn5475
2
这不会正常工作,因为它不会等待文件完成下载就继续 Promise 链。 - csotiriou

6
节点文件系统中的writeFile 默认情况下将数据编码为UTF8,这在您的情况下可能会成为问题。尝试将编码设置为null并跳过对接收到的数据进行编码。
fs.writeFile('/temp/my.pdf', response.data, {encoding: null}, (err) => {...}

如果你只需要声明编码(encoding),而不需要其他选项,你也可以将其声明为字符串(而不是选项对象)。字符串将被处理为编码值。可以这样使用:

fs.writeFile('/temp/my.pdf', response.data, 'null', (err) => {...}

更多阅读请参见文件系统API的write_file


@double-beep 感谢您的评论。我已经编辑了一些解释,并阅读了有关node fileSystem API中writeFile函数的材料。 :) - fedesc

5

有一种更简单的方法可以在几行代码内完成:

import fs from 'fs';

const fsPromises = fs.promises;

const fileResponse = await axios({
    url: fileUrl,
    method: "GET",
    responseType: "stream",
});

// Write file to disk (here I use fs.promise but you can use writeFileSync it's equal
await fsPromises.writeFile(filePath, fileResponse.data);

Axios 具有处理 streams 的内部能力,您不需要必须干预低级别的 Node APIs。

请查看 https://axios-http.com/docs/req_config(在文档中找到 responseType 部分以获取所有可用类型)。


什么是fsPromises? - 1.21 gigawatts
1
我编辑了答案以使其更清晰。谢谢! - Mattia Rasulo
我正在努力理解这个问题。为什么在这种情况下我们不需要使用管道?这种方法如何避免使用管道,而常规的createWriteStream却不能?这是否仅将数据写入文件,而没有保存任何与背压相关的数据到内存中? - Telion
我不完全确定,但我最好的猜测是axios在幕后处理所有这些事情。 - Mattia Rasulo

3
如果您只需要文件,请使用此选项。
const media_data =await axios({url: url, method: "get",  responseType: "arraybuffer"})
writeFile("./image.jpg", Buffer.from(media_data.data), {encoding: "binary"}, console.log)

3
我尝试过,确信使用response.data.pipefs.createWriteStream可以解决问题。
此外,我想添加我的情况和解决方案。
情况:
  • 使用koa开发node.js服务器
  • 使用axios通过url获取pdf文件
  • 使用pdf-parse解析pdf文件
  • 提取一些pdf信息并将其作为json返回给浏览器
解决方案:
const Koa = require('koa');
const app = new Koa();
const axios = require('axios')
const fs = require("fs")
const pdf = require('pdf-parse');
const utils = require('./utils')

app.listen(process.env.PORT || 3000)

app.use(async (ctx, next) => {
      let url = 'https://path/name.pdf'
      let resp = await axios({
          url: encodeURI(url),
          responseType: 'arraybuffer'
        })

        let data = await pdf(resp.data)

        ctx.body = {
            phone: utils.getPhone(data.text),
            email: utils.getEmail(data.text),
        }
})

在这个解决方案中,不需要编写文件和读取文件,因此更加高效。

在发送文件之前将整个数据文件缓冲到内存中,这种方式如何比流式传输更有效? - RashadRivera
我不明白你的意思?你说的“发送”是什么意思?在我的情况下,我只需要解析PDF文件,不需要响应。 - levy9527

2
这是我使用的方法,它还为图像文件创建了一个临时文件(如果未指定输出文件路径):
const fs = require('fs')
const axios = require('axios').default
const tmp = require('tmp');

const downloadFile = async (fileUrl, outputLocationPath) => {
    if(!outputLocationPath) {
        outputLocationPath = tmp.fileSync({ mode: 0o644, prefix: 'kuzzle-listener-', postfix: '.jpg' });
    }
    let path = typeof outputLocationPath === 'object' ? outputLocationPath.name : outputLocationPath
    const writer = fs.createWriteStream(path)
    const response = await axios.get(fileUrl, { responseType: 'arraybuffer' })
    return new Promise((resolve, reject) => {
        if(response.data instanceof Buffer) {
            writer.write(response.data)
            resolve(outputLocationPath.name)
        } else {
            response.data.pipe(writer)
            let error = null
            writer.on('error', err => {
                error = err
                writer.close()
                reject(err)
            })
            writer.on('close', () => {
                if (!error) {
                    resolve(outputLocationPath.name)
                }
            })
        }
    })
}

这是一个非常简单的Jest测试:

it('when downloadFile should downloaded', () => {
    downloadFile('https://i.ytimg.com/vi/HhpbzPMCKDc/hq720.jpg').then((file) => {
        console.log('file', file)
        expect(file).toBeTruthy()
        expect(file.length).toBeGreaterThan(10)
    })
})

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