summaryrefslogtreecommitdiff
path: root/frontend
diff options
context:
space:
mode:
authordiogo464 <[email protected]>2025-08-11 13:40:27 +0100
committerdiogo464 <[email protected]>2025-08-11 13:40:27 +0100
commitf69ca010b80703389fffe75fc6dca907e53df74d (patch)
tree1cf081f49b3793aae3f298ed50753841fd306424 /frontend
parent4af66f418b6837b6441b4e8eaf2d8ede585238b9 (diff)
basic file upload
Diffstat (limited to 'frontend')
-rw-r--r--frontend/app/api/upload/route.ts127
-rw-r--r--frontend/app/drive/[...path]/page.tsx10
-rw-r--r--frontend/app/drive/page.tsx10
-rw-r--r--frontend/components/FileUpload.tsx262
-rw-r--r--frontend/lib/constants.ts39
5 files changed, 447 insertions, 1 deletions
diff --git a/frontend/app/api/upload/route.ts b/frontend/app/api/upload/route.ts
new file mode 100644
index 0000000..eb1ecaa
--- /dev/null
+++ b/frontend/app/api/upload/route.ts
@@ -0,0 +1,127 @@
1import { NextRequest, NextResponse } from 'next/server'
2import { writeFile, unlink } from 'fs/promises'
3import { tmpdir } from 'os'
4import { join } from 'path'
5import { randomUUID } from 'crypto'
6import { Auth_get_user, Auth_user_can_upload } from '@/lib/auth'
7import { Drive_import } from '@/lib/drive'
8import { UPLOAD_MAX_FILE_SIZE, UPLOAD_MAX_FILES } from '@/lib/constants'
9import { revalidatePath } from 'next/cache'
10
11export async function POST(request: NextRequest) {
12 try {
13 // Check user authentication and permissions
14 const user = await Auth_get_user()
15 if (!user.isLoggedIn) {
16 return NextResponse.json({ error: 'User not authenticated' }, { status: 401 })
17 }
18
19 if (!Auth_user_can_upload(user)) {
20 return NextResponse.json({ error: 'User does not have upload permissions' }, { status: 403 })
21 }
22
23 // Parse form data
24 const formData = await request.formData()
25 const files = formData.getAll('files') as File[]
26 const targetPath = formData.get('targetPath') as string || ''
27
28 // Validate files
29 if (!files || files.length === 0) {
30 return NextResponse.json({ error: 'No files provided' }, { status: 400 })
31 }
32
33 if (files.length > UPLOAD_MAX_FILES) {
34 return NextResponse.json({
35 error: `Too many files. Maximum ${UPLOAD_MAX_FILES} files allowed`
36 }, { status: 400 })
37 }
38
39 // Validate each file
40 for (const file of files) {
41 if (file.size > UPLOAD_MAX_FILE_SIZE) {
42 return NextResponse.json({
43 error: `File '${file.name}' exceeds maximum size of ${UPLOAD_MAX_FILE_SIZE / (1024 * 1024)}MB`
44 }, { status: 400 })
45 }
46 }
47
48 const uploadResults = []
49 const tempFiles: string[] = []
50
51 try {
52 // Process each file
53 for (const file of files) {
54 // Create temporary file
55 const tempFileName = `${randomUUID()}-${file.name}`
56 const tempFilePath = join(tmpdir(), tempFileName)
57 tempFiles.push(tempFilePath)
58
59 // Save file to temporary location
60 const bytes = await file.arrayBuffer()
61 const buffer = Buffer.from(bytes)
62 await writeFile(tempFilePath, buffer)
63
64 // Determine target drive path
65 const driveFilePath = targetPath ? `${targetPath}/${file.name}` : `/${file.name}`
66
67 try {
68 // Import file using Drive_import
69 await Drive_import(tempFilePath, driveFilePath, user.email)
70 uploadResults.push({
71 filename: file.name,
72 success: true,
73 message: 'File uploaded successfully'
74 })
75 } catch (error) {
76 console.error(`Failed to import file ${file.name}:`, error)
77 uploadResults.push({
78 filename: file.name,
79 success: false,
80 message: error instanceof Error ? error.message : 'Unknown error during import'
81 })
82 }
83 }
84
85 // Clean up temporary files
86 for (const tempFile of tempFiles) {
87 try {
88 await unlink(tempFile)
89 } catch (error) {
90 console.error(`Failed to delete temp file ${tempFile}:`, error)
91 }
92 }
93
94 // Revalidate the target path to refresh the directory listing
95 revalidatePath(`/drive${targetPath}`)
96 revalidatePath('/drive')
97
98 // Check if any uploads succeeded
99 const successfulUploads = uploadResults.filter(result => result.success)
100 const failedUploads = uploadResults.filter(result => !result.success)
101
102 return NextResponse.json({
103 success: true,
104 message: `${successfulUploads.length} files uploaded successfully${failedUploads.length > 0 ? `, ${failedUploads.length} failed` : ''}`,
105 results: uploadResults
106 })
107
108 } catch (error) {
109 // Clean up temporary files on error
110 for (const tempFile of tempFiles) {
111 try {
112 await unlink(tempFile)
113 } catch (cleanupError) {
114 console.error(`Failed to delete temp file during cleanup ${tempFile}:`, cleanupError)
115 }
116 }
117 throw error
118 }
119
120 } catch (error) {
121 console.error('Upload error:', error)
122 return NextResponse.json(
123 { error: error instanceof Error ? error.message : 'Internal server error' },
124 { status: 500 }
125 )
126 }
127} \ No newline at end of file
diff --git a/frontend/app/drive/[...path]/page.tsx b/frontend/app/drive/[...path]/page.tsx
index 75a1bb1..60c2cca 100644
--- a/frontend/app/drive/[...path]/page.tsx
+++ b/frontend/app/drive/[...path]/page.tsx
@@ -2,7 +2,8 @@ import { Drive_ls, Drive_basename, Drive_parent } from "@/lib/drive"
2import { formatSize } from "@/lib/utils" 2import { formatSize } from "@/lib/utils"
3import Link from "next/link" 3import Link from "next/link"
4import { cookies } from 'next/headers'; 4import { cookies } from 'next/headers';
5import { Auth_get_user } from "@/lib/auth"; 5import { Auth_get_user, Auth_user_can_upload } from "@/lib/auth";
6import FileUpload from "@/components/FileUpload"
6 7
7interface DrivePageProps { 8interface DrivePageProps {
8 params: Promise<{ 9 params: Promise<{
@@ -130,6 +131,13 @@ export default async function DrivePage({ params }: DrivePageProps) {
130 </table> 131 </table>
131 </div> 132 </div>
132 </div> 133 </div>
134
135 {/* File Upload Component */}
136 {Auth_user_can_upload(user) && (
137 <FileUpload
138 targetPath={fullPath}
139 />
140 )}
133 </div> 141 </div>
134 </div> 142 </div>
135 ) 143 )
diff --git a/frontend/app/drive/page.tsx b/frontend/app/drive/page.tsx
index 218774a..5970a6a 100644
--- a/frontend/app/drive/page.tsx
+++ b/frontend/app/drive/page.tsx
@@ -1,8 +1,11 @@
1import { Drive_ls, Drive_basename } from "@/lib/drive" 1import { Drive_ls, Drive_basename } from "@/lib/drive"
2import { formatSize } from "@/lib/utils" 2import { formatSize } from "@/lib/utils"
3import { Auth_get_user, Auth_user_can_upload } from "@/lib/auth"
3import Link from "next/link" 4import Link from "next/link"
5import FileUpload from "@/components/FileUpload"
4 6
5export default async function DrivePage() { 7export default async function DrivePage() {
8 const user = await Auth_get_user()
6 const entries = await Drive_ls("", false) 9 const entries = await Drive_ls("", false)
7 10
8 // Sort entries: directories first, then files, both alphabetically 11 // Sort entries: directories first, then files, both alphabetically
@@ -88,6 +91,13 @@ export default async function DrivePage() {
88 </table> 91 </table>
89 </div> 92 </div>
90 </div> 93 </div>
94
95 {/* File Upload Component */}
96 {Auth_user_can_upload(user) && (
97 <FileUpload
98 targetPath=""
99 />
100 )}
91 </div> 101 </div>
92 </div> 102 </div>
93 ) 103 )
diff --git a/frontend/components/FileUpload.tsx b/frontend/components/FileUpload.tsx
new file mode 100644
index 0000000..8fbb919
--- /dev/null
+++ b/frontend/components/FileUpload.tsx
@@ -0,0 +1,262 @@
1'use client'
2
3import { useState, useRef } from 'react'
4import { UPLOAD_MAX_FILES, UPLOAD_MAX_FILE_SIZE } from '@/lib/constants'
5
6// Client-side file validation function
7function validateFile(file: File): { allowed: boolean; reason?: string } {
8 if (file.size > UPLOAD_MAX_FILE_SIZE) {
9 return { allowed: false, reason: `File size exceeds ${UPLOAD_MAX_FILE_SIZE / (1024 * 1024)}MB limit` };
10 }
11
12 return { allowed: true };
13}
14
15interface FileUploadProps {
16 targetPath: string
17 onUploadComplete?: () => void
18}
19
20interface UploadResult {
21 filename: string
22 success: boolean
23 message: string
24}
25
26export default function FileUpload({ targetPath, onUploadComplete }: FileUploadProps) {
27 const [isDragOver, setIsDragOver] = useState(false)
28 const [isUploading, setIsUploading] = useState(false)
29 const [selectedFiles, setSelectedFiles] = useState<File[]>([])
30 const [uploadResults, setUploadResults] = useState<UploadResult[]>([])
31 const [showResults, setShowResults] = useState(false)
32 const fileInputRef = useRef<HTMLInputElement>(null)
33
34 const handleFileSelect = (files: FileList) => {
35 const fileArray = Array.from(files)
36
37 // Validate file count
38 if (fileArray.length > UPLOAD_MAX_FILES) {
39 alert(`Too many files selected. Maximum ${UPLOAD_MAX_FILES} files allowed.`)
40 return
41 }
42
43 // Validate each file
44 const validFiles: File[] = []
45 for (const file of fileArray) {
46 const validation = validateFile(file)
47 if (!validation.allowed) {
48 alert(`File '${file.name}': ${validation.reason}`)
49 continue
50 }
51 validFiles.push(file)
52 }
53
54 setSelectedFiles(validFiles)
55 setUploadResults([])
56 setShowResults(false)
57 }
58
59 const handleDragOver = (e: React.DragEvent) => {
60 e.preventDefault()
61 setIsDragOver(true)
62 }
63
64 const handleDragLeave = (e: React.DragEvent) => {
65 e.preventDefault()
66 setIsDragOver(false)
67 }
68
69 const handleDrop = (e: React.DragEvent) => {
70 e.preventDefault()
71 setIsDragOver(false)
72
73 if (e.dataTransfer.files) {
74 handleFileSelect(e.dataTransfer.files)
75 }
76 }
77
78 const handleFileInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
79 if (e.target.files) {
80 handleFileSelect(e.target.files)
81 }
82 }
83
84 const handleUpload = async () => {
85 if (selectedFiles.length === 0) return
86
87 setIsUploading(true)
88 setUploadResults([])
89
90 try {
91 const formData = new FormData()
92 selectedFiles.forEach(file => {
93 formData.append('files', file)
94 })
95 formData.append('targetPath', targetPath)
96
97 const response = await fetch('/api/upload', {
98 method: 'POST',
99 body: formData,
100 })
101
102 const result = await response.json()
103
104 if (response.ok) {
105 setUploadResults(result.results || [])
106 setShowResults(true)
107 setSelectedFiles([])
108
109 // Clear file input
110 if (fileInputRef.current) {
111 fileInputRef.current.value = ''
112 }
113
114 // Refresh the page after successful upload
115 setTimeout(() => {
116 window.location.reload()
117 }, 1000)
118 } else {
119 alert(`Upload failed: ${result.error}`)
120 }
121 } catch (error) {
122 console.error('Upload error:', error)
123 alert('Upload failed: Network error')
124 } finally {
125 setIsUploading(false)
126 }
127 }
128
129 const removeFile = (index: number) => {
130 setSelectedFiles(prev => prev.filter((_, i) => i !== index))
131 }
132
133 const clearResults = () => {
134 setShowResults(false)
135 setUploadResults([])
136 }
137
138 return (
139 <div className="mb-6 p-4 border border-gray-200 dark:border-gray-700 rounded-lg bg-gray-50 dark:bg-gray-800">
140 <h2 className="text-lg font-semibold mb-4 text-gray-900 dark:text-gray-100">Upload Files</h2>
141
142 {/* Upload Results */}
143 {showResults && uploadResults.length > 0 && (
144 <div className="mb-4 p-3 bg-white dark:bg-gray-900 rounded border">
145 <div className="flex justify-between items-center mb-2">
146 <h3 className="font-medium text-gray-900 dark:text-gray-100">Upload Results</h3>
147 <button
148 onClick={clearResults}
149 className="text-sm text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
150 >
151 Clear
152 </button>
153 </div>
154 <div className="space-y-1">
155 {uploadResults.map((result, index) => (
156 <div
157 key={index}
158 className={`text-sm flex items-center gap-2 ${
159 result.success
160 ? 'text-green-600 dark:text-green-400'
161 : 'text-red-600 dark:text-red-400'
162 }`}
163 >
164 <span>{result.success ? '✓' : '✗'}</span>
165 <span className="font-medium">{result.filename}:</span>
166 <span>{result.message}</span>
167 </div>
168 ))}
169 </div>
170 </div>
171 )}
172
173 {/* File Drop Zone */}
174 <div
175 className={`border-2 border-dashed rounded-lg p-6 text-center transition-colors ${
176 isDragOver
177 ? 'border-blue-400 bg-blue-50 dark:bg-blue-900/20'
178 : 'border-gray-300 dark:border-gray-600 hover:border-gray-400 dark:hover:border-gray-500'
179 }`}
180 onDragOver={handleDragOver}
181 onDragLeave={handleDragLeave}
182 onDrop={handleDrop}
183 >
184 <div className="space-y-2">
185 <div className="text-4xl">📁</div>
186 <div className="text-gray-600 dark:text-gray-300">
187 <p className="font-medium">Drop files here or click to browse</p>
188 <p className="text-sm">
189 Maximum {UPLOAD_MAX_FILES} files, {UPLOAD_MAX_FILE_SIZE / (1024 * 1024)}MB each
190 </p>
191 </div>
192 <input
193 ref={fileInputRef}
194 type="file"
195 multiple
196 className="hidden"
197 onChange={handleFileInputChange}
198 />
199 <button
200 type="button"
201 onClick={() => fileInputRef.current?.click()}
202 disabled={isUploading}
203 className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 disabled:bg-gray-400 transition-colors"
204 >
205 Browse Files
206 </button>
207 </div>
208 </div>
209
210 {/* Selected Files */}
211 {selectedFiles.length > 0 && (
212 <div className="mt-4">
213 <h3 className="font-medium mb-2 text-gray-900 dark:text-gray-100">
214 Selected Files ({selectedFiles.length})
215 </h3>
216 <div className="space-y-2">
217 {selectedFiles.map((file, index) => (
218 <div
219 key={index}
220 className="flex items-center justify-between p-2 bg-white dark:bg-gray-900 rounded border"
221 >
222 <div className="flex items-center gap-2">
223 <span className="text-gray-400">📄</span>
224 <div>
225 <div className="text-sm font-medium text-gray-900 dark:text-gray-100">
226 {file.name}
227 </div>
228 <div className="text-xs text-gray-500 dark:text-gray-400">
229 {(file.size / 1024 / 1024).toFixed(2)} MB
230 </div>
231 </div>
232 </div>
233 <button
234 onClick={() => removeFile(index)}
235 disabled={isUploading}
236 className="text-red-500 hover:text-red-700 disabled:text-gray-400 text-sm"
237 >
238 Remove
239 </button>
240 </div>
241 ))}
242 </div>
243
244 <button
245 onClick={handleUpload}
246 disabled={isUploading || selectedFiles.length === 0}
247 className="mt-3 px-6 py-2 bg-green-500 text-white rounded hover:bg-green-600 disabled:bg-gray-400 transition-colors flex items-center gap-2"
248 >
249 {isUploading ? (
250 <>
251 <div className="animate-spin rounded-full h-4 w-4 border-2 border-white border-t-transparent"></div>
252 Uploading...
253 </>
254 ) : (
255 `Upload ${selectedFiles.length} file${selectedFiles.length > 1 ? 's' : ''}`
256 )}
257 </button>
258 </div>
259 )}
260 </div>
261 )
262} \ No newline at end of file
diff --git a/frontend/lib/constants.ts b/frontend/lib/constants.ts
new file mode 100644
index 0000000..8f74dd1
--- /dev/null
+++ b/frontend/lib/constants.ts
@@ -0,0 +1,39 @@
1// Upload configuration constants
2export const UPLOAD_MAX_FILE_SIZE = 100 * 1024 * 1024; // 100MB
3export const UPLOAD_MAX_FILES = 10; // Maximum files per upload
4export const UPLOAD_ALLOWED_TYPES = [
5 // Documents
6 'application/pdf',
7 'application/msword',
8 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
9 'application/vnd.ms-excel',
10 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
11 'application/vnd.ms-powerpoint',
12 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
13 'text/plain',
14 'text/csv',
15 // Images
16 'image/jpeg',
17 'image/png',
18 'image/gif',
19 'image/webp',
20 'image/svg+xml',
21 // Videos
22 'video/mp4',
23 'video/webm',
24 'video/ogg',
25 // Audio
26 'audio/mpeg',
27 'audio/wav',
28 'audio/ogg',
29 // Archives
30 'application/zip',
31 'application/x-rar-compressed',
32 'application/x-7z-compressed',
33 // Code/Text
34 'application/json',
35 'text/javascript',
36 'text/html',
37 'text/css',
38 'application/xml'
39]; // Empty array means all types allowed \ No newline at end of file