Розробка IPFS-інфраструктури
IPFS в production відрізняється від 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 директорія
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 з верифікуємістю.







