// File: monorepo/web/maplefile-backend/src/pages/User/FileManager/Files/FileUpload.jsx // UIX version - Theme-aware File Upload with Layout import React, { useState, useCallback, useEffect, useRef, useMemo, } from "react"; import { useNavigate, useLocation, useSearchParams } from "react-router"; import { useServices } from "../../../../services/Services"; import withPasswordProtection from "../../../../hocs/withPasswordProtection"; import Layout from "../../../../components/Layout/Layout"; import { Button, Alert, useUIXTheme, Breadcrumb, Card, Select, Checkbox, } from "../../../../components/UIX"; import { CloudArrowUpIcon, FolderIcon, ArrowLeftIcon, DocumentIcon, XMarkIcon, PhotoIcon, FilmIcon, MusicalNoteIcon, DocumentTextIcon, ArrowUpTrayIcon, ExclamationTriangleIcon, CheckCircleIcon, HomeIcon, TagIcon, } from "@heroicons/react/24/outline"; import TagSelector from "../../../../components/UIX/TagSelector/TagSelector"; const FileUpload = () => { const navigate = useNavigate(); const location = useLocation(); const [searchParams] = useSearchParams(); const { getThemeClasses } = useUIXTheme(); const fileInputRef = useRef(null); const isMountedRef = useRef(true); const { createFileManager, createCollectionManager, listCollectionManager, authManager, dashboardManager, } = useServices(); // Note: Tags are now embedded at file creation time via the API, // so we don't need fileTagManager for post-upload assignment const preSelectedCollectionId = searchParams.get("collection"); const preSelectedCollectionInfo = location.state?.preSelectedCollection; const [fileManager, setFileManager] = useState(null); const [files, setFiles] = useState([]); const [selectedCollection, setSelectedCollection] = useState( preSelectedCollectionId || "", ); const [availableCollections, setAvailableCollections] = useState([]); const [isLoadingCollections, setIsLoadingCollections] = useState(true); // Start with loading true const [isCollectionsInitialized, setIsCollectionsInitialized] = useState(false); const [isUploading, setIsUploading] = useState(false); const [isDragging, setIsDragging] = useState(false); const [error, setError] = useState(""); const [success, setSuccess] = useState(""); const [gdprConsent, setGdprConsent] = useState(false); const [uploadProgress, setUploadProgress] = useState({}); // Track progress per file const [selectedTagIds, setSelectedTagIds] = useState([]); // Tags to apply to all uploaded files useEffect(() => { isMountedRef.current = true; return () => { isMountedRef.current = false; }; }, []); useEffect(() => { const initializeManager = async () => { if (!authManager.isAuthenticated()) return; try { const { default: CreateFileManager } = await import("../../../../services/Manager/File/CreateFileManager.js"); const manager = new CreateFileManager(authManager); await manager.initialize(); if (isMountedRef.current) { setFileManager(manager); } } catch (err) { if (isMountedRef.current) { setError("Could not initialize upload service"); } } }; initializeManager(); }, [authManager]); const loadCollections = useCallback(async () => { if (!listCollectionManager) return; setIsLoadingCollections(true); try { const result = await listCollectionManager.listCollections(false); if (isMountedRef.current) { if (result.collections && result.collections.length > 0) { setAvailableCollections(result.collections); } setIsCollectionsInitialized(true); } } catch (err) { if (isMountedRef.current) { if (import.meta.env.DEV) { console.error("[FileUpload] Could not load folders:", err); } setError("Could not load folders"); setIsCollectionsInitialized(true); } } finally { if (isMountedRef.current) { setIsLoadingCollections(false); } } }, [listCollectionManager]); // Load collections when manager becomes available useEffect(() => { if (createCollectionManager && listCollectionManager && !isCollectionsInitialized) { loadCollections(); } }, [createCollectionManager, listCollectionManager, isCollectionsInitialized, loadCollections]); const handleDragOver = useCallback((e) => { e.preventDefault(); setIsDragging(true); }, []); const handleDragLeave = useCallback((e) => { e.preventDefault(); setIsDragging(false); }, []); // Helper function to generate file fingerprint for deduplication const getFileFingerprint = useCallback((file) => { return `${file.name}|${file.size}|${file.type}|${file.lastModified || ""}`; }, []); const addFiles = useCallback( (newFiles) => { const maxSize = 5 * 1024 * 1024 * 1024; // 5GB // Blocked file extensions for security const BLOCKED_EXTENSIONS = [ ".exe", ".bat", ".sh", ".cmd", ".com", ".scr", ".vbs", ".ps1", ".app", ".deb", ".rpm", ".dmg", ".pkg", ".msi", ".apk", ]; // Get fingerprints of existing files for deduplication const existingFingerprints = new Set( files.map((f) => getFileFingerprint(f.file)), ); const validFiles = newFiles.filter((file) => { // Check for duplicates const fingerprint = getFileFingerprint(file); if (existingFingerprints.has(fingerprint)) { setError(`${file.name} is already in the upload list`); return false; } // Check file size if (file.size > maxSize) { setError(`${file.name} is too large (max 5GB)`); return false; } // Check file extension for security const extension = file.name .toLowerCase() .substring(file.name.lastIndexOf(".")); if (BLOCKED_EXTENSIONS.includes(extension)) { setError(`${file.name} is not allowed (blocked file type)`); return false; } return true; }); const fileObjects = validFiles.map((file) => ({ id: crypto.randomUUID(), // Cryptographically secure random ID file, name: file.name, size: file.size, type: file.type, status: "pending", })); setFiles((prev) => [...prev, ...fileObjects]); if (fileInputRef.current) fileInputRef.current.value = ""; }, [files, getFileFingerprint], ); const handleDrop = useCallback( (e) => { e.preventDefault(); setIsDragging(false); const droppedFiles = Array.from(e.dataTransfer.files); addFiles(droppedFiles); }, [addFiles], ); const handleFileSelect = useCallback( (e) => { const selectedFiles = Array.from(e.target.files); addFiles(selectedFiles); }, [addFiles], ); const removeFile = useCallback((fileId) => { setFiles((files) => files.filter((f) => f.id !== fileId)); }, []); const formatFileSize = useCallback((bytes) => { if (!bytes) return "0 B"; const sizes = ["B", "KB", "MB", "GB"]; const i = Math.floor(Math.log(bytes) / Math.log(1024)); return `${Math.round(bytes / Math.pow(1024, i))} ${sizes[i]}`; }, []); const getFileIcon = useCallback( (file) => { const iconClass = "h-5 w-5"; if (file.type.startsWith("image/")) { return ( ); } if (file.type.startsWith("video/")) { return ( ); } if (file.type.startsWith("audio/")) { return ( ); } if (file.type.includes("pdf")) { return ( ); } if (file.type.includes("text") || file.name.endsWith(".txt")) { return ( ); } return ( ); }, [getThemeClasses], ); const clearAllRelevantCaches = useCallback(async () => { if (import.meta.env.DEV) { console.log("[FileUpload] Starting comprehensive cache clearing..."); } try { // Clear ListCollectionManager cache if (listCollectionManager) { if (import.meta.env.DEV) { console.log("[FileUpload] Clearing ListCollectionManager cache"); } listCollectionManager.clearAllCache(); } // Clear dashboard cache if (dashboardManager) { if (import.meta.env.DEV) { console.log("[FileUpload] Clearing dashboard cache"); } dashboardManager.clearAllCaches(); } // Clear file-related caches from localStorage with secure filtering if (import.meta.env.DEV) { console.log("[FileUpload] Clearing file caches from localStorage"); } // Use exact prefixes for security (whitelist approach) const knownPrefixes = [ "mapleopentech_file_list_", "mapleopentech_file_cache_", "mapleopentech_dashboard_", "mapleopentech_collection_", ]; const fileListKeys = Object.keys(localStorage).filter((key) => knownPrefixes.some((prefix) => key.startsWith(prefix)), ); fileListKeys.forEach((key) => { try { localStorage.removeItem(key); if (import.meta.env.DEV) { console.log("[FileUpload] Removed cache key:", key); } } catch (error) { if (import.meta.env.DEV) { console.warn( `[FileUpload] Failed to remove cache key: ${key}`, error, ); } } }); // Safely access and validate window.mapleopentechServices const services = window.mapleopentechServices; if ( services && typeof services === "object" && !Array.isArray(services) && services.constructor === Object ) { if (import.meta.env.DEV) { console.log( "[FileUpload] Clearing services caches via window.mapleopentechServices", ); } // Safely clear collection manager cache if ( services.getCollectionManager && typeof services.getCollectionManager.clearAllCache === "function" ) { try { services.getCollectionManager.clearAllCache(); if (import.meta.env.DEV) { console.log("[FileUpload] GetCollectionManager cache cleared"); } } catch (error) { if (import.meta.env.DEV) { console.warn( "[FileUpload] Failed to clear GetCollectionManager cache", error, ); } } } // Safely clear file manager cache if ( services.getFileManager && typeof services.getFileManager.clearAllCaches === "function" ) { try { services.getFileManager.clearAllCaches(); if (import.meta.env.DEV) { console.log("[FileUpload] GetFileManager cache cleared"); } } catch (error) { if (import.meta.env.DEV) { console.warn( "[FileUpload] Failed to clear GetFileManager cache", error, ); } } } // Safely clear dashboard cache if ( services.dashboardManager && typeof services.dashboardManager.clearAllCaches === "function" ) { try { services.dashboardManager.clearAllCaches(); if (import.meta.env.DEV) { console.log("[FileUpload] DashboardManager cache cleared"); } } catch (error) { if (import.meta.env.DEV) { console.warn( "[FileUpload] Failed to clear DashboardManager cache", error, ); } } } } else if (import.meta.env.DEV) { console.warn( "[FileUpload] window.mapleopentechServices is not available or invalid", ); } if (import.meta.env.DEV) { console.log("[FileUpload] ✅ Comprehensive cache clearing completed"); } } catch (error) { if (import.meta.env.DEV) { console.warn("[FileUpload] ⚠️ Some cache clearing failed:", error); } } }, [listCollectionManager, dashboardManager]); const triggerDashboardRefresh = useCallback(() => { if (import.meta.env.DEV) { console.log("[FileUpload] Triggering dashboard refresh event"); } // Dispatch custom event for dashboard refresh const refreshEvent = new CustomEvent("dashboardRefresh", { detail: { reason: "file_upload_completed", timestamp: Date.now() }, }); window.dispatchEvent(refreshEvent); // Also store in localStorage to signal refresh across tabs const refreshSignal = { event: "file_upload_completed", timestamp: Date.now(), collection: selectedCollection, fileCount: files.length, }; localStorage.setItem( "mapleopentech_upload_refresh_signal", JSON.stringify(refreshSignal), ); // Remove the signal after a short delay setTimeout(() => { localStorage.removeItem("mapleopentech_upload_refresh_signal"); }, 5000); }, [selectedCollection, files.length]); // Helper function to upload a single file with progress tracking const uploadSingleFile = useCallback( async (fileObj, tagIds = []) => { try { // Set initial progress setUploadProgress((prev) => ({ ...prev, [fileObj.id]: 0 })); setFiles((prev) => prev.map((f) => f.id === fileObj.id ? { ...f, status: "uploading" } : f, ), ); // Simulate progress for better UX (since actual progress requires backend support) const progressInterval = setInterval(() => { setUploadProgress((prev) => { const currentProgress = prev[fileObj.id] || 0; if (currentProgress < 90) { return { ...prev, [fileObj.id]: currentProgress + 10 }; } return prev; }); }, 200); // Pass tagIds to createAndUploadFileFromFile for embedding at creation time const uploadResult = await fileManager.createAndUploadFileFromFile( fileObj.file, selectedCollection, null, tagIds, // Tags are embedded at file creation time on the backend ); clearInterval(progressInterval); setUploadProgress((prev) => ({ ...prev, [fileObj.id]: 100 })); if (import.meta.env.DEV && tagIds.length > 0) { console.log( `[FileUpload] File ${uploadResult?.file?.id} created with ${tagIds.length} embedded tags`, ); } setFiles((prev) => prev.map((f) => f.id === fileObj.id ? { ...f, status: "complete" } : f, ), ); return { success: true, fileId: fileObj.id, uploadedFileId: uploadResult?.file?.id, }; } catch (err) { setUploadProgress((prev) => { const newProgress = { ...prev }; delete newProgress[fileObj.id]; return newProgress; }); setFiles((prev) => prev.map((f) => f.id === fileObj.id ? { ...f, status: "error", error: err.message } : f, ), ); return { success: false, fileId: fileObj.id, error: err.message }; } }, [fileManager, selectedCollection], ); const startUpload = useCallback(async () => { if (!fileManager || !selectedCollection || files.length === 0) { setError("Please select files and a folder"); return; } setIsUploading(true); setError(""); setSuccess(""); const filesToUpload = files.filter((f) => f.status === "pending"); // Upload files concurrently in batches of 3 const CONCURRENT_UPLOADS = 3; const results = []; for (let i = 0; i < filesToUpload.length; i += CONCURRENT_UPLOADS) { const batch = filesToUpload.slice(i, i + CONCURRENT_UPLOADS); const batchResults = await Promise.all( batch.map((fileObj) => uploadSingleFile(fileObj, selectedTagIds)), ); results.push(...batchResults); } const successCount = results.filter((r) => r.success).length; setIsUploading(false); if (successCount === filesToUpload.length && successCount > 0) { setSuccess("All files uploaded successfully!"); setGdprConsent(false); // Reset consent for next upload if (preSelectedCollectionId) { if (import.meta.env.DEV) { console.log( `[FileUpload] 📁 Upload successful - preparing redirect for ${successCount} files`, ); } await clearAllRelevantCaches(); triggerDashboardRefresh(); setTimeout(() => { if (import.meta.env.DEV) { console.log( `[FileUpload] 🔄 Redirecting to collection with comprehensive refresh state`, ); } navigate(`/file-manager/collections/${preSelectedCollectionId}`, { state: { refresh: true, refreshFiles: true, forceFileRefresh: true, uploadedFileCount: successCount, uploadTimestamp: Date.now(), cacheCleared: true, refreshDashboard: true, }, replace: false, }); }, 1500); } else { setTimeout(async () => { await clearAllRelevantCaches(); triggerDashboardRefresh(); navigate("/dashboard", { state: { refreshDashboard: true, uploadCompleted: true, uploadedFileCount: successCount, uploadTimestamp: Date.now(), }, replace: false, }); }, 1500); } } else { if (successCount > 0) { setSuccess(`${successCount} of ${filesToUpload.length} files uploaded`); await clearAllRelevantCaches(); triggerDashboardRefresh(); } } }, [ fileManager, selectedCollection, files, preSelectedCollectionId, clearAllRelevantCaches, triggerDashboardRefresh, navigate, uploadSingleFile, selectedTagIds, ]); const getBackUrl = useCallback(() => { return preSelectedCollectionId ? `/file-manager/collections/${preSelectedCollectionId}` : "/file-manager"; }, [preSelectedCollectionId]); // Memoized file computations for performance const fileStats = useMemo(() => { const totalSize = files.reduce((sum, file) => sum + file.size, 0); const pendingFiles = files.filter((f) => f.status === "pending"); const uploadingFiles = files.filter((f) => f.status === "uploading"); const completedFiles = files.filter((f) => f.status === "complete"); const errorFiles = files.filter((f) => f.status === "error"); return { totalSize, pendingFiles, uploadingFiles, completedFiles, errorFiles, }; }, [files]); const { totalSize, pendingFiles, uploadingFiles, completedFiles, errorFiles, } = fileStats; // Memoize collection options for Select component const collectionOptions = useMemo(() => { return availableCollections.map((collection) => ({ value: collection.id, label: collection.name || "Unnamed Folder", })); }, [availableCollections]); // Build breadcrumb const breadcrumbItems = [ { label: "My Files", to: "/file-manager", icon: HomeIcon, }, ]; if (preSelectedCollectionInfo?.name) { breadcrumbItems.push({ label: preSelectedCollectionInfo.name, to: `/file-manager/collections/${preSelectedCollectionId}`, }); } breadcrumbItems.push({ label: "Upload Files", isActive: true, }); return (
{/* Breadcrumb Navigation */} {/* Main Card */} {/* Header with icon, title, and back button */}
{/* Icon */}
{/* Title and subtitle */}

