mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2026-01-14 20:11:17 +01:00
Unified upload file unzippage
This commit is contained in:
parent
851e8f9394
commit
3db8c016a8
@ -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 = ({
|
||||
<Box p="md">
|
||||
|
||||
|
||||
{activeStirlingFileStubs.length === 0 && !zipExtractionProgress.isExtracting ? (
|
||||
{activeStirlingFileStubs.length === 0 ? (
|
||||
<Center h="60vh">
|
||||
<Stack align="center" gap="md">
|
||||
<Text size="lg" c="dimmed">📁</Text>
|
||||
@ -411,43 +308,6 @@ const FileEditor = ({
|
||||
<Text size="sm" c="dimmed">Upload PDF files, ZIP archives, or load from storage to get started</Text>
|
||||
</Stack>
|
||||
</Center>
|
||||
) : activeStirlingFileStubs.length === 0 && zipExtractionProgress.isExtracting ? (
|
||||
<Box>
|
||||
<SkeletonLoader type="controls" />
|
||||
|
||||
{/* ZIP Extraction Progress */}
|
||||
{zipExtractionProgress.isExtracting && (
|
||||
<Box mb="md" p="sm" style={{ backgroundColor: 'var(--mantine-color-orange-0)', borderRadius: 8 }}>
|
||||
<Group justify="space-between" mb="xs">
|
||||
<Text size="sm" fw={500}>Extracting ZIP archive...</Text>
|
||||
<Text size="sm" c="dimmed">{Math.round(zipExtractionProgress.progress)}%</Text>
|
||||
</Group>
|
||||
<Text size="xs" c="dimmed" mb="xs">
|
||||
{zipExtractionProgress.currentFile || 'Processing files...'}
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed" mb="xs">
|
||||
{zipExtractionProgress.extractedCount} of {zipExtractionProgress.totalFiles} files extracted
|
||||
</Text>
|
||||
<div style={{
|
||||
width: '100%',
|
||||
height: '4px',
|
||||
backgroundColor: 'var(--mantine-color-gray-2)',
|
||||
borderRadius: '2px',
|
||||
overflow: 'hidden'
|
||||
}}>
|
||||
<div style={{
|
||||
width: `${Math.round(zipExtractionProgress.progress)}%`,
|
||||
height: '100%',
|
||||
backgroundColor: 'var(--mantine-color-orange-6)',
|
||||
transition: 'width 0.3s ease'
|
||||
}} />
|
||||
</div>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
|
||||
<SkeletonLoader type="fileGrid" count={6} />
|
||||
</Box>
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
|
||||
@ -79,8 +79,21 @@ function FileContextInner({
|
||||
};
|
||||
|
||||
// File operations using unified addFiles helper with persistence
|
||||
const addRawFiles = useCallback(async (files: File[], options?: { insertAfterPageId?: string; selectFiles?: boolean }): Promise<StirlingFile[]> => {
|
||||
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<StirlingFile[]> => {
|
||||
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) {
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user