Support multiple-selections for Sanitize

This commit is contained in:
James 2025-08-06 16:44:26 +01:00
parent 243638d513
commit df9d99b8cb
6 changed files with 126 additions and 67 deletions

View File

@ -38,7 +38,8 @@
"save": "Save",
"saveToBrowser": "Save to Browser",
"close": "Close",
"filesSelected": "files selected",
"fileSelected": "Selected: {{filename}}",
"filesSelected": "{{count}} files selected",
"noFavourites": "No favourites added",
"downloadComplete": "Download Complete",
"bored": "Bored Waiting?",

View File

@ -38,7 +38,8 @@
"save": "Save",
"saveToBrowser": "Save to Browser",
"close": "Close",
"filesSelected": "files selected",
"fileSelected": "Selected: {{filename}}",
"filesSelected": "{{count}} files selected",
"noFavourites": "No favorites added",
"downloadComplete": "Download Complete",
"bored": "Bored Waiting?",

View File

@ -3,6 +3,7 @@ import { useTranslation } from 'react-i18next';
import { useFileContext } from '../../../contexts/FileContext';
import { FileOperation } from '../../../types/fileContext';
import { generateThumbnailForFile } from '../../../utils/thumbnailUtils';
import { zipFileService } from '../../../services/zipFileService';
import { SanitizeParameters } from './useSanitizeParameters';
export const useSanitizeOperation = () => {
@ -52,36 +53,6 @@ export const useSanitizeOperation = () => {
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[],
@ -91,47 +62,119 @@ export const useSanitizeOperation = () => {
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...'));
setStatus(selectedFiles.length === 1
? t('sanitize.processing', 'Sanitizing PDF...')
: t('sanitize.processingMultiple', 'Sanitizing {{count}} PDFs...', { count: selectedFiles.length })
);
const results: File[] = [];
const failedFiles: string[] = [];
try {
const formData = new FormData();
formData.append('fileInput', selectedFiles[0]);
// Process each file separately
for (let i = 0; i < selectedFiles.length; i++) {
const file = selectedFiles[i];
const { operation, operationId, fileId } = createOperation(parameters, [file]);
recordOperation(fileId, operation);
// 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());
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
})
);
const response = await fetch('/api/v1/security/sanitize-pdf', {
method: 'POST',
body: formData,
});
try {
const formData = new FormData();
formData.append('fileInput', file);
if (!response.ok) {
const errorText = await response.text();
markOperationFailed(fileId, operationId, errorText);
throw new Error(t('sanitize.error', 'Sanitization failed: {{error}}', { error: errorText }));
// 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 });
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');
failedFiles.push(file.name);
}
}
const blob = await response.blob();
const sanitizedFileName = generateSanitizedFileName(selectedFiles[0].name);
if (failedFiles.length > 0 && results.length === 0) {
throw new Error(`Failed to sanitize all files: ${failedFiles.join(', ')}`);
}
const url = URL.createObjectURL(blob);
setDownloadUrl(url);
setStatus(t('sanitize.completed', 'Sanitization completed successfully'));
if (failedFiles.length > 0) {
setStatus(`Sanitized ${results.length}/${selectedFiles.length} files. Failed: ${failedFiles.join(', ')}`);
}
// Process results and add to workbench
await processResults(blob, sanitizedFileName);
markOperationApplied(fileId, operationId);
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 })
);
} else {
setErrorMessage(t('sanitize.errorAllFilesFailed', 'All files failed to sanitize'));
}
} catch (error) {
console.error('Error in sanitization operation:', error);
const message = error instanceof Error ? error.message : t('sanitize.error.generic', 'Sanitization failed');
setErrorMessage(message);
setStatus(null);
@ -139,7 +182,7 @@ export const useSanitizeOperation = () => {
} finally {
setIsLoading(false);
}
}, [t, createOperation, recordOperation, markOperationApplied, markOperationFailed, processResults]);
}, [t, createOperation, recordOperation, markOperationApplied, markOperationFailed, addFiles]);
const resetResults = useCallback(() => {
if (downloadUrl) {

View File

@ -80,7 +80,7 @@ const toolDefinitions: Record<string, ToolDefinition> = {
id: "sanitize",
icon: <CleaningServicesIcon />,
component: React.lazy(() => import("../tools/Sanitize")),
maxFiles: 1,
maxFiles: -1,
category: "security",
description: "Remove potentially harmful elements from PDF files",
endpoints: ["sanitize-pdf"]

View File

@ -122,7 +122,11 @@ const Convert = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
isVisible={true}
isCollapsed={filesCollapsed}
isCompleted={filesCollapsed}
completedMessage={hasFiles ? `${selectedFiles.length} ${t("filesSelected", "files selected")}` : undefined}
completedMessage={hasFiles ?
selectedFiles.length === 1
? t('fileSelected', 'Selected: {{filename}}', { filename: selectedFiles[0].name })
: t('filesSelected', '{{count}} files selected', { count: selectedFiles.length })
: undefined}
>
<FileStatusIndicator
selectedFiles={selectedFiles}

View File

@ -82,7 +82,11 @@ const Sanitize = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
isVisible={true}
isCollapsed={filesCollapsed}
isCompleted={filesCollapsed}
completedMessage={hasFiles ? t('sanitize.files.selected', 'Selected: {{filename}}', { filename: selectedFiles[0]?.name }) : undefined}
completedMessage={hasFiles ?
selectedFiles.length === 1
? t('fileSelected', 'Selected: {{filename}}', { filename: selectedFiles[0].name })
: t('filesSelected', 'Selected: {{count}} files', { count: selectedFiles.length })
: undefined}
>
<FileStatusIndicator
selectedFiles={selectedFiles}
@ -135,13 +139,19 @@ const Sanitize = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
<Button
component="a"
href={sanitizeOperation.downloadUrl}
download={generateSanitizedFileName(selectedFiles[0]?.name)}
download={sanitizeOperation.files.length === 1
? generateSanitizedFileName(selectedFiles[0]?.name)
: 'sanitized_files.zip'
}
leftSection={<DownloadIcon />}
color="green"
fullWidth
mb="md"
>
{t("download", "Download")}
{sanitizeOperation.files.length === 1
? t("download", "Download")
: t("downloadZip", "Download ZIP")
}
</Button>
)}