Files
padmaja/lib/blob-storage.ts
2026-01-17 14:17:42 +05:30

269 lines
7.1 KiB
TypeScript

import { put, remove } from '@vercel/blob'
export interface UploadResult {
url: string
downloadUrl: string
pathname: string
size: number
uploadedAt: Date
}
export interface BlobStorageConfig {
token: string
folder?: string
allowedTypes?: string[]
maxSize?: number // in bytes
}
type PutBody = string | File | Blob | ReadableStream
// Helper function to check if the object has File-like properties
function isFileObject(obj: any): obj is { name: string; type: string; size: number } {
return obj && typeof obj.name === 'string' && typeof obj.type === 'string' && typeof obj.size === 'number'
}
class BlobStorageService {
private config: BlobStorageConfig
constructor(config: BlobStorageConfig) {
this.config = {
maxSize: 10 * 1024 * 1024, // 10MB default
allowedTypes: [
'image/jpeg',
'image/png',
'image/webp',
'image/gif',
'application/pdf',
'text/csv',
'application/vnd.ms-excel',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
],
...config
}
}
/**
* Upload a file to Vercel Blob storage
*/
async uploadFile(
file: PutBody,
filename: string,
options?: {
folder?: string
contentType?: string
addRandomSuffix?: boolean
}
): Promise<UploadResult> {
try {
// Validate file if it's a File-like object
if (isFileObject(file)) {
this.validateFile(file)
}
// Generate pathname
const folder = options?.folder || this.config.folder || 'uploads'
const timestamp = new Date().toISOString().split('T')[0]
const randomSuffix = options?.addRandomSuffix !== false ? `-${Date.now()}` : ''
const pathname = `${folder}/${timestamp}/${filename}${randomSuffix}`
// Upload to Vercel Blob
const blob = await put(pathname, file as string | Blob | File | ReadableStream, {
token: this.config.token,
contentType: options?.contentType || (isFileObject(file) ? file.type : undefined),
})
return {
url: blob.url,
downloadUrl: blob.url + '?download=1',
pathname: pathname,
size: isFileObject(file) ? file.size : 0,
uploadedAt: new Date()
}
} catch (error) {
console.error('Error uploading file to Blob storage:', error)
throw new Error(`Failed to upload file: ${error instanceof Error ? error.message : 'Unknown error'}`)
}
}
/**
* Upload multiple files
*/
async uploadMultipleFiles(
files: PutBody[],
options?: {
folder?: string
addRandomSuffix?: boolean
}
): Promise<UploadResult[]> {
const uploadPromises = files.map((file, index) => {
// Check if file has a name property (like File objects from FormData)
const filename = (file as any)?.name || `file-${index}`
return this.uploadFile(file, filename, options)
})
return Promise.all(uploadPromises)
}
/**
* Delete a file from Vercel Blob storage
*/
async deleteFile(url: string): Promise<void> {
try {
await remove(url, { token: this.config.token })
} catch (error) {
console.error('Error deleting file from Blob storage:', error)
throw new Error(`Failed to delete file: ${error instanceof Error ? error.message : 'Unknown error'}`)
}
}
/**
* Delete multiple files
*/
async deleteMultipleFiles(urls: string[]): Promise<void> {
const deletePromises = urls.map(url => this.deleteFile(url))
await Promise.all(deletePromises)
}
/**
* List files in a specific folder
* Note: list() is not available in the current @vercel/blob version
*/
async listFiles(options?: {
prefix?: string
limit?: number
cursor?: string
}): Promise<{
blobs: Array<{
url: string
pathname: string
size: number
uploadedAt: Date
}>
cursor?: string
hasMore: boolean
}> {
// list() function is not available in current @vercel/blob package
console.warn('listFiles: list() function not available in current @vercel/blob version')
return {
blobs: [],
cursor: undefined,
hasMore: false
}
}
/**
* Upload product images with optimization
*/
async uploadProductImages(
images: File[],
productName: string
): Promise<string[]> {
const folder = 'products'
const sanitizedProductName = productName.toLowerCase().replace(/[^a-z0-9]/g, '-')
const uploadResults = await Promise.all(
images.map(async (image, index) => {
const filename = `${sanitizedProductName}-${index + 1}.${image.name.split('.').pop()}`
const result = await this.uploadFile(image, filename, {
folder,
addRandomSuffix: false
})
return result.url
})
)
return uploadResults
}
/**
* Upload category image
*/
async uploadCategoryImage(
image: File,
categoryName: string
): Promise<string> {
const folder = 'categories'
const sanitizedCategoryName = categoryName.toLowerCase().replace(/[^a-z0-9]/g, '-')
const filename = `${sanitizedCategoryName}.${image.name.split('.').pop()}`
const result = await this.uploadFile(image, filename, {
folder,
addRandomSuffix: false
})
return result.url
}
/**
* Upload user avatar
*/
async uploadUserAvatar(
image: File,
userId: string
): Promise<string> {
const folder = 'avatars'
const filename = `user-${userId}.${image.name.split('.').pop()}`
const result = await this.uploadFile(image, filename, {
folder,
addRandomSuffix: false
})
return result.url
}
/**
* Validate file before upload
*/
private validateFile(file: { size: number; type: string }): void {
// Check file size
if (this.config.maxSize && file.size > this.config.maxSize) {
throw new Error(`File size exceeds maximum allowed size of ${this.config.maxSize / (1024 * 1024)}MB`)
}
// Check file type
if (this.config.allowedTypes && !this.config.allowedTypes.includes(file.type)) {
throw new Error(`File type ${file.type} is not allowed. Allowed types: ${this.config.allowedTypes.join(', ')}`)
}
}
/**
* Get optimized image URL with transformations
*/
getOptimizedImageUrl(
originalUrl: string,
options?: {
width?: number
height?: number
quality?: number
format?: 'webp' | 'jpeg' | 'png'
}
): string {
if (!options) return originalUrl
const url = new URL(originalUrl)
const searchParams = new URLSearchParams()
if (options.width) searchParams.set('w', options.width.toString())
if (options.height) searchParams.set('h', options.height.toString())
if (options.quality) searchParams.set('q', options.quality.toString())
if (options.format) searchParams.set('f', options.format)
if (searchParams.toString()) {
url.search = searchParams.toString()
}
return url.toString()
}
}
// Create singleton instance
const blobStorage = new BlobStorageService({
token: process.env.BLOB_READ_WRITE_TOKEN || '',
folder: 'padmaaja-rasooi',
maxSize: 10 * 1024 * 1024, // 10MB
})
export default blobStorage
export { BlobStorageService }