¿Cómo puedo descargar y guardar un archivo usando la API Fetch? (Nodo.js)

9 minutos de lectura

Avatar de usuario de Gloomy
Sombrío

Tengo la URL de un archivo posiblemente grande (más de 100 Mb), ¿cómo lo guardo en un directorio local usando fetch?

Miré a mi alrededor, pero no parece haber muchos recursos/tutoriales sobre cómo hacer esto.

¡Gracias!

  • Node.js no tiene Fetch integrado.

    – Ben Fortuna

    3 de junio de 2016 a las 12:45

  • ¿Por qué traer? ¿El nodo tiene soporte http?

    – Ahorro

    3 de junio de 2016 a las 12:47

  • Estoy creando una aplicación Electron, se admite la búsqueda. ¿Por qué buscar en lugar de http puro, porque es mucho más fácil de usar (o eso parecía hasta ahora).

    – Sombrío

    3 de junio de 2016 a las 12:53

  • Si alguien buscó una forma de guardar el archivo usando fetch api pero en el navegador (y encontró esta respuesta), eche un vistazo aquí: stackoverflow.com/a/42274086/350384

    – Mariusz Pawelsky

    16 de febrero de 2017 a las 12:36

  • Vea a continuación un ejemplo que usa las bibliotecas http/https nativas de Node.js. Tenga en cuenta que no tengo que lidiar con 301/302, por lo que es sencillo.

    – angstyloop

    4 oct 2022 a las 16:14

avatar de usuario de code_wrangler
code_wrangler

Solución actualizada en el Nodo 18:

const fs = require("fs");
const {mkdir,writeFile} = require("fs/promises");
const { Readable } = require('stream');
const { finished } = require('stream/promises');
const path = require("path");
const downloadFile = (async (url, folder=".") => {
  const res = await fetch(url);
  if (!fs.existsSync("downloads")) await mkdir("downloads"); //Optional if you already have downloads directory
  const destination = path.resolve("./downloads", folder);
  const fileStream = fs.createWriteStream(destination, { flags: 'wx' });
  await finished(Readable.fromWeb(res.body).pipe(fileStream));
});

downloadFile("<url_to_fetch>", "<filename>")

La respuesta anterior funciona hasta el nodo 16:

Usando la API Fetch, podría escribir una función que podría descargarse desde una URL como esta:

Necesitará node-fetch@2 correr npm i node-fetch@2

const fetch = require("node-fetch");
const fs = require("fs");
const downloadFile = (async (url, path) => {
  const res = await fetch(url);
  const fileStream = fs.createWriteStream(path);
  await new Promise((resolve, reject) => {
      res.body.pipe(fileStream);
      res.body.on("error", reject);
      fileStream.on("finish", resolve);
    });
});

  • Incluso podrías hacerlo un poco más corto escribiendo res.body.on('error', reject); y fileStream.on('finish', resolve);.

    – Ricki-BumbleDev

    14 de junio de 2020 a las 10:21


  • Esto da un error: res.body.pipe no es una función. NodeJS v18

    – retocar

    24 de abril de 2022 a las 6:53

  • La función que llama a downloadFile no espera a que resuelva la promesa. Llamo a esta función así-> espera descargar archivo (URL, ruta). ¿Te importaría corregirme?

    – Swapnil

    21 de junio de 2022 a las 3:19

  • @tinkerr intente importar y usar ‘node-fetch’ en lugar de la búsqueda normal

    – Alex Totolici

    24 de junio de 2022 a las 6:59

  • Esto no funciona en el nodo v18. Creo que stackoverflow.com/a/74722818 es una mejor solución en 2023.

    – Beto

    25 de enero a las 1:55

Si desea evitar explícitamente hacer una Promesa como en la otra respuesta muy buena, y está de acuerdo con la creación de un búfer de todo el archivo de más de 100 MB, entonces podría hacer algo más simple:

const fetch = require('node-fetch');
const {writeFile} = require('fs');
const {promisify} = require('util');
const writeFilePromise = promisify(writeFile);

function downloadFile(url, outputPath) {
  return fetch(url)
      .then(x => x.arrayBuffer())
      .then(x => writeFilePromise(outputPath, Buffer.from(x)));
}

Pero la otra respuesta será más eficiente con la memoria, ya que canaliza el flujo de datos recibido directamente a un archivo sin acumularlo todo en un búfer.

  • Probé este código pero obtuve un error… Recibí un error [Error: EISDIR: illegal operation on a directory, open ‘D:\Work\repo\’] { errno: -4068, código: ‘EISDIR’, syscall: ‘open’, ruta: ‘D:\\Work\\repo\\’ }

    – Scott Jones

    23 de mayo de 2022 a las 9:08


  • @ScottJones EISDIR significa “Error: IS Directory”: le está dando a Node un directorio cuando espera un archivo. Solo usa d:\work\repo\file.txt Por ejemplo

    – Ahmed Fasih

    23 mayo 2022 a las 16:45


