import { NextRequest, NextResponse } from 'next/server' import fs from 'fs' import path from 'path' import { writeFile, mkdir, unlink, readdir, stat } from 'fs/promises' // Use /data for Hugging Face Spaces persistent storage, fallback to public/data for local dev const DATA_DIR = process.env.SPACE_ID ? '/data' : path.join(process.cwd(), 'public', 'data') // Ensure data directory exists if (!fs.existsSync(DATA_DIR)) { fs.mkdirSync(DATA_DIR, { recursive: true }) } export async function GET(request: NextRequest) { const searchParams = request.nextUrl.searchParams const key = searchParams.get('key') const folder = searchParams.get('folder') || '' if (!key) { return NextResponse.json({ error: 'Passkey is required' }, { status: 400 }) } // Sanitize key to prevent directory traversal const sanitizedKey = key.replace(/[^a-zA-Z0-9_-]/g, '') if (!sanitizedKey) { return NextResponse.json({ error: 'Invalid passkey' }, { status: 400 }) } const userDir = path.join(DATA_DIR, sanitizedKey) const targetDir = path.join(userDir, folder) // Ensure user directory exists if (!fs.existsSync(userDir)) { // If it doesn't exist, return empty list (it will be created on first upload) return NextResponse.json({ files: [] }) } // Security check: ensure targetDir is within userDir if (!targetDir.startsWith(userDir)) { return NextResponse.json({ error: 'Access denied' }, { status: 403 }) } try { if (!fs.existsSync(targetDir)) { return NextResponse.json({ files: [] }) } const items = await readdir(targetDir) const files = await Promise.all(items.map(async (item) => { const itemPath = path.join(targetDir, item) const stats = await stat(itemPath) const relativePath = path.relative(userDir, itemPath) // Read content for text-based files (JSON, LaTeX, Dart, etc.) let content = undefined const ext = path.extname(item).toLowerCase() const textExtensions = ['.json', '.tex', '.dart', '.txt', '.md', '.html', '.css', '.js', '.ts', '.jsx', '.tsx'] if (!stats.isDirectory() && textExtensions.includes(ext)) { try { const fileContent = await fs.promises.readFile(itemPath, 'utf-8') content = fileContent } catch (err) { console.error(`Error reading ${item}:`, err) } } return { name: item, type: stats.isDirectory() ? 'folder' : 'file', size: stats.size, modified: stats.mtime.toISOString(), path: relativePath, extension: path.extname(item).substring(1), ...(content !== undefined && { content }) } })) return NextResponse.json({ files }) } catch (error) { console.error('Error listing files:', error) return NextResponse.json({ error: 'Failed to list files' }, { status: 500 }) } } export async function POST(request: NextRequest) { try { const contentType = request.headers.get('content-type') // Handle JSON body (for save_file action) if (contentType?.includes('application/json')) { const body = await request.json() // Support both 'passkey' and 'key' for backwards compatibility const { passkey, key, action, fileName, content, folder = '' } = body const actualKey = passkey || key if (!actualKey) { return NextResponse.json({ error: 'Passkey is required' }, { status: 400 }) } if (action === 'save_file') { if (!fileName || content === undefined || content === null) { console.log('Save file validation failed:', { fileName, contentLength: content?.length || 0 }) return NextResponse.json({ error: 'fileName and content are required' }, { status: 400 }) } const sanitizedKey = actualKey.replace(/[^a-zA-Z0-9_-]/g, '') const userDir = path.join(DATA_DIR, sanitizedKey) const targetDir = path.join(userDir, folder) // Ensure directories exist await mkdir(targetDir, { recursive: true }) const filePath = path.join(targetDir, fileName) // Debug logging console.log(`Saving file: ${filePath}, content length: ${content.length}`) await writeFile(filePath, content, 'utf-8') // Note: Auto-PDF generation is disabled to avoid external API dependencies // Users can manually compile LaTeX files using the TextEditor's "Compile" button return NextResponse.json({ success: true }) } return NextResponse.json({ error: 'Unknown action' }, { status: 400 }) } // Handle FormData (for file uploads) const formData = await request.formData() const file = formData.get('file') as File const key = formData.get('key') as string const folder = formData.get('folder') as string || '' if (!key) { return NextResponse.json({ error: 'Passkey is required' }, { status: 400 }) } if (!file) { return NextResponse.json({ error: 'No file provided' }, { status: 400 }) } const sanitizedKey = key.replace(/[^a-zA-Z0-9_-]/g, '') const userDir = path.join(DATA_DIR, sanitizedKey) const targetDir = path.join(userDir, folder) // Ensure directories exist await mkdir(targetDir, { recursive: true }) const buffer = Buffer.from(await file.arrayBuffer()) const filePath = path.join(targetDir, file.name) await writeFile(filePath, buffer) return NextResponse.json({ success: true }) } catch (error) { console.error('Error in POST handler:', error) return NextResponse.json({ error: 'Operation failed' }, { status: 500 }) } } export async function DELETE(request: NextRequest) { const searchParams = request.nextUrl.searchParams const key = searchParams.get('key') const filePathParam = searchParams.get('path') if (!key || !filePathParam) { return NextResponse.json({ error: 'Passkey and path are required' }, { status: 400 }) } const sanitizedKey = key.replace(/[^a-zA-Z0-9_-]/g, '') const userDir = path.join(DATA_DIR, sanitizedKey) const targetPath = path.join(userDir, filePathParam) // Security check if (!targetPath.startsWith(userDir)) { return NextResponse.json({ error: 'Access denied' }, { status: 403 }) } try { if (fs.existsSync(targetPath)) { await unlink(targetPath) return NextResponse.json({ success: true }) } else { return NextResponse.json({ error: 'File not found' }, { status: 404 }) } } catch (error) { console.error('Error deleting file:', error) return NextResponse.json({ error: 'Delete failed' }, { status: 500 }) } }