Clone
export class Storage {
constructor(r2Bucket) {
this.bucket = r2Bucket;
}
/**
* Upload a logo image to R2
* @param {string} key - The storage key (e.g., 'logos/user-id/filename.png')
* @param {ArrayBuffer|ReadableStream} data - The image data
* @param {string} contentType - The MIME type of the image
* @returns {Promise<{key: string, url: string}>}
*/
async uploadLogo(key, data, contentType) {
try {
const object = await this.bucket.put(key, data, {
httpMetadata: {
contentType: contentType,
cacheControl: 'public, max-age=31536000', // Cache for 1 year
}
});
if (!object) {
throw new Error('Failed to upload logo');
}
// Return the key for later retrieval
// In production, you might want to use a CDN URL here
return {
key: key,
etag: object.etag,
uploaded: object.uploaded
};
} catch (error) {
console.error('Logo upload error:', error);
throw new Error('Failed to upload logo to storage');
}
}
/**
* Get a logo from R2
* @param {string} key - The storage key
* @returns {Promise<Response>}
*/
async getLogo(key) {
try {
const object = await this.bucket.get(key);
if (!object) {
return null;
}
return new Response(object.body, {
headers: {
'Content-Type': object.httpMetadata?.contentType || 'image/png',
'Cache-Control': 'public, max-age=31536000',
'ETag': object.etag
}
});
} catch (error) {
console.error('Logo fetch error:', error);
return null;
}
}
/**
* Delete a logo from R2
* @param {string} key - The storage key
* @returns {Promise<void>}
*/
async deleteLogo(key) {
try {
await this.bucket.delete(key);
} catch (error) {
console.error('Logo deletion error:', error);
// Don't throw - deletion failures shouldn't break the flow
}
}
/**
* Generate a unique storage key for a logo
* @param {string} userId - The user ID
* @param {string} entryId - The entry ID
* @param {string} filename - Original filename
* @returns {string}
*/
static generateLogoKey(userId, entryId, filename) {
// Extract extension from filename
const ext = filename.split('.').pop().toLowerCase();
// Validate extension
const allowedExtensions = ['png', 'jpg', 'jpeg', 'svg', 'webp'];
if (!allowedExtensions.includes(ext)) {
throw new Error('Invalid file type. Allowed: PNG, JPG, SVG, WebP');
}
// Generate unique key: logos/userId/entryId.extension
return `logos/${userId}/${entryId}.${ext}`;
}
/**
* Validate image file
* @param {File|ArrayBuffer} file - The image file
* @param {number} maxSize - Maximum size in bytes (default: 5MB)
* @returns {boolean}
*/
static validateImage(file, maxSize = 5 * 1024 * 1024) {
// Check size
const size = file.size || file.byteLength;
if (size > maxSize) {
throw new Error(`File too large. Maximum size: ${maxSize / 1024 / 1024}MB`);
}
// Check MIME type if available
if (file.type) {
const allowedTypes = ['image/png', 'image/jpeg', 'image/svg+xml', 'image/webp'];
if (!allowedTypes.includes(file.type)) {
throw new Error('Invalid file type. Allowed: PNG, JPG, SVG, WebP');
}
}
return true;
}
}