File size: 5,002 Bytes
f56a29b | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 | /**
* Image Storage Utilities
*
* Store PDF images in IndexedDB to avoid sessionStorage 5MB limit.
* Images are stored as Blobs for efficient storage.
*/
import { db, type ImageFileRecord } from './database';
import { nanoid } from 'nanoid';
import { createLogger } from '@/lib/logger';
const log = createLogger('ImageStorage');
/**
* Convert base64 data URL to Blob
*/
function base64ToBlob(base64DataUrl: string): Blob {
const parts = base64DataUrl.split(',');
const mimeMatch = parts[0].match(/:(.*?);/);
const mimeType = mimeMatch ? mimeMatch[1] : 'image/png';
const base64Data = parts[1];
const byteString = atob(base64Data);
const arrayBuffer = new ArrayBuffer(byteString.length);
const uint8Array = new Uint8Array(arrayBuffer);
for (let i = 0; i < byteString.length; i++) {
uint8Array[i] = byteString.charCodeAt(i);
}
return new Blob([uint8Array], { type: mimeType });
}
/**
* Convert Blob to base64 data URL
*/
async function blobToBase64(blob: Blob): Promise<string> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onloadend = () => resolve(reader.result as string);
reader.onerror = reject;
reader.readAsDataURL(blob);
});
}
/**
* Store images in IndexedDB
* Returns array of stored image IDs
*/
export async function storeImages(
images: Array<{ id: string; src: string; pageNumber?: number }>,
): Promise<string[]> {
const sessionId = nanoid(10);
const storedIds: string[] = [];
for (const img of images) {
try {
const blob = base64ToBlob(img.src);
const mimeMatch = img.src.match(/data:(.*?);/);
const mimeType = mimeMatch ? mimeMatch[1] : 'image/png';
// Use session-prefixed ID to allow cleanup
const storageId = `session_${sessionId}_${img.id}`;
const record: ImageFileRecord = {
id: storageId,
blob,
filename: `${img.id}.png`,
mimeType,
size: blob.size,
createdAt: Date.now(),
};
await db.imageFiles.put(record);
storedIds.push(storageId);
} catch (error) {
log.error(`Failed to store image ${img.id}:`, error);
}
}
return storedIds;
}
/**
* Load images from IndexedDB and return as imageMapping
* @param imageIds - Array of storage IDs (session_xxx_img_1 format)
* @returns ImageMapping { img_1: "data:image/png;base64,..." }
*/
export async function loadImageMapping(imageIds: string[]): Promise<Record<string, string>> {
const mapping: Record<string, string> = {};
for (const storageId of imageIds) {
try {
const record = await db.imageFiles.get(storageId);
if (record) {
const base64 = await blobToBase64(record.blob);
// Extract original ID (img_1) from storage ID (session_xxx_img_1)
const originalId = storageId.replace(/^session_[^_]+_/, '');
mapping[originalId] = base64;
}
} catch (error) {
log.error(`Failed to load image ${storageId}:`, error);
}
}
return mapping;
}
/**
* Clean up images by session prefix
*/
export async function cleanupSessionImages(sessionId: string): Promise<void> {
try {
const prefix = `session_${sessionId}_`;
const allImages = await db.imageFiles.toArray();
const toDelete = allImages.filter((img) => img.id.startsWith(prefix));
for (const img of toDelete) {
await db.imageFiles.delete(img.id);
}
log.info(`Cleaned up ${toDelete.length} images for session ${sessionId}`);
} catch (error) {
log.error('Failed to cleanup session images:', error);
}
}
/**
* Clean up old images (older than specified hours)
*/
export async function cleanupOldImages(hoursOld: number = 24): Promise<void> {
try {
const cutoff = Date.now() - hoursOld * 60 * 60 * 1000;
await db.imageFiles.where('createdAt').below(cutoff).delete();
log.info(`Cleaned up images older than ${hoursOld} hours`);
} catch (error) {
log.error('Failed to cleanup old images:', error);
}
}
/**
* Get total size of stored images
*/
export async function getImageStorageSize(): Promise<number> {
const images = await db.imageFiles.toArray();
return images.reduce((total, img) => total + img.size, 0);
}
/**
* Store a PDF file as a Blob in IndexedDB.
* Returns a storage key that can be used to retrieve the blob later.
*/
export async function storePdfBlob(file: File): Promise<string> {
const storageKey = `pdf_${nanoid(10)}`;
const blob = new Blob([await file.arrayBuffer()], {
type: file.type || 'application/pdf',
});
const record: ImageFileRecord = {
id: storageKey,
blob,
filename: file.name,
mimeType: file.type || 'application/pdf',
size: blob.size,
createdAt: Date.now(),
};
await db.imageFiles.put(record);
return storageKey;
}
/**
* Load a PDF Blob from IndexedDB by its storage key.
*/
export async function loadPdfBlob(key: string): Promise<Blob | null> {
const record = await db.imageFiles.get(key);
return record?.blob ?? null;
}
|