Spaces:
Running
Running
File size: 7,268 Bytes
66097e1 e099ac1 66097e1 9523283 4416784 9523283 4416784 66097e1 4416784 66097e1 4416784 9523283 4416784 9523283 4416784 9523283 4416784 9523283 4416784 9523283 4416784 fc22af7 56ce56a 4416784 66097e1 4416784 66097e1 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 |
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 })
}
}
|