/** * GET /api/projects/[projectId]/file?path=apps/admin * * Returns directory listing or file content from the project's Gitea repo. * Response for directory: { type: "dir", items: [{ name, path, type }] } * Response for file: { type: "file", content: string, encoding: "utf8" | "base64" } */ import { NextResponse } from 'next/server'; import { getServerSession } from 'next-auth'; import { authOptions } from '@/lib/auth/authOptions'; import { query } from '@/lib/db-postgres'; const GITEA_API_URL = process.env.GITEA_API_URL ?? 'https://git.vibnai.com'; const GITEA_API_TOKEN = process.env.GITEA_API_TOKEN ?? ''; async function giteaGet(path: string) { const res = await fetch(`${GITEA_API_URL}/api/v1${path}`, { headers: { Authorization: `token ${GITEA_API_TOKEN}` }, next: { revalidate: 10 }, }); if (!res.ok) throw new Error(`Gitea ${res.status}: ${path}`); return res.json(); } const BINARY_EXTENSIONS = new Set([ 'png', 'jpg', 'jpeg', 'gif', 'webp', 'svg', 'ico', 'woff', 'woff2', 'ttf', 'eot', 'zip', 'tar', 'gz', 'pdf', ]); function isBinary(name: string): boolean { const ext = name.split('.').pop()?.toLowerCase() ?? ''; return BINARY_EXTENSIONS.has(ext); } export async function GET( req: Request, { params }: { params: Promise<{ projectId: string }> } ) { try { const { projectId } = await params; const session = await getServerSession(authOptions); if (!session?.user?.email) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } const { searchParams } = new URL(req.url); const filePath = searchParams.get('path') ?? ''; // Verify ownership + get giteaRepo const rows = await query<{ data: Record }>( `SELECT p.data FROM fs_projects p JOIN fs_users u ON u.id = p.user_id WHERE p.id = $1 AND u.data->>'email' = $2 LIMIT 1`, [projectId, session.user.email] ); if (rows.length === 0) { return NextResponse.json({ error: 'Project not found' }, { status: 404 }); } const giteaRepo = rows[0].data?.giteaRepo as string | undefined; if (!giteaRepo) { return NextResponse.json({ error: 'No Gitea repo connected' }, { status: 404 }); } const encodedPath = filePath ? encodeURIComponent(filePath).replace(/%2F/g, '/') : ''; const apiPath = `/repos/${giteaRepo}/contents/${encodedPath}`; const data = await giteaGet(apiPath); // Directory listing if (Array.isArray(data)) { const items = data .map((item: { name: string; path: string; type: string; size?: number }) => ({ name: item.name, path: item.path, type: item.type, // "file" | "dir" | "symlink" size: item.size, })) .sort((a, b) => { // Dirs first if (a.type === 'dir' && b.type !== 'dir') return -1; if (a.type !== 'dir' && b.type === 'dir') return 1; return a.name.localeCompare(b.name); }); return NextResponse.json({ type: 'dir', items }); } // Single file const item = data as { name: string; content?: string; encoding?: string; size?: number }; if (isBinary(item.name)) { return NextResponse.json({ type: 'file', content: '(binary file)', encoding: 'utf8' }); } // Gitea returns base64-encoded content const raw = item.content ?? ''; let content: string; try { content = Buffer.from(raw.replace(/\n/g, ''), 'base64').toString('utf8'); } catch { content = raw; } return NextResponse.json({ type: 'file', content, encoding: 'utf8', name: item.name }); } catch (err) { console.error('[file API]', err); return NextResponse.json({ error: 'Failed to fetch file' }, { status: 500 }); } }