From 60c830c88b4b75a18377290641d92bcb0e047e35 Mon Sep 17 00:00:00 2001 From: R0m1k3 Date: Wed, 4 Mar 2026 14:32:27 +0100 Subject: [PATCH] feat: implement automatic purging of old media and schedule its daily execution via cron. --- server/cron.ts | 12 +++++++ server/routes/reels.ts | 17 ++++----- server/services/media-purge.ts | 64 ++++++++++++++++++++++++++++++++++ 3 files changed, 85 insertions(+), 8 deletions(-) create mode 100644 server/services/media-purge.ts diff --git a/server/cron.ts b/server/cron.ts index 3e9ae47..31a3150 100644 --- a/server/cron.ts +++ b/server/cron.ts @@ -1,6 +1,7 @@ import cron from 'node-cron'; import { TokenManager } from './services/token_manager'; import { AnalyticsService } from './services/analytics'; +import { purgeOldMedia } from './services/media-purge'; export function startTokenCron() { // Run every day at midnight (00:00) — Token health check @@ -20,4 +21,15 @@ export function startTokenCron() { } }); console.log('[Cron] Analytics sync job scheduled (0 8,20 * * *).'); + + // Run every day at 03:00 AM — Cleanup old media from Cloudinary and DB + cron.schedule('0 3 * * *', async () => { + console.log('[Cron] Running daily media purge check...'); + try { + await purgeOldMedia(); + } catch (error) { + console.error('[Cron] Media purge check failed:', error); + } + }); + console.log('[Cron] Media purge job scheduled (0 3 * * *).'); } diff --git a/server/routes/reels.ts b/server/routes/reels.ts index 71cdc4c..51c6421 100644 --- a/server/routes/reels.ts +++ b/server/routes/reels.ts @@ -339,12 +339,13 @@ reelsRouter.post('/reels/preview', async (req: Request, res: Response) => { } let watermarkUrl: string | undefined = undefined; - if (req.user) { - const user = req.user as User; - const [userCloudinary] = await db.select().from(cloudinaryConfig).where(eq(cloudinaryConfig.userId, user.id)); - if (userCloudinary && userCloudinary.logoPublicId) { - watermarkUrl = `https://res.cloudinary.com/${userCloudinary.cloudName}/image/upload/${userCloudinary.logoPublicId}`; + try { + const config = await storage.getCloudinaryConfig(); + if (config && config.logoPublicId) { + watermarkUrl = `https://res.cloudinary.com/${config.cloudName}/image/upload/${config.logoPublicId}`; } + } catch (e) { + console.error('Error fetching watermark configuration', e); } // Traiter la vidéo via FFmpeg @@ -549,9 +550,9 @@ async function processReelBackground( let watermarkUrl: string | undefined = undefined; try { - const [userCloudinary] = await db.select().from(cloudinaryConfig).where(eq(cloudinaryConfig.userId, media.userId)); - if (userCloudinary && userCloudinary.logoPublicId) { - watermarkUrl = `https://res.cloudinary.com/${userCloudinary.cloudName}/image/upload/${userCloudinary.logoPublicId}`; + const config = await storage.getCloudinaryConfig(); + if (config && config.logoPublicId) { + watermarkUrl = `https://res.cloudinary.com/${config.cloudName}/image/upload/${config.logoPublicId}`; } } catch (e) { console.error('Error fetching watermark configuration', e); diff --git a/server/services/media-purge.ts b/server/services/media-purge.ts new file mode 100644 index 0000000..dc7406d --- /dev/null +++ b/server/services/media-purge.ts @@ -0,0 +1,64 @@ +import { db } from '../db'; +import { media } from '@shared/schema'; +import { eq, lt, isNotNull, and } from 'drizzle-orm'; +import { cloudinaryService } from './cloudinary'; + +/** + * Automates the cleanup of old media to save Cloudinary storage space. + * Scans the database for media older than 60 days, deletes the physical + * files from Cloudinary, and removes the database records (cascading to post_media). + */ +export async function purgeOldMedia() { + console.log('[Media Purge] Starting automatic purge of old media...'); + + try { + const sixtyDaysAgo = new Date(); + sixtyDaysAgo.setDate(sixtyDaysAgo.getDate() - 60); + + // Find all media older than 60 days that have a cloudinaryPublicId + const oldMedia = await db.query.media.findMany({ + where: (media, { and, lt, isNotNull }) => and( + lt(media.createdAt, sixtyDaysAgo), + isNotNull(media.cloudinaryPublicId) + ) + }); + + if (oldMedia.length === 0) { + console.log('[Media Purge] No old media found to purge.'); + return; + } + + console.log(`[Media Purge] Found ${oldMedia.length} media items older than 60 days to purge.`); + + let successCount = 0; + let errorCount = 0; + + for (const item of oldMedia) { + if (!item.cloudinaryPublicId) continue; + + try { + // Delete from Cloudinary (using global config, userId is passed but ignored internally) + await cloudinaryService.deleteMedia( + item.cloudinaryPublicId, + item.userId, + item.type === 'video' ? 'video' : 'image' + ); + + // Delete from Database (cascades to post_media, effectively leaving the post text intact) + await db.delete(media).where(eq(media.id, item.id)); + + successCount++; + console.log(`[Media Purge] Successfully deleted media ${item.id} (Cloudinary ID: ${item.cloudinaryPublicId})`); + } catch (err) { + // Cloudinary file might already be deleted or missing, but we still might want to clean DB if it's consistently failing. + // For safety, we only delete DB row if Cloudinary deletion succeeds or throws 'not found' (which we assume is fine). + console.error(`[Media Purge] Failed to delete media ${item.id} from Cloudinary:`, err); + errorCount++; + } + } + + console.log(`[Media Purge] Purge completed. Successfully deleted: ${successCount}, Failed: ${errorCount}.`); + } catch (error) { + console.error('[Media Purge] Fatal error during media purge process:', error); + } +}