Feature/v2/get all info on pdf (#5105)

# Description of Changes

- Addition of the get all info on PDF tool

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
This commit is contained in:
EthanHealy01
2025-12-03 20:02:42 +00:00
committed by GitHub
parent e59c717dc0
commit f8dbf171e1
19 changed files with 1526 additions and 8 deletions
@@ -0,0 +1,194 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import apiClient from '@app/services/apiClient';
import { useFileContext } from '@app/contexts/file/fileHooks';
import { ToolOperationHook } from '@app/hooks/tools/shared/useToolOperation';
import type { StirlingFile } from '@app/types/fileContext';
import { extractErrorMessage } from '@app/utils/toolErrorHandler';
import {
PdfInfoReportEntry,
INFO_JSON_FILENAME,
} from '@app/types/getPdfInfo';
import type { GetPdfInfoParameters } from '@app/hooks/tools/getPdfInfo/useGetPdfInfoParameters';
export interface GetPdfInfoOperationHook extends ToolOperationHook<GetPdfInfoParameters> {
results: PdfInfoReportEntry[];
}
export const useGetPdfInfoOperation = (): GetPdfInfoOperationHook => {
const { t } = useTranslation();
const { selectors } = useFileContext();
const [isLoading, setIsLoading] = useState(false);
const [status, setStatus] = useState('');
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const [files, setFiles] = useState<File[]>([]);
const [downloadUrl, setDownloadUrl] = useState<string | null>(null);
const [downloadFilename, setDownloadFilename] = useState('');
const [results, setResults] = useState<PdfInfoReportEntry[]>([]);
const cancelRequested = useRef(false);
const previousUrl = useRef<string | null>(null);
const cleanupDownloadUrl = useCallback(() => {
if (previousUrl.current) {
URL.revokeObjectURL(previousUrl.current);
previousUrl.current = null;
}
}, []);
const resetResults = useCallback(() => {
cancelRequested.current = false;
setResults([]);
setFiles([]);
cleanupDownloadUrl();
setDownloadUrl(null);
setDownloadFilename('');
setStatus('');
setErrorMessage(null);
}, [cleanupDownloadUrl]);
const clearError = useCallback(() => {
setErrorMessage(null);
}, []);
const executeOperation = useCallback(
async (_params: GetPdfInfoParameters, selectedFiles: StirlingFile[]) => {
if (selectedFiles.length === 0) {
setErrorMessage(t('noFileSelected', 'No files selected'));
return;
}
cancelRequested.current = false;
setIsLoading(true);
setStatus(t('getPdfInfo.processing', 'Extracting information...'));
setErrorMessage(null);
setResults([]);
setFiles([]);
cleanupDownloadUrl();
setDownloadUrl(null);
setDownloadFilename('');
try {
const aggregated: PdfInfoReportEntry[] = [];
const generatedAt = Date.now();
for (const file of selectedFiles) {
if (cancelRequested.current) break;
const formData = new FormData();
formData.append('fileInput', file);
try {
const response = await apiClient.post('/api/v1/security/get-info-on-pdf', formData, {
headers: { 'Content-Type': 'multipart/form-data' },
});
const stub = selectors.getStirlingFileStub(file.fileId);
const entry: PdfInfoReportEntry = {
fileId: file.fileId,
fileName: file.name,
fileSize: file.size ?? null,
lastModified: file.lastModified ?? null,
thumbnailUrl: stub?.thumbnailUrl ?? null,
data: response.data ?? {},
error: null,
summaryGeneratedAt: generatedAt,
};
aggregated.push(entry);
} catch (error) {
const stub = selectors.getStirlingFileStub(file.fileId);
aggregated.push({
fileId: file.fileId,
fileName: file.name,
fileSize: file.size ?? null,
lastModified: file.lastModified ?? null,
thumbnailUrl: stub?.thumbnailUrl ?? null,
data: {},
error: extractErrorMessage(error),
summaryGeneratedAt: generatedAt,
});
}
}
if (!cancelRequested.current) {
setResults(aggregated);
if (aggregated.length > 0) {
// Build V1-compatible JSON: use backend payloads directly.
const payloads = aggregated
.filter((e) => !e.error)
.map((e) => e.data);
const content = payloads.length === 1 ? payloads[0] : payloads;
const json = JSON.stringify(content, null, 2);
const resultFile = new File([json], INFO_JSON_FILENAME, { type: 'application/json' });
setFiles([resultFile]);
}
const anyError = aggregated.some((item) => item.error);
if (anyError) {
setErrorMessage(t('getPdfInfo.error.partial', 'Some files could not be processed.'));
}
setStatus(t('getPdfInfo.status.complete', 'Extraction complete'));
}
} catch (e) {
console.error('[getPdfInfo] unexpected failure', e);
setErrorMessage(t('getPdfInfo.error.unexpected', 'Unexpected error during extraction.'));
} finally {
setIsLoading(false);
}
},
[cleanupDownloadUrl, selectors, t]
);
const cancelOperation = useCallback(() => {
if (isLoading) {
cancelRequested.current = true;
setIsLoading(false);
setStatus(t('operationCancelled', 'Operation cancelled'));
}
}, [isLoading, t]);
const undoOperation = useCallback(async () => {
resetResults();
}, [resetResults]);
useEffect(() => {
return () => {
cleanupDownloadUrl();
};
}, [cleanupDownloadUrl]);
return useMemo<GetPdfInfoOperationHook>(
() => ({
files,
thumbnails: [],
isGeneratingThumbnails: false,
downloadUrl,
downloadFilename,
isLoading,
status,
errorMessage,
progress: null,
executeOperation,
resetResults,
clearError,
cancelOperation,
undoOperation,
results,
}),
[
cancelOperation,
clearError,
downloadFilename,
downloadUrl,
errorMessage,
executeOperation,
files,
isLoading,
resetResults,
results,
status,
]
);
};
@@ -0,0 +1,19 @@
import { BaseParameters } from '@app/types/parameters';
import { useBaseParameters, BaseParametersHook } from '@app/hooks/tools/shared/useBaseParameters';
export interface GetPdfInfoParameters extends BaseParameters {
// No parameters needed
}
export const defaultParameters: GetPdfInfoParameters = {};
export type GetPdfInfoParametersHook = BaseParametersHook<GetPdfInfoParameters>;
export const useGetPdfInfoParameters = (): GetPdfInfoParametersHook => {
return useBaseParameters({
defaultParameters,
endpointName: 'get-info-on-pdf',
});
};