Upload Files

Drag and drop or click to select files

{/* Back button */}
{/* Messages */}
{error && ( {error} )} {success && ( {success} )}
{/* Content */}
{/* No Folders - Show create folder prompt instead of upload UI */} {!preSelectedCollectionId && isCollectionsInitialized && !isLoadingCollections && availableCollections.length === 0 ? (

No folders yet

You need to create a folder before you can upload files. Folders help you organize your encrypted files.

) : ( <>
{/* Upload Area */}
{/* Collection Selector with Upload Button (only show if no pre-selected collection) */} {!preSelectedCollectionId && (
{/* Files List */} {files.length > 0 && (

{files.length} file{files.length !== 1 ? "s" : ""}{" "} selected

Total: {formatFileSize(totalSize)}
{files.map((file) => (
{getFileIcon(file)}

{file.name}

{formatFileSize(file.size)}
{/* Progress bar for uploading files */} {file.status === "uploading" && uploadProgress[file.id] !== undefined && (

{uploadProgress[file.id]}% uploaded

)} {file.status === "error" && file.error && (

{file.error}

)}
{file.status === "pending" && ( )} {file.status === "uploading" && (
)} {file.status === "complete" && (
)} {file.status === "error" && (
)}
))}
)}
{/* Sidebar - only show as separate column when collection is pre-selected */} {preSelectedCollectionId && (
{files.length > 0 && (

Upload Status

Pending {pendingFiles.length}
Uploading {uploadingFiles.length}
Completed {completedFiles.length}
{errorFiles.length > 0 && (
Errors {errorFiles.length}
)}
{completedFiles.length + errorFiles.length === files.length && files.length > 0 && !isUploading && pendingFiles.length === 0 && (

All uploads processed!

)}
)} {/* Tag Selection */} {files.length > 0 && pendingFiles.length > 0 && (

Add Tags

Tags will be applied to all uploaded files

)} {/* Consent & Security Notice */} {files.length > 0 && pendingFiles.length > 0 && (

I consent to encrypted upload

Files are encrypted end-to-end and only you and those you share with can decrypt them.

)} {/* Upload button in sidebar */}
)}
{/* Options section - show below content when no pre-selected collection */} {!preSelectedCollectionId && files.length > 0 && (
{/* Upload Status */}

Upload Status

Pending {pendingFiles.length}
Uploading {uploadingFiles.length}
Completed {completedFiles.length}
{errorFiles.length > 0 && (
Errors {errorFiles.length}
)}
{completedFiles.length + errorFiles.length === files.length && files.length > 0 && !isUploading && pendingFiles.length === 0 && (

All uploads processed!

)}
{/* Tag Selection */} {pendingFiles.length > 0 && (

Add Tags

Tags will be applied to all uploaded files

)} {/* Consent & Security Notice */} {pendingFiles.length > 0 && (

I consent to encrypted upload

Files are encrypted end-to-end and only you and those you share with can decrypt them.

)}
)} )}
); }; export default withPasswordProtection(FileUpload);