diff --git a/frontend/src/hooks/tools/sanitize/useSanitizeOperation.ts b/frontend/src/hooks/tools/sanitize/useSanitizeOperation.ts index 18db1d3bb..f10f46ff7 100644 --- a/frontend/src/hooks/tools/sanitize/useSanitizeOperation.ts +++ b/frontend/src/hooks/tools/sanitize/useSanitizeOperation.ts @@ -53,6 +53,103 @@ export const useSanitizeOperation = () => { return { operation, operationId, fileId }; }, []); + const buildFormData = useCallback((parameters: SanitizeParameters, file: File): FormData => { + const formData = new FormData(); + formData.append('fileInput', file); + + // Add parameters + formData.append('removeJavaScript', parameters.removeJavaScript.toString()); + formData.append('removeEmbeddedFiles', parameters.removeEmbeddedFiles.toString()); + formData.append('removeXMPMetadata', parameters.removeXMPMetadata.toString()); + formData.append('removeMetadata', parameters.removeMetadata.toString()); + formData.append('removeLinks', parameters.removeLinks.toString()); + formData.append('removeFonts', parameters.removeFonts.toString()); + + return formData; + }, []); + + const sanitizeFile = useCallback(async ( + file: File, + parameters: SanitizeParameters, + generateSanitizedFileName: (originalFileName?: string) => string, + operationId: string, + fileId: string + ): Promise => { + try { + const formData = buildFormData(parameters, file); + + const response = await fetch('/api/v1/security/sanitize-pdf', { + method: 'POST', + body: formData, + }); + + if (!response.ok) { + const errorText = await response.text(); + markOperationFailed(fileId, operationId, errorText); + console.error(`Error sanitizing file ${file.name}:`, errorText); + return null; + } + + const blob = await response.blob(); + const sanitizedFileName = generateSanitizedFileName(file.name); + const sanitizedFile = new File([blob], sanitizedFileName, { type: blob.type }); + + markOperationApplied(fileId, operationId); + return sanitizedFile; + } catch (error) { + console.error(`Error sanitizing file ${file.name}:`, error); + markOperationFailed(fileId, operationId, error instanceof Error ? error.message : 'Unknown error'); + return null; + } + }, [buildFormData, markOperationApplied, markOperationFailed]); + + const createDownloadInfo = useCallback(async (results: File[]): Promise => { + if (results.length === 1) { + const url = window.URL.createObjectURL(results[0]); + setDownloadUrl(url); + } else { + const { zipFile } = await zipFileService.createZipFromFiles(results, 'sanitized_files.zip'); + const url = window.URL.createObjectURL(zipFile); + setDownloadUrl(url); + } + }, []); + + const generateThumbnailsForResults = useCallback(async (results: File[]): Promise => { + const thumbnails = await Promise.all( + results.map(async (file) => { + try { + const thumbnail = await generateThumbnailForFile(file); + return thumbnail || ''; + } catch (error) { + console.warn(`Failed to generate thumbnail for ${file.name}:`, error); + return ''; + } + }) + ); + + setThumbnails(thumbnails); + }, []); + + const processResults = useCallback(async (results: File[]): Promise => { + setFiles(results); + setIsGeneratingThumbnails(true); + + // Add sanitized files to FileContext for future use + await addFiles(results); + + // Create download info - single file or ZIP + await createDownloadInfo(results); + + // Generate thumbnails + await generateThumbnailsForResults(results); + + setIsGeneratingThumbnails(false); + setStatus(results.length === 1 + ? t('sanitize.completed', 'Sanitization completed successfully') + : t('sanitize.completedMultiple', 'Sanitized {{count}} files successfully', { count: results.length }) + ); + }, [addFiles, createDownloadInfo, generateThumbnailsForResults, t]); + const executeOperation = useCallback(async ( parameters: SanitizeParameters, selectedFiles: File[], @@ -64,8 +161,8 @@ export const useSanitizeOperation = () => { setIsLoading(true); setErrorMessage(null); - setStatus(selectedFiles.length === 1 - ? t('sanitize.processing', 'Sanitizing PDF...') + setStatus(selectedFiles.length === 1 + ? t('sanitize.processing', 'Sanitizing PDF...') : t('sanitize.processingMultiple', 'Sanitizing {{count}} PDFs...', { count: selectedFiles.length }) ); @@ -79,49 +176,20 @@ export const useSanitizeOperation = () => { const { operation, operationId, fileId } = createOperation(parameters, [file]); recordOperation(fileId, operation); - setStatus(selectedFiles.length === 1 - ? t('sanitize.processing', 'Sanitizing PDF...') - : t('sanitize.processingFile', 'Processing file {{current}} of {{total}}: {{filename}}', { - current: i + 1, - total: selectedFiles.length, - filename: file.name + setStatus(selectedFiles.length === 1 + ? t('sanitize.processing', 'Sanitizing PDF...') + : t('sanitize.processingFile', 'Processing file {{current}} of {{total}}: {{filename}}', { + current: i + 1, + total: selectedFiles.length, + filename: file.name }) ); - try { - const formData = new FormData(); - formData.append('fileInput', file); - - // Add parameters - formData.append('removeJavaScript', parameters.removeJavaScript.toString()); - formData.append('removeEmbeddedFiles', parameters.removeEmbeddedFiles.toString()); - formData.append('removeXMPMetadata', parameters.removeXMPMetadata.toString()); - formData.append('removeMetadata', parameters.removeMetadata.toString()); - formData.append('removeLinks', parameters.removeLinks.toString()); - formData.append('removeFonts', parameters.removeFonts.toString()); - - const response = await fetch('/api/v1/security/sanitize-pdf', { - method: 'POST', - body: formData, - }); - - if (!response.ok) { - const errorText = await response.text(); - markOperationFailed(fileId, operationId, errorText); - console.error(`Error sanitizing file ${file.name}:`, errorText); - failedFiles.push(file.name); - continue; - } - - const blob = await response.blob(); - const sanitizedFileName = generateSanitizedFileName(file.name); - const sanitizedFile = new File([blob], sanitizedFileName, { type: blob.type }); - + const sanitizedFile = await sanitizeFile(file, parameters, generateSanitizedFileName, operationId, fileId); + + if (sanitizedFile) { results.push(sanitizedFile); - markOperationApplied(fileId, operationId); - } catch (error) { - console.error(`Error sanitizing file ${file.name}:`, error); - markOperationFailed(fileId, operationId, error instanceof Error ? error.message : 'Unknown error'); + } else { failedFiles.push(file.name); } } @@ -135,41 +203,7 @@ export const useSanitizeOperation = () => { } if (results.length > 0) { - setFiles(results); - setIsGeneratingThumbnails(true); - - // Add sanitized files to FileContext for future use - await addFiles(results); - - // Create download info - single file or ZIP - if (results.length === 1) { - const url = window.URL.createObjectURL(results[0]); - setDownloadUrl(url); - } else { - const { zipFile } = await zipFileService.createZipFromFiles(results, 'sanitized_files.zip'); - const url = window.URL.createObjectURL(zipFile); - setDownloadUrl(url); - } - - // Generate thumbnails - const thumbnails = await Promise.all( - results.map(async (file) => { - try { - const thumbnail = await generateThumbnailForFile(file); - return thumbnail || ''; - } catch (error) { - console.warn(`Failed to generate thumbnail for ${file.name}:`, error); - return ''; - } - }) - ); - - setThumbnails(thumbnails); - setIsGeneratingThumbnails(false); - setStatus(results.length === 1 - ? t('sanitize.completed', 'Sanitization completed successfully') - : t('sanitize.completedMultiple', 'Sanitized {{count}} files successfully', { count: results.length }) - ); + await processResults(results); } else { setErrorMessage(t('sanitize.errorAllFilesFailed', 'All files failed to sanitize')); } @@ -182,7 +216,7 @@ export const useSanitizeOperation = () => { } finally { setIsLoading(false); } - }, [t, createOperation, recordOperation, markOperationApplied, markOperationFailed, addFiles]); + }, [t, createOperation, recordOperation, sanitizeFile, processResults]); const resetResults = useCallback(() => { if (downloadUrl) { diff --git a/frontend/src/types/fileContext.ts b/frontend/src/types/fileContext.ts index d9c049ae7..292454065 100644 --- a/frontend/src/types/fileContext.ts +++ b/frontend/src/types/fileContext.ts @@ -7,7 +7,7 @@ import { PDFDocument, PDFPage, PageOperation } from './pageEditor'; export type ModeType = 'viewer' | 'pageEditor' | 'fileEditor' | 'merge' | 'split' | 'compress' | 'ocr'; -export type OperationType = 'merge' | 'split' | 'compress' | 'add' | 'remove' | 'replace' | 'convert' | 'upload' | 'ocr'; +export type OperationType = 'merge' | 'split' | 'compress' | 'add' | 'remove' | 'replace' | 'convert' | 'upload' | 'ocr' | 'sanitize'; export interface FileOperation { id: string; @@ -51,25 +51,25 @@ export interface FileContextState { // Core file management activeFiles: File[]; processedFiles: Map; - + // Current navigation state currentMode: ModeType; - + // Edit history and state fileEditHistory: Map; globalFileOperations: FileOperation[]; // New comprehensive operation history fileOperationHistory: Map; - + // UI state that persists across views selectedFileIds: string[]; selectedPageNumbers: number[]; viewerConfig: ViewerConfig; - + // Processing state isProcessing: boolean; processingProgress: number; - + // Export state lastExportConfig?: { filename: string; @@ -89,7 +89,7 @@ export interface FileContextActions { removeFiles: (fileIds: string[], deleteFromStorage?: boolean) => void; replaceFile: (oldFileId: string, newFile: File) => Promise; clearAllFiles: () => void; - + // Navigation setCurrentMode: (mode: ModeType) => void; // Selection management @@ -97,12 +97,12 @@ export interface FileContextActions { setSelectedPages: (pageNumbers: number[]) => void; updateProcessedFile: (file: File, processedFile: ProcessedFile) => void; clearSelections: () => void; - + // Edit operations applyPageOperations: (fileId: string, operations: PageOperation[]) => void; applyFileOperation: (operation: FileOperation) => void; undoLastOperation: (fileId?: string) => void; - + // Operation history management recordOperation: (fileId: string, operation: FileOperation | PageOperation) => void; markOperationApplied: (fileId: string, operationId: string) => void; @@ -110,31 +110,31 @@ export interface FileContextActions { getFileHistory: (fileId: string) => FileOperationHistory | undefined; getAppliedOperations: (fileId: string) => (FileOperation | PageOperation)[]; clearFileHistory: (fileId: string) => void; - + // Viewer state updateViewerConfig: (config: Partial) => void; - + // Export configuration setExportConfig: (config: FileContextState['lastExportConfig']) => void; - - + + // Utility getFileById: (fileId: string) => File | undefined; getProcessedFileById: (fileId: string) => ProcessedFile | undefined; getCurrentFile: () => File | undefined; getCurrentProcessedFile: () => ProcessedFile | undefined; - + // Context persistence saveContext: () => Promise; loadContext: () => Promise; resetContext: () => void; - + // Navigation guard system setHasUnsavedChanges: (hasChanges: boolean) => void; requestNavigation: (navigationFn: () => void) => boolean; confirmNavigation: () => void; cancelNavigation: () => void; - + // Memory management trackBlobUrl: (url: string) => void; trackPdfDocument: (fileId: string, pdfDoc: any) => void; @@ -163,4 +163,4 @@ export interface FileContextUrlParams { pageIds?: string[]; zoom?: number; page?: number; -} \ No newline at end of file +}