文章目录
一、Pinata API v3 核心变化概览
Pinata API v3 引入了重大升级,主要变化包括:
- 认证方式:从 API Key + Secret 改为 JWT 令牌
- 权限模型:通过端点分组精细控制权限(Pinning Servers 和 Data)
- 参数结构:强制要求
pinataMetadata
和pinataOptions
参数 - CID 支持:默认使用 CIDv1(Base32 编码)
创建 JWT 令牌
步骤 1:创建正确权限的 JWT 令牌
- 登录 Pinata 控制台
- 进入 “API Keys” 页面
- 点击 “New Key”
- 在权限设置中:
- 确保 “Pinning” 部分勾选 “Pin file to IPFS”
- 其他权限根据需求选择
- 生成后复制 JWT 令牌(以
eyJ...
开头的长字符串)
步骤 2:验证 JWT 令牌权限(可选)
使用 jwt.io 解码令牌,检查 scope
字段是否包含 pinFileToIPFS
:
{
"iss": "pinata",
"scope": {
"endpoints": [
{
"name": "pinFileToIPFS",
"actions": ["pin"]
}
]
}
// ...其他字段
}
权限组详解
权限组 | 接口名称 | 功能描述 | 应用场景 |
---|---|---|---|
Pinning Servers | addPinObject |
上传并固定文件 | 图片上传核心功能 |
getPinObject |
查询固定任务状态 | 大文件上传监控 | |
listPinObjects |
分页筛选已固定文件 | 内容管理后台 | |
removePinObject |
解除固定 | 清理过期内容 | |
replacePinObject |
替换已固定文件 | 无缝更新图片 | |
Data | pinList |
获取固定文件列表 | 快速内容清单 |
userPinnedDataTotal |
统计存储总量 | 成本监控与套餐管理 |
二、不同场景的 API 选择与参数配置
场景 1:基础图片上传(单张图片)
API 端点:addPinObject
权限需求:pinFileToIPFS
// 请求参数配置
const formData = new FormData();
formData.append('file', file); // 图片文件对象
console.log(formData.get('file'))
const metadata = JSON.stringify({
// name: 'voter-profile243',
name: file.name.replace(/\.[^/.]+$/, ""),
keyvalues: {
exampleKey: 'exampleValue',
type: 'profile-image',
userId: '12'
}
});
formData.append('pinataMetadata', metadata);
const pinataOptions = JSON.stringify({
cidVersion: 3,
customPinPolicy: {
regions: [
{
id: 'FRA1',
desiredReplicationCount: 1
},
{
id: 'NYC1',
desiredReplicationCount: 1
}
]
}
});
formData.append('pinataOptions', pinataOptions);
场景 2:批量图片上传(多张图片)
API 端点:addPinObject
(多次调用)
权限需求:pinFileToIPFS
// 批量上传函数
async function batchUploadImages(files) {
const results = [];
for (const file of files) {
const formData = new FormData();
formData.append('file', file);
const metadata = {
name: file.name,
keyvalues: { batchId: Date.now() }
};
formData.append('pinataMetadata', JSON.stringify(metadata));
const options = {
cidVersion: 1,
wrapWithDirectory: false
};
formData.append('pinataOptions', JSON.stringify(options));
const response = await fetch('https://api.pinata.cloud/pinning/pinFileToIPFS', {
method: 'POST',
headers: { Authorization: `Bearer ${JWT_TOKEN}` },
body: formData
});
results.push(await response.json());
}
return results;
}
场景 3:图片替换更新(无缝更新)
API 端点:replacePinObject
权限需求:pinFileToIPFS
+ replacePinObject
async function replaceImage(oldCid, newFile) {
// 1. 上传新图片
const uploadResponse = await addPinObject(newFile);
const newCid = uploadResponse.IpfsHash;
// 2. 替换固定
const response = await fetch(
`https://api.pinata.cloud/pinning/replacePinObject/${oldCid}?newCid=${newCid}`,
{
method: 'PUT',
headers: { Authorization: `Bearer ${JWT_TOKEN}` }
}
);
if (!response.ok) {
throw new Error('替换失败');
}
return { oldCid, newCid };
}
场景 4:大文件上传监控
API 端点组合:
addPinObject
(开始上传)getPinObject
(监控状态)
async function uploadLargeFile(file) {
// 开始上传
const uploadResponse = await addPinObject(file);
const requestId = uploadResponse.requestId;
// 监控状态
let status = 'queued';
while (status !== 'pinned' && status !== 'failed') {
await new Promise(resolve => setTimeout(resolve, 5000)); // 5秒轮询
const statusResponse = await fetch(
`https://api.pinata.cloud/pinning/getPinObject?requestId=${requestId}`,
{ headers: { Authorization: `Bearer ${JWT_TOKEN}` }
);
const data = await statusResponse.json();
status = data.status;
console.log(`上传状态: ${status}`);
}
return status === 'pinned' ? uploadResponse : null;
}
三、前端完整实现代码
基础依赖
npm install axios qs
核心工具类:PinataService.js
import axios from 'axios';
const JWT_TOKEN = process.env.REACT_APP_PINATA_JWT;
class PinataService {
// 上传图片
static async uploadImage(file, metadata = {}) {
const formData = new FormData();
formData.append('file', file);
// 默认元数据
const defaultMetadata = {
name: file.name.replace(/\.[^/.]+$/, ""),
keyvalues: {
type: 'image',
origin: 'web-upload'
}
};
// 合并自定义元数据
const finalMetadata = { ...defaultMetadata, ...metadata };
formData.append('pinataMetadata', JSON.stringify(finalMetadata));
// 固定选项
const options = {
cidVersion: 1,
wrapWithDirectory: false
};
formData.append('pinataOptions', JSON.stringify(options));
try {
const response = await axios.post(
'https://api.pinata.cloud/pinning/pinFileToIPFS',
formData,
{
headers: {
'Authorization': `Bearer ${JWT_TOKEN}`,
'Content-Type': 'multipart/form-data'
},
maxContentLength: Infinity, // 支持大文件
maxBodyLength: Infinity
}
);
return {
cid: response.data.IpfsHash,
url: `https://gateway.pinata.cloud/ipfs/${response.data.IpfsHash}`,
timestamp: response.data.Timestamp
};
} catch (error) {
this.handleError(error);
}
}
// 获取上传状态
static async getUploadStatus(requestId) {
try {
const response = await axios.get(
`https://api.pinata.cloud/pinning/getPinObject?requestId=${requestId}`,
{ headers: { Authorization: `Bearer ${JWT_TOKEN}` } }
);
return {
status: response.data.status,
created: response.data.created,
cid: response.data.cid
};
} catch (error) {
this.handleError(error);
}
}
// 替换图片
static async replaceImage(oldCid, newFile) {
const uploadResult = await this.uploadImage(newFile);
try {
await axios.put(
`https://api.pinata.cloud/pinning/replacePinObject/${oldCid}`,
{ newCid: uploadResult.cid },
{ headers: { Authorization: `Bearer ${JWT_TOKEN}` } }
);
return {
oldCid,
newCid: uploadResult.cid,
url: uploadResult.url
};
} catch (error) {
this.handleError(error);
}
}
// 错误处理
static handleError(error) {
if (error.response) {
// Pinata 返回的错误
const pinataError = error.response.data?.error || {};
throw new Error(
`Pinata Error [${error.response.status}]: ${pinataError.reason || pinataError.details || 'Unknown error'}`
);
} else {
throw new Error(`Network Error: ${error.message}`);
}
}
}
export default PinataService;
React 组件示例:ImageUploader.jsx
import React, { useState } from 'react';
import PinataService from './PinataService';
const ImageUploader = () => {
const [file, setFile] = useState(null);
const [preview, setPreview] = useState('');
const [result, setResult] = useState(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState('');
const handleFileChange = (e) => {
const selectedFile = e.target.files[0];
if (!selectedFile.type.match('image.*')) {
setError('请选择图片文件 (JPG, PNG, GIF)');
return;
}
if (selectedFile.size > 10 * 1024 * 1024) {
setError('文件大小不能超过10MB');
return;
}
setFile(selectedFile);
setPreview(URL.createObjectURL(selectedFile));
setError('');
};
const handleUpload = async () => {
if (!file) return;
setIsLoading(true);
setError('');
try {
const uploadResult = await PinataService.uploadImage(file, {
keyvalues: {
category: 'user-upload',
device: navigator.userAgent.substring(0, 30)
}
});
setResult(uploadResult);
} catch (err) {
setError(err.message);
} finally {
setIsLoading(false);
}
};
return (
<div className="image-upload-container">
<h2>Pinata v3 图片上传</h2>
<div className="upload-area">
<input
type="file"
id="image-upload"
accept="image/*"
onChange={handleFileChange}
disabled={isLoading}
/>
<label htmlFor="image-upload" className="upload-btn">
{file ? '更换图片' : '选择图片'}
</label>
{preview && (
<div className="preview-box">
<img src={preview} alt="预览" />
<p>{file.name} ({Math.round(file.size / 1024)}KB)</p>
</div>
)}
</div>
{error && <div className="error-message">{error}</div>}
<button
onClick={handleUpload}
disabled={!file || isLoading}
className="upload-button"
>
{isLoading ? '上传中...' : '上传到IPFS'}
</button>
{result && (
<div className="result-card">
<h3>上传成功!</h3>
<p><strong>CID:</strong> {result.cid}</p>
<div className="image-preview">
<img src={result.url} alt="上传结果" />
</div>
<a
href={result.url}
target="_blank"
rel="noopener noreferrer"
className="view-link"
>
在新标签页查看
</a>
</div>
)}
</div>
);
};
export default ImageUploader;
四、最佳实践与优化建议
1. 安全优化
// 在元数据中添加内容签名
const generateSignature = async (file) => {
const buffer = await file.arrayBuffer();
const hashBuffer = await crypto.subtle.digest('SHA-256', buffer);
return Array.from(new Uint8Array(hashBuffer))
.map(b => b.toString(16).padStart(2, '0'))
.join('');
};
// 在元数据中使用
const metadata = {
name: file.name,
keyvalues: {
signature: await generateSignature(file)
}
};
2. 性能优化
// Web Worker 处理大文件上传
const uploadInWorker = (file) => {
return new Promise((resolve, reject) => {
const worker = new Worker('./pinataUpload.worker.js');
worker.postMessage({
file,
token: JWT_TOKEN
});
worker.onmessage = (e) => {
if (e.data.error) {
reject(e.data.error);
} else {
resolve(e.data.result);
}
worker.terminate();
};
});
};
3. 错误恢复机制
async function resilientUpload(file, retries = 3) {
for (let attempt = 1; attempt <= retries; attempt++) {
try {
return await PinataService.uploadImage(file);
} catch (error) {
if (attempt === retries) throw error;
// 指数退避重试
const delay = 1000 * Math.pow(2, attempt);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
4. 成本监控集成
// 定期检查存储使用情况
async function checkStorageUsage() {
const response = await axios.get(
'https://api.pinata.cloud/data/userPinnedDataTotal',
{ headers: { Authorization: `Bearer ${JWT_TOKEN}` } }
);
const { pin_count, pin_size_total } = response.data;
const gbUsed = (pin_size_total / 1024 / 1024 / 1024).toFixed(2);
console.log(`已固定文件: ${pin_count}个, 使用存储: ${gbUsed}GB`);
// 免费账户超过0.8GB时警告
if (gbUsed > 0.8) {
console.warn('存储空间即将用尽,请考虑升级套餐');
}
}
// 每月执行一次
setInterval(checkStorageUsage, 30 * 24 * 60 * 60 * 1000);
五、常见问题解决方案
403 错误
- 检查 JWT 令牌是否有效且未过期
- 确保令牌包含所需端点权限(如
pinFileToIPFS
) - 验证请求头格式:
Authorization: Bearer YOUR_JWT
400 错误(参数错误)
- 确保
pinataMetadata
和pinataOptions
是有效的 JSON 字符串 - 检查文件类型是否符合要求(Pinata 禁止某些类型如 .exe)
- 确保
大文件上传失败
- 使用分块上传(前端分片 + 后端合并)
- 添加超时控制(30-60秒)
- 实现断点续传功能
CID 不一致
- 确保使用相同的 CID 版本(推荐 v1)
- 验证文件内容是否相同(不同文件名不影响 CID)
移动端兼容性问题
- 测试 iOS Safari 的内存限制
- 压缩图片后再上传(使用 canvas 压缩)
- 添加上传进度指示器