import type { BackupConfig, MinioSyncAutomationConfig, MinioSyncTaskConfig } from '../config/schema.js'; import type { MemoryStore } from '../memory/store.js'; import type { OutboundMessage } from '../channels/types.js'; import { parseInterval } from './heartbeat.js'; import { createMinioSyncTool } from '../tools/builtin/minio-sync.js'; import { auditLogger } from '../audit/index.js'; import type { Tool } from '../tools/types.js'; interface ChannelLookup { get(name: string): { send(peerId: string, message: OutboundMessage): Promise } | undefined; } export interface MinioSyncSchedulerDeps { config: MinioSyncAutomationConfig; backupConfig: BackupConfig; memoryStore?: MemoryStore; channelLookup: ChannelLookup; createSyncTool?: (backupConfig: BackupConfig, memoryStore: MemoryStore) => Tool; } export class MinioSyncScheduler { private readonly deps: MinioSyncSchedulerDeps; private timer: ReturnType | undefined; private running = false; constructor(deps: MinioSyncSchedulerDeps) { this.deps = deps; } start(): void { if (!this.deps.config.enabled) {return;} if (!this.deps.memoryStore) { console.warn('MinioSyncScheduler: automation.minio_sync is enabled but memory is disabled; skipping scheduler'); return; } if (this.deps.config.tasks.length === 0) { console.warn('MinioSyncScheduler: automation.minio_sync is enabled but no tasks are configured; skipping scheduler'); return; } const intervalMs = parseInterval(this.deps.config.interval); console.log(`MinioSyncScheduler: starting (interval=${this.deps.config.interval}, tasks=${this.deps.config.tasks.length})`); auditLogger?.systemStart('MinioSyncScheduler', { interval: this.deps.config.interval, tasks: this.deps.config.tasks.length, }); this.timer = setInterval(() => { this.runOnce().catch((err) => { console.error('MinioSyncScheduler: unexpected error during sync cycle:', err); }); }, intervalMs); if (this.deps.config.run_on_start) { this.runOnce().catch((err) => { console.error('MinioSyncScheduler: unexpected error during startup sync:', err); }); } } stop(): void { if (this.timer) { clearInterval(this.timer); this.timer = undefined; } auditLogger?.systemStop('MinioSyncScheduler'); } async runOnce(): Promise<{ succeeded: number; failed: number; details: string[] }> { if (!this.deps.memoryStore || this.running) { return { succeeded: 0, failed: 0, details: [] }; } this.running = true; try { const toolFactory = this.deps.createSyncTool ?? createMinioSyncTool; const tool = toolFactory(this.deps.backupConfig, this.deps.memoryStore); let succeeded = 0; let failed = 0; const details: string[] = []; for (const task of this.deps.config.tasks) { const result = await tool.execute(this.toToolArgs(task)); if (result.success) { succeeded++; details.push(`OK ${task.prefix}`); } else { failed++; details.push(`FAIL ${task.prefix}: ${result.error ?? 'Unknown error'}`); } } if (failed > 0) { await this.notify(`MinIO sync completed with failures.\n${details.join('\n')}`); } else if (this.deps.config.notify_on_success) { await this.notify(`MinIO sync completed successfully.\n${details.join('\n')}`); } return { succeeded, failed, details }; } finally { this.running = false; } } private toToolArgs(task: MinioSyncTaskConfig): Record { return { prefix: task.prefix, bucket: task.bucket, namespace_base: task.namespace_base, mode: task.mode, max_objects: task.max_objects, max_chars_per_object: task.max_chars_per_object, force: task.force, }; } private async notify(text: string): Promise { const notifyConfig = this.deps.config.notify; if (!notifyConfig) {return;} const channel = this.deps.channelLookup.get(notifyConfig.channel); if (!channel) { console.warn(`MinioSyncScheduler: notification channel '${notifyConfig.channel}' not found`); return; } try { await channel.send(notifyConfig.peer, { text }); } catch (err) { console.error('MinioSyncScheduler: failed to send notification:', err); } } }