From 50b468d6e90bb606474bccdbd732a068da91cc8f Mon Sep 17 00:00:00 2001 From: diogo464 Date: Tue, 12 Aug 2025 15:45:20 +0100 Subject: Implement working upload functionality MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace dummy upload functions with real API calls to /api/fs endpoint - Add file validation (size limits, file count) with user-friendly error messages - Support both root directory uploads and folder-specific uploads - Add loading states and progress indication during uploads - Implement auto-refresh of file tree after successful uploads - Handle errors gracefully with toast notifications showing success/failure counts - Update file size limit to 4GB and maintain 10 file upload limit 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- frontend/file-drive.tsx | 247 +++++++++++++++++++++++++++++++++++++--------- frontend/lib/constants.ts | 4 +- 2 files changed, 202 insertions(+), 49 deletions(-) diff --git a/frontend/file-drive.tsx b/frontend/file-drive.tsx index 2784c1a..123f088 100644 --- a/frontend/file-drive.tsx +++ b/frontend/file-drive.tsx @@ -37,6 +37,7 @@ import { Checkbox } from "@/components/ui/checkbox" import { toast } from "@/hooks/use-toast" import HistoryView from "./history-view" import { DriveTreeResponse, DriveTreeNode } from "@/lib/drive_types" +import { UPLOAD_MAX_FILE_SIZE, UPLOAD_MAX_FILES } from "@/lib/constants" function formatFileSize(bytes: number): string { @@ -94,6 +95,7 @@ export default function FileDrive() { const [currentItem, setCurrentItem] = useState(null) const [newName, setNewName] = useState("") const fileInputRef = useRef(null) + const [uploading, setUploading] = useState(false) const [isLoggedIn, setIsLoggedIn] = useState(true) // Mock logged in state const [currentView, setCurrentView] = useState<"drive" | "history">("drive") @@ -103,6 +105,21 @@ export default function FileDrive() { const usedStorage = 0;//calculateTotalSize(files) const storagePercentage = (usedStorage / maxStorage) * 100 + // Function to refresh file tree + const refreshFileTree = async () => { + try { + const treeResponse = await fetchDriveTree() + setFiles(treeResponse.root) + } catch (err) { + console.error('Error refreshing file tree:', err) + toast({ + title: "Failed to refresh", + description: "Could not refresh file list after upload", + variant: "destructive" + }) + } + } + // Load drive data on component mount useEffect(() => { async function loadDriveData() { @@ -205,19 +222,92 @@ export default function FileDrive() { } } - const handleFileUpload = (event: React.ChangeEvent) => { + const handleFileUpload = async (event: React.ChangeEvent) => { const uploadedFiles = event.target.files - if (uploadedFiles) { - const newFiles = Array.from(uploadedFiles).map((file, index) => ({ - path: `/upload-${Date.now()}-${index}`, - name: file.name, - type: "file" as const, - lastmod: Math.floor(Date.now() / 1000), - blob: null, - size: file.size, - author: "Current User", - })) - setFiles([...files, ...newFiles]) + if (!uploadedFiles || uploadedFiles.length === 0) return + + // Validate file count + if (uploadedFiles.length > UPLOAD_MAX_FILES) { + toast({ + title: "Too many files", + description: `You can only upload up to ${UPLOAD_MAX_FILES} files at once`, + variant: "destructive" + }) + return + } + + // Validate file sizes + const oversizedFiles = Array.from(uploadedFiles).filter(file => file.size > UPLOAD_MAX_FILE_SIZE) + if (oversizedFiles.length > 0) { + toast({ + title: "Files too large", + description: `Maximum file size is ${formatFileSize(UPLOAD_MAX_FILE_SIZE)}. Found ${oversizedFiles.length} oversized file(s)`, + variant: "destructive" + }) + return + } + + setUploading(true) + let successCount = 0 + let errorCount = 0 + + try { + // Upload files sequentially to avoid overwhelming the server + for (const file of Array.from(uploadedFiles)) { + try { + const formData = new FormData() + formData.append('file', file) + + const response = await fetch(`/api/fs/${encodeURIComponent(file.name)}`, { + method: 'PUT', + headers: { + 'AUTH': '1' // Development auth header + }, + body: formData + }) + + if (!response.ok) { + const error = await response.json() + throw new Error(error.error || `Upload failed with status ${response.status}`) + } + + successCount++ + } catch (error) { + console.error(`Failed to upload ${file.name}:`, error) + errorCount++ + } + } + + // Show results + if (successCount > 0) { + toast({ + title: "Upload successful", + description: `${successCount} file(s) uploaded successfully${errorCount > 0 ? `, ${errorCount} failed` : ''}` + }) + + // Refresh the file tree + await refreshFileTree() + } + + if (errorCount > 0 && successCount === 0) { + toast({ + title: "Upload failed", + description: `All ${errorCount} file(s) failed to upload`, + variant: "destructive" + }) + } + + } catch (error) { + console.error('Upload error:', error) + toast({ + title: "Upload failed", + description: error instanceof Error ? error.message : 'Unknown error occurred', + variant: "destructive" + }) + } finally { + setUploading(false) + // Reset the input + event.target.value = '' } } @@ -250,45 +340,102 @@ export default function FileDrive() { // Could also redirect to logout endpoint } - const handleFolderUpload = (event: React.ChangeEvent, folderPath: string) => { + const handleFolderUpload = async (event: React.ChangeEvent, folderPath: string) => { const uploadedFiles = event.target.files - if (uploadedFiles) { - const newFiles = Array.from(uploadedFiles).map((file, index) => ({ - path: `${folderPath}/${file.name}`, - name: file.name, - type: "file" as const, - lastmod: Math.floor(Date.now() / 1000), - blob: null, - size: file.size, - author: "Current User", - })) - - // Add files to the specific folder - const addToFolder = (items: DriveTreeNode[]): DriveTreeNode[] => { - return items.map((item) => { - if (item.path === folderPath && item.type === "dir") { - return { - ...item, - children: [...(item.children || []), ...newFiles], - size: (item.size || 0) + newFiles.reduce((total, file) => total + file.size, 0), - } - } - if (item.children) { - return { ...item, children: addToFolder(item.children) } + if (!uploadedFiles || uploadedFiles.length === 0) { + setUploadToFolder(null) + return + } + + // Validate file count + if (uploadedFiles.length > UPLOAD_MAX_FILES) { + toast({ + title: "Too many files", + description: `You can only upload up to ${UPLOAD_MAX_FILES} files at once`, + variant: "destructive" + }) + setUploadToFolder(null) + return + } + + // Validate file sizes + const oversizedFiles = Array.from(uploadedFiles).filter(file => file.size > UPLOAD_MAX_FILE_SIZE) + if (oversizedFiles.length > 0) { + toast({ + title: "Files too large", + description: `Maximum file size is ${formatFileSize(UPLOAD_MAX_FILE_SIZE)}. Found ${oversizedFiles.length} oversized file(s)`, + variant: "destructive" + }) + setUploadToFolder(null) + return + } + + setUploading(true) + let successCount = 0 + let errorCount = 0 + + try { + // Upload files sequentially to the target folder + for (const file of Array.from(uploadedFiles)) { + try { + const formData = new FormData() + formData.append('file', file) + + // Construct the upload path (folder + filename) + const uploadPath = `${folderPath.replace(/^\//, '')}/${file.name}` + + const response = await fetch(`/api/fs/${encodeURIComponent(uploadPath)}`, { + method: 'PUT', + headers: { + 'AUTH': '1' // Development auth header + }, + body: formData + }) + + if (!response.ok) { + const error = await response.json() + throw new Error(error.error || `Upload failed with status ${response.status}`) } - return item + + successCount++ + } catch (error) { + console.error(`Failed to upload ${file.name} to ${folderPath}:`, error) + errorCount++ + } + } + + // Show results + if (successCount > 0) { + toast({ + title: "Upload successful", + description: `${successCount} file(s) uploaded to folder${errorCount > 0 ? `, ${errorCount} failed` : ''}` + }) + + // Refresh the file tree + await refreshFileTree() + } + + if (errorCount > 0 && successCount === 0) { + toast({ + title: "Upload failed", + description: `All ${errorCount} file(s) failed to upload to folder`, + variant: "destructive" }) } - setFiles(addToFolder(files)) + } catch (error) { + console.error('Folder upload error:', error) toast({ - title: "Files uploaded successfully", - description: `${newFiles.length} file(s) uploaded to folder`, + title: "Upload failed", + description: error instanceof Error ? error.message : 'Unknown error occurred', + variant: "destructive" }) + } finally { + setUploading(false) + // Reset the input + event.target.value = '' + setUploadToFolder(null) } - // Reset the input - event.target.value = "" - setUploadToFolder(null) } const openFolderUpload = (folderPath: string) => { @@ -352,9 +499,12 @@ export default function FileDrive() { {item.type === "dir" && ( <> - openFolderUpload(item.path)}> + openFolderUpload(item.path)} + disabled={uploading} + > - Upload to Folder + {uploading ? "Uploading..." : "Upload to Folder"} @@ -434,9 +584,12 @@ export default function FileDrive() {
{currentView === "drive" && ( - )} {isLoggedIn ? ( diff --git a/frontend/lib/constants.ts b/frontend/lib/constants.ts index 8f74dd1..173e90d 100644 --- a/frontend/lib/constants.ts +++ b/frontend/lib/constants.ts @@ -1,5 +1,5 @@ // Upload configuration constants -export const UPLOAD_MAX_FILE_SIZE = 100 * 1024 * 1024; // 100MB +export const UPLOAD_MAX_FILE_SIZE = 4096 * 1024 * 1024; // 100MB export const UPLOAD_MAX_FILES = 10; // Maximum files per upload export const UPLOAD_ALLOWED_TYPES = [ // Documents @@ -36,4 +36,4 @@ export const UPLOAD_ALLOWED_TYPES = [ 'text/html', 'text/css', 'application/xml' -]; // Empty array means all types allowed \ No newline at end of file +]; // Empty array means all types allowed -- cgit