Spaces:
Running
Running
| 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 }) | |
| } | |
| } | |