/** * Client-side utility for generating pre-signed URLs from S3 keys */ interface PresignedUrlCache { url: string; expiresAt: number; } // Cache to avoid regenerating URLs that haven't expired const urlCache = new Map(); /** * Check if a URL is an S3 key (not a full URL) */ export function isS3Key(urlOrKey: string): boolean { // If it starts with http:// or https://, it's already a URL if (urlOrKey.startsWith('http://') || urlOrKey.startsWith('https://')) { return false; } // Otherwise, treat it as an S3 key return true; } /** * Get a pre-signed URL for an S3 key * @param key - The S3 object key * @param expiresIn - Expiration time in seconds (default: 1 hour) * @param useCache - Whether to use cached URLs (default: true) * @returns Pre-signed URL or the original key if it's already a URL */ export async function getPresignedUrl( key: string, expiresIn: number = 3600, useCache: boolean = true ): Promise { // If it's already a full URL, return it as-is if (!isS3Key(key)) { return key; } // Check cache if (useCache) { const cached = urlCache.get(key); if (cached && cached.expiresAt > Date.now()) { return cached.url; } } try { const response = await fetch('/api/s3/presigned-url', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ key, expiresIn }), }); if (!response.ok) { throw new Error('Failed to generate presigned URL'); } const data = await response.json(); // Cache the URL (expire 5 minutes before actual expiration for safety) if (useCache) { urlCache.set(key, { url: data.url, expiresAt: Date.now() + (expiresIn - 300) * 1000, }); } return data.url; } catch (error) { console.error('Error generating presigned URL:', error); // Fallback: return the key itself (might fail, but better than nothing) return key; } } /** * Get pre-signed URLs for multiple S3 keys at once * @param keys - Array of S3 object keys * @param expiresIn - Expiration time in seconds (default: 1 hour) * @returns Map of key to pre-signed URL */ export async function getMultiplePresignedUrls( keys: string[], expiresIn: number = 3600 ): Promise> { // Separate keys that need pre-signed URLs from full URLs const s3Keys: string[] = []; const result: Record = {}; for (const key of keys) { if (isS3Key(key)) { // Check cache first const cached = urlCache.get(key); if (cached && cached.expiresAt > Date.now()) { result[key] = cached.url; } else { s3Keys.push(key); } } else { // Already a full URL result[key] = key; } } // If no keys need pre-signed URLs, return early if (s3Keys.length === 0) { return result; } try { const response = await fetch('/api/s3/presigned-url', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ keys: s3Keys, expiresIn }), }); if (!response.ok) { throw new Error('Failed to generate presigned URLs'); } const data = await response.json(); const urls = data.urls as Record; // Cache the URLs const cacheExpiresAt = Date.now() + (expiresIn - 300) * 1000; for (const [key, url] of Object.entries(urls)) { urlCache.set(key, { url, expiresAt: cacheExpiresAt }); result[key] = url; } return result; } catch (error) { console.error('Error generating presigned URLs:', error); // Fallback: return the keys themselves for (const key of s3Keys) { result[key] = key; } return result; } } /** * Clear the URL cache (useful when you want to force refresh) */ export function clearUrlCache(): void { urlCache.clear(); } /** * Clear expired URLs from cache */ export function cleanupUrlCache(): void { const now = Date.now(); for (const [key, cached] of urlCache.entries()) { if (cached.expiresAt <= now) { urlCache.delete(key); } } } // Automatically cleanup cache every 5 minutes if (typeof window !== 'undefined') { setInterval(cleanupUrlCache, 5 * 60 * 1000); }