diff --git a/frontend/public/locales/en-GB/translation.json b/frontend/public/locales/en-GB/translation.json index 843d0b69d..783b66905 100644 --- a/frontend/public/locales/en-GB/translation.json +++ b/frontend/public/locales/en-GB/translation.json @@ -261,7 +261,10 @@ "description": "Configure general application preferences.", "autoUnzip": "Auto-unzip API responses", "autoUnzipDescription": "Automatically extract files from ZIP responses", - "autoUnzipTooltip": "Automatically extract ZIP files returned from API operations. Disable to keep ZIP files intact. This does not affect automation workflows." + "autoUnzipTooltip": "Automatically extract ZIP files returned from API operations. Disable to keep ZIP files intact. This does not affect automation workflows.", + "autoUnzipFileLimit": "Auto-unzip file limit", + "autoUnzipFileLimitDescription": "Maximum number of files to extract from ZIP", + "autoUnzipFileLimitTooltip": "Only unzip if the ZIP contains this many files or fewer. Set higher to extract larger ZIPs." }, "hotkeys": { "title": "Keyboard Shortcuts", diff --git a/frontend/src/components/shared/config/configSections/GeneralSection.tsx b/frontend/src/components/shared/config/configSections/GeneralSection.tsx index 059e7674f..07f71c484 100644 --- a/frontend/src/components/shared/config/configSections/GeneralSection.tsx +++ b/frontend/src/components/shared/config/configSections/GeneralSection.tsx @@ -1,11 +1,19 @@ -import React from 'react'; -import { Paper, Stack, Switch, Text, Tooltip } from '@mantine/core'; +import React, { useState, useEffect } from 'react'; +import { Paper, Stack, Switch, Text, Tooltip, NumberInput } from '@mantine/core'; import { useTranslation } from 'react-i18next'; import { usePreferences } from '../../../../contexts/PreferencesContext'; +const DEFAULT_AUTO_UNZIP_FILE_LIMIT = 4; + const GeneralSection: React.FC = () => { const { t } = useTranslation(); const { preferences, updatePreference } = usePreferences(); + const [fileLimitInput, setFileLimitInput] = useState(preferences.autoUnzipFileLimit); + + // Sync local state with preference changes + useEffect(() => { + setFileLimitInput(preferences.autoUnzipFileLimit); + }, [preferences.autoUnzipFileLimit]); return ( @@ -39,6 +47,39 @@ const GeneralSection: React.FC = () => { /> + + +
+
+ + {t('settings.general.autoUnzipFileLimit', 'Auto-unzip file limit')} + + + {t('settings.general.autoUnzipFileLimitDescription', 'Maximum number of files to extract from ZIP')} + +
+ { + const numValue = Number(fileLimitInput); + const finalValue = (!fileLimitInput || isNaN(numValue) || numValue < 1 || numValue > 100) ? DEFAULT_AUTO_UNZIP_FILE_LIMIT : numValue; + setFileLimitInput(finalValue); + updatePreference('autoUnzipFileLimit', finalValue); + }} + min={1} + max={100} + step={1} + disabled={!preferences.autoUnzip} + style={{ width: 90 }} + /> +
+
diff --git a/frontend/src/hooks/tools/shared/useToolResources.ts b/frontend/src/hooks/tools/shared/useToolResources.ts index 219bda255..366730885 100644 --- a/frontend/src/hooks/tools/shared/useToolResources.ts +++ b/frontend/src/hooks/tools/shared/useToolResources.ts @@ -85,8 +85,15 @@ export const useToolResources = () => { const extractZipFiles = useCallback(async (zipBlob: Blob, skipAutoUnzip = false): Promise => { try { - // Check if auto-unzip is disabled (unless explicitly skipped like in automation) - if (!skipAutoUnzip && !preferences.autoUnzip) { + // Check if we should extract based on preferences + const shouldExtract = await zipFileService.shouldUnzip( + zipBlob, + preferences.autoUnzip, + preferences.autoUnzipFileLimit, + skipAutoUnzip + ); + + if (!shouldExtract) { return [new File([zipBlob], 'result.zip', { type: 'application/zip' })]; } @@ -97,12 +104,19 @@ export const useToolResources = () => { console.error('useToolResources.extractZipFiles - Error:', error); return []; } - }, [preferences.autoUnzip]); + }, [preferences.autoUnzip, preferences.autoUnzipFileLimit]); const extractAllZipFiles = useCallback(async (zipBlob: Blob, skipAutoUnzip = false): Promise => { try { - // Check if auto-unzip is disabled (unless explicitly skipped like in automation) - if (!skipAutoUnzip && !preferences.autoUnzip) { + // Check if we should extract based on preferences + const shouldExtract = await zipFileService.shouldUnzip( + zipBlob, + preferences.autoUnzip, + preferences.autoUnzipFileLimit, + skipAutoUnzip + ); + + if (!shouldExtract) { return [new File([zipBlob], 'result.zip', { type: 'application/zip' })]; } @@ -113,7 +127,7 @@ export const useToolResources = () => { console.error('useToolResources.extractAllZipFiles - Error:', error); return []; } - }, [preferences.autoUnzip]); + }, [preferences.autoUnzip, preferences.autoUnzipFileLimit]); const createDownloadInfo = useCallback(async ( files: File[], diff --git a/frontend/src/services/preferencesService.ts b/frontend/src/services/preferencesService.ts index 94c17a66b..b7c22094e 100644 --- a/frontend/src/services/preferencesService.ts +++ b/frontend/src/services/preferencesService.ts @@ -2,10 +2,12 @@ import { indexedDBManager, DATABASE_CONFIGS } from './indexedDBManager'; export interface UserPreferences { autoUnzip: boolean; + autoUnzipFileLimit: number; } export const DEFAULT_PREFERENCES: UserPreferences = { autoUnzip: true, + autoUnzipFileLimit: 4, }; class PreferencesService { diff --git a/frontend/src/services/zipFileService.ts b/frontend/src/services/zipFileService.ts index 9803c96ed..45ec39219 100644 --- a/frontend/src/services/zipFileService.ts +++ b/frontend/src/services/zipFileService.ts @@ -324,6 +324,48 @@ export class ZipFileService { return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; } + /** + * Determine if a ZIP file should be extracted based on user preferences + * + * @param zipBlob - The ZIP file to check + * @param autoUnzip - User preference for auto-unzipping + * @param autoUnzipFileLimit - Maximum number of files to auto-extract + * @param skipAutoUnzip - Bypass preference check (for automation) + * @returns true if the ZIP should be extracted, false otherwise + */ + async shouldUnzip( + zipBlob: Blob | File, + autoUnzip: boolean, + autoUnzipFileLimit: number, + skipAutoUnzip: boolean = false + ): Promise { + try { + // Automation always extracts + if (skipAutoUnzip) { + return true; + } + + // Check if auto-unzip is enabled + if (!autoUnzip) { + return false; + } + + // Load ZIP and count files + const zip = new JSZip(); + const zipContents = await zip.loadAsync(zipBlob); + + // Count non-directory entries + const fileCount = Object.values(zipContents.files).filter(entry => !entry.dir).length; + + // Only extract if within limit + return fileCount <= autoUnzipFileLimit; + } catch (error) { + console.error('Error checking shouldUnzip:', error); + // On error, default to not extracting (safer) + return false; + } + } + /** * Extract all files from a ZIP archive (not limited to PDFs) */