From 97ad75f238e331d2e47d603bc7bc3cd62f57ed22 Mon Sep 17 00:00:00 2001 From: Sam Chau Date: Tue, 21 Oct 2025 08:24:50 +1030 Subject: [PATCH] chore(paths): move jobs into src --- .../jobs/definitions/cleanupBackups.ts | 0 src/jobs/definitions/cleanupLogs.ts | 75 +++++++++++++ .../jobs/definitions/createBackup.ts | 0 src/{utils => }/jobs/init.ts | 0 src/jobs/logic/cleanupLogs.ts | 66 +++++++++++ src/{utils => }/jobs/registry.ts | 0 src/{utils => }/jobs/runner.ts | 0 src/{utils => }/jobs/scheduler.ts | 0 src/{utils => }/jobs/types.ts | 0 src/utils/jobs/definitions/cleanupLogs.ts | 105 ------------------ 10 files changed, 141 insertions(+), 105 deletions(-) rename src/{utils => }/jobs/definitions/cleanupBackups.ts (100%) create mode 100644 src/jobs/definitions/cleanupLogs.ts rename src/{utils => }/jobs/definitions/createBackup.ts (100%) rename src/{utils => }/jobs/init.ts (100%) create mode 100644 src/jobs/logic/cleanupLogs.ts rename src/{utils => }/jobs/registry.ts (100%) rename src/{utils => }/jobs/runner.ts (100%) rename src/{utils => }/jobs/scheduler.ts (100%) rename src/{utils => }/jobs/types.ts (100%) delete mode 100644 src/utils/jobs/definitions/cleanupLogs.ts diff --git a/src/utils/jobs/definitions/cleanupBackups.ts b/src/jobs/definitions/cleanupBackups.ts similarity index 100% rename from src/utils/jobs/definitions/cleanupBackups.ts rename to src/jobs/definitions/cleanupBackups.ts diff --git a/src/jobs/definitions/cleanupLogs.ts b/src/jobs/definitions/cleanupLogs.ts new file mode 100644 index 0000000..f8faa6b --- /dev/null +++ b/src/jobs/definitions/cleanupLogs.ts @@ -0,0 +1,75 @@ +import { config } from '$config'; +import { logSettingsQueries } from '$db/queries/logSettings.ts'; +import { logger } from '$logger'; +import { cleanupLogs } from '../lib/cleanupLogs.ts'; +import type { JobDefinition, JobResult } from '../types.ts'; + +/** + * Cleanup old log files job + * Deletes daily log files (YYYY-MM-DD.log) older than the configured retention period + */ +export const cleanupLogsJob: JobDefinition = { + name: 'cleanup_logs', + description: 'Delete log files according to retention policy', + schedule: 'daily', + + handler: async (): Promise => { + try { + // Get log settings + const settings = logSettingsQueries.get(); + if (!settings) { + return { + success: false, + error: 'Log settings not found' + }; + } + + const retentionDays = settings.retention_days; + const logsDir = config.paths.logs; + + // Calculate cutoff date for logging + const cutoffDate = new Date(); + cutoffDate.setDate(cutoffDate.getDate() - retentionDays); + const cutoffDateStr = cutoffDate.toISOString().split('T')[0]; + + await logger.info(`Cleaning up logs older than ${retentionDays} days`, { + source: 'CleanupLogsJob', + meta: { cutoffDate: cutoffDateStr } + }); + + // Run cleanup + const result = await cleanupLogs(logsDir, retentionDays); + + // Log individual deletions + // Note: We don't have access to which specific files were deleted anymore + // If we need that, we can modify cleanupLogs to return the list + + // Log errors + for (const { file, error } of result.errors) { + await logger.error(`Failed to process log file: ${file}`, { + source: 'CleanupLogsJob', + meta: { file, error } + }); + } + + const message = `Cleanup completed: deleted ${result.deletedCount} file(s), ${result.errorCount} error(s)`; + + if (result.errorCount > 0 && result.deletedCount === 0) { + return { + success: false, + error: message + }; + } + + return { + success: true, + output: message + }; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : String(error) + }; + } + } +}; diff --git a/src/utils/jobs/definitions/createBackup.ts b/src/jobs/definitions/createBackup.ts similarity index 100% rename from src/utils/jobs/definitions/createBackup.ts rename to src/jobs/definitions/createBackup.ts diff --git a/src/utils/jobs/init.ts b/src/jobs/init.ts similarity index 100% rename from src/utils/jobs/init.ts rename to src/jobs/init.ts diff --git a/src/jobs/logic/cleanupLogs.ts b/src/jobs/logic/cleanupLogs.ts new file mode 100644 index 0000000..35d5c93 --- /dev/null +++ b/src/jobs/logic/cleanupLogs.ts @@ -0,0 +1,66 @@ +/** + * Core cleanup logic for log files + * Separated from job definition to avoid database/config dependencies for testing + */ + +export interface CleanupLogsResult { + deletedCount: number; + errorCount: number; + errors: Array<{ file: string; error: unknown }>; +} + +/** + * Core cleanup logic - deletes log files older than retention period + * Pure function that only depends on Deno APIs + * + * @param logsDir Directory containing log files + * @param retentionDays Number of days to retain logs + * @returns Cleanup result with counts + */ +export async function cleanupLogs( + logsDir: string, + retentionDays: number, +): Promise { + // Calculate cutoff date (YYYY-MM-DD format) + const cutoffDate = new Date(); + cutoffDate.setDate(cutoffDate.getDate() - retentionDays); + const cutoffDateStr = cutoffDate.toISOString().split("T")[0]; // YYYY-MM-DD + + let deletedCount = 0; + let errorCount = 0; + const errors: Array<{ file: string; error: unknown }> = []; + + // Regex to match daily log files: YYYY-MM-DD.log + const dateLogPattern = /^(\d{4}-\d{2}-\d{2})\.log$/; + + try { + for await (const entry of Deno.readDir(logsDir)) { + if (!entry.isFile) continue; + + // Only process log files matching YYYY-MM-DD.log pattern + const match = entry.name.match(dateLogPattern); + if (!match) continue; + + const logDate = match[1]; // Extract YYYY-MM-DD from filename + const filePath = `${logsDir}/${entry.name}`; + + try { + // Compare date strings directly (YYYY-MM-DD format sorts correctly) + if (logDate < cutoffDateStr) { + await Deno.remove(filePath); + deletedCount++; + } + } catch (error) { + errorCount++; + errors.push({ file: entry.name, error }); + } + } + } catch (error) { + // If we can't read the directory at all, that's a critical error + throw new Error( + `Failed to read logs directory: ${error instanceof Error ? error.message : String(error)}`, + ); + } + + return { deletedCount, errorCount, errors }; +} diff --git a/src/utils/jobs/registry.ts b/src/jobs/registry.ts similarity index 100% rename from src/utils/jobs/registry.ts rename to src/jobs/registry.ts diff --git a/src/utils/jobs/runner.ts b/src/jobs/runner.ts similarity index 100% rename from src/utils/jobs/runner.ts rename to src/jobs/runner.ts diff --git a/src/utils/jobs/scheduler.ts b/src/jobs/scheduler.ts similarity index 100% rename from src/utils/jobs/scheduler.ts rename to src/jobs/scheduler.ts diff --git a/src/utils/jobs/types.ts b/src/jobs/types.ts similarity index 100% rename from src/utils/jobs/types.ts rename to src/jobs/types.ts diff --git a/src/utils/jobs/definitions/cleanupLogs.ts b/src/utils/jobs/definitions/cleanupLogs.ts deleted file mode 100644 index ac9ab9b..0000000 --- a/src/utils/jobs/definitions/cleanupLogs.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { config } from '$config'; -import { logSettingsQueries } from '$db/queries/logSettings.ts'; -import { logger } from '$logger'; -import type { JobDefinition, JobResult } from '../types.ts'; - -/** - * Cleanup old log files job - * Deletes daily log files (YYYY-MM-DD.log) older than the configured retention period - */ -export const cleanupLogsJob: JobDefinition = { - name: 'cleanup_logs', - description: 'Delete log files according to retention policy', - schedule: 'daily', - - handler: async (): Promise => { - try { - // Get log settings - const settings = logSettingsQueries.get(); - if (!settings) { - return { - success: false, - error: 'Log settings not found' - }; - } - - const retentionDays = settings.retention_days; - const logsDir = config.paths.logs; - - // Calculate cutoff date (YYYY-MM-DD format) - const cutoffDate = new Date(); - cutoffDate.setDate(cutoffDate.getDate() - retentionDays); - const cutoffDateStr = cutoffDate.toISOString().split('T')[0]; // YYYY-MM-DD - - await logger.info(`Cleaning up logs older than ${retentionDays} days`, { - source: 'CleanupLogsJob', - meta: { cutoffDate: cutoffDateStr } - }); - - // Read all files in logs directory - let deletedCount = 0; - let errorCount = 0; - - // Regex to match daily log files: YYYY-MM-DD.log - const dateLogPattern = /^(\d{4}-\d{2}-\d{2})\.log$/; - - try { - for await (const entry of Deno.readDir(logsDir)) { - if (!entry.isFile) continue; - - // Only process log files matching YYYY-MM-DD.log pattern - const match = entry.name.match(dateLogPattern); - if (!match) continue; - - const logDate = match[1]; // Extract YYYY-MM-DD from filename - const filePath = `${logsDir}/${entry.name}`; - - try { - // Compare date strings directly (YYYY-MM-DD format sorts correctly) - if (logDate < cutoffDateStr) { - await Deno.remove(filePath); - deletedCount++; - - await logger.info(`Deleted old log file: ${entry.name}`, { - source: 'CleanupLogsJob', - meta: { file: entry.name, logDate } - }); - } - } catch (error) { - errorCount++; - await logger.error(`Failed to process log file: ${entry.name}`, { - source: 'CleanupLogsJob', - meta: { file: entry.name, error } - }); - } - } - } catch (error) { - return { - success: false, - error: `Failed to read logs directory: ${ - error instanceof Error ? error.message : String(error) - }` - }; - } - - const message = `Cleanup completed: deleted ${deletedCount} file(s), ${errorCount} error(s)`; - - if (errorCount > 0 && deletedCount === 0) { - return { - success: false, - error: message - }; - } - - return { - success: true, - output: message - }; - } catch (error) { - return { - success: false, - error: error instanceof Error ? error.message : String(error) - }; - } - } -};