Pinata API v3 图片上传完整指南:场景化实现与最佳实践

发布于:2025-07-08 ⋅ 阅读:(19) ⋅ 点赞:(0)

一、Pinata API v3 核心变化概览

Pinata API v3 引入了重大升级,主要变化包括:

  1. 认证方式:从 API Key + Secret 改为 JWT 令牌
  2. 权限模型:通过端点分组精细控制权限(Pinning Servers 和 Data)
  3. 参数结构:强制要求 pinataMetadatapinataOptions 参数
  4. CID 支持:默认使用 CIDv1(Base32 编码)

创建 JWT 令牌

步骤 1:创建正确权限的 JWT 令牌

  1. 登录 Pinata 控制台
  2. 进入 “API Keys” 页面
  3. 点击 “New Key”
  4. 在权限设置中:
    • 确保 “Pinning” 部分勾选 “Pin file to IPFS”
    • 其他权限根据需求选择
  5. 生成后复制 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);

五、常见问题解决方案

  1. 403 错误

    • 检查 JWT 令牌是否有效且未过期
    • 确保令牌包含所需端点权限(如 pinFileToIPFS
    • 验证请求头格式:Authorization: Bearer YOUR_JWT
  2. 400 错误(参数错误)

    • 确保 pinataMetadatapinataOptions 是有效的 JSON 字符串
    • 检查文件类型是否符合要求(Pinata 禁止某些类型如 .exe)
  3. 大文件上传失败

    • 使用分块上传(前端分片 + 后端合并)
    • 添加超时控制(30-60秒)
    • 实现断点续传功能
  4. CID 不一致

    • 确保使用相同的 CID 版本(推荐 v1)
    • 验证文件内容是否相同(不同文件名不影响 CID)
  5. 移动端兼容性问题

    • 测试 iOS Safari 的内存限制
    • 压缩图片后再上传(使用 canvas 压缩)
    • 添加上传进度指示器

网站公告

今日签到

点亮在社区的每一天
去签到