From f69ca010b80703389fffe75fc6dca907e53df74d Mon Sep 17 00:00:00 2001 From: diogo464 Date: Mon, 11 Aug 2025 13:40:27 +0100 Subject: basic file upload --- frontend/app/api/upload/route.ts | 127 ++++++++++++++++ frontend/app/drive/[...path]/page.tsx | 10 +- frontend/app/drive/page.tsx | 10 ++ frontend/components/FileUpload.tsx | 262 ++++++++++++++++++++++++++++++++++ frontend/lib/constants.ts | 39 +++++ 5 files changed, 447 insertions(+), 1 deletion(-) create mode 100644 frontend/app/api/upload/route.ts create mode 100644 frontend/components/FileUpload.tsx create mode 100644 frontend/lib/constants.ts (limited to 'frontend') 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 @@ +import { NextRequest, NextResponse } from 'next/server' +import { writeFile, unlink } from 'fs/promises' +import { tmpdir } from 'os' +import { join } from 'path' +import { randomUUID } from 'crypto' +import { Auth_get_user, Auth_user_can_upload } from '@/lib/auth' +import { Drive_import } from '@/lib/drive' +import { UPLOAD_MAX_FILE_SIZE, UPLOAD_MAX_FILES } from '@/lib/constants' +import { revalidatePath } from 'next/cache' + +export async function POST(request: NextRequest) { + try { + // Check user authentication and permissions + const user = await Auth_get_user() + if (!user.isLoggedIn) { + return NextResponse.json({ error: 'User not authenticated' }, { status: 401 }) + } + + if (!Auth_user_can_upload(user)) { + return NextResponse.json({ error: 'User does not have upload permissions' }, { status: 403 }) + } + + // Parse form data + const formData = await request.formData() + const files = formData.getAll('files') as File[] + const targetPath = formData.get('targetPath') as string || '' + + // Validate files + if (!files || files.length === 0) { + return NextResponse.json({ error: 'No files provided' }, { status: 400 }) + } + + if (files.length > UPLOAD_MAX_FILES) { + return NextResponse.json({ + error: `Too many files. Maximum ${UPLOAD_MAX_FILES} files allowed` + }, { status: 400 }) + } + + // Validate each file + for (const file of files) { + if (file.size > UPLOAD_MAX_FILE_SIZE) { + return NextResponse.json({ + error: `File '${file.name}' exceeds maximum size of ${UPLOAD_MAX_FILE_SIZE / (1024 * 1024)}MB` + }, { status: 400 }) + } + } + + const uploadResults = [] + const tempFiles: string[] = [] + + try { + // Process each file + for (const file of files) { + // Create temporary file + const tempFileName = `${randomUUID()}-${file.name}` + const tempFilePath = join(tmpdir(), tempFileName) + tempFiles.push(tempFilePath) + + // Save file to temporary location + const bytes = await file.arrayBuffer() + const buffer = Buffer.from(bytes) + await writeFile(tempFilePath, buffer) + + // Determine target drive path + const driveFilePath = targetPath ? `${targetPath}/${file.name}` : `/${file.name}` + + try { + // Import file using Drive_import + await Drive_import(tempFilePath, driveFilePath, user.email) + uploadResults.push({ + filename: file.name, + success: true, + message: 'File uploaded successfully' + }) + } catch (error) { + console.error(`Failed to import file ${file.name}:`, error) + uploadResults.push({ + filename: file.name, + success: false, + message: error instanceof Error ? error.message : 'Unknown error during import' + }) + } + } + + // Clean up temporary files + for (const tempFile of tempFiles) { + try { + await unlink(tempFile) + } catch (error) { + console.error(`Failed to delete temp file ${tempFile}:`, error) + } + } + + // Revalidate the target path to refresh the directory listing + revalidatePath(`/drive${targetPath}`) + revalidatePath('/drive') + + // Check if any uploads succeeded + const successfulUploads = uploadResults.filter(result => result.success) + const failedUploads = uploadResults.filter(result => !result.success) + + return NextResponse.json({ + success: true, + message: `${successfulUploads.length} files uploaded successfully${failedUploads.length > 0 ? `, ${failedUploads.length} failed` : ''}`, + results: uploadResults + }) + + } catch (error) { + // Clean up temporary files on error + for (const tempFile of tempFiles) { + try { + await unlink(tempFile) + } catch (cleanupError) { + console.error(`Failed to delete temp file during cleanup ${tempFile}:`, cleanupError) + } + } + throw error + } + + } catch (error) { + console.error('Upload error:', error) + return NextResponse.json( + { error: error instanceof Error ? error.message : 'Internal server error' }, + { status: 500 } + ) + } +} \ 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" import { formatSize } from "@/lib/utils" import Link from "next/link" import { cookies } from 'next/headers'; -import { Auth_get_user } from "@/lib/auth"; +import { Auth_get_user, Auth_user_can_upload } from "@/lib/auth"; +import FileUpload from "@/components/FileUpload" interface DrivePageProps { params: Promise<{ @@ -130,6 +131,13 @@ export default async function DrivePage({ params }: DrivePageProps) { + + {/* File Upload Component */} + {Auth_user_can_upload(user) && ( + + )} ) 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 @@ import { Drive_ls, Drive_basename } from "@/lib/drive" import { formatSize } from "@/lib/utils" +import { Auth_get_user, Auth_user_can_upload } from "@/lib/auth" import Link from "next/link" +import FileUpload from "@/components/FileUpload" export default async function DrivePage() { + const user = await Auth_get_user() const entries = await Drive_ls("", false) // Sort entries: directories first, then files, both alphabetically @@ -88,6 +91,13 @@ export default async function DrivePage() { + + {/* File Upload Component */} + {Auth_user_can_upload(user) && ( + + )} ) 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 @@ +'use client' + +import { useState, useRef } from 'react' +import { UPLOAD_MAX_FILES, UPLOAD_MAX_FILE_SIZE } from '@/lib/constants' + +// Client-side file validation function +function validateFile(file: File): { allowed: boolean; reason?: string } { + if (file.size > UPLOAD_MAX_FILE_SIZE) { + return { allowed: false, reason: `File size exceeds ${UPLOAD_MAX_FILE_SIZE / (1024 * 1024)}MB limit` }; + } + + return { allowed: true }; +} + +interface FileUploadProps { + targetPath: string + onUploadComplete?: () => void +} + +interface UploadResult { + filename: string + success: boolean + message: string +} + +export default function FileUpload({ targetPath, onUploadComplete }: FileUploadProps) { + const [isDragOver, setIsDragOver] = useState(false) + const [isUploading, setIsUploading] = useState(false) + const [selectedFiles, setSelectedFiles] = useState([]) + const [uploadResults, setUploadResults] = useState([]) + const [showResults, setShowResults] = useState(false) + const fileInputRef = useRef(null) + + const handleFileSelect = (files: FileList) => { + const fileArray = Array.from(files) + + // Validate file count + if (fileArray.length > UPLOAD_MAX_FILES) { + alert(`Too many files selected. Maximum ${UPLOAD_MAX_FILES} files allowed.`) + return + } + + // Validate each file + const validFiles: File[] = [] + for (const file of fileArray) { + const validation = validateFile(file) + if (!validation.allowed) { + alert(`File '${file.name}': ${validation.reason}`) + continue + } + validFiles.push(file) + } + + setSelectedFiles(validFiles) + setUploadResults([]) + setShowResults(false) + } + + const handleDragOver = (e: React.DragEvent) => { + e.preventDefault() + setIsDragOver(true) + } + + const handleDragLeave = (e: React.DragEvent) => { + e.preventDefault() + setIsDragOver(false) + } + + const handleDrop = (e: React.DragEvent) => { + e.preventDefault() + setIsDragOver(false) + + if (e.dataTransfer.files) { + handleFileSelect(e.dataTransfer.files) + } + } + + const handleFileInputChange = (e: React.ChangeEvent) => { + if (e.target.files) { + handleFileSelect(e.target.files) + } + } + + const handleUpload = async () => { + if (selectedFiles.length === 0) return + + setIsUploading(true) + setUploadResults([]) + + try { + const formData = new FormData() + selectedFiles.forEach(file => { + formData.append('files', file) + }) + formData.append('targetPath', targetPath) + + const response = await fetch('/api/upload', { + method: 'POST', + body: formData, + }) + + const result = await response.json() + + if (response.ok) { + setUploadResults(result.results || []) + setShowResults(true) + setSelectedFiles([]) + + // Clear file input + if (fileInputRef.current) { + fileInputRef.current.value = '' + } + + // Refresh the page after successful upload + setTimeout(() => { + window.location.reload() + }, 1000) + } else { + alert(`Upload failed: ${result.error}`) + } + } catch (error) { + console.error('Upload error:', error) + alert('Upload failed: Network error') + } finally { + setIsUploading(false) + } + } + + const removeFile = (index: number) => { + setSelectedFiles(prev => prev.filter((_, i) => i !== index)) + } + + const clearResults = () => { + setShowResults(false) + setUploadResults([]) + } + + return ( +
+

Upload Files

+ + {/* Upload Results */} + {showResults && uploadResults.length > 0 && ( +
+
+

Upload Results

+ +
+
+ {uploadResults.map((result, index) => ( +
+ {result.success ? '✓' : '✗'} + {result.filename}: + {result.message} +
+ ))} +
+
+ )} + + {/* File Drop Zone */} +
+
+
📁
+
+

Drop files here or click to browse

+

+ Maximum {UPLOAD_MAX_FILES} files, {UPLOAD_MAX_FILE_SIZE / (1024 * 1024)}MB each +

+
+ + +
+
+ + {/* Selected Files */} + {selectedFiles.length > 0 && ( +
+

+ Selected Files ({selectedFiles.length}) +

+
+ {selectedFiles.map((file, index) => ( +
+
+ 📄 +
+
+ {file.name} +
+
+ {(file.size / 1024 / 1024).toFixed(2)} MB +
+
+
+ +
+ ))} +
+ + +
+ )} +
+ ) +} \ 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 @@ +// Upload configuration constants +export const UPLOAD_MAX_FILE_SIZE = 100 * 1024 * 1024; // 100MB +export const UPLOAD_MAX_FILES = 10; // Maximum files per upload +export const UPLOAD_ALLOWED_TYPES = [ + // Documents + 'application/pdf', + 'application/msword', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'application/vnd.ms-excel', + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'application/vnd.ms-powerpoint', + 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + 'text/plain', + 'text/csv', + // Images + 'image/jpeg', + 'image/png', + 'image/gif', + 'image/webp', + 'image/svg+xml', + // Videos + 'video/mp4', + 'video/webm', + 'video/ogg', + // Audio + 'audio/mpeg', + 'audio/wav', + 'audio/ogg', + // Archives + 'application/zip', + 'application/x-rar-compressed', + 'application/x-7z-compressed', + // Code/Text + 'application/json', + 'text/javascript', + 'text/html', + 'text/css', + 'application/xml' +]; // Empty array means all types allowed \ No newline at end of file -- cgit