Reuben_OS / app /api /data /route.ts
Reubencf's picture
Fix LaTeX API errors: use texlive.net and disable auto-compile on save
fc22af7
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 })
}
}