better flyer icons + archive
All checks were successful
Deploy to Web Server flyer-crawler.projectium.com / deploy (push) Successful in 4m25s

This commit is contained in:
2025-12-03 14:13:44 -08:00
parent ba778a20d7
commit 383e8e3d25
6 changed files with 211 additions and 15 deletions

View File

@@ -55,6 +55,17 @@ export const analyticsQueue = new Queue<AnalyticsJobData>('analytics-reporting',
},
});
export const cleanupQueue = new Queue<CleanupJobData>('file-cleanup', {
connection,
defaultJobOptions: {
attempts: 3,
backoff: {
type: 'exponential',
delay: 30000, // Retry cleanup after 30 seconds
},
removeOnComplete: true, // No need to keep successful cleanup jobs
},
});
// --- Job Data Interfaces ---
interface FlyerJobData {
@@ -78,6 +89,10 @@ interface AnalyticsJobData {
reportDate: string; // e.g., '2024-10-26'
}
interface CleanupJobData {
flyerId: number;
}
/**
* The main worker process for handling flyer jobs.
* This should be run as a separate process.
@@ -86,6 +101,8 @@ export const flyerWorker = new Worker<FlyerJobData>(
'flyer-processing',
async (job: Job<FlyerJobData>) => {
const { filePath, originalFileName, checksum, userId } = job.data;
const createdImagePaths: string[] = [];
let jobSucceeded = false;
logger.info(`[Worker] Processing job ${job.id} for file: ${originalFileName}`);
try {
@@ -115,6 +132,9 @@ export const flyerWorker = new Worker<FlyerJobData>(
for (const img of generatedImages) {
imagePaths.push({ path: path.join(outputDir, img), mimetype: 'image/jpeg' });
const imagePath = path.join(outputDir, img);
imagePaths.push({ path: imagePath, mimetype: 'image/jpeg' });
createdImagePaths.push(imagePath); // Track generated images for cleanup
}
logger.info(`[Worker] Converted PDF to ${imagePaths.length} images.`);
@@ -156,6 +176,7 @@ export const flyerWorker = new Worker<FlyerJobData>(
});
// TODO: Cleanup temporary files (original PDF and generated images)
jobSucceeded = true; // Mark the job as successful before the finally block.
return { flyerId: newFlyer.flyer_id };
} catch (error: unknown) {
@@ -169,6 +190,26 @@ export const flyerWorker = new Worker<FlyerJobData>(
await job.updateProgress({ message: `Error: ${errorMessage}` });
// Re-throw the error to let BullMQ know the job has failed and should be retried or marked as failed.
throw error;
} finally {
// This block will run after the try/catch, regardless of success or failure.
if (jobSucceeded) {
logger.info(`[Worker] Job ${job.id} succeeded. Cleaning up temporary files.`);
try {
// Delete the generated JPEG images from the PDF conversion.
for (const imagePath of createdImagePaths) {
await fs.unlink(imagePath);
logger.debug(`[Worker] Deleted temporary image: ${imagePath}`);
}
// Finally, delete the original uploaded file (PDF or image).
await fs.unlink(filePath);
logger.debug(`[Worker] Deleted original upload: ${filePath}`);
} catch (cleanupError) {
logger.error(`[Worker] Job ${job.id} completed, but failed during file cleanup.`, { error: cleanupError });
// We don't re-throw here because the main job was successful.
}
} else {
logger.warn(`[Worker] Job ${job.id} failed. Temporary files will not be cleaned up to allow for manual inspection.`);
}
}
},
{
@@ -236,4 +277,51 @@ export const analyticsWorker = new Worker<AnalyticsJobData>(
}
);
/**
* A dedicated worker for cleaning up flyer-related files from the filesystem.
* This is triggered manually by an admin after a flyer has been reviewed.
*/
export const cleanupWorker = new Worker<CleanupJobData>(
'file-cleanup',
async (job: Job<CleanupJobData>) => {
const { flyerId } = job.data;
logger.info(`[CleanupWorker] Starting file cleanup for flyer ID: ${flyerId}`);
try {
// 1. Fetch the flyer from the database to get its file paths.
const flyer = await db.getFlyerById(flyerId);
if (!flyer) {
throw new Error(`Flyer with ID ${flyerId} not found. Cannot perform cleanup.`);
}
// 2. Determine the base path for the flyer images.
const storagePath = process.env.STORAGE_PATH || '/var/www/flyer-crawler.projectium.com/flyer-images';
// 3. Delete the main flyer image.
const mainImagePath = path.join(storagePath, path.basename(flyer.image_url));
await fs.unlink(mainImagePath).catch(err => logger.warn(`[CleanupWorker] Could not delete main image (may not exist): ${mainImagePath}`, { error: err.message }));
logger.info(`[CleanupWorker] Deleted main image: ${mainImagePath}`);
// 4. Delete the flyer icon.
if (flyer.icon_url) {
const iconPath = path.join(storagePath, 'icons', path.basename(flyer.icon_url));
await fs.unlink(iconPath).catch(err => logger.warn(`[CleanupWorker] Could not delete icon (may not exist): ${iconPath}`, { error: err.message }));
logger.info(`[CleanupWorker] Deleted icon: ${iconPath}`);
}
// Note: This process does not delete the original PDF, as its path is not stored.
// A more advanced implementation could store the original path in the job data and pass it here.
} catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : 'An unknown cleanup error occurred';
logger.error(`[CleanupWorker] Job ${job.id} for flyer ${flyerId} failed.`, { error: errorMessage });
throw error; // Re-throw to let BullMQ handle the failure and retry.
}
},
{
connection,
concurrency: 10, // Cleanup is not very resource-intensive.
}
);
logger.info('All workers started and listening for jobs.');