podzahr/lib/presigned-url.ts
nfel 9478aa319f
main: added presigned url + favicon
Signed-off-by: nfel <nfilsaraee@gmail.com>
2026-01-01 19:06:11 +03:30

168 lines
4.2 KiB
TypeScript

/**
* 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<string, PresignedUrlCache>();
/**
* 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<string> {
// 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<Record<string, string>> {
// Separate keys that need pre-signed URLs from full URLs
const s3Keys: string[] = [];
const result: Record<string, string> = {};
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<string, string>;
// 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);
}