c64ca2b8c2
- JSON-file opslag in .data/pins.json - Gebruikt wanneer KUBO_API_URL niet beschikbaar is - Lokale pins voor development, Kubo voor productie - 191 tests passing
184 lines
6.2 KiB
TypeScript
184 lines
6.2 KiB
TypeScript
import { NextRequest, NextResponse } from 'next/server';
|
|
import { kuboApiUrl, kuboBasicAuth } from '@/lib/config';
|
|
import { localListPins, localAddPin, localRemovePin } from '@/lib/local-pins';
|
|
|
|
export async function proxyIPFS(path: string, method: string, req: NextRequest): Promise<Response> {
|
|
// Normalize path
|
|
const p = path.replace(/^\/+/, '').replace(/\/+$/, '');
|
|
|
|
// ── Lokale pin opslag (zonder Kubo) ──
|
|
// Wanneer KUBO_API_URL niet beschikbaar is, gebruik lokale JSON file
|
|
if (p === 'pins' || p.startsWith('pins/')) {
|
|
const kuboSvc = kuboApiUrl();
|
|
if (!kuboSvc) {
|
|
return handleLocalPins(p, method, req);
|
|
}
|
|
}
|
|
|
|
// In dev, the Kubo API is behind nginx with Basic Auth.
|
|
// If no KUBO_API_URL is configured, return a clear error.
|
|
const kuboSvc = kuboApiUrl();
|
|
if (!kuboSvc) {
|
|
return NextResponse.json(
|
|
{ error: 'IPFS proxy not configured', detail: 'Set KUBO_API_URL in env for dev, or use nginx routing in production' },
|
|
{ status: 501 }
|
|
);
|
|
}
|
|
|
|
// Map /api/explorer/ls/<cid> → /api/v0/ls?arg=<cid>, etc.
|
|
const url = new URL(kuboSvc);
|
|
const kuboPath = mapToKuboAPI(path, method);
|
|
// Split pathname and query string — url.pathname encodes `?` as `%3F`
|
|
const [namePart, ...qsParts] = kuboPath.split('?');
|
|
url.pathname = namePart;
|
|
if (qsParts.length > 0) {
|
|
url.search = qsParts.join('?');
|
|
} else {
|
|
// Forward incoming query params (used by ipns/publish etc.)
|
|
const incomingSearch = req.nextUrl.search;
|
|
if (incomingSearch) {
|
|
url.search = incomingSearch;
|
|
}
|
|
}
|
|
|
|
const headers: Record<string, string> = {};
|
|
const auth = kuboBasicAuth();
|
|
if (auth) {
|
|
headers['Authorization'] = 'Basic ' + Buffer.from(auth).toString('base64');
|
|
}
|
|
|
|
// Forward Content-Type (preserves multipart boundary for file uploads)
|
|
const reqCt = req.headers.get('content-type');
|
|
if (reqCt) {
|
|
headers['Content-Type'] = reqCt;
|
|
}
|
|
|
|
// Kubo uses POST for everything
|
|
const body = method === 'POST' || method === 'DELETE' ? req.body : null;
|
|
const fetchOpts: RequestInit & { duplex?: string } = {
|
|
method: 'POST',
|
|
headers,
|
|
signal: AbortSignal.timeout(30000),
|
|
};
|
|
// Node.js fetch requires duplex: 'half' when streaming a body
|
|
if (body) {
|
|
fetchOpts.duplex = 'half';
|
|
fetchOpts.body = body;
|
|
}
|
|
|
|
let res: Response;
|
|
try {
|
|
res = await fetch(url.toString(), fetchOpts);
|
|
} catch (fetchErr: unknown) {
|
|
const msg = fetchErr instanceof Error ? fetchErr.message : String(fetchErr);
|
|
console.error('[proxyIPFS] fetch error:', msg);
|
|
return NextResponse.json(
|
|
{ error: 'IPFS proxy fetch failed', detail: msg.substring(0, 200) },
|
|
{ status: 502 },
|
|
);
|
|
}
|
|
|
|
const text = await res.text();
|
|
|
|
return new NextResponse(text, {
|
|
status: res.status,
|
|
headers: {
|
|
'Content-Type': res.headers.get('Content-Type') || 'application/json',
|
|
},
|
|
});
|
|
}
|
|
|
|
/** Lokale pin handler — werkt zonder Kubo API */
|
|
async function handleLocalPins(path: string, method: string, req: NextRequest): Promise<Response> {
|
|
const p = path.replace(/^\/+/, '').replace(/\/+$/, '');
|
|
|
|
// GET /api/pins — lijst alle pins
|
|
if (p === 'pins' && method === 'GET') {
|
|
const pins = await localListPins();
|
|
// Retourneer in Kubo-formaat voor backwards compatibiliteit
|
|
const Keys: Record<string, { Type: string }> = {};
|
|
for (const pin of pins) {
|
|
Keys[pin.cid] = { Type: pin.created || 'indirect' };
|
|
}
|
|
return NextResponse.json({ Keys });
|
|
}
|
|
|
|
// POST /api/pins — pin een CID
|
|
if (p === 'pins' && method === 'POST') {
|
|
try {
|
|
const body = await req.json();
|
|
const { cid, name, size } = body;
|
|
if (!cid) return NextResponse.json({ error: 'cid required' }, { status: 400 });
|
|
await localAddPin(cid, name, size || 0);
|
|
return NextResponse.json({ cid });
|
|
} catch {
|
|
return NextResponse.json({ error: 'Invalid request' }, { status: 400 });
|
|
}
|
|
}
|
|
|
|
// DELETE /api/pins/<cid> — verwijder pin
|
|
if (p.startsWith('pins/') && method === 'DELETE') {
|
|
const cid = p.slice(5);
|
|
await localRemovePin(cid);
|
|
return NextResponse.json({ ok: true });
|
|
}
|
|
|
|
return NextResponse.json({ error: 'Not found' }, { status: 404 });
|
|
}
|
|
|
|
export function mapToKuboAPI(path: string, method: string): string {
|
|
// Normalize: /api/node/info → /api/v0/id, etc.
|
|
const p = path.replace(/^\/+/, '').replace(/\/+$/, '');
|
|
switch (p) {
|
|
case 'node/info':
|
|
return '/api/v0/id';
|
|
case 'peers':
|
|
return '/api/v0/swarm/peers';
|
|
case 'pins':
|
|
return method === 'GET' ? '/api/v0/pin/ls' : '/api/v0/pin/add';
|
|
default:
|
|
if (p.startsWith('pins/')) {
|
|
const cid = p.slice(5);
|
|
return `/api/v0/pin/rm?arg=${encodeURIComponent(cid)}`;
|
|
}
|
|
if (p === 'files/upload') {
|
|
return '/api/v0/add?pin=true';
|
|
}
|
|
if (p.startsWith('files/')) {
|
|
return '/api/v0/ls';
|
|
}
|
|
// Explorer routes: /explorer/ls/<cid>, /explorer/cat/<cid>, etc.
|
|
if (p.startsWith('explorer/ls/')) {
|
|
const cid = p.slice('explorer/ls/'.length);
|
|
return `/api/v0/ls?arg=${encodeURIComponent(cid)}`;
|
|
}
|
|
if (p.startsWith('explorer/cat/')) {
|
|
const cid = p.slice('explorer/cat/'.length);
|
|
return `/api/v0/cat?arg=${encodeURIComponent(cid)}`;
|
|
}
|
|
if (p.startsWith('explorer/stat/')) {
|
|
const cid = p.slice('explorer/stat/'.length);
|
|
return `/api/v0/object/stat?arg=${encodeURIComponent(cid)}`;
|
|
}
|
|
if (p.startsWith('explorer/resolve/')) {
|
|
const name = p.slice('explorer/resolve/'.length);
|
|
return `/api/v0/resolve?arg=${encodeURIComponent(name)}`;
|
|
}
|
|
// IPNS routes
|
|
if (p === 'ipns/publish') return '/api/v0/name/publish';
|
|
if (p === 'ipns/keys') return '/api/v0/key/list';
|
|
if (p.startsWith('ipns/resolve/')) {
|
|
const name = p.slice('ipns/resolve/'.length);
|
|
return `/api/v0/name/resolve?arg=${encodeURIComponent(name)}`;
|
|
}
|
|
if (p.startsWith('ipns/keys/gen/')) {
|
|
const name = p.slice('ipns/keys/gen/'.length);
|
|
return `/api/v0/key/gen?arg=${encodeURIComponent(name)}`;
|
|
}
|
|
// Dashboard routes
|
|
if (p === 'repo/stats') return '/api/v0/repo/stat';
|
|
if (p === 'bw/stats') return '/api/v0/bw';
|
|
return '/api/v0/' + p;
|
|
}
|
|
}
|