first commit
This commit is contained in:
268
lib/blob-storage.ts
Normal file
268
lib/blob-storage.ts
Normal file
@@ -0,0 +1,268 @@
|
||||
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 }
|
||||
Reference in New Issue
Block a user