From 3db8c016a8ae44ec735d95396477a75c45d8b88c Mon Sep 17 00:00:00 2001 From: Connor Yoh Date: Tue, 14 Oct 2025 10:26:11 +0100 Subject: [PATCH] Unified upload file unzippage --- .../src/components/fileEditor/FileEditor.tsx | 164 ++---------------- frontend/src/contexts/FileContext.tsx | 17 +- frontend/src/contexts/file/fileActions.ts | 57 ++++++ 3 files changed, 84 insertions(+), 154 deletions(-) diff --git a/frontend/src/components/fileEditor/FileEditor.tsx b/frontend/src/components/fileEditor/FileEditor.tsx index 28e754a56..856c55b83 100644 --- a/frontend/src/components/fileEditor/FileEditor.tsx +++ b/frontend/src/components/fileEditor/FileEditor.tsx @@ -1,6 +1,6 @@ import React, { useState, useCallback, useRef, useMemo, useEffect } from 'react'; import { - Text, Center, Box, LoadingOverlay, Stack, Group + Text, Center, Box, LoadingOverlay, Stack } from '@mantine/core'; import { Dropzone } from '@mantine/dropzone'; import { useFileSelection, useFileState, useFileManagement, useFileActions } from '../../contexts/FileContext'; @@ -10,7 +10,6 @@ import { detectFileExtension } from '../../utils/fileUtils'; import FileEditorThumbnail from './FileEditorThumbnail'; import AddFileCard from './AddFileCard'; import FilePickerModal from '../shared/FilePickerModal'; -import SkeletonLoader from '../shared/SkeletonLoader'; import { FileId, StirlingFile } from '../../types/fileContext'; import { alert } from '../toast'; import { downloadBlob } from '../../utils/downloadUtils'; @@ -68,19 +67,6 @@ const FileEditor = ({ } }, [toolMode]); const [showFilePickerModal, setShowFilePickerModal] = useState(false); - const [zipExtractionProgress, setZipExtractionProgress] = useState<{ - isExtracting: boolean; - currentFile: string; - progress: number; - extractedCount: number; - totalFiles: number; - }>({ - isExtracting: false, - currentFile: '', - progress: 0, - extractedCount: 0, - totalFiles: 0 - }); // Get selected file IDs from context (defensive programming) const contextSelectedIds = Array.isArray(selectedFileIds) ? selectedFileIds : []; @@ -92,115 +78,26 @@ const FileEditor = ({ const localSelectedIds = contextSelectedIds; // Process uploaded files using context + // ZIP extraction is now handled automatically in FileContext based on user preferences const handleFileUpload = useCallback(async (uploadedFiles: File[]) => { _setError(null); try { - const allExtractedFiles: File[] = []; - const errors: string[] = []; - - for (const file of uploadedFiles) { - if (file.type === 'application/pdf') { - // Handle PDF files normally - allExtractedFiles.push(file); - } else if (file.type === 'application/zip' || file.type === 'application/x-zip-compressed' || file.name.toLowerCase().endsWith('.zip')) { - // Handle ZIP files - extract all files except HTML - try { - // Check if ZIP contains HTML files - if so, don't extract - const containsHtml = await zipFileService.containsHtmlFiles(file); - - if (containsHtml) { - // HTML files should stay zipped - allExtractedFiles.push(file); - continue; - } - - // Validate ZIP file first - const validation = await zipFileService.validateZipFile(file); - - if (validation.isValid && validation.containsFiles) { - // ZIP contains files - extract them - setZipExtractionProgress({ - isExtracting: true, - currentFile: file.name, - progress: 0, - extractedCount: 0, - totalFiles: validation.fileCount - }); - - const extractionResult = await zipFileService.extractAllFiles(file, (progress) => { - setZipExtractionProgress({ - isExtracting: true, - currentFile: progress.currentFile, - progress: progress.progress, - extractedCount: progress.extractedCount, - totalFiles: progress.totalFiles - }); - }); - - // Reset extraction progress - setZipExtractionProgress({ - isExtracting: false, - currentFile: '', - progress: 0, - extractedCount: 0, - totalFiles: 0 - }); - - if (extractionResult.success) { - allExtractedFiles.push(...extractionResult.extractedFiles); - - if (extractionResult.errors.length > 0) { - errors.push(...extractionResult.errors); - } - } else { - errors.push(`Failed to extract ZIP file "${file.name}": ${extractionResult.errors.join(', ')}`); - } - } else { - // ZIP is empty or invalid - treat as regular file - allExtractedFiles.push(file); - } - } catch (zipError) { - errors.push(`Failed to process ZIP file "${file.name}": ${zipError instanceof Error ? zipError.message : 'Unknown error'}`); - setZipExtractionProgress({ - isExtracting: false, - currentFile: '', - progress: 0, - extractedCount: 0, - totalFiles: 0 - }); - } - } else { - allExtractedFiles.push(file); - } - } - - // Show any errors - if (errors.length > 0) { - showError(errors.join('\n')); - } - - // Process all extracted files - if (allExtractedFiles.length > 0) { - // Add files to context and select them automatically - await addFiles(allExtractedFiles, { selectFiles: true }); - showStatus(`Added ${allExtractedFiles.length} files`, 'success'); + if (uploadedFiles.length > 0) { + // FileContext will automatically handle ZIP extraction based on user preferences + // - Respects autoUnzip setting + // - Respects autoUnzipFileLimit + // - HTML ZIPs stay intact + // - Non-ZIP files pass through unchanged + await addFiles(uploadedFiles, { selectFiles: true }); + showStatus(`Added ${uploadedFiles.length} file(s)`, 'success'); } } catch (err) { const errorMessage = err instanceof Error ? err.message : 'Failed to process files'; showError(errorMessage); console.error('File processing error:', err); - - // Reset extraction progress on error - setZipExtractionProgress({ - isExtracting: false, - currentFile: '', - progress: 0, - extractedCount: 0, - totalFiles: 0 - }); } - }, [addFiles]); + }, [addFiles, showStatus, showError]); const toggleFile = useCallback((fileId: FileId) => { const currentSelectedIds = contextSelectedIdsRef.current; @@ -403,7 +300,7 @@ const FileEditor = ({ - {activeStirlingFileStubs.length === 0 && !zipExtractionProgress.isExtracting ? ( + {activeStirlingFileStubs.length === 0 ? (
📁 @@ -411,43 +308,6 @@ const FileEditor = ({ Upload PDF files, ZIP archives, or load from storage to get started
- ) : activeStirlingFileStubs.length === 0 && zipExtractionProgress.isExtracting ? ( - - - - {/* ZIP Extraction Progress */} - {zipExtractionProgress.isExtracting && ( - - - Extracting ZIP archive... - {Math.round(zipExtractionProgress.progress)}% - - - {zipExtractionProgress.currentFile || 'Processing files...'} - - - {zipExtractionProgress.extractedCount} of {zipExtractionProgress.totalFiles} files extracted - -
-
-
- - )} - - - - ) : (
=> { - const stirlingFiles = await addFiles({ files, ...options }, stateRef, filesRef, dispatch, lifecycleManager, enablePersistence); + const addRawFiles = useCallback(async (files: File[], options?: { insertAfterPageId?: string; selectFiles?: boolean; skipAutoUnzip?: boolean }): Promise => { + const stirlingFiles = await addFiles( + { + files, + ...options, + // For direct file uploads: ALWAYS unzip (except HTML ZIPs) + // skipAutoUnzip bypasses preference checks - HTML detection still applies + skipAutoUnzip: true + }, + stateRef, + filesRef, + dispatch, + lifecycleManager, + enablePersistence + ); // Auto-select the newly added files if requested if (options?.selectFiles && stirlingFiles.length > 0) { diff --git a/frontend/src/contexts/file/fileActions.ts b/frontend/src/contexts/file/fileActions.ts index 3f3ec07c7..c1b23d408 100644 --- a/frontend/src/contexts/file/fileActions.ts +++ b/frontend/src/contexts/file/fileActions.ts @@ -18,6 +18,7 @@ import { FileLifecycleManager } from './lifecycle'; import { buildQuickKeySet } from './fileSelectors'; import { StirlingFile } from '../../types/fileContext'; import { fileStorage } from '../../services/fileStorage'; +import { zipFileService } from '../../services/zipFileService'; const DEBUG = process.env.NODE_ENV === 'development'; /** @@ -172,6 +173,11 @@ interface AddFileOptions { // Auto-selection after adding selectFiles?: boolean; + + // Auto-unzip control + autoUnzip?: boolean; + autoUnzipFileLimit?: number; + skipAutoUnzip?: boolean; // When true: always unzip (except HTML). Used for file uploads. When false: respect autoUnzip/autoUnzipFileLimit preferences. Used for tool outputs. } /** @@ -198,7 +204,58 @@ export async function addFiles( const { files = [] } = options; if (DEBUG) console.log(`📄 addFiles(raw): Adding ${files.length} files with immediate thumbnail generation`); + // ZIP pre-processing: Extract ZIP files with configurable behavior + // - File uploads: skipAutoUnzip=true → always extract (except HTML) + // - Tool outputs: skipAutoUnzip=false → respect user preferences + const filesToProcess: File[] = []; + const autoUnzip = options.autoUnzip ?? true; // Default to true + const autoUnzipFileLimit = options.autoUnzipFileLimit ?? 4; // Default limit + const skipAutoUnzip = options.skipAutoUnzip ?? false; + for (const file of files) { + // Check if file is a ZIP + if (zipFileService.isZipFile(file)) { + try { + if (DEBUG) console.log(`📄 addFiles: Detected ZIP file: ${file.name}`); + + // Check if ZIP contains HTML files - if so, keep as ZIP + const containsHtml = await zipFileService.containsHtmlFiles(file); + if (containsHtml) { + if (DEBUG) console.log(`📄 addFiles: ZIP contains HTML, keeping as ZIP: ${file.name}`); + filesToProcess.push(file); + continue; + } + + // Apply extraction with preferences + const extractedFiles = await zipFileService.extractWithPreferences(file, { + autoUnzip, + autoUnzipFileLimit, + skipAutoUnzip + }); + + if (extractedFiles.length === 1 && extractedFiles[0] === file) { + // ZIP was not extracted (over limit or autoUnzip disabled) + if (DEBUG) console.log(`📄 addFiles: ZIP not extracted (preferences): ${file.name}`); + } else { + // ZIP was extracted + if (DEBUG) console.log(`📄 addFiles: Extracted ${extractedFiles.length} files from ZIP: ${file.name}`); + } + + filesToProcess.push(...extractedFiles); + } catch (error) { + console.error(`📄 addFiles: Failed to process ZIP file ${file.name}:`, error); + // On error, keep the ZIP file as-is + filesToProcess.push(file); + } + } else { + // Not a ZIP file, add as-is + filesToProcess.push(file); + } + } + + if (DEBUG) console.log(`📄 addFiles: After ZIP processing, ${filesToProcess.length} files to add`); + + for (const file of filesToProcess) { const quickKey = createQuickKey(file); // Soft deduplication: Check if file already exists by metadata