avatar de usuario de antonok
antonok

Las respuestas más antiguas aquí implican node-fetchpero desde Node.js v18.x esto se puede hacer sin dependencias adicionales.

El cuerpo de una respuesta de búsqueda es un corriente web. Se puede convertir en un nodo. fs corriente usando Readable.fromWebque luego se puede canalizar a un flujo de escritura creado por fs.createWriteStream. Si lo desea, el flujo resultante se puede convertir en un Promise usando la versión prometida de stream.finished.

const fs = require('fs');
const { Readable } = require('stream');
const { finished } = require('stream/promises');

const stream = fs.createWriteStream('output.txt');
const { body } = await fetch('https://example.com');
await finished(Readable.fromWeb(body).pipe(stream));

  • Eso también se puede compactar muy bien en una línea. const download = async (url, path) => Readable.fromWeb((await fetch(url)).body).pipe(fs.createWriteStream(path))

    – Jamby

    29 de diciembre de 2022 a las 8:42


  • ¿Esto descarga todo el archivo (await fetch(...)) antes de iniciar la secuencia de escritura?

    – 1252748

    2 de febrero a las 0:50


  • @1252748 await fetch(...) finaliza después de que los encabezados de respuesta se hayan recibido por completo, pero antes de que se reciba el cuerpo de la respuesta. El cuerpo se transmitirá al archivo mientras llega. El segundo await se puede omitir para realizar otras tareas mientras el flujo del cuerpo aún está en progreso.

    – antonok

    2 de febrero a las 22:09

  • Argument of type 'ReadableStream<Uint8Array>' is not assignable to parameter of type 'ReadableStream<any>'. Type 'ReadableStream<Uint8Array>' is missing the following properties from type 'ReadableStream<any>': values, [Symbol.asyncIterator]ts(2345)

    – RonH

    8 de marzo a las 13:41

  • @RonH lamentablemente parece que hay 2 diferente ReadableStream definiciones, según stackoverflow.com/questions/63630114/…. Deberías poder lanzar body a la correcta ReadableStream de 'stream/web'; es decir import { ReadableStream } from 'stream/web'; y body as ReadableStream<any>.

    – antonok

    8 de marzo a las 22:11

Avatar de usuario de Ihor Sakailiuk
Ihor Sakailiuk

const {createWriteStream} = require('fs');
const {pipeline} = require('stream/promises');
const fetch = require('node-fetch');

const downloadFile = async (url, path) => pipeline(
    (await fetch(url)).body,
    createWriteStream(path)
);

Avatar de usuario de Pedro Américo
pedro americo

import { existsSync } from "fs";
import { mkdir, writeFile } from "fs/promises";
import { join } from "path";

export const download = async (url: string, ...folders: string[]) => {
    const fileName = url.split("/").pop();

    const path = join("./downloads", ...folders);

    if (!existsSync(path)) await mkdir(path);

    const filePath = join(path, fileName);

    const response = await fetch(url);

    const blob = await response.blob();

    // const bos = Buffer.from(await blob.arrayBuffer())
    const bos = blob.stream();

    await writeFile(filePath, bos);

    return { path, fileName, filePath };
};

// call like that ↓
await download("file-url", "subfolder-1", "subfolder-2", ...)

  • Su respuesta podría mejorarse agregando más información sobre lo que hace el código y cómo ayuda al OP.

    – Tyler2P

    9 de agosto de 2022 a las 8:38

  • esto almacenará todo el archivo de 100 MB en la memoria antes de escribirlo, lo que podría funcionar, pero probablemente desee evitarlo si es posible

    – Andy

    6 jun a las 17:00

Avatar de usuario de Hossein
Hossein

Estaba buscando el mismo uso, quería obtener un montón de puntos finales de API y guardar las respuestas json en algunos archivos estáticos, así que se me ocurrió crear mi propia solución, espero que ayude

