在Node.js项目中接入IPFS,通常并不是把文件直接扔给一个远程API那么简单。IPFS以内容寻址替代位置寻址,上传后得到的CID才是后续访问文件的唯一凭证。下面围绕本地节点和ipfs-http-client展开,先把环境跑通,再封装成可用服务。

准备IPFS节点与Node.js依赖
首先需要一个可访问的IPFS节点。可以选择本地安装go-ipfs或使用基于浏览器的js-ipfs,也可以使用Infura提供的远程节点,但生产环境应评估数据隐私。本地节点控制力最强,调试方便。以本地节点为例,需要先启动守护进程:ipfs daemon。默认API地址为/ip4/127.0.0.1/tcp/5001。Node.js通过ipfs-http-client连接这个地址。安装依赖的命令如下:npm install ipfs-http-client。需要注意版本差异,旧版导入方式为require('ipfs-http-client'),新版本则导入了create函数。下面给出新版本的连接示例。
const { create } = require('ipfs-http-client');
const ipfs = create({
host: '127.0.0.1',
port: 5001,
protocol: 'http'
});
async function checkNode() {
const version = await ipfs.version();
console.log('IPFS node version:', version);
}
checkNode().catch(console.error);
如果使用老版本客户端,可以直接写const ipfs = require('ipfs-http-client')('127.0.0.1', '5001', { protocol: 'http' }),但这种写法在大版本升级后已不推荐。建议锁定依赖版本并阅读迁移指南。另外也可以选择helia,它是基于现代JavaScript的IPFS实现,可直接在Node.js中运行,但与ipfs-http-client的接口不同。本文以ipfs-http-client为主,因为它连接已有节点更直接。
上传文件并理解内容寻址
上传文件使用ipfs.add方法,参数可以是Buffer、ReadableStream或文件路径。用Buffer上传小文件最直接。先读取文件内容,再交给ipfs.add处理。需要注意的是,新版本的ipfs.add返回一个异步可迭代对象,必须使用for await...of遍历才能获得文件信息。下面代码展示了如何上传一个Buffer并打印CID。
const fs = require('fs');
const { create } = require('ipfs-http-client');
const ipfs = create({
host: '127.0.0.1',
port: 5001,
protocol: 'http'
});
async function uploadBuffer() {
const fileBuffer = fs.readFileSync('./hello.txt');
const addedFiles = [];
for await (const file of ipfs.add(fileBuffer)) {
addedFiles.push(file);
}
const firstFile = addedFiles[0];
console.log('CID:', firstFile.cid.toString());
console.log('Size:', firstFile.size);
}
uploadBuffer().catch(console.error);
CID是内容寻址的核心。它由文件内容的哈希经过编码生成,相同内容一定得到相同CID,任何一字节的改动都会导致完全不同的CID。这一特性带来了天然的去重和完整性校验,但也意味着更新文件后必须更新所有引用该CID的地方。如果需要上传整个目录,可以使用ipfs.addFromFs或ipfs.addAll,并设置递归选项。
async function uploadFromPath() {
const results = [];
for await (const file of ipfs.addFromFs('./assets', { recursive: true })) {
results.push(file);
console.log(file.path, file.cid.toString());
}
return results;
}
读取文件与固定数据
上传完成后,应用需要能够通过CID把文件读回来。ipfs.cat方法接收一个CID字符串,返回一个异步可迭代的Uint8Array分块。由于文件可能被拆分成多个块传输,必须把分块拼接成完整的Buffer。下面代码展示了完整的读取过程。
async function retrieveFile(cidString) {
const chunks = [];
for await (const chunk of ipfs.cat(cidString)) {
chunks.push(chunk);
}
return Buffer.concat(chunks);
}
async function testRead() {
const cid = 'bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi';
const data = await retrieveFile(cid);
console.log('Retrieved bytes:', data.length);
}
固定数据是去中心化存储中最容易忽视的一步。IPFS节点会执行垃圾回收,未固定的数据可能在某次回收后被删除,导致CID失效。使用ipfs.pin.add可以通知节点保留该内容。对于正式上线的应用,必须对重要文件执行固定操作,否则看似上传成功的文件可能在几小时后无法访问。下面代码给出了固定方法。
async function pinContent(cidString) {
const result = await ipfs.pin.add(cidString);
console.log('Pinned:', result.cid.toString());
return result.cid.toString();
}
如果想进一步降低数据丢失风险,可以借助商业pinning服务,如Pinata、Infura等。它们会保存多个副本并提供稳定的固定服务。对于公共节点,不要假设节点会长期保存你的数据,节点所有者可能随时清理或重启。
封装Node.js存储模块
把上传、下载、固定功能集中到一个模块中,并加入重试与超时逻辑,可以提升服务健壮性。网络请求可能因为节点暂时不可用而失败,简单重试能解决大部分瞬时错误。下面是一个完整的IpfsStorage类封装,涵盖上传、下载与固定方法。
const fs = require('fs');
const { create } = require('ipfs-http-client');
class IpfsStorage {
constructor(config) {
this.ipfs = create(config);
}
async upload(buffer) {
for (let attempt = 1; attempt <= 3; attempt++) {
try {
const files = [];
for await (const file of this.ipfs.add(buffer)) {
files.push(file);
}
if (files.length === 0) {
throw new Error('upload returned empty result');
}
return {
cid: files[0].cid.toString(),
size: files[0].size
};
} catch (err) {
if (attempt === 3) {
throw err;
}
}
}
}
async download(cidString) {
const chunks = [];
for await (const chunk of this.ipfs.cat(cidString)) {
chunks.push(chunk);
}
return Buffer.concat(chunks);
}
async pin(cidString) {
const result = await this.ipfs.pin.add(cidString);
return result.cid.toString();
}
}
const storage = new IpfsStorage({
host: '127.0.0.1',
port: 5001,
protocol: 'http'
});
module.exports = IpfsStorage;
在真实项目中,可以为upload和download增加超时控制,例如使用AbortController或Promise.race。同时建议记录每次上传耗时、CID和文件大小,方便追踪存储状态。通过将存储逻辑与业务逻辑解耦,后续切换到底层不同节点或迁移到远程固定服务时,只需调整构造函数参数,不会影响调用方。
接入IPFS并不是终点,节点在线、固定策略和CID管理共同决定了数据的可用性。把上述代码放入项目并配合定时pin检查,可以构建一个相对稳定的去中心化文件存储模块。