Reubencf commited on
Commit
ff732bb
·
1 Parent(s): 453beea

downloading fixes

Browse files
app/api/public/route.ts CHANGED
@@ -119,6 +119,49 @@ export async function GET(request: NextRequest) {
119
  // Upload to public folder
120
  export async function POST(request: NextRequest) {
121
  try {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
122
  const formData = await request.formData()
123
  const file = formData.get('file') as File
124
  const folder = formData.get('folder') as string || ''
 
119
  // Upload to public folder
120
  export async function POST(request: NextRequest) {
121
  try {
122
+ const contentType = request.headers.get('content-type')
123
+
124
+ // Handle JSON body (for save_file action)
125
+ if (contentType?.includes('application/json')) {
126
+ const body = await request.json()
127
+ const { action, fileName, content, folder = '' } = body
128
+
129
+ if (action === 'save_file') {
130
+ if (!fileName || content === undefined) {
131
+ return NextResponse.json({ error: 'fileName and content are required' }, { status: 400 })
132
+ }
133
+
134
+ const targetDir = path.join(PUBLIC_DIR, folder)
135
+
136
+ // Security check
137
+ if (!targetDir.startsWith(PUBLIC_DIR)) {
138
+ return NextResponse.json({ error: 'Invalid path' }, { status: 400 })
139
+ }
140
+
141
+ if (!fs.existsSync(targetDir)) {
142
+ fs.mkdirSync(targetDir, { recursive: true })
143
+ }
144
+
145
+ const filePath = path.join(targetDir, fileName)
146
+
147
+ // Write file (overwrite if exists)
148
+ fs.writeFileSync(filePath, content, 'utf-8')
149
+
150
+ // Update or create metadata
151
+ const metadataPath = filePath + '.meta.json'
152
+ const existingMetadata = fs.existsSync(metadataPath)
153
+ ? JSON.parse(fs.readFileSync(metadataPath, 'utf-8'))
154
+ : {}
155
+
156
+ fs.writeFileSync(metadataPath, JSON.stringify({
157
+ ...existingMetadata,
158
+ lastModified: new Date().toISOString()
159
+ }))
160
+
161
+ return NextResponse.json({ success: true })
162
+ }
163
+ }
164
+
165
  const formData = await request.formData()
166
  const file = formData.get('file') as File
167
  const folder = formData.get('folder') as string || ''
app/api/sessions/download/route.ts ADDED
@@ -0,0 +1,106 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextRequest, NextResponse } from 'next/server'
2
+ import fs from 'fs'
3
+ import path from 'path'
4
+
5
+ // Use /data for Hugging Face Spaces persistent storage
6
+ const DATA_DIR = process.env.SPACE_ID
7
+ ? '/data'
8
+ : path.join(process.cwd(), 'public', 'data')
9
+ const PUBLIC_DIR = path.join(DATA_DIR, 'public')
10
+
11
+ export async function GET(request: NextRequest) {
12
+ try {
13
+ const searchParams = request.nextUrl.searchParams
14
+ const fileName = searchParams.get('file')
15
+ const isPublic = searchParams.get('public') === 'true'
16
+
17
+ const key = searchParams.get('key')
18
+
19
+ if (!fileName) {
20
+ return NextResponse.json({ error: 'Filename is required' }, { status: 400 })
21
+ }
22
+
23
+ let filePath
24
+ let rootDir
25
+
26
+ if (isPublic) {
27
+ rootDir = PUBLIC_DIR
28
+ filePath = path.join(PUBLIC_DIR, fileName)
29
+ } else {
30
+ if (!key) {
31
+ return NextResponse.json({ error: 'Passkey is required for secure files' }, { status: 403 })
32
+ }
33
+ const sanitizedKey = key.replace(/[^a-zA-Z0-9_-]/g, '')
34
+ rootDir = path.join(DATA_DIR, sanitizedKey)
35
+ filePath = path.join(rootDir, fileName)
36
+ }
37
+
38
+ // Security check
39
+ if (!filePath.startsWith(rootDir)) {
40
+ return NextResponse.json({ error: 'Invalid path' }, { status: 400 })
41
+ }
42
+
43
+ if (!fs.existsSync(filePath)) {
44
+ return NextResponse.json({ error: 'File not found' }, { status: 404 })
45
+ }
46
+
47
+ const stats = fs.statSync(filePath)
48
+ if (stats.isDirectory()) {
49
+ return NextResponse.json({ error: 'Cannot download directory' }, { status: 400 })
50
+ }
51
+
52
+ const fileBuffer = fs.readFileSync(filePath)
53
+ const ext = path.extname(fileName).toLowerCase()
54
+
55
+ // Determine content type
56
+ let contentType = 'application/octet-stream'
57
+ const mimeTypes: Record<string, string> = {
58
+ '.pdf': 'application/pdf',
59
+ '.doc': 'application/msword',
60
+ '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
61
+ '.xls': 'application/vnd.ms-excel',
62
+ '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
63
+ '.ppt': 'application/vnd.ms-powerpoint',
64
+ '.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
65
+ '.txt': 'text/plain',
66
+ '.md': 'text/markdown',
67
+ '.json': 'application/json',
68
+ '.html': 'text/html',
69
+ '.css': 'text/css',
70
+ '.js': 'text/javascript',
71
+ '.ts': 'text/typescript',
72
+ '.py': 'text/x-python',
73
+ '.java': 'text/x-java',
74
+ '.cpp': 'text/x-c++',
75
+ '.jpg': 'image/jpeg',
76
+ '.jpeg': 'image/jpeg',
77
+ '.png': 'image/png',
78
+ '.gif': 'image/gif',
79
+ '.svg': 'image/svg+xml',
80
+ '.mp3': 'audio/mpeg',
81
+ '.mp4': 'video/mp4',
82
+ '.zip': 'application/zip',
83
+ '.rar': 'application/x-rar-compressed',
84
+ '.tex': 'text/x-tex',
85
+ '.dart': 'text/x-dart'
86
+ }
87
+
88
+ if (mimeTypes[ext]) {
89
+ contentType = mimeTypes[ext]
90
+ }
91
+
92
+ const headers = new Headers({
93
+ 'Content-Type': contentType,
94
+ 'Content-Length': fileBuffer.length.toString(),
95
+ 'Content-Disposition': `attachment; filename="${fileName}"`
96
+ })
97
+
98
+ return new NextResponse(fileBuffer, { headers })
99
+ } catch (error) {
100
+ console.error('Error downloading file:', error)
101
+ return NextResponse.json(
102
+ { error: 'Failed to download file' },
103
+ { status: 500 }
104
+ )
105
+ }
106
+ }
app/components/FileManager.tsx CHANGED
@@ -213,16 +213,9 @@ export function FileManager({ currentPath, onNavigate, onClose, onOpenFlutterApp
213
 
214
  const handleDownload = (file: FileItem) => {
215
  if (sidebarSelection === 'public') {
216
- window.open(`/api/sessions/download?file=${encodeURIComponent(file.name)}&public=true`, '_blank')
217
  } else if (sidebarSelection === 'secure' && passkey) {
218
- // For secure files, we might need a specialized download endpoint that accepts the key
219
- // For now, let's assume we can't easily download via GET without exposing the key in URL
220
- // We'll implement a temporary solution or just block it for now,
221
- // but the user asked for "viewing" mainly.
222
- // Let's try to use the same download endpoint but we need to update it to support keys.
223
- // Actually, let's just open it and see if we can pass the key.
224
- // Ideally we should POST to get a temp URL, but for this hackathon:
225
- alert('Direct download from Secure Data is restricted. Please view files directly.')
226
  }
227
  }
228
 
 
213
 
214
  const handleDownload = (file: FileItem) => {
215
  if (sidebarSelection === 'public') {
216
+ window.open(`/api/sessions/download?file=${encodeURIComponent(file.path)}&public=true`, '_blank')
217
  } else if (sidebarSelection === 'secure' && passkey) {
218
+ window.open(`/api/sessions/download?file=${encodeURIComponent(file.path)}&key=${encodeURIComponent(passkey)}`, '_blank')
 
 
 
 
 
 
 
219
  }
220
  }
221
 
app/components/TextEditor.tsx CHANGED
@@ -41,7 +41,7 @@ export function TextEditor({
41
  const [saveStatus, setSaveStatus] = useState<'idle' | 'saving' | 'saved' | 'error'>('idle')
42
  const [lastSaved, setLastSaved] = useState<Date | null>(null)
43
  const [hasChanges, setHasChanges] = useState(false)
44
-
45
  // Note: PDF compilation is not available in the web environment
46
  // Users should download .tex files and compile locally
47
 
@@ -102,9 +102,38 @@ export function TextEditor({
102
  }
103
  }, [code, initialContent])
104
 
105
- // Download .tex file
106
  const handleDownload = () => {
107
- const blob = new Blob([code], { type: 'text/plain' })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
108
  const url = URL.createObjectURL(blob)
109
  const a = document.createElement('a')
110
  a.href = url
@@ -114,7 +143,12 @@ export function TextEditor({
114
  }
115
 
116
  const handleSave = async () => {
117
- if (!passkey) {
 
 
 
 
 
118
  alert('Please enter your passkey first!')
119
  return
120
  }
@@ -123,12 +157,14 @@ export function TextEditor({
123
  setSaveStatus('saving')
124
 
125
  try {
126
- const response = await fetch('/api/data', {
 
 
127
  method: 'POST',
128
  headers: { 'Content-Type': 'application/json' },
129
  body: JSON.stringify({
130
  action: 'save_file',
131
- passkey: passkey,
132
  fileName: fileName,
133
  content: code,
134
  folder: filePath
@@ -141,7 +177,7 @@ export function TextEditor({
141
  setSaveStatus('saved')
142
  setLastSaved(new Date())
143
  setHasChanges(false)
144
-
145
  // Reset status after 3 seconds
146
  setTimeout(() => {
147
  setSaveStatus('idle')
@@ -196,18 +232,18 @@ export function TextEditor({
196
  <FileText size={14} className="text-blue-400" />
197
  <span>{fileName}</span>
198
  </div>
199
-
200
  {hasChanges && (
201
  <span className="text-xs text-yellow-500">● Modified</span>
202
  )}
203
-
204
  {saveStatus === 'saved' && (
205
  <div className="flex items-center gap-1 text-xs text-green-500">
206
  <Check size={14} />
207
  <span>Saved {lastSaved?.toLocaleTimeString()}</span>
208
  </div>
209
  )}
210
-
211
  {saveStatus === 'error' && (
212
  <div className="flex items-center gap-1 text-xs text-red-500">
213
  <WarningCircle size={14} />
 
41
  const [saveStatus, setSaveStatus] = useState<'idle' | 'saving' | 'saved' | 'error'>('idle')
42
  const [lastSaved, setLastSaved] = useState<Date | null>(null)
43
  const [hasChanges, setHasChanges] = useState(false)
44
+
45
  // Note: PDF compilation is not available in the web environment
46
  // Users should download .tex files and compile locally
47
 
 
102
  }
103
  }, [code, initialContent])
104
 
105
+ // Download file
106
  const handleDownload = () => {
107
+ const ext = fileName.split('.').pop()?.toLowerCase() || 'txt'
108
+ let mimeType = 'text/plain'
109
+
110
+ const mimeTypes: Record<string, string> = {
111
+ 'js': 'text/javascript',
112
+ 'ts': 'text/typescript',
113
+ 'tsx': 'text/typescript',
114
+ 'jsx': 'text/javascript',
115
+ 'json': 'application/json',
116
+ 'html': 'text/html',
117
+ 'css': 'text/css',
118
+ 'md': 'text/markdown',
119
+ 'py': 'text/x-python',
120
+ 'java': 'text/x-java',
121
+ 'c': 'text/x-c',
122
+ 'cpp': 'text/x-c++',
123
+ 'dart': 'application/vnd.dart',
124
+ 'tex': 'application/x-tex',
125
+ 'xml': 'text/xml',
126
+ 'yaml': 'text/yaml',
127
+ 'yml': 'text/yaml',
128
+ 'sh': 'application/x-sh',
129
+ 'sql': 'application/sql'
130
+ }
131
+
132
+ if (mimeTypes[ext]) {
133
+ mimeType = mimeTypes[ext]
134
+ }
135
+
136
+ const blob = new Blob([code], { type: mimeType })
137
  const url = URL.createObjectURL(blob)
138
  const a = document.createElement('a')
139
  a.href = url
 
143
  }
144
 
145
  const handleSave = async () => {
146
+ // Determine if this is a public file or secure file
147
+ // If passkey is empty, we assume it's a public file (unless explicitly told otherwise, but we don't have an isPublic prop)
148
+ // Ideally, we should have an isPublic prop, but for now, empty passkey implies public context in this app structure.
149
+ const isPublic = !passkey
150
+
151
+ if (!passkey && !isPublic) {
152
  alert('Please enter your passkey first!')
153
  return
154
  }
 
157
  setSaveStatus('saving')
158
 
159
  try {
160
+ const endpoint = isPublic ? '/api/public' : '/api/data'
161
+
162
+ const response = await fetch(endpoint, {
163
  method: 'POST',
164
  headers: { 'Content-Type': 'application/json' },
165
  body: JSON.stringify({
166
  action: 'save_file',
167
+ passkey: passkey, // Optional for public
168
  fileName: fileName,
169
  content: code,
170
  folder: filePath
 
177
  setSaveStatus('saved')
178
  setLastSaved(new Date())
179
  setHasChanges(false)
180
+
181
  // Reset status after 3 seconds
182
  setTimeout(() => {
183
  setSaveStatus('idle')
 
232
  <FileText size={14} className="text-blue-400" />
233
  <span>{fileName}</span>
234
  </div>
235
+
236
  {hasChanges && (
237
  <span className="text-xs text-yellow-500">● Modified</span>
238
  )}
239
+
240
  {saveStatus === 'saved' && (
241
  <div className="flex items-center gap-1 text-xs text-green-500">
242
  <Check size={14} />
243
  <span>Saved {lastSaved?.toLocaleTimeString()}</span>
244
  </div>
245
  )}
246
+
247
  {saveStatus === 'error' && (
248
  <div className="flex items-center gap-1 text-xs text-red-500">
249
  <WarningCircle size={14} />