const fetch = require('node-fetch'),
    fs = require('fs'),
    VERSIOINS_FILE_PATH = './static/data/versions.json',
    endpoints = [
        {
            name: 'example1',
            type: 'exampleType1',
            url: 'https://example.com/api/url/1',
            filePath: './static/data/exampleResult1.json',
            updateFrequency: 7 // days
        },
        {
            name: 'example2',
            type: 'exampleType1',
            url: 'https://example.com/api/url/2',
            filePath: './static/data/exampleResult2.json',
            updateFrequency: 7
        },
        {
            name: 'example3',
            type: 'exampleType2',
            url: 'https://example.com/api/url/3',
            filePath: './static/data/exampleResult3.json',
            updateFrequency: 30
        },
        {
            name: 'example4',
            type: 'exampleType2',
            url: 'https://example.com/api/url/4',
            filePath: './static/data/exampleResult4.json',
            updateFrequency: 30
        },
    ],
    checkOrCreateFolder = () => {
        var dir="./static/data/";
        if (!fs.existsSync(dir)) {
            fs.mkdirSync(dir);
        }
    },
    syncStaticData = () => {
        checkOrCreateFolder();
        let fetchList = [],
            versions = [];
        endpoints.forEach(endpoint => {
            if (requiresUpdate(endpoint)) {
                console.log(`Updating ${endpoint.name} data... : `, endpoint.filePath);
                fetchList.push(endpoint)
            } else {
                console.log(`Using cached ${endpoint.name} data... : `, endpoint.filePath);
                let endpointVersion = JSON.parse(fs.readFileSync(endpoint.filePath, 'utf8')).lastUpdate;
                versions.push({
                    name: endpoint.name + "Data",
                    version: endpointVersion
                });
            }
        })
        if (fetchList.length > 0) {
            Promise.all(fetchList.map(endpoint => fetch(endpoint.url, { "method": "GET" })))
                .then(responses => Promise.all(responses.map(response => response.json())))
                .then(results => {
                    results.forEach((endpointData, index) => {
                        let endpoint = fetchList[index]
                        let processedData = processData(endpoint.type, endpointData.data)
                        let fileData = {
                            data: processedData,
                            lastUpdate: Date.now() // unix timestamp
                        }
                        versions.push({
                            name: endpoint.name + "Data",
                            version: fileData.lastUpdate
                        })
                        fs.writeFileSync(endpoint.filePath, JSON.stringify(fileData));
                        console.log('updated data: ', endpoint.filePath);
                    })
                })
                .catch(err => console.log(err));
        }
        fs.writeFileSync(VERSIOINS_FILE_PATH, JSON.stringify(versions));
        console.log('updated versions: ', VERSIOINS_FILE_PATH);
    },
    recursiveRemoveKey = (object, keyname) => {
        object.forEach((item) => {
            if (item.items) { //items is the nesting key, if it exists, recurse , change as required
                recursiveRemoveKey(item.items, keyname)
            }
            delete item[keyname];
        })
    },
    processData = (type, data) => {
        //any thing you want to do with the data before it is written to the file
        let processedData = type === 'vehicle' ? processType1Data(data) : processType2Data(data);
        return processedData;
    },
    processType1Data = data => {
        let fetchedData = [...data]
        recursiveRemoveKey(fetchedData, 'count')
        return fetchedData
    },
    processType2Data = data => {
        let fetchedData = [...data]
        recursiveRemoveKey(fetchedData, 'keywords')
        return fetchedData
    },
    requiresUpdate = endpoint => {
        if (fs.existsSync(endpoint.filePath)) {
            let fileData = JSON.parse(fs.readFileSync(endpoint.filePath));
            let lastUpdate = fileData.lastUpdate;
            let now = new Date();
            let diff = now - lastUpdate;
            let diffDays = Math.ceil(diff / (1000 * 60 * 60 * 24));
            if (diffDays >= endpoint.updateFrequency) {
                return true;
            } else {
                return false;
            }
        }
        return true
    };

syncStaticData();

enlace a la esencia de github

  • Su respuesta podría mejorarse agregando más información sobre lo que hace el código y cómo ayuda al OP.

    – Tyler2P

    9 de agosto de 2022 a las 8:38

  • esto almacenará todo el archivo de 100 MB en la memoria antes de escribirlo, lo que podría funcionar, pero probablemente desee evitarlo si es posible

    – Andy

    6 jun a las 17:00

avatar de usuario de angstyloop
angustioso

Si no necesita lidiar con las respuestas 301/302 (cuando las cosas se han movido), puede hacerlo en una sola línea con las bibliotecas nativas de Node.js http y/o https.

Puede ejecutar este ejemplo oneliner en el node caparazón. solo usa https módulo para descargar un archivo zip GNU de algún código fuente al directorio donde inició el node caparazón. (Empiezas un node concha escribiendo node en la línea de comandos de su sistema operativo donde se instaló Node.js).

require('https').get("https://codeload.github.com/angstyloop/js-utils/tar.gz/refs/heads/develop", it => it.pipe(require('fs').createWriteStream("develop.tar.gz")));

Si no necesita/quiere HTTPS, use esto en su lugar:

require('http').get("http://codeload.github.com/angstyloop/js-utils/tar.gz/refs/heads/develop", it => it.pipe(require('fs').createWriteStream("develop.tar.gz")));

¿Ha sido útil esta solución?