mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-15 19:10:47 +02:00
Feature/v2/filehistory (#4370)
File History --------- Co-authored-by: Connor Yoh <[email protected]>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { FileMetadata } from '../types/file';
|
||||
import { StirlingFileStub } from '../types/fileContext';
|
||||
import { fileStorage } from '../services/fileStorage';
|
||||
import { zipFileService } from '../services/zipFileService';
|
||||
|
||||
@@ -9,14 +9,14 @@ import { zipFileService } from '../services/zipFileService';
|
||||
*/
|
||||
export function downloadBlob(blob: Blob, filename: string): void {
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = filename;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
|
||||
|
||||
// Clean up the blob URL
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
@@ -26,23 +26,23 @@ export function downloadBlob(blob: Blob, filename: string): void {
|
||||
* @param file - The file object with storage information
|
||||
* @throws Error if file cannot be retrieved from storage
|
||||
*/
|
||||
export async function downloadFileFromStorage(file: FileMetadata): Promise<void> {
|
||||
export async function downloadFileFromStorage(file: StirlingFileStub): Promise<void> {
|
||||
const lookupKey = file.id;
|
||||
const storedFile = await fileStorage.getFile(lookupKey);
|
||||
|
||||
if (!storedFile) {
|
||||
const stirlingFile = await fileStorage.getStirlingFile(lookupKey);
|
||||
|
||||
if (!stirlingFile) {
|
||||
throw new Error(`File "${file.name}" not found in storage`);
|
||||
}
|
||||
|
||||
const blob = new Blob([storedFile.data], { type: storedFile.type });
|
||||
downloadBlob(blob, storedFile.name);
|
||||
|
||||
// StirlingFile is already a File object, just download it
|
||||
downloadBlob(stirlingFile, stirlingFile.name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads multiple files as individual downloads
|
||||
* @param files - Array of files to download
|
||||
*/
|
||||
export async function downloadMultipleFiles(files: FileMetadata[]): Promise<void> {
|
||||
export async function downloadMultipleFiles(files: StirlingFileStub[]): Promise<void> {
|
||||
for (const file of files) {
|
||||
await downloadFileFromStorage(file);
|
||||
}
|
||||
@@ -53,36 +53,33 @@ export async function downloadMultipleFiles(files: FileMetadata[]): Promise<void
|
||||
* @param files - Array of files to include in ZIP
|
||||
* @param zipFilename - Optional custom ZIP filename (defaults to timestamped name)
|
||||
*/
|
||||
export async function downloadFilesAsZip(files: FileMetadata[], zipFilename?: string): Promise<void> {
|
||||
export async function downloadFilesAsZip(files: StirlingFileStub[], zipFilename?: string): Promise<void> {
|
||||
if (files.length === 0) {
|
||||
throw new Error('No files provided for ZIP download');
|
||||
}
|
||||
|
||||
// Convert stored files to File objects
|
||||
const fileObjects: File[] = [];
|
||||
const filesToZip: File[] = [];
|
||||
for (const fileWithUrl of files) {
|
||||
const lookupKey = fileWithUrl.id;
|
||||
const storedFile = await fileStorage.getFile(lookupKey);
|
||||
|
||||
if (storedFile) {
|
||||
const file = new File([storedFile.data], storedFile.name, {
|
||||
type: storedFile.type,
|
||||
lastModified: storedFile.lastModified
|
||||
});
|
||||
fileObjects.push(file);
|
||||
const stirlingFile = await fileStorage.getStirlingFile(lookupKey);
|
||||
|
||||
if (stirlingFile) {
|
||||
// StirlingFile is already a File object!
|
||||
filesToZip.push(stirlingFile);
|
||||
}
|
||||
}
|
||||
|
||||
if (fileObjects.length === 0) {
|
||||
|
||||
if (filesToZip.length === 0) {
|
||||
throw new Error('No valid files found in storage for ZIP download');
|
||||
}
|
||||
|
||||
// Generate default filename if not provided
|
||||
const finalZipFilename = zipFilename ||
|
||||
const finalZipFilename = zipFilename ||
|
||||
`files-${new Date().toISOString().slice(0, 19).replace(/[:-]/g, '')}.zip`;
|
||||
|
||||
|
||||
// Create and download ZIP
|
||||
const { zipFile } = await zipFileService.createZipFromFiles(fileObjects, finalZipFilename);
|
||||
const { zipFile } = await zipFileService.createZipFromFiles(filesToZip, finalZipFilename);
|
||||
downloadBlob(zipFile, finalZipFilename);
|
||||
}
|
||||
|
||||
@@ -94,7 +91,7 @@ export async function downloadFilesAsZip(files: FileMetadata[], zipFilename?: st
|
||||
* @param options - Download options
|
||||
*/
|
||||
export async function downloadFiles(
|
||||
files: FileMetadata[],
|
||||
files: StirlingFileStub[],
|
||||
options: {
|
||||
forceZip?: boolean;
|
||||
zipFilename?: string;
|
||||
@@ -133,8 +130,8 @@ export function downloadFileObject(file: File, filename?: string): void {
|
||||
* @param mimeType - MIME type (defaults to text/plain)
|
||||
*/
|
||||
export function downloadTextAsFile(
|
||||
content: string,
|
||||
filename: string,
|
||||
content: string,
|
||||
filename: string,
|
||||
mimeType: string = 'text/plain'
|
||||
): void {
|
||||
const blob = new Blob([content], { type: mimeType });
|
||||
@@ -149,4 +146,4 @@ export function downloadTextAsFile(
|
||||
export function downloadJsonAsFile(data: any, filename: string): void {
|
||||
const content = JSON.stringify(data, null, 2);
|
||||
downloadTextAsFile(content, filename, 'application/json');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* File History Utilities
|
||||
*
|
||||
* Helper functions for IndexedDB-based file history management.
|
||||
* Handles file history operations and lineage tracking.
|
||||
*/
|
||||
import { StirlingFileStub } from '../types/fileContext';
|
||||
|
||||
/**
|
||||
* Group files by processing branches - each branch ends in a leaf file
|
||||
* Returns Map<fileId, lineagePath[]> where fileId is the leaf and lineagePath is the path back to original
|
||||
*/
|
||||
export function groupFilesByOriginal(StirlingFileStubs: StirlingFileStub[]): Map<string, StirlingFileStub[]> {
|
||||
const groups = new Map<string, StirlingFileStub[]>();
|
||||
|
||||
// Create a map for quick lookups
|
||||
const fileMap = new Map<string, StirlingFileStub>();
|
||||
for (const record of StirlingFileStubs) {
|
||||
fileMap.set(record.id, record);
|
||||
}
|
||||
|
||||
// Find leaf files (files that are not parents of any other files AND have version history)
|
||||
// Original files (v0) should only be leaves if they have no processed versions at all
|
||||
const leafFiles = StirlingFileStubs.filter(stub => {
|
||||
const isParentOfOthers = StirlingFileStubs.some(otherStub => otherStub.parentFileId === stub.id);
|
||||
const isOriginalOfOthers = StirlingFileStubs.some(otherStub => otherStub.originalFileId === stub.id);
|
||||
|
||||
// A file is a leaf if:
|
||||
// 1. It's not a parent of any other files, AND
|
||||
// 2. It has processing history (versionNumber > 0) OR it's not referenced as original by others
|
||||
return !isParentOfOthers && (stub.versionNumber && stub.versionNumber > 0 || !isOriginalOfOthers);
|
||||
});
|
||||
|
||||
// For each leaf file, build its complete lineage path back to original
|
||||
for (const leafFile of leafFiles) {
|
||||
const lineagePath: StirlingFileStub[] = [];
|
||||
let currentFile: StirlingFileStub | undefined = leafFile;
|
||||
|
||||
// Trace back through parentFileId chain to build this specific branch
|
||||
while (currentFile) {
|
||||
lineagePath.push(currentFile);
|
||||
|
||||
// Move to parent file in this branch
|
||||
let nextFile: StirlingFileStub | undefined = undefined;
|
||||
|
||||
if (currentFile.parentFileId) {
|
||||
nextFile = fileMap.get(currentFile.parentFileId);
|
||||
} else if (currentFile.originalFileId && currentFile.originalFileId !== currentFile.id) {
|
||||
// For v1 files, the original file might be referenced by originalFileId
|
||||
nextFile = fileMap.get(currentFile.originalFileId);
|
||||
}
|
||||
|
||||
// Check for infinite loops before moving to next
|
||||
if (nextFile && lineagePath.some(file => file.id === nextFile!.id)) {
|
||||
break;
|
||||
}
|
||||
|
||||
currentFile = nextFile;
|
||||
}
|
||||
|
||||
// Sort lineage with latest version first (leaf at top)
|
||||
lineagePath.sort((a, b) => (b.versionNumber || 0) - (a.versionNumber || 0));
|
||||
|
||||
// Use leaf file ID as the group key - each branch gets its own group
|
||||
groups.set(leafFile.id, lineagePath);
|
||||
}
|
||||
|
||||
return groups;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a file has version history
|
||||
*/
|
||||
export function hasVersionHistory(fileStub: StirlingFileStub): boolean {
|
||||
return !!(fileStub.originalFileId && fileStub.versionNumber && fileStub.versionNumber > 0);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import { FileOperation } from '../types/fileContext';
|
||||
*/
|
||||
export const createOperation = <TParams = void>(
|
||||
operationType: string,
|
||||
params: TParams,
|
||||
_params: TParams,
|
||||
selectedFiles: File[]
|
||||
): { operation: FileOperation; operationId: string; fileId: FileId } => {
|
||||
const operationId = `${operationType}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
@@ -20,7 +20,6 @@ export const createOperation = <TParams = void>(
|
||||
status: 'pending',
|
||||
metadata: {
|
||||
originalFileName: selectedFiles[0]?.name,
|
||||
parameters: params,
|
||||
fileSize: selectedFiles.reduce((sum, f) => sum + f.size, 0)
|
||||
}
|
||||
} as any /* FIX ME*/;
|
||||
|
||||
@@ -8,11 +8,12 @@ export type ResponseHandler = (blob: Blob, originalFiles: File[]) => Promise<Fil
|
||||
* - If a tool-specific responseHandler is provided, it is used.
|
||||
* - If responseHeaders provided and contains Content-Disposition, uses that filename.
|
||||
* - Otherwise, create a single file using the filePrefix + original name.
|
||||
* - If filePrefix is empty, preserves the original filename.
|
||||
*/
|
||||
export async function processResponse(
|
||||
blob: Blob,
|
||||
originalFiles: File[],
|
||||
filePrefix: string,
|
||||
filePrefix?: string,
|
||||
responseHandler?: ResponseHandler,
|
||||
responseHeaders?: Record<string, any>
|
||||
): Promise<File[]> {
|
||||
@@ -36,7 +37,13 @@ export async function processResponse(
|
||||
|
||||
// Default behavior: use filePrefix + original name
|
||||
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';
|
||||
return [new File([blob], name, { type })];
|
||||
|
||||
// File was modified by tool processing - set lastModified to current time
|
||||
return [new File([blob], name, {
|
||||
type,
|
||||
lastModified: Date.now()
|
||||
})];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user