我有一个可能很大(100+ Mb)文件的 URL,如何使用 fetch 将其保存在本地目录中?
我环顾四周,但似乎没有很多关于如何做到这一点的资源/教程。
谢谢!
更新了节点 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>")
旧答案一直有效到节点 16:
使用 Fetch API,您可以编写一个可以从如下 URL 下载的函数:
您需要
node-fetch@2
运行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);
});
});
这里较旧的答案涉及
node-fetch
,但由于Node.js v18.x
,这可以在没有额外依赖的情况下完成。
获取响应的主体是一个网络流。可以使用
fs
将其转换为 Node
Readable.fromWeb
流,然后可以将其通过管道传输到由 fs.createWriteStream
创建的写入流中。如果需要,可以使用 Promise
的承诺版本将结果流转换为
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));
如果您想避免像其他非常好的答案那样明确做出 Promise,并且可以构建整个 100+ MB 文件的缓冲区,那么您可以做一些更简单的事情:
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)));
}
但另一个答案将更加节省内存,因为它将接收到的数据流直接传输到文件中,而不将所有数据累积在缓冲区中。
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)
);
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", ...)
我正在寻找类似的用法,想要获取一堆 api 端点并将 json 响应保存到一些静态文件,所以我想出了创建自己的解决方案,希望它有所帮助
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();
如果您不需要处理 301/302 响应(当内容已移动时),您实际上可以使用 Node.js 本机库
http
和/或 https
在一行中完成此操作。
您可以在
node
shell 中运行此示例 oneliner。它只是使用 https
模块将一些源代码的 GNU zip 文件下载到您启动 node
shell 的目录。 (您可以通过在已安装 Node.js 的操作系统的命令行中键入 node
来启动 node
shell)。
require('https').get("https://codeload.github.com/angstyloop/js-utils/tar.gz/refs/heads/develop", it => it.pipe(require('fs').createWriteStream("develop.tar.gz")));
如果您不需要/想要 HTTPS,请改用此:
require('http').get("http://codeload.github.com/angstyloop/js-utils/tar.gz/refs/heads/develop", it => it.pipe(require('fs').createWriteStream("develop.tar.gz")));
这为我的节点 18 和大概 16 完成了工作。只有 fs 和 node-fetch (可能与其他 fetch 库一起使用)作为依赖项。
const fs = require('fs');
const fetch = require("node-fetch");
async function downloadImage(imageUrl){
//imageurl https://example.com/uploads/image.jpg
imageUrl = imageUrl.split('/').slice(-1) //image.jpg
const res = await fetch(imageUrl);
const fileStream = fs.createWriteStream(`./folder/${imageUrl}`);
await new Promise((resolve, reject) => {
res.body.pipe(fileStream);
res.body.on("error", reject);
fileStream.on("finish", resolve);
});
};
@code_wrangler 之前的最佳答案被分为节点 16 和 18 解决方案(这就像 16 解决方案),但在节点 18 上,节点 18 解决方案为我创建了一个 0 字节文件,并花费了我一些时间。
现在使用现代 Nodejs api 很容易做到这一点:
import { writeFile } from 'fs/promises'
const response = await fetch('https://example.com/pdf')
const buffer = Buffer.from(await response.arrayBuffer())
await writeFile('document.pdf', buffer)
与这个答案相同,但现代化的进口等。