From 841f89b1d5d40b4787bbbb131351acb059e937a5 Mon Sep 17 00:00:00 2001 From: maikrolf <40924921+maikrolf@users.noreply.github.com> Date: Sat, 19 Sep 2026 19:58:28 +0200 Subject: [PATCH] feat: lokale file opslag zonder Kubo - Bestanden uploaden naar .data/files/ - Auto-pin na upload - Lijst, download en verwijder bestanden - CID hash generatie via SHA-256 - 191 tests passing --- src/app/api/[...path]/proxy-ipfs.ts | 74 ++++++++++++++++- src/lib/local-files.ts | 121 ++++++++++++++++++++++++++++ 2 files changed, 194 insertions(+), 1 deletion(-) create mode 100644 src/lib/local-files.ts diff --git a/src/app/api/[...path]/proxy-ipfs.ts b/src/app/api/[...path]/proxy-ipfs.ts index dff7149..d0debf3 100644 --- a/src/app/api/[...path]/proxy-ipfs.ts +++ b/src/app/api/[...path]/proxy-ipfs.ts @@ -1,17 +1,33 @@ import { NextRequest, NextResponse } from 'next/server'; import { kuboApiUrl, kuboBasicAuth } from '@/lib/config'; import { localListPins, localAddPin, localRemovePin } from '@/lib/local-pins'; +import { localSaveFile, localGetFile, localListFiles, localDeleteFile } from '@/lib/local-files'; export async function proxyIPFS(path: string, method: string, req: NextRequest): Promise { // Normalize path — verwijder leading/trailing slashes const p = path.replace(/^\/+/, '').replace(/\/+$/, ''); // ── Lokale pin opslag (altijd) ── - // Pins worden lokaal opgeslagen in .data/pins.json if (p === 'pins' || p.startsWith('pins/')) { return handleLocalPins(p, method, req); } + // ── Lokale file opslag (altijd) ── + if (p === 'files/upload' && method === 'POST') { + return handleLocalUpload(req); + } + if (p === 'files' && method === 'GET') { + return handleLocalListFiles(); + } + if (p.startsWith('files/') && method === 'GET') { + const cid = p.slice(6); + return handleLocalGetFile(cid); + } + if (p.startsWith('files/') && method === 'DELETE') { + const cid = p.slice(6); + return handleLocalDeleteFile(cid); + } + // IPFS routes gaan naar Kubo (wanneer beschikbaar) const kuboSvc = kuboApiUrl(); if (!kuboSvc) { @@ -133,6 +149,62 @@ async function handleLocalPins(path: string, method: string, req: NextRequest): return NextResponse.json({ error: 'Not found' }, { status: 404 }); } +/** Lokale file upload handler */ +async function handleLocalUpload(req: NextRequest): Promise { + try { + const clonedReq = req.clone(); + const formData = await clonedReq.formData(); + const file = formData.get('file') as File | null; + + if (!file) { + return NextResponse.json({ error: 'No file provided' }, { status: 400 }); + } + + // Lees bestand als buffer + const arrayBuffer = await file.arrayBuffer(); + const buffer = Buffer.from(arrayBuffer); + + // Sla lokaal op + const result = await localSaveFile(file.name, buffer, file.size); + + // Auto-pin na upload + const { localAddPin } = await import('@/lib/local-pins'); + await localAddPin(result.cid, file.name, result.size); + + return NextResponse.json({ cid: result.cid, size: result.size, name: file.name }); + } catch (e) { + console.error('[handleLocalUpload] error:', e); + return NextResponse.json({ error: 'Upload failed' }, { status: 500 }); + } +} + +/** Lokale file list handler */ +async function handleLocalListFiles(): Promise { + const files = await localListFiles(); + return NextResponse.json(files); +} + +/** Lokale file download handler */ +async function handleLocalGetFile(cid: string): Promise { + const file = await localGetFile(cid); + if (!file) { + return NextResponse.json({ error: 'File not found' }, { status: 404 }); + } + + return new NextResponse(new Uint8Array(file.buffer), { + headers: { + 'Content-Type': 'application/octet-stream', + 'Content-Disposition': `attachment; filename="${file.name}"`, + }, + }); +} + +/** Lokale file delete handler */ +async function handleLocalDeleteFile(cid: string): Promise { + await localDeleteFile(cid); + return NextResponse.json({ ok: true }); +} + export function mapToKuboAPI(path: string, method: string): string { // Normalize: /api/node/info → /api/v0/id, etc. const p = path.replace(/^\/+/, '').replace(/\/+$/, ''); diff --git a/src/lib/local-files.ts b/src/lib/local-files.ts new file mode 100644 index 0000000..c2b6fe4 --- /dev/null +++ b/src/lib/local-files.ts @@ -0,0 +1,121 @@ +/** + * Local file storage — disk-based + * Gebruikt wanneer geen Kubo API beschikbaar is. + * Slaat bestanden op in .data/files/ en houdt metadata bij in .data/files.json + */ + +import { readFile, writeFile, mkdir, unlink, readdir, stat } from 'fs/promises'; +import { join } from 'path'; +import { createHash } from 'crypto'; + +const DATA_DIR = join(process.cwd(), '.data', 'files'); +const FILES_DB = join(process.cwd(), '.data', 'files.json'); + +interface FileRecord { + cid: string; + name: string; + size: number; + modified: string; + path: string; // relatief pad in .data/files/ +} + +async function ensureDir() { + await mkdir(DATA_DIR, { recursive: true }); +} + +async function readDb(): Promise> { + try { + const data = await readFile(FILES_DB, 'utf-8'); + return JSON.parse(data); + } catch { + return {}; + } +} + +async function writeDb(db: Record): Promise { + await ensureDir(); + await writeFile(FILES_DB, JSON.stringify(db, null, 2), 'utf-8'); +} + +/** Genereer een CID-achtige hash van de bestandsinhoud */ +function generateCid(buffer: Buffer): string { + const hash = createHash('sha256').update(buffer).digest('hex'); + // IPFS-style base32 hash (Qm prefix + base32 hash) + return 'Qm' + hash.slice(0, 44); +} + +/** Sla een bestand op en retourneer CID + metadata */ +export async function localSaveFile( + name: string, + buffer: Buffer, + size: number +): Promise<{ cid: string; size: number }> { + await ensureDir(); + + const cid = generateCid(buffer); + const filePath = join(DATA_DIR, `${cid}-${name}`); + + // Sla het bestand op + await writeFile(filePath, buffer); + + // Update de database + const db = await readDb(); + db[cid] = { + cid, + name, + size, + modified: new Date().toISOString(), + path: `${cid}-${name}`, + }; + await writeDb(db); + + return { cid, size }; +} + +/** Haal een bestand op */ +export async function localGetFile(cid: string): Promise<{ buffer: Buffer; name: string } | null> { + const db = await readDb(); + const record = db[cid]; + if (!record) return null; + + const filePath = join(DATA_DIR, record.path); + try { + const buffer = await readFile(filePath); + return { buffer, name: record.name }; + } catch { + return null; + } +} + +/** Lijst alle bestanden */ +export async function localListFiles(): Promise { + const db = await readDb(); + return Object.values(db); +} + +/** Verwijder een bestand */ +export async function localDeleteFile(cid: string): Promise { + const db = await readDb(); + const record = db[cid]; + if (!record) return; + + // Verwijder het bestand van disk + const filePath = join(DATA_DIR, record.path); + try { + await unlink(filePath); + } catch { + // Bestand bestaat niet meer — niet erg + } + + // Verwijder uit de database + delete db[cid]; + await writeDb(db); +} + +/** Bereken totale opslag */ +export async function localGetStorageUsage(): Promise<{ files: number; totalBytes: number }> { + const db = await readDb(); + const files = Object.values(db); + const totalBytes = files.reduce((sum, f) => sum + f.size, 0); + return { files: files.length, totalBytes }; +}