Files
Stirling-PDF/frontend/src/core/services/serverFolderApiService.ts
T
Reece 54b3f0553d feat: server-folder watch folders with SSE auth, TTL cleanup, and on-demand processing
- Add sseToken exchange flow (POST /sse-token → GET /events?sseToken=) replacing JWT-in-URL
- Server folder outputs stay on server; IDB holds only metadata (serverOutputFilenames)
- Trigger immediate processing on upload via POST /server-folder/{id}/process
- Scheduled scan skips session-managed folders (on-demand only)
- Per-folder output TTL with hourly cleanup; delete-on-download fires on local FS export only
- .ready marker prevents partial-upload race in collectFilesForProcessing
- Fix concurrent map mutation in cleanupOldJobs with removeIf
- Update Playwright tests to use sseToken flow and trigger endpoint
2026-03-30 18:50:47 +01:00

121 lines
4.7 KiB
TypeScript

/**
* Client for the server-side Watch Folder API.
*
* These endpoints manage subdirectories under the server's watchedFolders root.
* PipelineDirectoryProcessor scans them every 60 seconds and processes any files found.
*/
import apiClient from '@app/services/apiClient';
import { getSessionId } from '@app/hooks/useSSEConnection';
export interface ServerFolderOutputFile {
filename: string;
size: number;
lastModified: number;
}
/** Create the server-side watch subdirectory and write pipeline.json + session.json. */
export async function createServerFolder(
folderId: string,
name: string,
configJson: string,
outputTtlHours?: number | null,
deleteOutputOnDownload?: boolean
): Promise<void> {
const formData = new FormData();
formData.append('folderId', folderId);
formData.append('name', name);
formData.append('sessionId', getSessionId());
formData.append('json', configJson);
if (outputTtlHours != null) formData.append('outputTtlHours', String(outputTtlHours));
if (deleteOutputOnDownload) formData.append('deleteOutputOnDownload', 'true');
await apiClient.post('/api/v1/pipeline/server-folder', formData);
}
/** Update pipeline.json for an existing server watch folder. */
export async function updateServerFolder(
folderId: string,
name: string,
configJson: string,
outputTtlHours?: number | null,
deleteOutputOnDownload?: boolean
): Promise<void> {
const formData = new FormData();
formData.append('name', name);
formData.append('json', configJson);
if (outputTtlHours != null) formData.append('outputTtlHours', String(outputTtlHours));
if (deleteOutputOnDownload) formData.append('deleteOutputOnDownload', 'true');
await apiClient.put(`/api/v1/pipeline/server-folder/${folderId}`, formData);
}
/**
* Update session.json with the current sessionId.
* Called on mount so SSE notifications reach the current browser session
* even if localStorage was cleared since the folder was created.
*/
export async function updateServerFolderSession(folderId: string): Promise<void> {
await apiClient.put(`/api/v1/pipeline/server-folder/${folderId}/session`, {
sessionId: getSessionId(),
});
}
/** Delete the watch subdirectory and its output folder from the server. */
export async function deleteServerFolder(folderId: string): Promise<void> {
await apiClient.delete(`/api/v1/pipeline/server-folder/${folderId}`);
}
/**
* Upload a file into the server watch folder for PipelineDirectoryProcessor to pick up.
* The file is stored as {@code {fileId}.{ext}} so the backend can include the fileId in the
* SSE completion event — no filename-based matching needed on the frontend.
*/
export async function uploadFileToServerFolder(folderId: string, fileId: string, file: File): Promise<void> {
const formData = new FormData();
formData.append('fileInput', file);
formData.append('fileId', fileId);
await apiClient.post(`/api/v1/pipeline/server-folder/${folderId}/files`, formData);
}
/** List output files that PipelineDirectoryProcessor has written to the finished folder. */
export async function listServerFolderOutput(
folderId: string
): Promise<ServerFolderOutputFile[]> {
const response = await apiClient.get<ServerFolderOutputFile[]>(
`/api/v1/pipeline/server-folder/${folderId}/output`
);
return response.data;
}
/**
* Trigger immediate async processing for a server watch folder.
* Called after uploading a file so it doesn't wait for the 60-second scheduled scan.
*/
export async function triggerServerFolderProcessing(folderId: string): Promise<void> {
await apiClient.post(`/api/v1/pipeline/server-folder/${folderId}/process`);
}
/** Delete a specific output file from the server's processed/ dir (used with deleteOutputOnDownload). */
export async function deleteServerFolderOutput(folderId: string, filename: string): Promise<void> {
await apiClient.delete(`/api/v1/pipeline/server-folder/${folderId}/output/${encodeURIComponent(filename)}`);
}
/** Download a specific output file from the finished folder. */
export async function downloadServerFolderOutput(
folderId: string,
filename: string
): Promise<File> {
const response = await apiClient.get<Blob>(
`/api/v1/pipeline/server-folder/${folderId}/output/${encodeURIComponent(filename)}`,
{ responseType: 'blob' }
);
// Prefer the server's Content-Type; fall back to extension-based detection since
// some environments serve blobs as application/octet-stream regardless of the file type.
const mimeType = (response.data.type && response.data.type !== 'application/octet-stream')
? response.data.type
: filename.toLowerCase().endsWith('.pdf') ? 'application/pdf' : response.data.type;
return new File([response.data], filename, {
type: mimeType,
lastModified: Date.now(),
});
}