Put sanitized files in workspace instead of download

This commit is contained in:
James 2025-08-06 14:44:41 +01:00
parent 54b3295798
commit 243638d513
2 changed files with 129 additions and 17 deletions

View File

@ -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<string | null>(null);
const [downloadUrl, setDownloadUrl] = useState<string | null>(null);
const [status, setStatus] = useState<string | null>(null);
const [files, setFiles] = useState<File[]>([]);
const [thumbnails, setThumbnails] = useState<string[]>([]);
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,
};
};
};

View File

@ -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")}
</Button>
)}
<ResultsPreview
files={sanitizeOperation.files.map((file, index) => ({
file,
thumbnail: sanitizeOperation.thumbnails[index]
}))}
onFileClick={handleThumbnailClick}
isGeneratingThumbnails={sanitizeOperation.isGeneratingThumbnails}
title={t("sanitize.sanitizationResults", "Sanitization Results")}
/>
</Stack>
</ToolStep>
</Stack>