import { v2 as cloudinary } from 'cloudinary' import { prisma } from './database' import { extractPublicId } from './cloudinary' import { formatBytes } from '$lib/utils/format' export { formatBytes } export interface CloudinaryResource { public_id: string secure_url: string resource_type: string type: string format: string version: number width?: number height?: number bytes: number created_at: string folder?: string } export interface AuditResult { totalCloudinaryFiles: number totalDatabaseReferences: number orphanedFiles: CloudinaryResource[] orphanedTotalBytes: number missingFromCloudinary: string[] } /** * Fetches all resources from Cloudinary with pagination */ export async function fetchAllCloudinaryResources(): Promise { const resources: CloudinaryResource[] = [] let nextCursor: string | undefined do { try { const result = await cloudinary.api.resources({ type: 'upload', max_results: 500, next_cursor: nextCursor }) resources.push(...result.resources) nextCursor = result.next_cursor } catch (error) { console.error('Error fetching Cloudinary resources:', error) throw error } } while (nextCursor) return resources } /** * Gets all Cloudinary URLs/public IDs referenced in the database */ export async function fetchAllDatabaseCloudinaryReferences(): Promise> { const publicIds = new Set() // Get all Media table URLs const mediaRecords = await prisma.media.findMany({ select: { url: true, thumbnailUrl: true } }) for (const media of mediaRecords) { if (media.url?.includes('cloudinary.com')) { const publicId = extractPublicId(media.url) if (publicId) publicIds.add(publicId) } if (media.thumbnailUrl?.includes('cloudinary.com')) { const publicId = extractPublicId(media.thumbnailUrl) if (publicId) publicIds.add(publicId) } } // Get Project images const projects = await prisma.project.findMany({ select: { featuredImage: true, logoUrl: true, gallery: true } }) for (const project of projects) { if (project.featuredImage?.includes('cloudinary.com')) { const publicId = extractPublicId(project.featuredImage) if (publicId) publicIds.add(publicId) } if (project.logoUrl?.includes('cloudinary.com')) { const publicId = extractPublicId(project.logoUrl) if (publicId) publicIds.add(publicId) } if (project.gallery && typeof project.gallery === 'object') { const gallery = project.gallery as any[] for (const item of gallery) { if (item.url?.includes('cloudinary.com')) { const publicId = extractPublicId(item.url) if (publicId) publicIds.add(publicId) } } } } // Get Post images const posts = await prisma.post.findMany({ select: { featuredImage: true, attachments: true } }) for (const post of posts) { if (post.featuredImage?.includes('cloudinary.com')) { const publicId = extractPublicId(post.featuredImage) if (publicId) publicIds.add(publicId) } if (post.attachments && typeof post.attachments === 'object') { const attachments = post.attachments as any[] for (const attachment of attachments) { if (attachment.url?.includes('cloudinary.com')) { const publicId = extractPublicId(attachment.url) if (publicId) publicIds.add(publicId) } } } } return publicIds } /** * Performs a comprehensive audit of Cloudinary resources vs database references */ export async function auditCloudinaryResources(): Promise { console.log('Starting Cloudinary audit...') // Fetch all resources from Cloudinary const cloudinaryResources = await fetchAllCloudinaryResources() console.log(`Found ${cloudinaryResources.length} files in Cloudinary`) // Fetch all database references const databasePublicIds = await fetchAllDatabaseCloudinaryReferences() console.log(`Found ${databasePublicIds.size} Cloudinary references in database`) // Find orphaned files (in Cloudinary but not in database) const orphanedFiles: CloudinaryResource[] = [] let orphanedTotalBytes = 0 for (const resource of cloudinaryResources) { // Skip thumbnails generated by Cloudinary (they have specific naming patterns) if (resource.public_id.includes('_thumbnail_')) { continue } if (!databasePublicIds.has(resource.public_id)) { orphanedFiles.push(resource) orphanedTotalBytes += resource.bytes || 0 } } // Find missing files (in database but not in Cloudinary) const cloudinaryPublicIds = new Set(cloudinaryResources.map((r) => r.public_id)) const missingFromCloudinary: string[] = [] for (const publicId of databasePublicIds) { if (!cloudinaryPublicIds.has(publicId)) { missingFromCloudinary.push(publicId) } } return { totalCloudinaryFiles: cloudinaryResources.length, totalDatabaseReferences: databasePublicIds.size, orphanedFiles, orphanedTotalBytes, missingFromCloudinary } } /** * Deletes orphaned files from Cloudinary */ export async function deleteOrphanedFiles( publicIds: string[], dryRun = true ): Promise<{ attempted: number succeeded: number failed: string[] }> { const results = { attempted: publicIds.length, succeeded: 0, failed: [] as string[] } if (dryRun) { console.log(`DRY RUN: Would delete ${publicIds.length} files`) return { ...results, succeeded: publicIds.length } } for (const publicId of publicIds) { try { await cloudinary.uploader.destroy(publicId) results.succeeded++ console.log(`Deleted: ${publicId}`) } catch (error) { results.failed.push(publicId) console.error(`Failed to delete ${publicId}:`, error) } } return results }