Разработка IPFS-инфраструктуры
IPFS в продакшне отличается от IPFS в туториале примерно так же, как работающий сервер отличается от npm start. Основная проблема — content addressing гарантирует, что данные не изменятся, но не гарантирует доступность. Если нода, которая пинит ваш CID, упадёт — данные недоступны. Для NFT метаданных, которые должны жить вечно, это неприемлемо.
Архитектура: что реально нужно
┌─────────────────────────────────────────────────────┐
│ Your Application │
│ │
│ Upload: File → IPFS Node → CID → Smart Contract │
│ Fetch: CID → IPFS Gateway → File │
└───────────────────┬─────────────────────────────────┘
│
┌───────────┼───────────┐
▼ ▼ ▼
Your IPFS Pinata / Cloudflare
Node Web3.Storage IPFS GW
(primary) (redundancy) (public GW)
Принцип: загружаем через свою ноду + пиним в нескольких сервисах для redundancy. Раздаём через собственный gateway или Cloudflare.
Своя IPFS нода: go-ipfs / Kubo
# Установка Kubo (go-ipfs)
wget https://dist.ipfs.tech/kubo/v0.27.0/kubo_v0.27.0_linux-amd64.tar.gz
tar xvzf kubo_v0.27.0_linux-amd64.tar.gz
cd kubo && sudo bash install.sh
# Инициализация
ipfs init --profile server
# Оптимизация конфига для server (не desktop)
ipfs config Addresses.API /ip4/127.0.0.1/tcp/5001
ipfs config Addresses.Gateway /ip4/0.0.0.0/tcp/8080
ipfs config --json API.HTTPHeaders.Access-Control-Allow-Origin '["*"]'
ipfs config --json Swarm.ConnMgr.HighWater 200
ipfs config --json Swarm.ConnMgr.LowWater 100
# Отключаем автоматический GC (управляем вручную)
ipfs config --json Datastore.GCPeriod '"0s"'
Systemd unit:
[Unit]
Description=IPFS Daemon
After=network.target
[Service]
User=ipfs
Environment=IPFS_PATH=/data/ipfs
ExecStart=/usr/local/bin/ipfs daemon --migrate=true --enable-gc=false
Restart=on-failure
RestartSec=10
[Install]
WantedBy=multi-user.target
Загрузка и пиннинг
import { create } from 'kubo-rpc-client';
import * as fs from 'fs';
const ipfs = create({ url: 'http://localhost:5001' });
async function uploadFile(filePath: string): Promise<string> {
const file = fs.readFileSync(filePath);
const result = await ipfs.add(file, {
pin: true, // сразу пиним локально
cidVersion: 1, // CIDv1 более современный, base32 кодировка
});
return result.cid.toString();
}
// Загрузка директории (например, папка с NFT метаданными)
async function uploadDirectory(dirPath: string): Promise<string> {
const files = [];
for (const filename of fs.readdirSync(dirPath)) {
files.push({
path: filename,
content: fs.readFileSync(`${dirPath}/${filename}`),
});
}
let rootCid = '';
for await (const file of ipfs.addAll(files, {
wrapWithDirectory: true,
pin: true,
cidVersion: 1,
})) {
if (file.path === '') { // root directory
rootCid = file.cid.toString();
}
}
return rootCid; // ipfs://rootCid/filename.json
}
Remote pinning: Pinata и Web3.Storage
Локального пиннинга недостаточно. Если ваш сервер лежит — никто не получит данные. Пиним в двух-трёх сервисах:
// Пиннинг в Pinata через Remote Pin API (IPFS стандарт)
async function pinToPinata(cid: string, name: string): Promise<void> {
const response = await fetch('https://api.pinata.cloud/psa/pins', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.PINATA_JWT}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
cid,
name,
origins: [`/dns4/your-ipfs-node.example.com/tcp/4001/p2p/${YOUR_NODE_ID}`],
}),
});
if (!response.ok) throw new Error(`Pinata pin failed: ${response.statusText}`);
}
// Проверка статуса пиннинга
async function checkPinStatus(cid: string): Promise<string> {
const response = await fetch(`https://api.pinata.cloud/psa/pins?cid=${cid}`, {
headers: { 'Authorization': `Bearer ${process.env.PINATA_JWT}` },
});
const data = await response.json();
return data.results[0]?.status ?? 'not_found'; // 'queued'|'pinning'|'pinned'|'failed'
}
Стратегия пиннинга для NFT проекта:
- Собственная нода — первичное хранилище, быстрый доступ
- Pinata — первый backup, надёжный сервис, платный
- web3.storage (Filecoin-backed) — второй backup, хранит данные на Filecoin с верифицируемыми deal receipts
- Nft.storage — специализированно для NFT, Filecoin + IPFS
IPFS Gateway
Браузеры не понимают ipfs:// нативно. Нужен HTTP gateway:
# Nginx как reverse proxy для IPFS gateway
server {
listen 443 ssl;
server_name ipfs.yourdomain.com;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
# Кешируем IPFS контент — он иммутабельный, можно кешировать навсегда
proxy_cache ipfs_cache;
proxy_cache_valid 200 365d;
add_header Cache-Control "public, max-age=31536000, immutable";
# CORS для браузерного fetch
add_header Access-Control-Allow-Origin "*";
}
}
Cloudflare также предоставляет IPFS gateway — через cloudflare-ipfs.com/ipfs/{CID} или через Workers с кастомным доменом. Это хороший способ получить CDN поверх IPFS.
NFT метаданные: стандарт и структура
{
"name": "Token #42",
"description": "...",
"image": "ipfs://bafybeig.../image.png",
"attributes": [
{ "trait_type": "Background", "value": "Blue" }
]
}
Изображение — отдельный CID, метаданные — отдельный CID. В смарт-контракте храним baseURI = "ipfs://bafybei.../", tokenURI возвращает baseURI + tokenId + ".json".
Garbage Collection и мониторинг
# Ручной GC (запускаем по расписанию, не оставляем на автомате)
ipfs repo gc
# Статистика репозитория
ipfs repo stat
# Список всех локальных pins
ipfs pin ls --type=recursive | wc -l
# Проверка что конкретный CID доступен локально
ipfs pin ls bafybeig... 2>/dev/null && echo "pinned" || echo "not pinned"
Мониторинг: Prometheus экспортер для IPFS (ipfs stats bw, ipfs stats repo) + алерт на ipfs swarm peers — если меньше 10 пиров, нода изолирована. Алерт если pin count неожиданно уменьшился (GC удалил что-то важное).
Когда IPFS не нужен
IPFS избыточен для временных данных (аватарки пользователей, которые меняются), данных требующих удаления (GDPR), и данных с частым обновлением. Для этого — обычный S3 или CDN. IPFS имеет смысл для: NFT метаданных и медиа (иммутабельность важна), смарт-контрактных артефактов (ABI, bytecode), decentralized storage с верифицируемостью.







