mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-09-18 09:29:24 +00:00
Remove prefixes
This commit is contained in:
parent
16a0a6dafe
commit
1eb0d0a0d1
231
FILE_HISTORY_SPECIFICATION.md
Normal file
231
FILE_HISTORY_SPECIFICATION.md
Normal file
@ -0,0 +1,231 @@
|
|||||||
|
# Stirling PDF File History Specification
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Stirling PDF implements a comprehensive file history tracking system that embeds metadata directly into PDF documents using the PDF keywords field. This system tracks tool operations, version progression, and file lineage through the processing pipeline.
|
||||||
|
|
||||||
|
## PDF Metadata Format
|
||||||
|
|
||||||
|
### Storage Mechanism
|
||||||
|
File history is stored in the PDF **Keywords** field as a JSON string with the prefix `stirling-history:`.
|
||||||
|
|
||||||
|
### Metadata Structure
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface PDFHistoryMetadata {
|
||||||
|
stirlingHistory: {
|
||||||
|
originalFileId: string; // UUID of the root file in the version chain
|
||||||
|
parentFileId?: string; // UUID of the immediate parent file
|
||||||
|
versionNumber: number; // Version number (1, 2, 3, etc.)
|
||||||
|
toolChain: ToolOperation[]; // Array of applied tool operations
|
||||||
|
createdBy: 'Stirling-PDF'; // System identifier
|
||||||
|
formatVersion: '1.0'; // Metadata format version
|
||||||
|
createdAt: number; // Timestamp when version was created
|
||||||
|
lastModified: number; // Timestamp when last modified
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ToolOperation {
|
||||||
|
toolName: string; // Tool identifier (e.g., 'compress', 'sanitize')
|
||||||
|
timestamp: number; // When the tool was applied
|
||||||
|
parameters?: Record<string, any>; // Tool-specific parameters (optional)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example PDF Keywords Field
|
||||||
|
```
|
||||||
|
Keywords: ["user-keyword", "stirling-history:{\"stirlingHistory\":{\"originalFileId\":\"abc123\",\"versionNumber\":2,\"toolChain\":[{\"toolName\":\"compress\",\"timestamp\":1756825614618},{\"toolName\":\"sanitize\",\"timestamp\":1756825631545}],\"createdBy\":\"Stirling-PDF\",\"formatVersion\":\"1.0\"}}"]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Version Numbering System
|
||||||
|
|
||||||
|
### Version Progression
|
||||||
|
- **v0**: Original uploaded file (no Stirling PDF processing)
|
||||||
|
- **v1**: First tool applied to original file
|
||||||
|
- **v2**: Second tool applied (inherits from v1)
|
||||||
|
- **v3**: Third tool applied (inherits from v2)
|
||||||
|
- **etc.**
|
||||||
|
|
||||||
|
### Version Relationships
|
||||||
|
```
|
||||||
|
document.pdf (v0)
|
||||||
|
↓ compress
|
||||||
|
document.pdf (v1: compress)
|
||||||
|
↓ sanitize
|
||||||
|
document.pdf (v2: compress → sanitize)
|
||||||
|
↓ ocr
|
||||||
|
document.pdf (v3: compress → sanitize → ocr)
|
||||||
|
```
|
||||||
|
|
||||||
|
## File Lineage Tracking
|
||||||
|
|
||||||
|
### Original File ID
|
||||||
|
The `originalFileId` remains constant throughout the entire version chain, enabling grouping of all versions of the same logical document.
|
||||||
|
|
||||||
|
### Parent-Child Relationships
|
||||||
|
Each processed file references its immediate parent via `parentFileId`, creating a complete audit trail.
|
||||||
|
|
||||||
|
### Tool Chain
|
||||||
|
The `toolChain` array maintains the complete sequence of tool operations applied to reach the current version.
|
||||||
|
|
||||||
|
## Implementation Architecture
|
||||||
|
|
||||||
|
### Frontend Components
|
||||||
|
|
||||||
|
#### 1. PDF Metadata Service (`pdfMetadataService.ts`)
|
||||||
|
- **PDF-lib Integration**: Uses pdf-lib for metadata injection/extraction
|
||||||
|
- **Caching**: ContentCache with 10-minute TTL for performance
|
||||||
|
- **Encryption Support**: Handles encrypted PDFs with `ignoreEncryption: true`
|
||||||
|
|
||||||
|
**Key Methods:**
|
||||||
|
```typescript
|
||||||
|
// Inject metadata into PDF
|
||||||
|
injectHistoryMetadata(pdfBytes: ArrayBuffer, originalFileId: string, parentFileId?: string, toolChain: ToolOperation[], versionNumber: number): Promise<ArrayBuffer>
|
||||||
|
|
||||||
|
// Extract metadata from PDF
|
||||||
|
extractHistoryMetadata(pdfBytes: ArrayBuffer): Promise<PDFHistoryMetadata | null>
|
||||||
|
|
||||||
|
// Create new version with incremented number
|
||||||
|
createNewVersion(pdfBytes: ArrayBuffer, parentFileId: string, toolOperation: ToolOperation): Promise<ArrayBuffer>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. File History Utilities (`fileHistoryUtils.ts`)
|
||||||
|
- **FileContext Integration**: Links PDF metadata with React state management
|
||||||
|
- **Version Management**: Handles version grouping and latest version filtering
|
||||||
|
- **Tool Integration**: Prepares files for tool processing with history injection
|
||||||
|
|
||||||
|
**Key Functions:**
|
||||||
|
```typescript
|
||||||
|
// Extract history from File and update FileRecord
|
||||||
|
extractFileHistory(file: File, record: FileRecord): Promise<FileRecord>
|
||||||
|
|
||||||
|
// Inject history before tool processing
|
||||||
|
injectHistoryForTool(file: File, sourceFileRecord: FileRecord, toolName: string, parameters?): Promise<File>
|
||||||
|
|
||||||
|
// Group files by original ID for version management
|
||||||
|
groupFilesByOriginal(fileRecords: FileRecord[]): Map<string, FileRecord[]>
|
||||||
|
|
||||||
|
// Get only latest version of each file group
|
||||||
|
getLatestVersions(fileRecords: FileRecord[]): FileRecord[]
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. Tool Operation Integration (`useToolOperation.ts`)
|
||||||
|
- **Automatic Injection**: All tool operations automatically inject history metadata
|
||||||
|
- **Version Progression**: Reads current version from PDF and increments appropriately
|
||||||
|
- **Universal Support**: Works with single-file, multi-file, and custom tool patterns
|
||||||
|
|
||||||
|
### Data Flow
|
||||||
|
|
||||||
|
```
|
||||||
|
1. User uploads PDF → No history (v0)
|
||||||
|
2. Tool processing begins → prepareFilesWithHistory() injects current state
|
||||||
|
3. Backend processes PDF → Returns processed file with embedded history
|
||||||
|
4. FileContext adds result → extractFileHistory() reads embedded metadata
|
||||||
|
5. UI displays file → Shows version badges and tool chain
|
||||||
|
```
|
||||||
|
|
||||||
|
## UI Integration
|
||||||
|
|
||||||
|
### File Manager
|
||||||
|
- **Version Toggle**: Switch between "Latest Only" and "All Versions" views
|
||||||
|
- **Version Badges**: v0, v1, v2 indicators on file items
|
||||||
|
- **History Dropdown**: Version timeline with restore functionality
|
||||||
|
- **Tool Chain Display**: Complete processing history in file details panel
|
||||||
|
|
||||||
|
### Active Files Workbench
|
||||||
|
- **Version Metadata**: Version number in file metadata line (e.g., "PDF file - 3 Pages - v2")
|
||||||
|
- **Tool Chain Overlay**: Bottom overlay showing tool sequence (e.g., "compress → sanitize")
|
||||||
|
- **Real-time Updates**: Immediate display after tool processing
|
||||||
|
|
||||||
|
## Storage and Persistence
|
||||||
|
|
||||||
|
### PDF Metadata
|
||||||
|
- **Embedded in PDF**: History travels with the document across downloads/uploads
|
||||||
|
- **Keywords Field**: Uses standard PDF metadata field for maximum compatibility
|
||||||
|
- **Multiple Keywords**: System handles multiple history entries and extracts latest version
|
||||||
|
|
||||||
|
### IndexedDB Storage
|
||||||
|
- **Client-side Persistence**: FileMetadata includes extracted history information
|
||||||
|
- **Lazy Loading**: History extracted when files are accessed from storage
|
||||||
|
- **Batch Processing**: Large collections processed in batches of 5 to prevent memory issues
|
||||||
|
|
||||||
|
### Memory Management
|
||||||
|
- **ContentCache**: 10-minute TTL, 50-file capacity for metadata extraction results
|
||||||
|
- **Cleanup**: Automatic cache eviction and expired entry removal
|
||||||
|
- **Large File Support**: No artificial size limits (supports 100GB+ PDFs)
|
||||||
|
|
||||||
|
## Tool Configuration
|
||||||
|
|
||||||
|
### Filename Preservation
|
||||||
|
Most tools preserve the original filename to maintain file identity:
|
||||||
|
|
||||||
|
**No Prefix (Filename Preserved):**
|
||||||
|
- compress, repair, sanitize, addPassword, removePassword, changePermissions, removeCertificateSign, unlockPdfForms, ocr, addWatermark
|
||||||
|
|
||||||
|
**With Prefix (Different Content):**
|
||||||
|
- split (`split_` - creates multiple files)
|
||||||
|
- convert (`converted_` - changes file format)
|
||||||
|
|
||||||
|
### Configuration Pattern
|
||||||
|
```typescript
|
||||||
|
export const toolOperationConfig = {
|
||||||
|
toolType: ToolType.singleFile,
|
||||||
|
operationType: 'toolName',
|
||||||
|
endpoint: '/api/v1/category/tool-endpoint',
|
||||||
|
filePrefix: '', // Empty for filename preservation
|
||||||
|
buildFormData: buildToolFormData,
|
||||||
|
defaultParameters
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Handling and Resilience
|
||||||
|
|
||||||
|
### Graceful Degradation
|
||||||
|
- **Extraction Failures**: Files display normally without history if metadata extraction fails
|
||||||
|
- **Encrypted PDFs**: System handles encrypted documents with `ignoreEncryption` option
|
||||||
|
- **Corrupted Metadata**: Invalid history metadata is silently ignored with fallback to basic file info
|
||||||
|
|
||||||
|
### Performance Considerations
|
||||||
|
- **Caching**: Metadata extraction results are cached to avoid re-parsing
|
||||||
|
- **Batch Processing**: Large file collections processed in controlled batches
|
||||||
|
- **Async Extraction**: History extraction doesn't block file operations
|
||||||
|
|
||||||
|
## Developer Guidelines
|
||||||
|
|
||||||
|
### Adding History to New Tools
|
||||||
|
1. **Set `filePrefix: ''`** in tool configuration to preserve filenames
|
||||||
|
2. **Use existing patterns**: Tool operations automatically inherit history injection
|
||||||
|
3. **Custom processors**: Must handle history injection manually if using custom response handlers
|
||||||
|
|
||||||
|
### Testing File History
|
||||||
|
1. **Upload a PDF**: Should show no version (v0)
|
||||||
|
2. **Apply any tool**: Should show v1 with tool name in history
|
||||||
|
3. **Apply another tool**: Should show v2 with tool chain sequence
|
||||||
|
4. **Check file manager**: Version toggle and history dropdown should work
|
||||||
|
5. **Check workbench**: Tool chain overlay should appear on thumbnails
|
||||||
|
|
||||||
|
### Debugging
|
||||||
|
Enable development mode logging to see:
|
||||||
|
- History injection: `📄 Injected PDF history metadata`
|
||||||
|
- History extraction: `📄 History extraction completed`
|
||||||
|
- Version progression: Version number increments and tool chain updates
|
||||||
|
|
||||||
|
## Future Enhancements
|
||||||
|
|
||||||
|
### Possible Extensions
|
||||||
|
- **Branching**: Support for parallel processing branches from same source
|
||||||
|
- **Diff Tracking**: Track specific changes made by each tool
|
||||||
|
- **User Attribution**: Add user information to tool operations
|
||||||
|
- **Timestamp Precision**: Enhanced timestamp tracking for audit trails
|
||||||
|
- **Export Options**: Export complete processing history as JSON/XML
|
||||||
|
|
||||||
|
### Compatibility
|
||||||
|
- **PDF Standard Compliance**: Uses standard PDF Keywords field for broad compatibility
|
||||||
|
- **Backwards Compatibility**: PDFs without history metadata work normally
|
||||||
|
- **Future Versions**: Format version field enables future metadata schema evolution
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Last Updated**: January 2025
|
||||||
|
**Format Version**: 1.0
|
||||||
|
**Implementation**: Stirling PDF Frontend v2
|
@ -30,7 +30,6 @@ export const addPasswordOperationConfig = {
|
|||||||
buildFormData: buildAddPasswordFormData,
|
buildFormData: buildAddPasswordFormData,
|
||||||
operationType: 'addPassword',
|
operationType: 'addPassword',
|
||||||
endpoint: '/api/v1/security/add-password',
|
endpoint: '/api/v1/security/add-password',
|
||||||
filePrefix: 'encrypted_', // Will be overridden in hook with translation
|
|
||||||
defaultParameters: fullDefaultParameters,
|
defaultParameters: fullDefaultParameters,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
@ -39,7 +38,6 @@ export const useAddPasswordOperation = () => {
|
|||||||
|
|
||||||
return useToolOperation<AddPasswordFullParameters>({
|
return useToolOperation<AddPasswordFullParameters>({
|
||||||
...addPasswordOperationConfig,
|
...addPasswordOperationConfig,
|
||||||
filePrefix: t('addPassword.filenamePrefix', 'encrypted') + '_',
|
|
||||||
getErrorMessage: createStandardErrorHandler(t('addPassword.error.failed', 'An error occurred while encrypting the PDF.'))
|
getErrorMessage: createStandardErrorHandler(t('addPassword.error.failed', 'An error occurred while encrypting the PDF.'))
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -39,7 +39,6 @@ export const addWatermarkOperationConfig = {
|
|||||||
buildFormData: buildAddWatermarkFormData,
|
buildFormData: buildAddWatermarkFormData,
|
||||||
operationType: 'watermark',
|
operationType: 'watermark',
|
||||||
endpoint: '/api/v1/security/add-watermark',
|
endpoint: '/api/v1/security/add-watermark',
|
||||||
filePrefix: 'watermarked_', // Will be overridden in hook with translation
|
|
||||||
defaultParameters,
|
defaultParameters,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
@ -48,7 +47,6 @@ export const useAddWatermarkOperation = () => {
|
|||||||
|
|
||||||
return useToolOperation<AddWatermarkParameters>({
|
return useToolOperation<AddWatermarkParameters>({
|
||||||
...addWatermarkOperationConfig,
|
...addWatermarkOperationConfig,
|
||||||
filePrefix: t('watermark.filenamePrefix', 'watermarked') + '_',
|
|
||||||
getErrorMessage: createStandardErrorHandler(t('watermark.error.failed', 'An error occurred while adding watermark to the PDF.'))
|
getErrorMessage: createStandardErrorHandler(t('watermark.error.failed', 'An error occurred while adding watermark to the PDF.'))
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -3,7 +3,6 @@ import { useCallback } from 'react';
|
|||||||
import { executeAutomationSequence } from '../../../utils/automationExecutor';
|
import { executeAutomationSequence } from '../../../utils/automationExecutor';
|
||||||
import { useFlatToolRegistry } from '../../../data/useTranslatedToolRegistry';
|
import { useFlatToolRegistry } from '../../../data/useTranslatedToolRegistry';
|
||||||
import { AutomateParameters } from '../../../types/automation';
|
import { AutomateParameters } from '../../../types/automation';
|
||||||
import { AUTOMATION_CONSTANTS } from '../../../constants/automation';
|
|
||||||
|
|
||||||
export function useAutomateOperation() {
|
export function useAutomateOperation() {
|
||||||
const toolRegistry = useFlatToolRegistry();
|
const toolRegistry = useFlatToolRegistry();
|
||||||
@ -43,6 +42,5 @@ export function useAutomateOperation() {
|
|||||||
toolType: ToolType.custom,
|
toolType: ToolType.custom,
|
||||||
operationType: 'automate',
|
operationType: 'automate',
|
||||||
customProcessor,
|
customProcessor,
|
||||||
filePrefix: '' // No prefix needed since automation handles naming internally
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -28,7 +28,6 @@ export const changePermissionsOperationConfig = {
|
|||||||
buildFormData: buildChangePermissionsFormData,
|
buildFormData: buildChangePermissionsFormData,
|
||||||
operationType: 'change-permissions',
|
operationType: 'change-permissions',
|
||||||
endpoint: '/api/v1/security/add-password', // Change Permissions is a fake endpoint for the Add Password tool
|
endpoint: '/api/v1/security/add-password', // Change Permissions is a fake endpoint for the Add Password tool
|
||||||
filePrefix: 'permissions_',
|
|
||||||
defaultParameters,
|
defaultParameters,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
@ -28,7 +28,6 @@ export const compressOperationConfig = {
|
|||||||
buildFormData: buildCompressFormData,
|
buildFormData: buildCompressFormData,
|
||||||
operationType: 'compress',
|
operationType: 'compress',
|
||||||
endpoint: '/api/v1/misc/compress-pdf',
|
endpoint: '/api/v1/misc/compress-pdf',
|
||||||
filePrefix: 'compressed_',
|
|
||||||
defaultParameters,
|
defaultParameters,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
@ -84,7 +84,7 @@ export const createFileFromResponse = (
|
|||||||
targetExtension = 'pdf';
|
targetExtension = 'pdf';
|
||||||
}
|
}
|
||||||
|
|
||||||
const fallbackFilename = `${originalName}_converted.${targetExtension}`;
|
const fallbackFilename = `${originalName}.${targetExtension}`;
|
||||||
|
|
||||||
return createFileFromApiResponse(responseData, headers, fallbackFilename);
|
return createFileFromApiResponse(responseData, headers, fallbackFilename);
|
||||||
};
|
};
|
||||||
@ -137,7 +137,6 @@ export const convertOperationConfig = {
|
|||||||
toolType: ToolType.custom,
|
toolType: ToolType.custom,
|
||||||
customProcessor: convertProcessor, // Can't use callback version here
|
customProcessor: convertProcessor, // Can't use callback version here
|
||||||
operationType: 'convert',
|
operationType: 'convert',
|
||||||
filePrefix: 'converted_',
|
|
||||||
defaultParameters,
|
defaultParameters,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
@ -88,8 +88,8 @@ export const ocrResponseHandler = async (blob: Blob, originalFiles: File[], extr
|
|||||||
throw new Error(`Response is not a valid PDF. Header: "${head}"`);
|
throw new Error(`Response is not a valid PDF. Header: "${head}"`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const base = stripExt(originalFiles[0].name);
|
const originalName = originalFiles[0].name;
|
||||||
return [new File([blob], `ocr_${base}.pdf`, { type: 'application/pdf' })];
|
return [new File([blob], originalName, { type: 'application/pdf' })];
|
||||||
};
|
};
|
||||||
|
|
||||||
// Static configuration object (without t function dependencies)
|
// Static configuration object (without t function dependencies)
|
||||||
@ -98,7 +98,6 @@ export const ocrOperationConfig = {
|
|||||||
buildFormData: buildOCRFormData,
|
buildFormData: buildOCRFormData,
|
||||||
operationType: 'ocr',
|
operationType: 'ocr',
|
||||||
endpoint: '/api/v1/misc/ocr-pdf',
|
endpoint: '/api/v1/misc/ocr-pdf',
|
||||||
filePrefix: 'ocr_',
|
|
||||||
defaultParameters,
|
defaultParameters,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
@ -16,7 +16,6 @@ export const removeCertificateSignOperationConfig = {
|
|||||||
buildFormData: buildRemoveCertificateSignFormData,
|
buildFormData: buildRemoveCertificateSignFormData,
|
||||||
operationType: 'remove-certificate-sign',
|
operationType: 'remove-certificate-sign',
|
||||||
endpoint: '/api/v1/security/remove-cert-sign',
|
endpoint: '/api/v1/security/remove-cert-sign',
|
||||||
filePrefix: 'unsigned_', // Will be overridden in hook with translation
|
|
||||||
defaultParameters,
|
defaultParameters,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
@ -25,7 +24,6 @@ export const useRemoveCertificateSignOperation = () => {
|
|||||||
|
|
||||||
return useToolOperation<RemoveCertificateSignParameters>({
|
return useToolOperation<RemoveCertificateSignParameters>({
|
||||||
...removeCertificateSignOperationConfig,
|
...removeCertificateSignOperationConfig,
|
||||||
filePrefix: t('removeCertSign.filenamePrefix', 'unsigned') + '_',
|
|
||||||
getErrorMessage: createStandardErrorHandler(t('removeCertSign.error.failed', 'An error occurred while removing certificate signatures.'))
|
getErrorMessage: createStandardErrorHandler(t('removeCertSign.error.failed', 'An error occurred while removing certificate signatures.'))
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -17,7 +17,6 @@ export const removePasswordOperationConfig = {
|
|||||||
buildFormData: buildRemovePasswordFormData,
|
buildFormData: buildRemovePasswordFormData,
|
||||||
operationType: 'removePassword',
|
operationType: 'removePassword',
|
||||||
endpoint: '/api/v1/security/remove-password',
|
endpoint: '/api/v1/security/remove-password',
|
||||||
filePrefix: 'decrypted_', // Will be overridden in hook with translation
|
|
||||||
defaultParameters,
|
defaultParameters,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
@ -26,7 +25,6 @@ export const useRemovePasswordOperation = () => {
|
|||||||
|
|
||||||
return useToolOperation<RemovePasswordParameters>({
|
return useToolOperation<RemovePasswordParameters>({
|
||||||
...removePasswordOperationConfig,
|
...removePasswordOperationConfig,
|
||||||
filePrefix: t('removePassword.filenamePrefix', 'decrypted') + '_',
|
|
||||||
getErrorMessage: createStandardErrorHandler(t('removePassword.error.failed', 'An error occurred while removing the password from the PDF.'))
|
getErrorMessage: createStandardErrorHandler(t('removePassword.error.failed', 'An error occurred while removing the password from the PDF.'))
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -16,7 +16,6 @@ export const repairOperationConfig = {
|
|||||||
buildFormData: buildRepairFormData,
|
buildFormData: buildRepairFormData,
|
||||||
operationType: 'repair',
|
operationType: 'repair',
|
||||||
endpoint: '/api/v1/misc/repair',
|
endpoint: '/api/v1/misc/repair',
|
||||||
filePrefix: 'repaired_', // Will be overridden in hook with translation
|
|
||||||
defaultParameters,
|
defaultParameters,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
@ -25,7 +24,6 @@ export const useRepairOperation = () => {
|
|||||||
|
|
||||||
return useToolOperation<RepairParameters>({
|
return useToolOperation<RepairParameters>({
|
||||||
...repairOperationConfig,
|
...repairOperationConfig,
|
||||||
filePrefix: t('repair.filenamePrefix', 'repaired') + '_',
|
|
||||||
getErrorMessage: createStandardErrorHandler(t('repair.error.failed', 'An error occurred while repairing the PDF.'))
|
getErrorMessage: createStandardErrorHandler(t('repair.error.failed', 'An error occurred while repairing the PDF.'))
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -25,7 +25,6 @@ export const sanitizeOperationConfig = {
|
|||||||
buildFormData: buildSanitizeFormData,
|
buildFormData: buildSanitizeFormData,
|
||||||
operationType: 'sanitize',
|
operationType: 'sanitize',
|
||||||
endpoint: '/api/v1/security/sanitize-pdf',
|
endpoint: '/api/v1/security/sanitize-pdf',
|
||||||
filePrefix: 'sanitized_', // Will be overridden in hook with translation
|
|
||||||
multiFileEndpoint: false,
|
multiFileEndpoint: false,
|
||||||
defaultParameters,
|
defaultParameters,
|
||||||
} as const;
|
} as const;
|
||||||
@ -35,7 +34,6 @@ export const useSanitizeOperation = () => {
|
|||||||
|
|
||||||
return useToolOperation<SanitizeParameters>({
|
return useToolOperation<SanitizeParameters>({
|
||||||
...sanitizeOperationConfig,
|
...sanitizeOperationConfig,
|
||||||
filePrefix: t('sanitize.filenamePrefix', 'sanitized') + '_',
|
|
||||||
getErrorMessage: createStandardErrorHandler(t('sanitize.error.failed', 'An error occurred while sanitising the PDF.'))
|
getErrorMessage: createStandardErrorHandler(t('sanitize.error.failed', 'An error occurred while sanitising the PDF.'))
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -6,7 +6,7 @@ import type { ProcessingProgress } from './useToolState';
|
|||||||
export interface ApiCallsConfig<TParams = void> {
|
export interface ApiCallsConfig<TParams = void> {
|
||||||
endpoint: string | ((params: TParams) => string);
|
endpoint: string | ((params: TParams) => string);
|
||||||
buildFormData: (params: TParams, file: File) => FormData;
|
buildFormData: (params: TParams, file: File) => FormData;
|
||||||
filePrefix: string;
|
filePrefix?: string;
|
||||||
responseHandler?: ResponseHandler;
|
responseHandler?: ResponseHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -34,7 +34,7 @@ interface BaseToolOperationConfig<TParams> {
|
|||||||
operationType: string;
|
operationType: string;
|
||||||
|
|
||||||
/** Prefix added to processed filenames (e.g., 'compressed_', 'split_') */
|
/** Prefix added to processed filenames (e.g., 'compressed_', 'split_') */
|
||||||
filePrefix: string;
|
filePrefix?: string;
|
||||||
|
|
||||||
/** How to handle API responses (e.g., ZIP extraction, single file response) */
|
/** How to handle API responses (e.g., ZIP extraction, single file response) */
|
||||||
responseHandler?: ResponseHandler;
|
responseHandler?: ResponseHandler;
|
||||||
@ -258,7 +258,7 @@ export const useToolOperation = <TParams>(
|
|||||||
// Replace input files with processed files (consumeFiles handles pinning)
|
// Replace input files with processed files (consumeFiles handles pinning)
|
||||||
const inputFileIds: FileId[] = [];
|
const inputFileIds: FileId[] = [];
|
||||||
const inputFileRecords: FileRecord[] = [];
|
const inputFileRecords: FileRecord[] = [];
|
||||||
|
|
||||||
// Build parallel arrays of IDs and records for undo tracking
|
// Build parallel arrays of IDs and records for undo tracking
|
||||||
for (const file of validFiles) {
|
for (const file of validFiles) {
|
||||||
const fileId = findFileId(file);
|
const fileId = findFileId(file);
|
||||||
@ -274,9 +274,9 @@ export const useToolOperation = <TParams>(
|
|||||||
console.warn(`No file ID found for file: ${file.name}`);
|
console.warn(`No file ID found for file: ${file.name}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const outputFileIds = await consumeFiles(inputFileIds, processedFiles);
|
const outputFileIds = await consumeFiles(inputFileIds, processedFiles);
|
||||||
|
|
||||||
// Store operation data for undo (only store what we need to avoid memory bloat)
|
// Store operation data for undo (only store what we need to avoid memory bloat)
|
||||||
lastOperationRef.current = {
|
lastOperationRef.current = {
|
||||||
inputFiles: validFiles, // Keep original File objects for undo
|
inputFiles: validFiles, // Keep original File objects for undo
|
||||||
@ -341,17 +341,17 @@ export const useToolOperation = <TParams>(
|
|||||||
try {
|
try {
|
||||||
// Undo the consume operation
|
// Undo the consume operation
|
||||||
await undoConsumeFiles(inputFiles, inputFileRecords, outputFileIds);
|
await undoConsumeFiles(inputFiles, inputFileRecords, outputFileIds);
|
||||||
|
|
||||||
// Clear results and operation tracking
|
// Clear results and operation tracking
|
||||||
resetResults();
|
resetResults();
|
||||||
lastOperationRef.current = null;
|
lastOperationRef.current = null;
|
||||||
|
|
||||||
// Show success message
|
// Show success message
|
||||||
actions.setStatus(t('undoSuccess', 'Operation undone successfully'));
|
actions.setStatus(t('undoSuccess', 'Operation undone successfully'));
|
||||||
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
let errorMessage = extractErrorMessage(error);
|
let errorMessage = extractErrorMessage(error);
|
||||||
|
|
||||||
// Provide more specific error messages based on error type
|
// Provide more specific error messages based on error type
|
||||||
if (error.message?.includes('Mismatch between input files')) {
|
if (error.message?.includes('Mismatch between input files')) {
|
||||||
errorMessage = t('undoDataMismatch', 'Cannot undo: operation data is corrupted');
|
errorMessage = t('undoDataMismatch', 'Cannot undo: operation data is corrupted');
|
||||||
@ -360,9 +360,9 @@ export const useToolOperation = <TParams>(
|
|||||||
} else if (error.name === 'QuotaExceededError') {
|
} else if (error.name === 'QuotaExceededError') {
|
||||||
errorMessage = t('undoQuotaError', 'Cannot undo: insufficient storage space');
|
errorMessage = t('undoQuotaError', 'Cannot undo: insufficient storage space');
|
||||||
}
|
}
|
||||||
|
|
||||||
actions.setError(`${t('undoFailed', 'Failed to undo operation')}: ${errorMessage}`);
|
actions.setError(`${t('undoFailed', 'Failed to undo operation')}: ${errorMessage}`);
|
||||||
|
|
||||||
// Don't clear the operation data if undo failed - user might want to try again
|
// Don't clear the operation data if undo failed - user might want to try again
|
||||||
}
|
}
|
||||||
}, [undoConsumeFiles, resetResults, actions, t]);
|
}, [undoConsumeFiles, resetResults, actions, t]);
|
||||||
|
@ -16,7 +16,6 @@ export const singleLargePageOperationConfig = {
|
|||||||
buildFormData: buildSingleLargePageFormData,
|
buildFormData: buildSingleLargePageFormData,
|
||||||
operationType: 'single-large-page',
|
operationType: 'single-large-page',
|
||||||
endpoint: '/api/v1/general/pdf-to-single-page',
|
endpoint: '/api/v1/general/pdf-to-single-page',
|
||||||
filePrefix: 'single_page_', // Will be overridden in hook with translation
|
|
||||||
defaultParameters,
|
defaultParameters,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
@ -25,7 +24,6 @@ export const useSingleLargePageOperation = () => {
|
|||||||
|
|
||||||
return useToolOperation<SingleLargePageParameters>({
|
return useToolOperation<SingleLargePageParameters>({
|
||||||
...singleLargePageOperationConfig,
|
...singleLargePageOperationConfig,
|
||||||
filePrefix: t('pdfToSinglePage.filenamePrefix', 'single_page') + '_',
|
|
||||||
getErrorMessage: createStandardErrorHandler(t('pdfToSinglePage.error.failed', 'An error occurred while converting to single page.'))
|
getErrorMessage: createStandardErrorHandler(t('pdfToSinglePage.error.failed', 'An error occurred while converting to single page.'))
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -61,7 +61,6 @@ export const splitOperationConfig = {
|
|||||||
buildFormData: buildSplitFormData,
|
buildFormData: buildSplitFormData,
|
||||||
operationType: 'splitPdf',
|
operationType: 'splitPdf',
|
||||||
endpoint: getSplitEndpoint,
|
endpoint: getSplitEndpoint,
|
||||||
filePrefix: 'split_',
|
|
||||||
defaultParameters,
|
defaultParameters,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
@ -16,7 +16,6 @@ export const unlockPdfFormsOperationConfig = {
|
|||||||
buildFormData: buildUnlockPdfFormsFormData,
|
buildFormData: buildUnlockPdfFormsFormData,
|
||||||
operationType: 'unlock-pdf-forms',
|
operationType: 'unlock-pdf-forms',
|
||||||
endpoint: '/api/v1/misc/unlock-pdf-forms',
|
endpoint: '/api/v1/misc/unlock-pdf-forms',
|
||||||
filePrefix: 'unlocked_forms_', // Will be overridden in hook with translation
|
|
||||||
defaultParameters,
|
defaultParameters,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
@ -25,7 +24,6 @@ export const useUnlockPdfFormsOperation = () => {
|
|||||||
|
|
||||||
return useToolOperation<UnlockPdfFormsParameters>({
|
return useToolOperation<UnlockPdfFormsParameters>({
|
||||||
...unlockPdfFormsOperationConfig,
|
...unlockPdfFormsOperationConfig,
|
||||||
filePrefix: t('unlockPDFForms.filenamePrefix', 'unlocked_forms') + '_',
|
|
||||||
getErrorMessage: createStandardErrorHandler(t('unlockPDFForms.error.failed', 'An error occurred while unlocking PDF forms.'))
|
getErrorMessage: createStandardErrorHandler(t('unlockPDFForms.error.failed', 'An error occurred while unlocking PDF forms.'))
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -6,11 +6,12 @@ export type ResponseHandler = (blob: Blob, originalFiles: File[]) => Promise<Fil
|
|||||||
* Processes a blob response into File(s).
|
* Processes a blob response into File(s).
|
||||||
* - If a tool-specific responseHandler is provided, it is used.
|
* - If a tool-specific responseHandler is provided, it is used.
|
||||||
* - Otherwise, create a single file using the filePrefix + original name.
|
* - Otherwise, create a single file using the filePrefix + original name.
|
||||||
|
* - If filePrefix is empty, preserves the original filename.
|
||||||
*/
|
*/
|
||||||
export async function processResponse(
|
export async function processResponse(
|
||||||
blob: Blob,
|
blob: Blob,
|
||||||
originalFiles: File[],
|
originalFiles: File[],
|
||||||
filePrefix: string,
|
filePrefix?: string,
|
||||||
responseHandler?: ResponseHandler
|
responseHandler?: ResponseHandler
|
||||||
): Promise<File[]> {
|
): Promise<File[]> {
|
||||||
if (responseHandler) {
|
if (responseHandler) {
|
||||||
@ -19,7 +20,8 @@ export async function processResponse(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const original = originalFiles[0]?.name ?? 'result.pdf';
|
const original = originalFiles[0]?.name ?? 'result.pdf';
|
||||||
const name = `${filePrefix}${original}`;
|
// Only add prefix if it's not empty - this preserves original filenames for file history
|
||||||
|
const name = filePrefix ? `${filePrefix}${original}` : original;
|
||||||
const type = blob.type || 'application/octet-stream';
|
const type = blob.type || 'application/octet-stream';
|
||||||
return [new File([blob], name, { type })];
|
return [new File([blob], name, { type })];
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user