From 243638d51362fc4ea06bb249d24f0b07689a0d30 Mon Sep 17 00:00:00 2001 From: James Date: Wed, 6 Aug 2025 14:44:41 +0100 Subject: [PATCH] Put sanitized files in workspace instead of download --- .../tools/sanitize/useSanitizeOperation.ts | 109 +++++++++++++++++- frontend/src/tools/Sanitize.tsx | 37 ++++-- 2 files changed, 129 insertions(+), 17 deletions(-) diff --git a/frontend/src/hooks/tools/sanitize/useSanitizeOperation.ts b/frontend/src/hooks/tools/sanitize/useSanitizeOperation.ts index 0d92f4c07..8dc003939 100644 --- a/frontend/src/hooks/tools/sanitize/useSanitizeOperation.ts +++ b/frontend/src/hooks/tools/sanitize/useSanitizeOperation.ts @@ -1,22 +1,99 @@ -import { useState, useCallback } from 'react'; +import { useState, useCallback, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; +import { useFileContext } from '../../../contexts/FileContext'; +import { FileOperation } from '../../../types/fileContext'; +import { generateThumbnailForFile } from '../../../utils/thumbnailUtils'; import { SanitizeParameters } from './useSanitizeParameters'; export const useSanitizeOperation = () => { const { t } = useTranslation(); + const { + recordOperation, + markOperationApplied, + markOperationFailed, + addFiles + } = useFileContext(); + const [isLoading, setIsLoading] = useState(false); const [errorMessage, setErrorMessage] = useState(null); const [downloadUrl, setDownloadUrl] = useState(null); const [status, setStatus] = useState(null); + const [files, setFiles] = useState([]); + const [thumbnails, setThumbnails] = useState([]); + const [isGeneratingThumbnails, setIsGeneratingThumbnails] = useState(false); + + const createOperation = useCallback(( + parameters: SanitizeParameters, + selectedFiles: File[] + ): { operation: FileOperation; operationId: string; fileId: string } => { + const operationId = `sanitize-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; + const fileId = selectedFiles[0].name; + + const operation: FileOperation = { + id: operationId, + type: 'sanitize', + timestamp: Date.now(), + fileIds: selectedFiles.map(f => f.name), + status: 'pending', + metadata: { + originalFileName: selectedFiles[0].name, + parameters: { + removeJavaScript: parameters.removeJavaScript, + removeEmbeddedFiles: parameters.removeEmbeddedFiles, + removeXMPMetadata: parameters.removeXMPMetadata, + removeMetadata: parameters.removeMetadata, + removeLinks: parameters.removeLinks, + removeFonts: parameters.removeFonts, + }, + fileSize: selectedFiles[0].size + } + }; + + return { operation, operationId, fileId }; + }, []); + + const processResults = useCallback(async (blob: Blob, filename: string) => { + try { + // Create sanitized file + const sanitizedFile = new File([blob], filename, { type: blob.type }); + + // Set local state for preview + setFiles([sanitizedFile]); + setThumbnails([]); + setIsGeneratingThumbnails(true); + + // Add sanitized file to FileContext for future use + await addFiles([sanitizedFile]); + + // Generate thumbnail for preview + try { + const thumbnail = await generateThumbnailForFile(sanitizedFile); + if (thumbnail) { + setThumbnails([thumbnail]); + } + } catch (error) { + console.warn(`Failed to generate thumbnail for ${filename}:`, error); + setThumbnails(['']); + } + + setIsGeneratingThumbnails(false); + } catch (error) { + console.warn('Failed to process sanitization result:', error); + } + }, [addFiles]); const executeOperation = useCallback(async ( parameters: SanitizeParameters, - selectedFiles: File[] + selectedFiles: File[], + generateSanitizedFileName: (originalFileName?: string) => string ) => { if (selectedFiles.length === 0) { throw new Error(t('error.noFilesSelected', 'No files selected')); } + const { operation, operationId, fileId } = createOperation(parameters, selectedFiles); + recordOperation(fileId, operation); + setIsLoading(true); setErrorMessage(null); setStatus(t('sanitize.processing', 'Sanitizing PDF...')); @@ -24,7 +101,7 @@ export const useSanitizeOperation = () => { try { const formData = new FormData(); formData.append('fileInput', selectedFiles[0]); - + // Add parameters formData.append('removeJavaScript', parameters.removeJavaScript.toString()); formData.append('removeEmbeddedFiles', parameters.removeEmbeddedFiles.toString()); @@ -40,13 +117,20 @@ export const useSanitizeOperation = () => { if (!response.ok) { const errorText = await response.text(); + markOperationFailed(fileId, operationId, errorText); throw new Error(t('sanitize.error', 'Sanitization failed: {{error}}', { error: errorText })); } const blob = await response.blob(); + const sanitizedFileName = generateSanitizedFileName(selectedFiles[0].name); + const url = URL.createObjectURL(blob); setDownloadUrl(url); setStatus(t('sanitize.completed', 'Sanitization completed successfully')); + + // Process results and add to workbench + await processResults(blob, sanitizedFileName); + markOperationApplied(fileId, operationId); } catch (error) { const message = error instanceof Error ? error.message : t('sanitize.error.generic', 'Sanitization failed'); setErrorMessage(message); @@ -55,12 +139,15 @@ export const useSanitizeOperation = () => { } finally { setIsLoading(false); } - }, [t]); + }, [t, createOperation, recordOperation, markOperationApplied, markOperationFailed, processResults]); const resetResults = useCallback(() => { if (downloadUrl) { URL.revokeObjectURL(downloadUrl); } + setFiles([]); + setThumbnails([]); + setIsGeneratingThumbnails(false); setDownloadUrl(null); setErrorMessage(null); setStatus(null); @@ -70,13 +157,25 @@ export const useSanitizeOperation = () => { setErrorMessage(null); }, []); + // Cleanup blob URLs on unmount to prevent memory leaks + useEffect(() => { + return () => { + if (downloadUrl) { + URL.revokeObjectURL(downloadUrl); + } + }; + }, [downloadUrl]); + return { isLoading, errorMessage, downloadUrl, status, + files, + thumbnails, + isGeneratingThumbnails, executeOperation, resetResults, clearError, }; -}; \ No newline at end of file +}; diff --git a/frontend/src/tools/Sanitize.tsx b/frontend/src/tools/Sanitize.tsx index 394f6c5f3..c5068ae02 100644 --- a/frontend/src/tools/Sanitize.tsx +++ b/frontend/src/tools/Sanitize.tsx @@ -9,20 +9,23 @@ import ToolStep, { ToolStepContainer } from "../components/tools/shared/ToolStep import OperationButton from "../components/tools/shared/OperationButton"; import ErrorNotification from "../components/tools/shared/ErrorNotification"; import FileStatusIndicator from "../components/tools/shared/FileStatusIndicator"; +import ResultsPreview from "../components/tools/shared/ResultsPreview"; import SanitizeSettings from "../components/tools/sanitize/SanitizeSettings"; import { useSanitizeParameters } from "../hooks/tools/sanitize/useSanitizeParameters"; import { useSanitizeOperation } from "../hooks/tools/sanitize/useSanitizeOperation"; import { BaseToolProps } from "../types/tool"; +import { useFileContext } from "../contexts/FileContext"; const generateSanitizedFileName = (originalFileName?: string): string => { const baseName = originalFileName?.replace(/\.[^/.]+$/, '') || 'document'; - return `${baseName}_sanitized.pdf`; + return `sanitized_${baseName}.pdf`; }; const Sanitize = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { const { t } = useTranslation(); const { selectedFiles } = useToolFileSelection(); + const { setCurrentMode } = useFileContext(); const sanitizeParams = useSanitizeParameters(); const sanitizeOperation = useSanitizeOperation(); @@ -41,17 +44,11 @@ const Sanitize = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { try { await sanitizeOperation.executeOperation( sanitizeParams.parameters, - selectedFiles + selectedFiles, + generateSanitizedFileName ); - if (sanitizeOperation.downloadUrl && onComplete) { - // Create a File object from the download URL for completion callback - const response = await fetch(sanitizeOperation.downloadUrl); - const blob = await response.blob(); - const sanitizedFileName = generateSanitizedFileName(selectedFiles[0]?.name); - const file = new File([blob], sanitizedFileName, { - type: 'application/pdf' - }); - onComplete([file]); + if (sanitizeOperation.files && onComplete) { + onComplete(sanitizeOperation.files); } } catch (error) { if (onError) { @@ -65,8 +62,14 @@ const Sanitize = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { onPreviewFile?.(null); }; + const handleThumbnailClick = (file: File) => { + onPreviewFile?.(file); + sessionStorage.setItem('previousMode', 'sanitize'); + setCurrentMode('viewer'); + }; + const hasFiles = selectedFiles.length > 0; - const hasResults = sanitizeOperation.downloadUrl !== null; + const hasResults = sanitizeOperation.files.length > 0; const filesCollapsed = hasFiles; const settingsCollapsed = hasResults; @@ -141,6 +144,16 @@ const Sanitize = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { {t("download", "Download")} )} + + ({ + file, + thumbnail: sanitizeOperation.thumbnails[index] + }))} + onFileClick={handleThumbnailClick} + isGeneratingThumbnails={sanitizeOperation.isGeneratingThumbnails} + title={t("sanitize.sanitizationResults", "Sanitization Results")} + />