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:
@@ -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(/\/+$/, '');
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
Reference in New Issue
Block a user