2025-06-05 11:12:39 +01:00
|
|
|
/**
|
|
|
|
* IndexedDB File Storage Service
|
|
|
|
* Provides high-capacity file storage for PDF processing
|
2025-08-21 17:30:26 +01:00
|
|
|
* Now uses centralized IndexedDB manager
|
2025-06-05 11:12:39 +01:00
|
|
|
*/
|
|
|
|
|
2025-08-28 10:56:07 +01:00
|
|
|
import { FileId } from '../types/file';
|
2025-08-21 17:30:26 +01:00
|
|
|
import { indexedDBManager, DATABASE_CONFIGS } from './indexedDBManager';
|
|
|
|
|
2025-06-05 11:12:39 +01:00
|
|
|
export interface StoredFile {
|
2025-08-28 10:56:07 +01:00
|
|
|
id: FileId;
|
2025-06-05 11:12:39 +01:00
|
|
|
name: string;
|
|
|
|
type: string;
|
|
|
|
size: number;
|
|
|
|
lastModified: number;
|
|
|
|
data: ArrayBuffer;
|
|
|
|
thumbnail?: string;
|
|
|
|
url?: string; // For compatibility with existing components
|
2025-09-04 11:26:55 +01:00
|
|
|
isLeaf?: boolean; // True if this file is a leaf node (hasn't been processed yet)
|
2025-06-05 11:12:39 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
export interface StorageStats {
|
|
|
|
used: number;
|
|
|
|
available: number;
|
|
|
|
fileCount: number;
|
|
|
|
quota?: number;
|
|
|
|
}
|
|
|
|
|
|
|
|
class FileStorageService {
|
2025-08-21 17:30:26 +01:00
|
|
|
private readonly dbConfig = DATABASE_CONFIGS.FILES;
|
|
|
|
private readonly storeName = 'files';
|
2025-06-05 11:12:39 +01:00
|
|
|
|
|
|
|
/**
|
2025-08-21 17:30:26 +01:00
|
|
|
* Get database connection using centralized manager
|
2025-06-05 11:12:39 +01:00
|
|
|
*/
|
2025-08-21 17:30:26 +01:00
|
|
|
private async getDatabase(): Promise<IDBDatabase> {
|
|
|
|
return indexedDBManager.openDatabase(this.dbConfig);
|
2025-06-05 11:12:39 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2025-08-21 17:30:26 +01:00
|
|
|
* Store a file in IndexedDB with external UUID
|
2025-06-05 11:12:39 +01:00
|
|
|
*/
|
2025-09-04 11:26:55 +01:00
|
|
|
async storeFile(file: File, fileId: FileId, thumbnail?: string, isLeaf: boolean = true): Promise<StoredFile> {
|
2025-08-21 17:30:26 +01:00
|
|
|
const db = await this.getDatabase();
|
2025-06-05 11:12:39 +01:00
|
|
|
|
|
|
|
const arrayBuffer = await file.arrayBuffer();
|
2025-08-11 09:16:16 +01:00
|
|
|
|
2025-06-05 11:12:39 +01:00
|
|
|
const storedFile: StoredFile = {
|
2025-08-21 17:30:26 +01:00
|
|
|
id: fileId, // Use provided UUID
|
2025-06-05 11:12:39 +01:00
|
|
|
name: file.name,
|
|
|
|
type: file.type,
|
|
|
|
size: file.size,
|
|
|
|
lastModified: file.lastModified,
|
|
|
|
data: arrayBuffer,
|
2025-09-04 11:26:55 +01:00
|
|
|
thumbnail,
|
|
|
|
isLeaf
|
2025-06-05 11:12:39 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
try {
|
2025-08-21 17:30:26 +01:00
|
|
|
const transaction = db.transaction([this.storeName], 'readwrite');
|
2025-06-05 11:12:39 +01:00
|
|
|
const store = transaction.objectStore(this.storeName);
|
2025-08-11 09:16:16 +01:00
|
|
|
|
2025-06-05 11:12:39 +01:00
|
|
|
// Debug logging
|
|
|
|
console.log('Object store keyPath:', store.keyPath);
|
2025-08-21 17:30:26 +01:00
|
|
|
console.log('Storing file with UUID:', {
|
|
|
|
id: storedFile.id, // Now a UUID from FileContext
|
2025-08-11 09:16:16 +01:00
|
|
|
name: storedFile.name,
|
2025-06-05 11:12:39 +01:00
|
|
|
hasData: !!storedFile.data,
|
2025-09-04 11:26:55 +01:00
|
|
|
dataSize: storedFile.data.byteLength,
|
|
|
|
isLeaf: storedFile.isLeaf
|
2025-06-05 11:12:39 +01:00
|
|
|
});
|
2025-08-11 09:16:16 +01:00
|
|
|
|
2025-06-05 11:12:39 +01:00
|
|
|
const request = store.add(storedFile);
|
|
|
|
|
|
|
|
request.onerror = () => {
|
|
|
|
console.error('IndexedDB add error:', request.error);
|
|
|
|
console.error('Failed object:', storedFile);
|
|
|
|
reject(request.error);
|
|
|
|
};
|
|
|
|
request.onsuccess = () => {
|
|
|
|
console.log('File stored successfully with ID:', storedFile.id);
|
|
|
|
resolve(storedFile);
|
|
|
|
};
|
|
|
|
} catch (error) {
|
|
|
|
console.error('Transaction error:', error);
|
|
|
|
reject(error);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Retrieve a file from IndexedDB
|
|
|
|
*/
|
2025-08-28 10:56:07 +01:00
|
|
|
async getFile(id: FileId): Promise<StoredFile | null> {
|
2025-08-21 17:30:26 +01:00
|
|
|
const db = await this.getDatabase();
|
2025-06-05 11:12:39 +01:00
|
|
|
|
|
|
|
return new Promise((resolve, reject) => {
|
2025-08-21 17:30:26 +01:00
|
|
|
const transaction = db.transaction([this.storeName], 'readonly');
|
2025-06-05 11:12:39 +01:00
|
|
|
const store = transaction.objectStore(this.storeName);
|
|
|
|
const request = store.get(id);
|
|
|
|
|
|
|
|
request.onerror = () => reject(request.error);
|
|
|
|
request.onsuccess = () => resolve(request.result || null);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get all stored files (WARNING: loads all data into memory)
|
|
|
|
*/
|
|
|
|
async getAllFiles(): Promise<StoredFile[]> {
|
2025-08-21 17:30:26 +01:00
|
|
|
const db = await this.getDatabase();
|
2025-06-05 11:12:39 +01:00
|
|
|
|
|
|
|
return new Promise((resolve, reject) => {
|
2025-08-21 17:30:26 +01:00
|
|
|
const transaction = db.transaction([this.storeName], 'readonly');
|
2025-06-05 11:12:39 +01:00
|
|
|
const store = transaction.objectStore(this.storeName);
|
|
|
|
const request = store.getAll();
|
|
|
|
|
|
|
|
request.onerror = () => reject(request.error);
|
|
|
|
request.onsuccess = () => {
|
|
|
|
// Filter out null/corrupted entries
|
2025-08-11 09:16:16 +01:00
|
|
|
const files = request.result.filter(file =>
|
|
|
|
file &&
|
|
|
|
file.data &&
|
|
|
|
file.name &&
|
2025-06-05 11:12:39 +01:00
|
|
|
typeof file.size === 'number'
|
|
|
|
);
|
|
|
|
resolve(files);
|
|
|
|
};
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get metadata of all stored files (without loading data into memory)
|
|
|
|
*/
|
|
|
|
async getAllFileMetadata(): Promise<Omit<StoredFile, 'data'>[]> {
|
2025-08-21 17:30:26 +01:00
|
|
|
const db = await this.getDatabase();
|
2025-06-05 11:12:39 +01:00
|
|
|
|
|
|
|
return new Promise((resolve, reject) => {
|
2025-08-21 17:30:26 +01:00
|
|
|
const transaction = db.transaction([this.storeName], 'readonly');
|
2025-06-05 11:12:39 +01:00
|
|
|
const store = transaction.objectStore(this.storeName);
|
|
|
|
const request = store.openCursor();
|
|
|
|
const files: Omit<StoredFile, 'data'>[] = [];
|
|
|
|
|
|
|
|
request.onerror = () => reject(request.error);
|
|
|
|
request.onsuccess = (event) => {
|
|
|
|
const cursor = (event.target as IDBRequest).result;
|
|
|
|
if (cursor) {
|
|
|
|
const storedFile = cursor.value;
|
|
|
|
// Only extract metadata, skip the data field
|
|
|
|
if (storedFile && storedFile.name && typeof storedFile.size === 'number') {
|
|
|
|
files.push({
|
|
|
|
id: storedFile.id,
|
|
|
|
name: storedFile.name,
|
|
|
|
type: storedFile.type,
|
|
|
|
size: storedFile.size,
|
|
|
|
lastModified: storedFile.lastModified,
|
|
|
|
thumbnail: storedFile.thumbnail
|
|
|
|
});
|
|
|
|
}
|
|
|
|
cursor.continue();
|
|
|
|
} else {
|
2025-08-21 17:30:26 +01:00
|
|
|
// Metadata loaded efficiently without file data
|
2025-06-05 11:12:39 +01:00
|
|
|
resolve(files);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Delete a file from IndexedDB
|
|
|
|
*/
|
2025-08-28 10:56:07 +01:00
|
|
|
async deleteFile(id: FileId): Promise<void> {
|
2025-08-21 17:30:26 +01:00
|
|
|
const db = await this.getDatabase();
|
2025-06-05 11:12:39 +01:00
|
|
|
|
|
|
|
return new Promise((resolve, reject) => {
|
2025-08-21 17:30:26 +01:00
|
|
|
const transaction = db.transaction([this.storeName], 'readwrite');
|
2025-06-05 11:12:39 +01:00
|
|
|
const store = transaction.objectStore(this.storeName);
|
|
|
|
const request = store.delete(id);
|
|
|
|
|
|
|
|
request.onerror = () => reject(request.error);
|
|
|
|
request.onsuccess = () => resolve();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2025-08-08 15:15:09 +01:00
|
|
|
/**
|
|
|
|
* Update the lastModified timestamp of a file (for most recently used sorting)
|
|
|
|
*/
|
2025-08-28 10:56:07 +01:00
|
|
|
async touchFile(id: FileId): Promise<boolean> {
|
2025-08-21 17:30:26 +01:00
|
|
|
const db = await this.getDatabase();
|
2025-08-08 15:15:09 +01:00
|
|
|
return new Promise((resolve, reject) => {
|
2025-08-21 17:30:26 +01:00
|
|
|
const transaction = db.transaction([this.storeName], 'readwrite');
|
2025-08-08 15:15:09 +01:00
|
|
|
const store = transaction.objectStore(this.storeName);
|
2025-08-28 10:56:07 +01:00
|
|
|
|
2025-08-08 15:15:09 +01:00
|
|
|
const getRequest = store.get(id);
|
|
|
|
getRequest.onsuccess = () => {
|
|
|
|
const file = getRequest.result;
|
|
|
|
if (file) {
|
|
|
|
// Update lastModified to current timestamp
|
|
|
|
file.lastModified = Date.now();
|
|
|
|
const updateRequest = store.put(file);
|
|
|
|
updateRequest.onsuccess = () => resolve(true);
|
|
|
|
updateRequest.onerror = () => reject(updateRequest.error);
|
|
|
|
} else {
|
|
|
|
resolve(false); // File not found
|
|
|
|
}
|
|
|
|
};
|
|
|
|
getRequest.onerror = () => reject(getRequest.error);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2025-09-04 11:26:55 +01:00
|
|
|
/**
|
|
|
|
* Mark a file as no longer being a leaf (it has been processed)
|
|
|
|
*/
|
|
|
|
async markFileAsProcessed(id: FileId): Promise<boolean> {
|
|
|
|
const db = await this.getDatabase();
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
const transaction = db.transaction([this.storeName], 'readwrite');
|
|
|
|
const store = transaction.objectStore(this.storeName);
|
|
|
|
|
|
|
|
const getRequest = store.get(id);
|
|
|
|
getRequest.onsuccess = () => {
|
|
|
|
const file = getRequest.result;
|
|
|
|
if (file) {
|
|
|
|
file.isLeaf = false;
|
|
|
|
const updateRequest = store.put(file);
|
|
|
|
updateRequest.onsuccess = () => resolve(true);
|
|
|
|
updateRequest.onerror = () => reject(updateRequest.error);
|
|
|
|
} else {
|
|
|
|
resolve(false); // File not found
|
|
|
|
}
|
|
|
|
};
|
|
|
|
getRequest.onerror = () => reject(getRequest.error);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get only leaf files (files that haven't been processed yet)
|
|
|
|
*/
|
|
|
|
async getLeafFiles(): Promise<StoredFile[]> {
|
|
|
|
const db = await this.getDatabase();
|
|
|
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
const transaction = db.transaction([this.storeName], 'readonly');
|
|
|
|
const store = transaction.objectStore(this.storeName);
|
|
|
|
const request = store.openCursor();
|
|
|
|
const leafFiles: StoredFile[] = [];
|
|
|
|
|
|
|
|
request.onerror = () => reject(request.error);
|
|
|
|
request.onsuccess = (event) => {
|
|
|
|
const cursor = (event.target as IDBRequest).result;
|
|
|
|
if (cursor) {
|
|
|
|
const storedFile = cursor.value;
|
|
|
|
if (storedFile && storedFile.isLeaf !== false) { // Default to true if undefined
|
|
|
|
leafFiles.push(storedFile);
|
|
|
|
}
|
|
|
|
cursor.continue();
|
|
|
|
} else {
|
|
|
|
resolve(leafFiles);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get metadata of only leaf files (without loading data into memory)
|
|
|
|
*/
|
|
|
|
async getLeafFileMetadata(): Promise<Omit<StoredFile, 'data'>[]> {
|
|
|
|
const db = await this.getDatabase();
|
|
|
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
const transaction = db.transaction([this.storeName], 'readonly');
|
|
|
|
const store = transaction.objectStore(this.storeName);
|
|
|
|
const request = store.openCursor();
|
|
|
|
const files: Omit<StoredFile, 'data'>[] = [];
|
|
|
|
|
|
|
|
request.onerror = () => reject(request.error);
|
|
|
|
request.onsuccess = (event) => {
|
|
|
|
const cursor = (event.target as IDBRequest).result;
|
|
|
|
if (cursor) {
|
|
|
|
const storedFile = cursor.value;
|
|
|
|
// Only include leaf files (default to true if undefined for backward compatibility)
|
|
|
|
if (storedFile && storedFile.name && typeof storedFile.size === 'number' && storedFile.isLeaf !== false) {
|
|
|
|
files.push({
|
|
|
|
id: storedFile.id,
|
|
|
|
name: storedFile.name,
|
|
|
|
type: storedFile.type,
|
|
|
|
size: storedFile.size,
|
|
|
|
lastModified: storedFile.lastModified,
|
|
|
|
thumbnail: storedFile.thumbnail,
|
|
|
|
isLeaf: storedFile.isLeaf
|
|
|
|
});
|
|
|
|
}
|
|
|
|
cursor.continue();
|
|
|
|
} else {
|
|
|
|
resolve(files);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2025-06-05 11:12:39 +01:00
|
|
|
/**
|
|
|
|
* Clear all stored files
|
|
|
|
*/
|
|
|
|
async clearAll(): Promise<void> {
|
2025-08-21 17:30:26 +01:00
|
|
|
const db = await this.getDatabase();
|
2025-06-05 11:12:39 +01:00
|
|
|
|
|
|
|
return new Promise((resolve, reject) => {
|
2025-08-21 17:30:26 +01:00
|
|
|
const transaction = db.transaction([this.storeName], 'readwrite');
|
2025-06-05 11:12:39 +01:00
|
|
|
const store = transaction.objectStore(this.storeName);
|
|
|
|
const request = store.clear();
|
|
|
|
|
|
|
|
request.onerror = () => reject(request.error);
|
|
|
|
request.onsuccess = () => resolve();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get storage statistics (only our IndexedDB usage)
|
|
|
|
*/
|
|
|
|
async getStorageStats(): Promise<StorageStats> {
|
|
|
|
let used = 0;
|
|
|
|
let available = 0;
|
|
|
|
let quota: number | undefined;
|
|
|
|
let fileCount = 0;
|
2025-08-11 09:16:16 +01:00
|
|
|
|
2025-06-05 11:12:39 +01:00
|
|
|
try {
|
|
|
|
// Get browser quota for context
|
|
|
|
if ('storage' in navigator && 'estimate' in navigator.storage) {
|
|
|
|
const estimate = await navigator.storage.estimate();
|
|
|
|
quota = estimate.quota;
|
|
|
|
available = estimate.quota || 0;
|
|
|
|
}
|
2025-08-11 09:16:16 +01:00
|
|
|
|
2025-06-05 11:12:39 +01:00
|
|
|
// Calculate our actual IndexedDB usage from file metadata
|
|
|
|
const files = await this.getAllFileMetadata();
|
|
|
|
used = files.reduce((total, file) => total + (file?.size || 0), 0);
|
|
|
|
fileCount = files.length;
|
2025-08-11 09:16:16 +01:00
|
|
|
|
2025-06-05 11:12:39 +01:00
|
|
|
// Adjust available space
|
|
|
|
if (quota) {
|
|
|
|
available = quota - used;
|
|
|
|
}
|
2025-08-11 09:16:16 +01:00
|
|
|
|
2025-06-05 11:12:39 +01:00
|
|
|
} catch (error) {
|
|
|
|
console.warn('Could not get storage stats:', error);
|
|
|
|
// If we can't read metadata, database might be purged
|
|
|
|
used = 0;
|
|
|
|
fileCount = 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
|
|
used,
|
|
|
|
available,
|
|
|
|
fileCount,
|
|
|
|
quota
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get file count quickly without loading metadata
|
|
|
|
*/
|
|
|
|
async getFileCount(): Promise<number> {
|
2025-08-21 17:30:26 +01:00
|
|
|
const db = await this.getDatabase();
|
2025-06-05 11:12:39 +01:00
|
|
|
|
|
|
|
return new Promise((resolve, reject) => {
|
2025-08-21 17:30:26 +01:00
|
|
|
const transaction = db.transaction([this.storeName], 'readonly');
|
2025-06-05 11:12:39 +01:00
|
|
|
const store = transaction.objectStore(this.storeName);
|
|
|
|
const request = store.count();
|
|
|
|
|
|
|
|
request.onerror = () => reject(request.error);
|
|
|
|
request.onsuccess = () => resolve(request.result);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Check all IndexedDB databases to see if files are in another version
|
|
|
|
*/
|
|
|
|
async debugAllDatabases(): Promise<void> {
|
|
|
|
console.log('=== Checking All IndexedDB Databases ===');
|
2025-08-11 09:16:16 +01:00
|
|
|
|
2025-06-05 11:12:39 +01:00
|
|
|
if ('databases' in indexedDB) {
|
|
|
|
try {
|
|
|
|
const databases = await indexedDB.databases();
|
|
|
|
console.log('Found databases:', databases);
|
2025-08-11 09:16:16 +01:00
|
|
|
|
2025-06-05 11:12:39 +01:00
|
|
|
for (const dbInfo of databases) {
|
|
|
|
if (dbInfo.name?.includes('stirling') || dbInfo.name?.includes('pdf')) {
|
|
|
|
console.log(`Checking database: ${dbInfo.name} (version: ${dbInfo.version})`);
|
|
|
|
try {
|
|
|
|
const db = await new Promise<IDBDatabase>((resolve, reject) => {
|
|
|
|
const request = indexedDB.open(dbInfo.name!, dbInfo.version);
|
|
|
|
request.onsuccess = () => resolve(request.result);
|
|
|
|
request.onerror = () => reject(request.error);
|
|
|
|
});
|
2025-08-11 09:16:16 +01:00
|
|
|
|
2025-06-05 11:12:39 +01:00
|
|
|
console.log(`Database ${dbInfo.name} object stores:`, Array.from(db.objectStoreNames));
|
|
|
|
db.close();
|
|
|
|
} catch (error) {
|
|
|
|
console.error(`Failed to open database ${dbInfo.name}:`, error);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} catch (error) {
|
|
|
|
console.error('Failed to list databases:', error);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
console.log('indexedDB.databases() not supported');
|
|
|
|
}
|
2025-08-11 09:16:16 +01:00
|
|
|
|
2025-06-05 11:12:39 +01:00
|
|
|
// Also check our specific database with different versions
|
|
|
|
for (let version = 1; version <= 3; version++) {
|
|
|
|
try {
|
2025-08-21 17:30:26 +01:00
|
|
|
console.log(`Trying to open ${this.dbConfig.name} version ${version}...`);
|
2025-06-05 11:12:39 +01:00
|
|
|
const db = await new Promise<IDBDatabase>((resolve, reject) => {
|
2025-08-21 17:30:26 +01:00
|
|
|
const request = indexedDB.open(this.dbConfig.name, version);
|
2025-06-05 11:12:39 +01:00
|
|
|
request.onsuccess = () => resolve(request.result);
|
|
|
|
request.onerror = () => reject(request.error);
|
|
|
|
request.onupgradeneeded = () => {
|
|
|
|
// Don't actually upgrade, just check
|
|
|
|
request.transaction?.abort();
|
|
|
|
};
|
|
|
|
});
|
2025-08-11 09:16:16 +01:00
|
|
|
|
2025-06-05 11:12:39 +01:00
|
|
|
console.log(`Version ${version} object stores:`, Array.from(db.objectStoreNames));
|
2025-08-11 09:16:16 +01:00
|
|
|
|
2025-06-05 11:12:39 +01:00
|
|
|
if (db.objectStoreNames.contains('files')) {
|
|
|
|
const transaction = db.transaction(['files'], 'readonly');
|
|
|
|
const store = transaction.objectStore('files');
|
|
|
|
const countRequest = store.count();
|
|
|
|
countRequest.onsuccess = () => {
|
|
|
|
console.log(`Version ${version} files store has ${countRequest.result} entries`);
|
|
|
|
};
|
|
|
|
}
|
2025-08-11 09:16:16 +01:00
|
|
|
|
2025-06-05 11:12:39 +01:00
|
|
|
db.close();
|
|
|
|
} catch (error) {
|
2025-08-11 09:16:16 +01:00
|
|
|
if (error instanceof Error) {
|
|
|
|
console.log(`Version ${version} not accessible:`, error.message);
|
|
|
|
}
|
2025-06-05 11:12:39 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Debug method to check what's actually in the database
|
|
|
|
*/
|
|
|
|
async debugDatabaseContents(): Promise<void> {
|
2025-08-21 17:30:26 +01:00
|
|
|
const db = await this.getDatabase();
|
2025-06-05 11:12:39 +01:00
|
|
|
|
|
|
|
return new Promise((resolve, reject) => {
|
2025-08-21 17:30:26 +01:00
|
|
|
const transaction = db.transaction([this.storeName], 'readonly');
|
2025-06-05 11:12:39 +01:00
|
|
|
const store = transaction.objectStore(this.storeName);
|
2025-08-11 09:16:16 +01:00
|
|
|
|
2025-06-05 11:12:39 +01:00
|
|
|
// First try getAll to see if there's anything
|
|
|
|
const getAllRequest = store.getAll();
|
|
|
|
getAllRequest.onsuccess = () => {
|
|
|
|
console.log('=== Raw getAll() result ===');
|
|
|
|
console.log('Raw entries found:', getAllRequest.result.length);
|
|
|
|
getAllRequest.result.forEach((item, index) => {
|
|
|
|
console.log(`Raw entry ${index}:`, {
|
|
|
|
keys: Object.keys(item || {}),
|
|
|
|
id: item?.id,
|
|
|
|
name: item?.name,
|
|
|
|
size: item?.size,
|
|
|
|
type: item?.type,
|
|
|
|
hasData: !!item?.data,
|
|
|
|
dataSize: item?.data?.byteLength,
|
|
|
|
fullObject: item
|
|
|
|
});
|
|
|
|
});
|
|
|
|
};
|
2025-08-11 09:16:16 +01:00
|
|
|
|
2025-06-05 11:12:39 +01:00
|
|
|
// Then try cursor
|
|
|
|
const cursorRequest = store.openCursor();
|
|
|
|
console.log('=== IndexedDB Cursor Debug ===');
|
|
|
|
let count = 0;
|
|
|
|
|
|
|
|
cursorRequest.onerror = () => {
|
|
|
|
console.error('Cursor error:', cursorRequest.error);
|
|
|
|
reject(cursorRequest.error);
|
|
|
|
};
|
2025-08-11 09:16:16 +01:00
|
|
|
|
2025-06-05 11:12:39 +01:00
|
|
|
cursorRequest.onsuccess = (event) => {
|
|
|
|
const cursor = (event.target as IDBRequest).result;
|
|
|
|
if (cursor) {
|
|
|
|
count++;
|
|
|
|
const value = cursor.value;
|
|
|
|
console.log(`Cursor File ${count}:`, {
|
|
|
|
id: value?.id,
|
|
|
|
name: value?.name,
|
|
|
|
size: value?.size,
|
|
|
|
type: value?.type,
|
|
|
|
hasData: !!value?.data,
|
|
|
|
dataSize: value?.data?.byteLength,
|
|
|
|
hasThumbnail: !!value?.thumbnail,
|
|
|
|
allKeys: Object.keys(value || {})
|
|
|
|
});
|
|
|
|
cursor.continue();
|
|
|
|
} else {
|
|
|
|
console.log(`=== End Cursor Debug - Found ${count} files ===`);
|
|
|
|
resolve();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2025-08-21 17:30:26 +01:00
|
|
|
* Convert StoredFile back to pure File object without mutations
|
|
|
|
* Returns a clean File object - use FileContext.addStoredFiles() for proper metadata handling
|
2025-06-05 11:12:39 +01:00
|
|
|
*/
|
|
|
|
createFileFromStored(storedFile: StoredFile): File {
|
|
|
|
if (!storedFile || !storedFile.data) {
|
|
|
|
throw new Error('Invalid stored file: missing data');
|
|
|
|
}
|
2025-08-11 09:16:16 +01:00
|
|
|
|
2025-06-05 11:12:39 +01:00
|
|
|
if (!storedFile.name || typeof storedFile.size !== 'number') {
|
|
|
|
throw new Error('Invalid stored file: missing metadata');
|
|
|
|
}
|
2025-08-11 09:16:16 +01:00
|
|
|
|
2025-06-05 11:12:39 +01:00
|
|
|
const blob = new Blob([storedFile.data], { type: storedFile.type });
|
|
|
|
const file = new File([blob], storedFile.name, {
|
|
|
|
type: storedFile.type,
|
|
|
|
lastModified: storedFile.lastModified
|
|
|
|
});
|
2025-08-11 09:16:16 +01:00
|
|
|
|
2025-08-21 17:30:26 +01:00
|
|
|
// Use FileContext.addStoredFiles() to properly associate with metadata
|
2025-06-05 11:12:39 +01:00
|
|
|
return file;
|
|
|
|
}
|
|
|
|
|
2025-08-21 17:30:26 +01:00
|
|
|
/**
|
|
|
|
* Convert StoredFile to the format expected by FileContext.addStoredFiles()
|
|
|
|
* This is the recommended way to load stored files into FileContext
|
|
|
|
*/
|
2025-08-28 10:56:07 +01:00
|
|
|
createFileWithMetadata(storedFile: StoredFile): { file: File; originalId: FileId; metadata: { thumbnail?: string } } {
|
2025-08-21 17:30:26 +01:00
|
|
|
const file = this.createFileFromStored(storedFile);
|
2025-08-28 10:56:07 +01:00
|
|
|
|
2025-08-21 17:30:26 +01:00
|
|
|
return {
|
|
|
|
file,
|
|
|
|
originalId: storedFile.id,
|
|
|
|
metadata: {
|
|
|
|
thumbnail: storedFile.thumbnail
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2025-06-05 11:12:39 +01:00
|
|
|
/**
|
|
|
|
* Create blob URL for stored file
|
|
|
|
*/
|
|
|
|
createBlobUrl(storedFile: StoredFile): string {
|
|
|
|
const blob = new Blob([storedFile.data], { type: storedFile.type });
|
|
|
|
return URL.createObjectURL(blob);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get file data as ArrayBuffer for streaming/chunked processing
|
|
|
|
*/
|
2025-08-28 10:56:07 +01:00
|
|
|
async getFileData(id: FileId): Promise<ArrayBuffer | null> {
|
2025-06-05 11:12:39 +01:00
|
|
|
try {
|
|
|
|
const storedFile = await this.getFile(id);
|
|
|
|
return storedFile ? storedFile.data : null;
|
|
|
|
} catch (error) {
|
|
|
|
console.warn(`Failed to get file data for ${id}:`, error);
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Create a temporary blob URL that gets revoked automatically
|
|
|
|
*/
|
2025-08-28 10:56:07 +01:00
|
|
|
async createTemporaryBlobUrl(id: FileId): Promise<string | null> {
|
2025-06-05 11:12:39 +01:00
|
|
|
const data = await this.getFileData(id);
|
|
|
|
if (!data) return null;
|
2025-08-11 09:16:16 +01:00
|
|
|
|
2025-06-05 11:12:39 +01:00
|
|
|
const blob = new Blob([data], { type: 'application/pdf' });
|
|
|
|
const url = URL.createObjectURL(blob);
|
2025-08-11 09:16:16 +01:00
|
|
|
|
2025-06-05 11:12:39 +01:00
|
|
|
// Auto-revoke after a short delay to free memory
|
|
|
|
setTimeout(() => {
|
|
|
|
URL.revokeObjectURL(url);
|
|
|
|
}, 10000); // 10 seconds
|
2025-08-11 09:16:16 +01:00
|
|
|
|
2025-06-05 11:12:39 +01:00
|
|
|
return url;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Update thumbnail for an existing file
|
|
|
|
*/
|
2025-08-28 10:56:07 +01:00
|
|
|
async updateThumbnail(id: FileId, thumbnail: string): Promise<boolean> {
|
2025-08-21 17:30:26 +01:00
|
|
|
const db = await this.getDatabase();
|
2025-06-05 11:12:39 +01:00
|
|
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
try {
|
2025-08-21 17:30:26 +01:00
|
|
|
const transaction = db.transaction([this.storeName], 'readwrite');
|
2025-06-05 11:12:39 +01:00
|
|
|
const store = transaction.objectStore(this.storeName);
|
|
|
|
const getRequest = store.get(id);
|
|
|
|
|
|
|
|
getRequest.onsuccess = () => {
|
|
|
|
const storedFile = getRequest.result;
|
|
|
|
if (storedFile) {
|
|
|
|
storedFile.thumbnail = thumbnail;
|
|
|
|
const updateRequest = store.put(storedFile);
|
2025-08-11 09:16:16 +01:00
|
|
|
|
2025-06-05 11:12:39 +01:00
|
|
|
updateRequest.onsuccess = () => {
|
|
|
|
console.log('Thumbnail updated for file:', id);
|
|
|
|
resolve(true);
|
|
|
|
};
|
|
|
|
updateRequest.onerror = () => {
|
|
|
|
console.error('Failed to update thumbnail:', updateRequest.error);
|
|
|
|
resolve(false);
|
|
|
|
};
|
|
|
|
} else {
|
|
|
|
resolve(false);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
getRequest.onerror = () => {
|
|
|
|
console.error('Failed to get file for thumbnail update:', getRequest.error);
|
|
|
|
resolve(false);
|
|
|
|
};
|
|
|
|
} catch (error) {
|
|
|
|
console.error('Transaction error during thumbnail update:', error);
|
|
|
|
resolve(false);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Check if storage quota is running low
|
|
|
|
*/
|
|
|
|
async isStorageLow(): Promise<boolean> {
|
|
|
|
const stats = await this.getStorageStats();
|
|
|
|
if (!stats.quota) return false;
|
2025-08-11 09:16:16 +01:00
|
|
|
|
2025-06-05 11:12:39 +01:00
|
|
|
const usagePercent = stats.used / stats.quota;
|
|
|
|
return usagePercent > 0.8; // Consider low if over 80% used
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Clean up old files if storage is low
|
|
|
|
*/
|
|
|
|
async cleanupOldFiles(maxFiles: number = 50): Promise<void> {
|
|
|
|
const files = await this.getAllFileMetadata();
|
2025-08-11 09:16:16 +01:00
|
|
|
|
2025-06-05 11:12:39 +01:00
|
|
|
if (files.length <= maxFiles) return;
|
2025-08-11 09:16:16 +01:00
|
|
|
|
2025-06-05 11:12:39 +01:00
|
|
|
// Sort by last modified (oldest first)
|
|
|
|
files.sort((a, b) => a.lastModified - b.lastModified);
|
2025-08-11 09:16:16 +01:00
|
|
|
|
2025-06-05 11:12:39 +01:00
|
|
|
// Delete oldest files
|
|
|
|
const filesToDelete = files.slice(0, files.length - maxFiles);
|
|
|
|
for (const file of filesToDelete) {
|
|
|
|
await this.deleteFile(file.id);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Export singleton instance
|
|
|
|
export const fileStorage = new FileStorageService();
|
|
|
|
|
|
|
|
// Helper hook for React components
|
|
|
|
export function useFileStorage() {
|
|
|
|
return fileStorage;
|
2025-08-11 09:16:16 +01:00
|
|
|
}
|