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
This commit is contained in:
maikrolf
2026-09-19 19:58:28 +02:00
parent d3004506a4
commit 841f89b1d5
2 changed files with 194 additions and 1 deletions
+73 -1
View File
@@ -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<Response> {
// 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<Response> {
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<Response> {
const files = await localListFiles();
return NextResponse.json(files);
}
/** Lokale file download handler */
async function handleLocalGetFile(cid: string): Promise<Response> {
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<Response> {
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(/\/+$/, '');
+121
View File
@@ -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<Record<string, FileRecord>> {
try {
const data = await readFile(FILES_DB, 'utf-8');
return JSON.parse(data);
} catch {
return {};
}
}
async function writeDb(db: Record<string, FileRecord>): Promise<void> {
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<FileRecord[]> {
const db = await readDb();
return Object.values(db);
}
/** Verwijder een bestand */
export async function localDeleteFile(cid: string): Promise<void> {
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 };
}